From 6d067dd74ad12492adf7102f503081f24818eac8 Mon Sep 17 00:00:00 2001 From: Louis Capitanchik <contact@louiscap.co> Date: Sat, 13 Feb 2021 01:02:45 +0000 Subject: [PATCH] Add endpoints for managing oauth client redirects, fix model methods for refresh token flow --- src/database/models/AccessToken.js | 13 ++++++++ src/database/models/RefreshToken.js | 13 ++++++++ src/database/models/User.js | 3 +- src/domain/auth/AuthServer.js | 25 ++++++-------- src/http/controllers/api/oauth.js | 51 +++++++++++++++++++++++++++++ src/http/middleware/Profiler.js | 14 +++++++- src/http/params/oauth_client.js | 7 ++++ src/http/routes.js | 6 ++++ 8 files changed, 115 insertions(+), 17 deletions(-) create mode 100644 src/http/params/oauth_client.js diff --git a/src/database/models/AccessToken.js b/src/database/models/AccessToken.js index ec8c8c2..16d060d 100644 --- a/src/database/models/AccessToken.js +++ b/src/database/models/AccessToken.js @@ -19,6 +19,19 @@ class AccessToken extends BaseModel { } } + async toOAuthInterface() { + const client = await this.getOAuthClient() + const user = await this.getUser() + + return { + accessToken: this.token, + accessTokenExpiresAt: this.expires_at, + scope: this.scope, + client: client.toOAuthInterface(), + user, + } + } + toJSON() { const user = this.user ? {user: this.user} : {} return { diff --git a/src/database/models/RefreshToken.js b/src/database/models/RefreshToken.js index eb53732..214edc4 100644 --- a/src/database/models/RefreshToken.js +++ b/src/database/models/RefreshToken.js @@ -19,6 +19,19 @@ class RefreshToken extends BaseModel { } } + async toOAuthInterface() { + const client = await this.getOAuthClient() + const user = await this.getUser() + + return { + refreshToken: this.token, + refreshTokenExpiresAt: this.expires_at, + scope: this.scope, + client: client.toOAuthInterface(), + user, + } + } + toJSON() { const user = this.user ? { user: this.user } : { } return { diff --git a/src/database/models/User.js b/src/database/models/User.js index 26231af..c6ec41b 100644 --- a/src/database/models/User.js +++ b/src/database/models/User.js @@ -109,13 +109,14 @@ class User extends BaseModel { return await crypto.encrypt(JSON.stringify({ session: this.id })) } - async asJWTToken() { + async asJWTToken(extras = {}) { const { sign } = require('core/utils/jwt') return await sign({ session: { id: this.id, roles: ['overseer', 'user'], }, + ...extras, }) } diff --git a/src/domain/auth/AuthServer.js b/src/domain/auth/AuthServer.js index 07ae5e3..d0b21a7 100644 --- a/src/domain/auth/AuthServer.js +++ b/src/domain/auth/AuthServer.js @@ -15,18 +15,10 @@ function createTokenPair(user, client, access, refresh = {}) { } } -async function createClientEncryptedToken(client, user, scope) { - const payload = { - client_id: client.id, - user_id: user.id, - scope: scope, - } - - if (client.secret == null) { - return crypto.encrypt(payload) - } - - return await crypto.encryptWith(Buffer.from(client.secret, 'hex'), JSON.stringify(payload)) +async function createClientEncryptedToken(client, userModel, scope) { + const user = await User.findByPk(userModel.id) + const payload = user.asJWTToken({ cid: client.id, scope }) + return payload } const model = { @@ -50,8 +42,9 @@ const model = { getAccessToken(token) { return AccessToken.findOne({ include: [ { model: User }, { model: OAuthClient } ], where: { token: { [Op.eq]: token } } }) }, - getRefreshToken(token) { - return RefreshToken.findOne({ include: [ { model: User }, { model: OAuthClient } ], where: { token: { [Op.eq]: token } } }) + async getRefreshToken(token) { + const t = await RefreshToken.findOne({ include: [ { model: User }, { model: OAuthClient } ], where: { token: { [Op.eq]: token } } }) + return await t?.toOAuthInterface() }, getUser: async function getAuthUser(email, password) { @@ -188,6 +181,8 @@ class KoaOAuthServer { } } + console.log(ctx.request.query, query) + const authState = await crypto.encrypt(JSON.stringify({ redirect: 'authorize', query: ctx.request.query })) if (!user) { return ctx.redirect(`/login?login_state=${ authState }`) @@ -259,7 +254,7 @@ const scopeDescriptionMap = { description: 'Full access to your account, including the ability to create, update and delete any user information, metrics and files.' } } -function describeScopeRequest(scope) { +function describeScopeRequest(scope = '*') { const scopes = scope.split(' ') return scopes.map(s => scopeDescriptionMap[s]) } diff --git a/src/http/controllers/api/oauth.js b/src/http/controllers/api/oauth.js index 6091b5a..a82997e 100644 --- a/src/http/controllers/api/oauth.js +++ b/src/http/controllers/api/oauth.js @@ -24,4 +24,55 @@ exports.listClients = async ctx => { ctx.body = { clients: clients.map(c => c.toOAuthInterface()) } +} + +exports.addClientRedirect = async ctx => { + const client = ctx.models?.oauthClient + const user = await ctx.services['core.auth'].getUser() + + if (client?.owner_id !== user.id) { + throw new HttpError(403, 'You do not have permission to modify this oauth client') + } + + const uri = ctx.request.body?.uri + + if (!uri?.trim()) { + throw new HttpError(400, 'Must provide a URI to add to the client') + } + + const uris = new Set(client.redirect_uris) + if (!uris.has(uri)) { + client.redirect_uris = [ + ...client.redirect_uris, + uri, + ] + + await client.save() + } + + ctx.body = { client: client.toOAuthInterface() } +} + +exports.removeClientRedirect = async ctx => { + const client = ctx.models?.oauthClient + const user = await ctx.services['core.auth'].getUser() + + if (client.owner_id !== user.id) { + throw new HttpError(403, 'You do not have permission to modify this oauth client') + } + + const uri = ctx.request.body?.uri + + if (!uri?.trim()) { + throw new HttpError(400, 'Must provide a URI to add to the client') + } + + const uris = new Set(client.redirect_uris) + if (uris.has(uri)) { + uris.delete(uri) + client.redirect_uris = Array.from(uris) + + await client.save() + } + ctx.body = { client: client.toOAuthInterface() } } \ No newline at end of file diff --git a/src/http/middleware/Profiler.js b/src/http/middleware/Profiler.js index 6c76f07..d9b1e37 100644 --- a/src/http/middleware/Profiler.js +++ b/src/http/middleware/Profiler.js @@ -43,6 +43,18 @@ module.exports = async (ctx, next) => { } finally { t.setName(`[${ ctx.method }] ${ ctx._matchedRouteName ?? ctx._matchedRoute ?? ctx.path }`) t.setHttpStatus(ctx.status) - threadContext.stopTransaction() + const user = ctx.services['core.auth']._user + Sentry.configureScope(scope => { + if (user) { + scope.setUser({ + name: user.name, + email: user.email, + id: user.id, + ip_address: ctx.ip, + }) + } + + threadContext.stopTransaction() + }) } } \ No newline at end of file diff --git a/src/http/params/oauth_client.js b/src/http/params/oauth_client.js new file mode 100644 index 0000000..ed386d8 --- /dev/null +++ b/src/http/params/oauth_client.js @@ -0,0 +1,7 @@ +const { OAuthClient } = require('database/models') + +module.exports = async (id, ctx, next) => { + ctx.models = ctx.models ?? {} + ctx.models.oauthClient = await OAuthClient.findByPk(id) + return await next() +} \ No newline at end of file diff --git a/src/http/routes.js b/src/http/routes.js index ae0e221..c57794a 100644 --- a/src/http/routes.js +++ b/src/http/routes.js @@ -1,4 +1,6 @@ const controller = (name, method) => require(`./controllers/${ name }`)[method] +const param = (name) => require(`./params/${ name }`) + const AuthServer = require('domain/auth/AuthServer') const { env, config } = require('bootstrap') @@ -103,8 +105,12 @@ function mount(api) { api.post('/auth/reset-token', controller('api/auth', 'triggerPasswordReset')) api.post('/auth/reset-password', controller('api/auth', 'handlePasswordReset')) + api.param('oauthClientId', param('oauth_client')) + api.get('/oauth/clients', controller('api/oauth', 'listClients')) api.post('/oauth/clients', controller('api/oauth', 'createClient')) + api.post('/oauth/clients/:oauthClientId/redirects', controller('api/oauth', 'addClientRedirect')) + api.delete('/oauth/clients/:oauthClientId/redirects', controller('api/oauth', 'removeClientRedirect')) api.get('/self', controller('api/user', 'self')) api.get('/self/bundles', controller('api/app', 'getBundles')) -- GitLab