Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:20-bookworm-slim
FROM node:22-bookworm-slim

RUN groupadd --gid 10000 apiuser \
&& useradd --uid 10001 --gid apiuser --shell /bin/bash --create-home apiuser
Expand Down
2 changes: 1 addition & 1 deletion doc/Setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Follow the below steps to get set up for developing and testing the OAuth Agent

Ensure that these tools are installed locally:

- [Node.js 20+](https://nodejs.org/en/download/)
- [Node.js 22+](https://nodejs.org/en/download/)
- [Docker](https://www.docker.com/products/docker-desktop)
- [jq](https://stedolan.github.io/jq/download/)

Expand Down
1,885 changes: 972 additions & 913 deletions package-lock.json

Large diffs are not rendered by default.

34 changes: 14 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,27 @@
"license": "Apache-2.0",
"type": "module",
"engines": {
"node": ">=20"
"node": ">=22"
},
"dependencies": {
"base64url": "^3.0.1",
"cookie": "^1.0.1",
"cookie": "^1.1.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"express": "^4.21.2",
"jose": "^6.0.10",
"node-fetch": "^3.3.2",
"url-parse": "^1.5.10"
"express": "^5.2.1",
"jose": "^6.1.3"
},
"devDependencies": {
"@types/chai": "^4.3.11",
"@types/cookie": "^0.6.0",
"@types/chai": "^4.3.20",
"@types/cookie-parser": "^1.4.6",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/mocha": "^10.0.6",
"@types/node-fetch": "^2.6.9",
"@types/set-cookie-parser": "^2.4.7",
"@types/url-parse": "^1.4.11",
"chai": "^4.3.10",
"mocha": "^10.5.2",
"set-cookie-parser": "^2.6.0",
"tsx": "^4.19.3",
"typescript": "^5.8.3",
"wiremock": "^3.8.0"
"@types/express": "^5.0.6",
"@types/mocha": "^10.0.10",
"@types/set-cookie-parser": "^2.4.10",
"chai": "^4.5.0",
"mocha": "^11.7.5",
"set-cookie-parser": "^2.7.2",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"wiremock": "^3.13.2"
}
}
3 changes: 1 addition & 2 deletions src/controller/ClaimsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ import {getIDCookieName, getIDTokenClaims, ValidateRequestOptions} from '../lib/
import {config} from '../config.js'
import validateExpressRequest from '../validateExpressRequest.js'
import {InvalidCookieException} from '../lib/exceptions/index.js'
import {asyncCatch} from '../middleware/exceptionMiddleware.js';

class ClaimsController {
public router = express.Router()

constructor() {
this.router.get('/', asyncCatch(this.getClaims))
this.router.get('/', this.getClaims)
}

getClaims = async (req: express.Request, res: express.Response, next: express.NextFunction) => {
Expand Down
5 changes: 2 additions & 3 deletions src/controller/LoginController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,13 @@ import {
} from '../lib/index.js'
import {config} from '../config.js'
import validateExpressRequest from '../validateExpressRequest.js'
import {asyncCatch} from '../middleware/exceptionMiddleware.js';

class LoginController {
public router = express.Router()

constructor() {
this.router.post('/start', asyncCatch(this.startLogin))
this.router.post('/end', asyncCatch(this.handlePageLoad))
this.router.post('/start', this.startLogin)
this.router.post('/end', this.handlePageLoad)
}

/*
Expand Down
3 changes: 1 addition & 2 deletions src/controller/LogoutController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ import {config} from '../config.js'
import {getATCookieName, getCookiesForUnset, getLogoutURL, ValidateRequestOptions} from '../lib/index.js'
import {InvalidCookieException} from '../lib/exceptions/index.js'
import validateExpressRequest from '../validateExpressRequest.js'
import {asyncCatch} from '../middleware/exceptionMiddleware.js';

class LogoutController {
public router = express.Router()

constructor() {
this.router.post('/', asyncCatch(this.logoutUser))
this.router.post('/', this.logoutUser)
}

logoutUser = async (req: express.Request, res: express.Response, next: express.NextFunction) => {
Expand Down
3 changes: 1 addition & 2 deletions src/controller/RefreshTokenController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,12 @@ import {
} from '../lib/index.js'
import {InvalidCookieException} from '../lib/exceptions/index.js'
import validateExpressRequest from '../validateExpressRequest.js'
import {asyncCatch} from '../middleware/exceptionMiddleware.js';

class RefreshTokenController {
public router = express.Router()

constructor() {
this.router.post('/', asyncCatch(this.RefreshTokenFromCookie))
this.router.post('/', this.RefreshTokenFromCookie)
}

RefreshTokenFromCookie = async (req: express.Request, res: express.Response, next: express.NextFunction) => {
Expand Down
3 changes: 1 addition & 2 deletions src/controller/UserInfoController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ import {getATCookieName, getUserInfo, ValidateRequestOptions} from '../lib/index
import {config} from '../config.js'
import validateExpressRequest from '../validateExpressRequest.js'
import {InvalidCookieException} from '../lib/exceptions/index.js'
import {asyncCatch} from '../middleware/exceptionMiddleware.js';

class UserInfoController {
public router = express.Router()

constructor() {
this.router.get('/', asyncCatch(this.getUserInfo))
this.router.get('/', this.getUserInfo)
}

getUserInfo = async (req: express.Request, res: express.Response, next: express.NextFunction) => {
Expand Down
39 changes: 39 additions & 0 deletions src/lib/base64url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2022 Curity AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export class Base64Url {

public static encode(input: Buffer): string {

return input.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '')
}

public static decode(input: string): Buffer {

const base64 = input
.replace(/-/g, '+')
.replace(/_/g, '/')

return Buffer.from(base64, 'base64')
}

public static decodeToString(input: string): string {
return Base64Url.decode(input).toString()
}
}
6 changes: 3 additions & 3 deletions src/lib/cookieEncrypter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
*/

import crypto from 'crypto'
import base64url from 'base64url';
import {SerializeOptions, serialize} from 'cookie'
import {CookieDecryptionException, InvalidCookieException} from '../lib/exceptions/index.js'
import {Base64Url} from './base64url.js'

const VERSION_SIZE = 1;
const GCM_IV_SIZE = 12;
Expand All @@ -40,12 +40,12 @@ function encryptCookie(encKeyHex: string, plaintext: string): string {

const allBytes = Buffer.concat([versionBytes, ivBytes, ciphertextBytes, tagBytes])

return base64url.encode(allBytes)
return Base64Url.encode(allBytes)
}

function decryptCookie(encKeyHex: string, encryptedbase64value: string): string {

const allBytes = base64url.toBuffer(encryptedbase64value)
const allBytes = Base64Url.decode(encryptedbase64value)

const minSize = VERSION_SIZE + GCM_IV_SIZE + 1 + GCM_TAG_SIZE
if (allBytes.length < minSize) {
Expand Down
1 change: 0 additions & 1 deletion src/lib/getToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/

import fetch from 'node-fetch'
import {decryptCookie} from './cookieEncrypter.js'
import {Grant} from './grant.js'
import OAuthAgentConfiguration from './oauthAgentConfiguration.js'
Expand Down
1 change: 0 additions & 1 deletion src/lib/getUserInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/

import fetch from 'node-fetch'
import {decryptCookie} from './cookieEncrypter.js'
import {Grant} from './grant.js'
import OAuthAgentConfiguration from './oauthAgentConfiguration.js'
Expand Down
47 changes: 25 additions & 22 deletions src/lib/loginHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/

import urlparse from 'url-parse'
import {ClientOptions} from './clientOptions.js';
import OAuthAgentConfiguration from './oauthAgentConfiguration.js';
import {generateHash, generateRandomString} from './pkce.js';
Expand Down Expand Up @@ -51,37 +50,41 @@ export function createAuthorizationRequest(config: OAuthAgentConfiguration, opti

export async function handleAuthorizationResponse(pageUrl?: string): Promise<any> {

const data = getUrlParts(pageUrl)

if (data.state && data.code) {

return {
code: data.code,
state: data.state,
}
const args = parseUrl(pageUrl)
if (!args) {
return {}
}

if (data.state && data.error) {
const state = args.get('state') || ''
const code = args.get('code') || ''
const error = args.get('error') || ''

if (state && error) {

throw new AuthorizationResponseException(
data.error,
data.error_description || 'Login failed at the Authorization Server')
const errorDescription = args.get('error_description') || 'Login failed at the Authorization Server'
throw new AuthorizationResponseException(error, errorDescription)
}

return {
code: null,
state: null,
code,
state,
}
}

function getUrlParts(url?: string): any {

if (url) {
const urlData = urlparse(url, true)
if (urlData.query) {
return urlData.query
function parseUrl(urlString?: string): URLSearchParams | null {

try {

if (urlString) {

const url = new URL(urlString)
return new URLSearchParams(url.search)
}

} catch {

console.log('Invalid URL received')
}

return {}
return null
}
16 changes: 0 additions & 16 deletions src/middleware/exceptionMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,3 @@ export default function exceptionMiddleware(
}
response.send(data)
}

/*
* Unhandled promise rejections may not be caught properly
* https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
*/
export function asyncCatch(fn: any): any {

return (request: Request, response: Response, next: NextFunction) => {

Promise
.resolve(fn(request, response, next))
.catch((e) => {
exceptionMiddleware(e, request, response, next)
})
};
}
7 changes: 4 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ if (config.corsEnabled) {
}

app.use(cookieParser())
app.use('*', express.json())
app.use('*', loggingMiddleware)
app.use('*', exceptionMiddleware)
app.use('*_', express.json())
app.use('*_', loggingMiddleware)
app.set('etag', false)

const controllers = {
Expand All @@ -59,6 +58,8 @@ for (const [path, controller] of Object.entries(controllers)) {
app.use(config.endpointsPrefix + path, controller.router)
}

app.use('*_', exceptionMiddleware)

if (config.serverCertPath) {

const pfx = fs.readFileSync(config.serverCertPath);
Expand Down
2 changes: 1 addition & 1 deletion test/end-to-end/idsvr/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ services:
# A SQL database used by the Curity Identity Server
#
curity-data:
image: postgres:15.3
image: postgres:18.1
hostname: dbserver
volumes:
- ./data-backup.sql:/docker-entrypoint-initdb.d/data-backup.sql
Expand Down
1 change: 0 additions & 1 deletion test/integration/claimsControllerTests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {assert, expect} from 'chai';
import fetch from 'node-fetch';
import {config} from '../../src/config.js';
import {performLogin} from './testUtils.js'
import {OAuthAgentClaimsResponse, OAuthAgentErrorResponse} from "./responses.js";
Expand Down
1 change: 0 additions & 1 deletion test/integration/extensibilityTests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {assert, expect} from 'chai'
import fetch from 'node-fetch'
import {config} from '../../src/config.js'
import {OauthAgentStartResponse} from "./responses.js";

Expand Down
1 change: 0 additions & 1 deletion test/integration/loginControllerTests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {assert, expect} from 'chai'
import fetch, {RequestInit} from 'node-fetch';
import {config} from '../../src/config.js'
import {fetchStubbedResponse, performLogin, startLogin} from './testUtils.js'
import {OAuthAgentEndResponse, OAuthAgentErrorResponse, OauthAgentStartResponse} from "./responses.js";
Expand Down
1 change: 0 additions & 1 deletion test/integration/logoutControllerTests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {assert, expect} from 'chai'
import fetch, {RequestInit} from 'node-fetch'
import {config} from '../../src/config.js'
import {getCookieString, performLogin} from './testUtils.js'
import {OAuthAgentErrorResponse, OAuthAgentLogoutResponse} from "./responses.js";
Expand Down
1 change: 0 additions & 1 deletion test/integration/refreshTokenControllerTests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {assert, expect} from 'chai'
import fetch, {RequestInit} from 'node-fetch'
import {config} from '../../src/config.js'
import {fetchStubbedResponse, getCookieString, performLogin} from './testUtils.js'
import {OAuthAgentErrorResponse} from "./responses.js";
Expand Down
Loading