implement OAuth PKCE

This implements Proof Key for Code Exchange a.k.a. RFC 7636.
This commit is contained in:
Johann150 2022-11-07 00:12:21 +01:00 committed by Gitea
parent 15b3ab6d13
commit 5291f29581
5 changed files with 67 additions and 0 deletions

View 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"`);
}
}

View file

@ -40,4 +40,10 @@ export class AuthSession {
}) })
@JoinColumn() @JoinColumn()
public app: App | null; public app: App | null;
@Column('text', {
nullable: true,
comment: 'PKCE code_challenge value, if provided (OAuth only)',
})
pkceChallenge: string | null;
} }

View file

@ -10,6 +10,7 @@ export async function oauth(ctx: Koa.Context): void {
grant_type, grant_type,
code, code,
redirect_uri, redirect_uri,
code_verifier,
} = ctx.request.body; } = ctx.request.body;
// check if any of the parameters are null or empty string // check if any of the parameters are null or empty string
@ -79,6 +80,34 @@ export async function oauth(ctx: Koa.Context): void {
return; 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 // check redirect URI
if (!compareUrl(app.callbackUrl, redirect_uri)) { if (!compareUrl(app.callbackUrl, redirect_uri)) {
ctx.response.status = 400; ctx.response.status = 400;

View file

@ -52,6 +52,10 @@ export const paramDef = {
type: 'string', type: 'string',
minLength: 1, minLength: 1,
}, },
pkceChallenge: {
type: 'string',
minLength: 1,
},
}, },
required: ['clientId'] required: ['clientId']
}, { }, {
@ -93,6 +97,7 @@ export default define(meta, paramDef, async (ps) => {
createdAt: new Date(), createdAt: new Date(),
appId: app.id, appId: app.id,
token, token,
pkceChallenge: ps.pkceChallenge,
}).then(x => AuthSessions.findOneByOrFail(x.identifiers[0])); }).then(x => AuthSessions.findOneByOrFail(x.identifiers[0]));
return { return {

View file

@ -64,6 +64,20 @@ onMounted(async () => {
if (params.get('response_type') === 'code') { if (params.get('response_type') === 'code') {
// OAuth request detected! // 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 // as a kind of hack, we first have to start the session for the OAuth client
const clientId = params.get('client_id'); const clientId = params.get('client_id');
if (!clientId) { if (!clientId) {
@ -75,6 +89,7 @@ onMounted(async () => {
clientId, clientId,
// make the server check the redirect, if provided // make the server check the redirect, if provided
callbackUrl: params.get('redirect_uri') ?? undefined, callbackUrl: params.get('redirect_uri') ?? undefined,
pkceChallenge: params.get('code_challenge') ?? undefined,
}).catch(e => { }).catch(e => {
const response = { const response = {
error: 'server_error', error: 'server_error',