From 7db7fdd9e2d7114fa8c99aa0bfe9d88ac043536a Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 16 Oct 2022 01:11:47 +0200 Subject: [PATCH] add API route for OAuth access token retrieval --- .../backend/src/server/api/common/oauth.ts | 94 +++++++++++++++++++ .../api/endpoints/auth/session/oauth.ts | 5 + packages/backend/src/server/api/index.ts | 4 + 3 files changed, 103 insertions(+) create mode 100644 packages/backend/src/server/api/common/oauth.ts create mode 100644 packages/backend/src/server/api/endpoints/auth/session/oauth.ts diff --git a/packages/backend/src/server/api/common/oauth.ts b/packages/backend/src/server/api/common/oauth.ts new file mode 100644 index 000000000..731cd73fa --- /dev/null +++ b/packages/backend/src/server/api/common/oauth.ts @@ -0,0 +1,94 @@ +import Koa from 'koa'; +import { IsNull, Not } from 'typeorm'; +import { Apps, AuthSessions, AccessTokens } from '@/models/index.js'; +import config from '@/config/index.js'; + +export async function oauth(ctx: Koa.Context): void { + const { + grant_type, + code, + // TODO: check redirect_uri + // since this is also not checked in the legacy app authentication + // it seems pointless to check it here, and it is also not stored. + redirect_uri, + } = ctx.request.body; + + // check if any of the parameters are null or empty string + if ([grant_type, code].some(x => !x)) { + ctx.response.status = 400; + ctx.response.body = { + error: 'invalid_request', + }; + return; + } + + if (grant_type !== 'authorization_code') { + ctx.response.status = 400; + ctx.response.body = { + error: 'unsupported_grant_type', + error_description: 'only authorization_code grants are supported', + }; + return; + } + + const authHeader = ctx.headers.authorization; + if (!authHeader?.toLowerCase().startsWith('basic ')) { + ctx.response.status = 401; + ctx.response.set('WWW-Authenticate', 'Basic'); + ctx.response.body = { + error: 'invalid_client', + error_description: 'HTTP Basic Authentication required', + }; + return; + } + + const [client_id, client_secret] = new Buffer(authHeader.slice(6), 'base64') + .toString('ascii') + .split(':', 2); + + const [app, session] = await Promise.all([ + Apps.findOneBy({ + id: client_id, + secret: client_secret, + }), + AuthSessions.findOneBy({ + appId: client_id, + token: code, + // only check for approved auth sessions + userId: Not(IsNull()), + }), + ]); + if (app == null) { + ctx.response.status = 401; + ctx.response.set('WWW-Authenticate', 'Basic'); + ctx.response.body = { + error: 'invalid_client', + error_description: 'authentication failed', + }; + return; + } + if (session == null) { + ctx.response.status = 400; + ctx.response.body = { + error: 'invalid_grant', + }; + return; + } + + const [ token ] = await Promise.all([ + AccessTokens.findOneByOrFail({ + appId: client_id, + userId: session.userId, + }), + // session is single use + AuthSessions.delete(session.id), + ]); + + ctx.response.status = 200; + ctx.response.body = { + access_token: token.token, + token_type: 'bearer', + // FIXME: per-token permissions + scope: app.permission.join(' '), + }; +}; diff --git a/packages/backend/src/server/api/endpoints/auth/session/oauth.ts b/packages/backend/src/server/api/endpoints/auth/session/oauth.ts new file mode 100644 index 000000000..d6aa6caab --- /dev/null +++ b/packages/backend/src/server/api/endpoints/auth/session/oauth.ts @@ -0,0 +1,5 @@ +/* +This route is already in use, but the functionality is provided +by '@/server/api/common/oauth.ts'. The route is not here because +that route requires more deep level access to HTTP data. +*/ diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts index 140649dcc..456f5ff11 100644 --- a/packages/backend/src/server/api/index.ts +++ b/packages/backend/src/server/api/index.ts @@ -15,6 +15,7 @@ import { handler } from './api-handler.js'; import signup from './private/signup.js'; import signin from './private/signin.js'; import signupPending from './private/signup-pending.js'; +import { oauth } from './common/oauth.js'; import discord from './service/discord.js'; import github from './service/github.js'; import twitter from './service/twitter.js'; @@ -74,6 +75,9 @@ for (const endpoint of endpoints) { } } +// the OAuth endpoint does some shenanigans and can not use the normal API handler +router.post('/auth/session/oauth', oauth); + router.post('/signup', signup); router.post('/signin', signin); router.post('/signup-pending', signupPending);