initial implementation of pcke

This commit is contained in:
Evert Prants 2022-12-03 10:10:11 +02:00
parent ef4a5abac9
commit 4babfa801a
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
6 changed files with 59 additions and 4 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@icynet/oauth2-provider", "name": "@icynet/oauth2-provider",
"version": "1.0.6", "version": "1.0.7",
"description": "OAuth2.0 Provider for Icy Network", "description": "OAuth2.0 Provider for Icy Network",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -6,6 +6,7 @@ import {
InvalidScope, InvalidScope,
AccessDenied, AccessDenied,
InteractionRequired, InteractionRequired,
InvalidGrant,
} from '../model/error'; } from '../model/error';
import { OAuth2User } from '../model/model'; import { OAuth2User } from '../model/model';
import { data as dataResponse } from '../utils/response'; 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'); 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(' '); const prompt = ((req.query.prompt || '') as string).split(' ');
let resObj: Record<string, string | number> = {}; let resObj: Record<string, string | number> = {};
let consented = false; let consented = false;
@ -191,6 +199,8 @@ export const authorization = wrap(async (req, res) => {
scope, scope,
oauth2.model.code.ttl, oauth2.model.code.ttl,
req.query.nonce as string, req.query.nonce as string,
codeChallenge,
codeChallengeMethod
); );
resObj = { code: data, ...resObj }; resObj = { code: data, ...resObj };

View File

@ -94,7 +94,8 @@ export const token = wrap(async (req, res) => {
tokenResponse = await tokens.authorizationCode( tokenResponse = await tokens.authorizationCode(
oauth2, oauth2,
client, client,
req.body.code req.body.code,
req.body.code_verifier
); );
break; break;
case 'password': case 'password':

View File

@ -5,6 +5,7 @@ import {
OAuth2Code, OAuth2Code,
OAuth2TokenResponse, OAuth2TokenResponse,
} from '../../model/model'; } from '../../model/model';
import { createS256, safeCompare } from '../../utils/crypto-tools';
/** /**
* Issue an access token by authorization code * Issue an access token by authorization code
@ -16,7 +17,8 @@ import {
export async function authorizationCode( export async function authorizationCode(
oauth2: OAuth2, oauth2: OAuth2,
client: OAuth2Client, client: OAuth2Client,
providedCode: string providedCode: string,
codeVerifier?: string,
): Promise<OAuth2TokenResponse> { ): Promise<OAuth2TokenResponse> {
const respObj: OAuth2TokenResponse = { const respObj: OAuth2TokenResponse = {
token_type: 'bearer', token_type: 'bearer',
@ -58,6 +60,26 @@ export async function authorizationCode(
const userId = oauth2.model.code.getUserId(code); const userId = oauth2.model.code.getUserId(code);
const clientId = oauth2.model.code.getClientId(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) { if (oauth2.model.refreshToken.invalidateOld) {
try { try {
await oauth2.model.refreshToken.removeByUserIdClientId(userId, clientId); await oauth2.model.refreshToken.removeByUserIdClientId(userId, clientId);

View File

@ -32,6 +32,8 @@ export interface OAuth2Code {
client_id: string | number; client_id: string | number;
scope: string; scope: string;
nonce?: string; nonce?: string;
code_challenge_method?: 'plain' | 'S256';
code_challenge?: string;
} }
/** /**
@ -180,7 +182,9 @@ export interface OAuth2CodeAdapter {
clientId: string | number, clientId: string | number,
scope: string | string[], scope: string | string[],
ttl: number, ttl: number,
nonce?: string nonce?: string,
code_challenge?: string,
code_challenge_method?: 'plain' | 'S256',
) => Promise<string>; ) => Promise<string>;
/** /**
@ -212,6 +216,11 @@ export interface OAuth2CodeAdapter {
* Check the time-to-live value * Check the time-to-live value
*/ */
checkTTL: (code: OAuth2Code) => boolean; checkTTL: (code: OAuth2Code) => boolean;
/**
* Get PCKE info
*/
getCodeChallenge?: (code: OAuth2Code) => { method: 'plain' | 'S256'; challenge: string; }
} }
/** /**

13
src/utils/crypto-tools.ts Normal file
View File

@ -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');
}