initial implementation of pcke
This commit is contained in:
parent
ef4a5abac9
commit
4babfa801a
@ -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",
|
||||||
|
@ -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 };
|
||||||
|
@ -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':
|
||||||
|
@ -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);
|
||||||
|
@ -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
13
src/utils/crypto-tools.ts
Normal 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');
|
||||||
|
}
|
Reference in New Issue
Block a user