diff --git a/src/database/models/AccessToken.js b/src/database/models/AccessToken.js index ec8c8c2ac6639e9aeab922b129fa058ecbe82757..16d060d2e89dc972683d1ff3cca6c20a551a7ed6 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 eb537328cebe72cf34bcb930cc1aff017ed25c77..214edc4471682109bfbc73f4d93531b9fc1429fa 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 26231af8c74b74683aa0cfe55f481634065b421a..c6ec41b92775b1ea9c635f10cf2749afdeef9156 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 07ae5e3edecfad80b6fe5de81f5db6c65a343989..d0b21a7ddac9a6d8c6fdc376d05682ff9edd5b5a 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 6091b5aa72b7cb4d11daf2b2228dae4bf72f6084..a82997e69c096dae35aea9a1de61fe1524156c1f 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 6c76f074fe75e4a55eac5bc3810ae39502abf885..d9b1e37c028b5b68789666d492ecbc86425ba6fd 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 0000000000000000000000000000000000000000..ed386d809639ac9ce6a3df5d3db0e32029615ab3 --- /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 ae0e221c2a05c8f405c8eacc6590ade07802b7f4..c57794a95edd4b2c8a6b84ee235954de96f5d076 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'))