Skip to content

Commit

Permalink
feat(oauth2): add pkce support
Browse files Browse the repository at this point in the history
fix #58
  • Loading branch information
nfroidure committed Oct 28, 2020
1 parent 8ec8dd3 commit e3a2a5d
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 25 deletions.
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
53 changes: 52 additions & 1 deletion packages/whook-oauth2/src/handlers/getOAuth2Authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
OAuth2GranterService,
} from '../services/oAuth2Granters';
import type { LogService } from 'common-services';
import { CODE_CHALLENGE_METHODS } from '../services/oAuth2CodeGranter';

/* Architecture Note #1: OAuth2 authorize
This endpoint simply redirect the user to the authentication
Expand Down Expand Up @@ -78,6 +79,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,
},
},
};

export const definition: WhookAPIHandlerDefinition = {
method: 'get',
Expand All @@ -103,6 +127,12 @@ export const definition: WhookAPIHandlerDefinition = {
{
$ref: `#/components/parameters/${stateParameter.name}`,
},
{
$ref: `#/components/parameters/${codeChallengeParameter.name}`,
},
{
$ref: `#/components/parameters/${codeChallengeMethodParameter.name}`,
},
],
responses: {
'302': {
Expand Down Expand Up @@ -132,13 +162,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;
authorizeParameters?: { [name: string]: unknown };
},
): Promise<WhookResponse> {
Expand All @@ -153,6 +187,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,
Expand All @@ -164,7 +207,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 @@ -41,6 +41,10 @@ export const authorizationCodeTokenRequestBodySchema: WhookAPISchemaDefinition =
type: 'string',
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
139 changes: 131 additions & 8 deletions packages/whook-oauth2/src/services/oAuth2CodeGranter.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import initOAuth2CodeGranter from './oAuth2CodeGranter';
import type { BaseAuthenticationData } from '@whook/authorization';
import initOAuth2CodeGranter, {
base64UrlEncode,
hashCodeVerifier,
} from './oAuth2CodeGranter';

describe('OAuth2CodeGranter', () => {
const oAuth2Code = {
Expand Down Expand Up @@ -32,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 @@ -47,13 +55,17 @@ 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 +91,8 @@ describe('OAuth2CodeGranter', () => {
},
"authorizerResult": Object {
"applicationId": "abbacaca-abba-caca-abba-cacaabbacaca",
"codeChallenge": undefined,
"codeChallengeMethod": undefined,
"redirectURI": "https://www.example.com",
"scope": "user",
},
Expand All @@ -92,3 +106,112 @@ 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 e3a2a5d

Please sign in to comment.