Skip to content

Commit

Permalink
feat(@whook/oauth2): add pkce support
Browse files Browse the repository at this point in the history
fix #58
  • Loading branch information
nfroidure committed Sep 27, 2021
1 parent da55737 commit 4faa53e
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 21 deletions.
6 changes: 5 additions & 1 deletion packages/whook-oauth2/src/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ Object {
},
"a_grant_code",
"http://redirect.example.com/yolo",
undefined,
],
],
"oAuth2CodeCreateCalls": Array [],
Expand Down Expand Up @@ -173,7 +174,10 @@ Object {
"scope": "user",
},
"http://redirect.example.com/yolo?a_param=a_value",
Object {},
Object {
"codeChallenge": "",
"codeChallengeMethod": "plain",
},
],
],
"oAuth2PasswordCheckCalls": Array [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ Object {
"redirectURI": "https://www.example.com",
"scope": "user",
},
Object {},
Object {
"codeChallenge": "",
"codeChallengeMethod": "plain",
},
],
],
"logCalls": Array [],
Expand Down
50 changes: 49 additions & 1 deletion packages/whook-oauth2/src/handlers/getOAuth2Authorize.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { autoHandler } from 'knifecycle';
import camelCase from 'camelcase';
import YError from 'yerror';
import { refersTo } from '@whook/whook';
import { CODE_CHALLENGE_METHODS } from '../services/oAuth2CodeGranter';
import type {
WhookAPIHandlerDefinition,
WhookAPIParameterDefinition,
Expand Down Expand Up @@ -79,6 +81,29 @@ export const stateParameter: WhookAPIParameterDefinition = {
},
},
};
export const codeChallengeParameter: WhookAPIParameterDefinition = {
name: 'code_challenge',
parameter: {
in: 'query',
name: 'code_challenge',
required: false,
schema: {
type: 'string',
},
},
};
export const codeChallengeMethodParameter: WhookAPIParameterDefinition = {
name: 'code_challenge_method',
parameter: {
in: 'query',
name: 'code_challenge_method',
required: false,
schema: {
type: 'string',
enum: CODE_CHALLENGE_METHODS as unknown as string[],
},
},
};

export const definition: WhookAPIHandlerDefinition = {
method: 'get',
Expand All @@ -104,6 +129,8 @@ export const definition: WhookAPIHandlerDefinition = {
{
$ref: `#/components/parameters/${stateParameter.name}`,
},
refersTo(codeChallengeParameter),
refersTo(codeChallengeMethodParameter),
],
responses: {
'302': {
Expand Down Expand Up @@ -133,13 +160,17 @@ async function getOAuth2Authorize(
redirect_uri: demandedRedirectURI = '',
scope: demandedScope = '',
state,
code_challenge: codeChallenge = '',
code_challenge_method: codeChallengeMethod = 'plain',
...authorizeParameters
}: {
response_type: string;
client_id: string;
redirect_uri?: string;
scope?: string;
state: string;
code_challenge?: string;
code_challenge_method?: string;
} & Record<string, unknown>,
): Promise<WhookResponse> {
const url = new URL(OAUTH2.authenticateURL);
Expand All @@ -153,6 +184,15 @@ async function getOAuth2Authorize(
if (!granter) {
throw new YError('E_UNKNOWN_AUTHORIZER_TYPE', responseType);
}
if (responseType === 'code') {
if (!codeChallenge) {
if (OAUTH2.forcePKCE) {
throw new YError('E_PKCE_REQUIRED', responseType);
}
}
} else if (codeChallenge) {
throw new YError('E_PKCE_NOT_SUPPORTED', responseType);
}

const { applicationId, redirectURI, scope } =
await granter.authorizer.authorize(
Expand All @@ -161,7 +201,15 @@ async function getOAuth2Authorize(
redirectURI: demandedRedirectURI,
scope: demandedScope,
},
camelCaseObjectProperties(authorizeParameters),
camelCaseObjectProperties({
...authorizeParameters,
...(responseType === 'code'
? {
codeChallenge,
codeChallengeMethod,
}
: {}),
}),
);

url.searchParams.set('type', responseType);
Expand Down
4 changes: 4 additions & 0 deletions packages/whook-oauth2/src/handlers/postOAuth2Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export const authorizationCodeTokenRequestBodySchema: WhookAPISchemaDefinition =
format: 'uri',
},
},
code_verifier: {
type: 'string',
pattern: '^[\\d\\w\\-/\\._~]+$',
},
},
};

Expand Down
4 changes: 4 additions & 0 deletions packages/whook-oauth2/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
getOAuth2AuthorizeRedirectURIParameter,
getOAuth2AuthorizeScopeParameter,
getOAuth2AuthorizeStateParameter,
getOAuth2AuthorizeCodeChallengeParameter,
getOAuth2AuthorizeCodeChallengeMethodParameter,
initPostOAuth2Acknowledge,
postOAuth2AcknowledgeDefinition,
initPostOAuth2Token,
Expand Down Expand Up @@ -108,6 +110,8 @@ describe('OAuth2 server', () => {
getOAuth2AuthorizeRedirectURIParameter,
getOAuth2AuthorizeScopeParameter,
getOAuth2AuthorizeStateParameter,
getOAuth2AuthorizeCodeChallengeParameter,
getOAuth2AuthorizeCodeChallengeMethodParameter,
].reduce(
(parametersHash, { name, parameter }) => ({
...parametersHash,
Expand Down
13 changes: 12 additions & 1 deletion packages/whook-oauth2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import initGetOAuth2Authorize, {
redirectURIParameter as getOAuth2AuthorizeRedirectURIParameter,
scopeParameter as getOAuth2AuthorizeScopeParameter,
stateParameter as getOAuth2AuthorizeStateParameter,
codeChallengeParameter as getOAuth2AuthorizeCodeChallengeParameter,
codeChallengeMethodParameter as getOAuth2AuthorizeCodeChallengeMethodParameter,
} from './handlers/getOAuth2Authorize';
import initPostOAuth2Acknowledge, {
definition as postOAuth2AcknowledgeDefinition,
Expand All @@ -21,10 +23,14 @@ import initOAuth2Granters, {
OAUTH2_ERRORS_DESCRIPTORS,
} from './services/oAuth2Granters';
import initOAuth2ClientCredentialsGranter from './services/oAuth2ClientCredentialsGranter';
import initOAuth2CodeGranter from './services/oAuth2CodeGranter';
import initOAuth2CodeGranter, {
base64UrlEncode,
hashCodeVerifier,
} from './services/oAuth2CodeGranter';
import initOAuth2PasswordGranter from './services/oAuth2PasswordGranter';
import initOAuth2RefreshTokenGranter from './services/oAuth2RefreshTokenGranter';
import initOAuth2TokenGranter from './services/oAuth2TokenGranter';
import type { CodeChallengeMethod } from './services/oAuth2CodeGranter';
import type {
OAuth2CodeService,
OAuth2PasswordService,
Expand Down Expand Up @@ -56,6 +62,7 @@ import type {
} from './services/authCookies';

export type {
CodeChallengeMethod,
OAuth2CodeService,
OAuth2PasswordService,
OAuth2AccessTokenService,
Expand All @@ -77,6 +84,10 @@ export {
getOAuth2AuthorizeRedirectURIParameter,
getOAuth2AuthorizeScopeParameter,
getOAuth2AuthorizeStateParameter,
getOAuth2AuthorizeCodeChallengeParameter,
getOAuth2AuthorizeCodeChallengeMethodParameter,
base64UrlEncode,
hashCodeVerifier,
initPostOAuth2Acknowledge,
postOAuth2AcknowledgeDefinition,
initPostOAuth2Token,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Object {
},
"yolo",
"https://www.example.com/oauth2/code",
"",
],
],
"oAuth2CodeCreateCalls": Array [
Expand All @@ -42,7 +43,10 @@ Object {
"scope": "user",
},
"https://www.example.com/oauth2/code",
Object {},
Object {
"codeChallenge": "",
"codeChallengeMethod": "plain",
},
],
],
}
Expand Down
80 changes: 73 additions & 7 deletions packages/whook-oauth2/src/services/oAuth2CodeGranter.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import initOAuth2CodeGranter from './oAuth2CodeGranter';
import initOAuth2CodeGranter, {
base64UrlEncode,
hashCodeVerifier,
} from './oAuth2CodeGranter';

describe('OAuth2CodeGranter', () => {
const oAuth2Code = {
Expand Down Expand Up @@ -31,11 +34,17 @@ describe('OAuth2CodeGranter', () => {
scope: 'user',
});

const authorizerResult = await oAuth2CodeGranter.authorizer.authorize({
clientId: 'abbacaca-abba-caca-abba-cacaabbacaca',
redirectURI: 'https://www.example.com/oauth2/code',
scope: 'user',
});
const authorizerResult = await oAuth2CodeGranter.authorizer.authorize(
{
clientId: 'abbacaca-abba-caca-abba-cacaabbacaca',
redirectURI: 'https://www.example.com/oauth2/code',
scope: 'user',
},
{
codeChallenge: '',
codeChallengeMethod: 'plain',
},
);
const acknowledgerResult = await oAuth2CodeGranter.acknowledger.acknowledge(
{
applicationId: 'abbacaca-abba-caca-abba-cacaabbacaca',
Expand All @@ -46,14 +55,18 @@ describe('OAuth2CodeGranter', () => {
redirectURI: 'https://www.example.com/oauth2/code',
scope: 'user',
},
{},
{
codeChallenge: '',
codeChallengeMethod: 'plain',
},
);
const authenticatorResult =
await oAuth2CodeGranter.authenticator.authenticate(
{
clientId: 'abbacaca-abba-caca-abba-cacaabbacaca',
redirectURI: 'https://www.example.com/oauth2/code',
code: 'yolo',
codeVerifier: '',
},
{
applicationId: 'abbacaca-abba-caca-abba-cacaabbacaca',
Expand All @@ -79,6 +92,8 @@ describe('OAuth2CodeGranter', () => {
},
"authorizerResult": Object {
"applicationId": "abbacaca-abba-caca-abba-cacaabbacaca",
"codeChallenge": "",
"codeChallengeMethod": "plain",
"redirectURI": "https://www.example.com",
"scope": "user",
},
Expand All @@ -92,3 +107,54 @@ describe('OAuth2CodeGranter', () => {
}).toMatchSnapshot();
});
});

describe('base64UrlEncode()', () => {
test('should work like here https://tools.ietf.org/html/rfc7636#appendix-A', () => {
expect(
base64UrlEncode(
Buffer.from([
116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173, 187,
186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83, 132, 141,
121,
]),
),
).toEqual('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
});
});

describe('base64UrlEncode()', () => {
test('should work with plain method', () => {
expect(
hashCodeVerifier(
Buffer.from('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'),
'plain',
),
).toEqual(Buffer.from('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'));
});

test('should work with S256 like here https://tools.ietf.org/html/rfc7636#appendix-A', () => {
expect(
hashCodeVerifier(
Buffer.from('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'),
'S256',
),
).toEqual(
Buffer.from([
19, 211, 30, 150, 26, 26, 216, 236, 47, 22, 177, 12, 76, 152, 46, 8,
118, 168, 120, 173, 109, 241, 68, 86, 110, 225, 137, 74, 203, 112, 249,
195,
]),
);
});

test('should work base64 url encode like here https://tools.ietf.org/html/rfc7636#appendix-A', () => {
expect(
base64UrlEncode(
hashCodeVerifier(
Buffer.from('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'),
'S256',
),
),
).toEqual('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM');
});
});
Loading

0 comments on commit 4faa53e

Please sign in to comment.