diff --git a/package.json b/package.json index d584b28..972681f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@icynet/oauth2-provider", - "version": "1.0.6", + "version": "1.0.7", "description": "OAuth2.0 Provider for Icy Network", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/controller/authorization.ts b/src/controller/authorization.ts index c633ed4..ace7df6 100644 --- a/src/controller/authorization.ts +++ b/src/controller/authorization.ts @@ -6,6 +6,7 @@ import { InvalidScope, AccessDenied, InteractionRequired, + InvalidGrant, } from '../model/error'; import { OAuth2User } from '../model/model'; import { data as dataResponse } from '../utils/response'; @@ -138,6 +139,13 @@ export const authorization = wrap(async (req, res) => { req.oauth2.logger.debug('User fetched from request'); } + const codeChallenge = req.query.code_challenge as string; + const codeChallengeMethod = (req.query.code_challenge_method as 'plain' | 'S256') || 'plain'; + + if (codeChallengeMethod && !['plain', 'S256'].includes(codeChallengeMethod)) { + throw new InvalidGrant('Invalid code challenge method'); + } + const prompt = ((req.query.prompt || '') as string).split(' '); let resObj: Record = {}; let consented = false; @@ -191,6 +199,8 @@ export const authorization = wrap(async (req, res) => { scope, oauth2.model.code.ttl, req.query.nonce as string, + codeChallenge, + codeChallengeMethod ); resObj = { code: data, ...resObj }; diff --git a/src/controller/token.ts b/src/controller/token.ts index 8336437..e008517 100644 --- a/src/controller/token.ts +++ b/src/controller/token.ts @@ -94,7 +94,8 @@ export const token = wrap(async (req, res) => { tokenResponse = await tokens.authorizationCode( oauth2, client, - req.body.code + req.body.code, + req.body.code_verifier ); break; case 'password': diff --git a/src/controller/tokens/authorizationCode.ts b/src/controller/tokens/authorizationCode.ts index b6cf5d3..e0d563e 100644 --- a/src/controller/tokens/authorizationCode.ts +++ b/src/controller/tokens/authorizationCode.ts @@ -5,6 +5,7 @@ import { OAuth2Code, OAuth2TokenResponse, } from '../../model/model'; +import { createS256, safeCompare } from '../../utils/crypto-tools'; /** * Issue an access token by authorization code @@ -16,7 +17,8 @@ import { export async function authorizationCode( oauth2: OAuth2, client: OAuth2Client, - providedCode: string + providedCode: string, + codeVerifier?: string, ): Promise { const respObj: OAuth2TokenResponse = { token_type: 'bearer', @@ -58,6 +60,26 @@ export async function authorizationCode( const userId = oauth2.model.code.getUserId(code); const clientId = oauth2.model.code.getClientId(code); + if (oauth2.model.code.getCodeChallenge) { + const { challenge, method } = oauth2.model.code.getCodeChallenge(code); + + if (challenge && method) { + if (!codeVerifier) { + throw new InvalidGrant('Code verifier is required!'); + } + + if (method === 'plain' && !safeCompare(challenge, codeVerifier)) { + throw new InvalidGrant('Invalid code verifier!'); + } + + if (method === 'S256' && !safeCompare(createS256(codeVerifier), challenge)) { + throw new InvalidGrant('Invalid code verifier!'); + } + } + + oauth2.logger.debug('Code passed PCKE check'); + } + if (oauth2.model.refreshToken.invalidateOld) { try { await oauth2.model.refreshToken.removeByUserIdClientId(userId, clientId); diff --git a/src/model/model.ts b/src/model/model.ts index c66da86..501d02c 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -32,6 +32,8 @@ export interface OAuth2Code { client_id: string | number; scope: string; nonce?: string; + code_challenge_method?: 'plain' | 'S256'; + code_challenge?: string; } /** @@ -180,7 +182,9 @@ export interface OAuth2CodeAdapter { clientId: string | number, scope: string | string[], ttl: number, - nonce?: string + nonce?: string, + code_challenge?: string, + code_challenge_method?: 'plain' | 'S256', ) => Promise; /** @@ -212,6 +216,11 @@ export interface OAuth2CodeAdapter { * Check the time-to-live value */ checkTTL: (code: OAuth2Code) => boolean; + + /** + * Get PCKE info + */ + getCodeChallenge?: (code: OAuth2Code) => { method: 'plain' | 'S256'; challenge: string; } } /** diff --git a/src/utils/crypto-tools.ts b/src/utils/crypto-tools.ts new file mode 100644 index 0000000..2fedd32 --- /dev/null +++ b/src/utils/crypto-tools.ts @@ -0,0 +1,13 @@ +import { timingSafeEqual, createHash } from 'crypto'; + +export function safeCompare(token: string, token2: string) { + return timingSafeEqual(Buffer.from(token), Buffer.from(token2)); +} + +export function sha256hash(input: string) { + return createHash('sha256').update(input).digest(); +} + +export function createS256(input: string) { + return sha256hash(Buffer.from(input).toString('ascii')).toString('base64'); +}