forked from FoundKeyGang/FoundKey
implement OAuth PKCE
This implements Proof Key for Code Exchange a.k.a. RFC 7636.
This commit is contained in:
parent
15b3ab6d13
commit
5291f29581
5 changed files with 67 additions and 0 deletions
12
packages/backend/migration/1667738304733-pkce.js
Normal file
12
packages/backend/migration/1667738304733-pkce.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
export class pkce1667738304733 {
|
||||
name = 'pkce1667738304733'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "auth_session" ADD "pkceChallenge" text`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "auth_session"."pkceChallenge" IS 'PKCE code_challenge value, if provided (OAuth only)'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "auth_session" DROP COLUMN "pkceChallenge"`);
|
||||
}
|
||||
}
|
|
@ -40,4 +40,10 @@ export class AuthSession {
|
|||
})
|
||||
@JoinColumn()
|
||||
public app: App | null;
|
||||
|
||||
@Column('text', {
|
||||
nullable: true,
|
||||
comment: 'PKCE code_challenge value, if provided (OAuth only)',
|
||||
})
|
||||
pkceChallenge: string | null;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ export async function oauth(ctx: Koa.Context): void {
|
|||
grant_type,
|
||||
code,
|
||||
redirect_uri,
|
||||
code_verifier,
|
||||
} = ctx.request.body;
|
||||
|
||||
// check if any of the parameters are null or empty string
|
||||
|
@ -79,6 +80,34 @@ export async function oauth(ctx: Koa.Context): void {
|
|||
return;
|
||||
}
|
||||
|
||||
// check PKCE challenge, if provided before
|
||||
if (session.pkceChallenge) {
|
||||
// Also checking the client's homework, the RFC says:
|
||||
//> minimum length of 43 characters and a maximum length of 128 characters
|
||||
if (!code_verifier || code_verifier.length < 43 || code_verifier.length > 128) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = {
|
||||
error: 'invalid_grant',
|
||||
error_description: 'invalid or missing PKCE code_verifier',
|
||||
};
|
||||
return;
|
||||
} else {
|
||||
// verify that (from RFC 7636):
|
||||
//> BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(code_verifier);
|
||||
|
||||
if (hash.digest('base64url') !== code_challenge) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = {
|
||||
error: 'invalid_grant',
|
||||
error_description: 'invalid PKCE code_verifier',
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check redirect URI
|
||||
if (!compareUrl(app.callbackUrl, redirect_uri)) {
|
||||
ctx.response.status = 400;
|
||||
|
|
|
@ -52,6 +52,10 @@ export const paramDef = {
|
|||
type: 'string',
|
||||
minLength: 1,
|
||||
},
|
||||
pkceChallenge: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
},
|
||||
},
|
||||
required: ['clientId']
|
||||
}, {
|
||||
|
@ -93,6 +97,7 @@ export default define(meta, paramDef, async (ps) => {
|
|||
createdAt: new Date(),
|
||||
appId: app.id,
|
||||
token,
|
||||
pkceChallenge: ps.pkceChallenge,
|
||||
}).then(x => AuthSessions.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
return {
|
||||
|
|
|
@ -64,6 +64,20 @@ onMounted(async () => {
|
|||
if (params.get('response_type') === 'code') {
|
||||
// OAuth request detected!
|
||||
|
||||
// if PKCE is used, check that it is a supported method
|
||||
// the default value for code_challenge_method if not supplied is 'plain', which is not supported.
|
||||
if (params.has('code_challenge') && params.get('code_challenge_method') !== 'S256') {
|
||||
if (params.has('redirect_uri')) {
|
||||
location.href = appendQuery(params.get('redirect_uri'), query({
|
||||
error: 'invalid_request',
|
||||
error_description: 'unsupported code_challenge_method, only "S256" is supported',
|
||||
}));
|
||||
} else {
|
||||
state = 'oauth-error';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// as a kind of hack, we first have to start the session for the OAuth client
|
||||
const clientId = params.get('client_id');
|
||||
if (!clientId) {
|
||||
|
@ -75,6 +89,7 @@ onMounted(async () => {
|
|||
clientId,
|
||||
// make the server check the redirect, if provided
|
||||
callbackUrl: params.get('redirect_uri') ?? undefined,
|
||||
pkceChallenge: params.get('code_challenge') ?? undefined,
|
||||
}).catch(e => {
|
||||
const response = {
|
||||
error: 'server_error',
|
||||
|
|
Loading…
Reference in a new issue