From f5e4e09c9f5b2aedb5d99ada9ad36abaed81c7a8 Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Thu, 19 Sep 2024 10:58:37 +0300 Subject: [PATCH] chore(server): auth IoC 11 - createAppTokenFromAccessCodeFactory (#3032) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(server): auth IoC 3 - getAllAppsCreatedByUserFactory * minor fix * chore(server): auth IoC 4 - getAllAppsAuthorizedByUserFactory * chore(server): auth IoC 5 - createAppFactory * chore(server): auth IoC 6 - updateAppFactory * chore(server): auth IoC 7 - deleteAppFactory * chore(server): auth IoC 8 - revokeExistingAppCredentialsForUserFactory * chore(server): auth IoC 9 - revokeRefreshTokenFactory * chore(server): auth IoC 10 - createAuthorizationCodeFactory * chore(server): auth IoC 11 - createAppTokenFromAccessCodeFactory --------- Co-authored-by: Gergő Jedlicska --- .../server/modules/auth/domain/operations.ts | 27 +++++++- .../server/modules/auth/repositories/apps.ts | 22 +++++++ packages/server/modules/auth/rest/index.js | 26 +++++++- packages/server/modules/auth/services/apps.js | 53 ---------------- .../modules/auth/services/serverApps.ts | 61 +++++++++++++++++++ .../modules/auth/tests/apps.graphql.spec.js | 26 +++++++- .../server/modules/auth/tests/apps.spec.js | 25 +++++++- 7 files changed, 177 insertions(+), 63 deletions(-) diff --git a/packages/server/modules/auth/domain/operations.ts b/packages/server/modules/auth/domain/operations.ts index 62920e254..fd0eb363d 100644 --- a/packages/server/modules/auth/domain/operations.ts +++ b/packages/server/modules/auth/domain/operations.ts @@ -5,9 +5,14 @@ import { UserServerApp } from '@/modules/auth/domain/types' import { ScopeRecord } from '@/modules/auth/helpers/types' +import { + AuthorizationCodeRecord, + RefreshTokenRecord +} from '@/modules/auth/repositories' import { ServerAppRecord } from '@/modules/core/helpers/types' import { MarkNullableOptional } from '@/modules/shared/helpers/typeHelper' -import { ServerScope } from '@speckle/shared' +import { Optional, ServerScope } from '@speckle/shared' +import { SetOptional } from 'type-fest' export type GetApp = (params: { id: string }) => Promise @@ -56,6 +61,16 @@ export type UpdateApp = (params: { export type DeleteApp = (params: { id: string }) => Promise +export type GetAuthorizationCode = (params: { + id: string +}) => Promise> + +export type DeleteAuthorizationCode = (params: { id: string }) => Promise + +export type CreateRefreshToken = (params: { + token: SetOptional +}) => Promise + export type CreateAuthorizationCode = (params: { appId: string userId: string @@ -63,3 +78,13 @@ export type CreateAuthorizationCode = (params: { }) => Promise export type InitializeDefaultApps = () => Promise + +export type CreateAppTokenFromAccessCode = (params: { + appId: string + appSecret: string + accessCode: string + challenge: string +}) => Promise<{ + token: string + refreshToken: string +}> diff --git a/packages/server/modules/auth/repositories/apps.ts b/packages/server/modules/auth/repositories/apps.ts index 821f1b776..5637f1461 100644 --- a/packages/server/modules/auth/repositories/apps.ts +++ b/packages/server/modules/auth/repositories/apps.ts @@ -3,12 +3,15 @@ import { getDefaultApp } from '@/modules/auth/defaultApps' import { CreateApp, CreateAuthorizationCode, + CreateRefreshToken, DeleteApp, + DeleteAuthorizationCode, GetAllAppsAuthorizedByUser, GetAllAppsCreatedByUser, GetAllPublicApps, GetAllScopes, GetApp, + GetAuthorizationCode, RegisterDefaultApp, RevokeExistingAppCredentials, RevokeExistingAppCredentialsForUser, @@ -377,3 +380,22 @@ export const createAuthorizationCodeFactory = await tables.authorizationCodes(deps.db).insert(ac) return ac.id } + +export const getAuthorizationCodeFactory = + (deps: { db: Knex }): GetAuthorizationCode => + async ({ id }) => { + return await tables.authorizationCodes(deps.db).select().where({ id }).first() + } + +export const deleteAuthorizationCodeFactory = + (deps: { db: Knex }): DeleteAuthorizationCode => + async ({ id }) => { + return await tables.authorizationCodes(deps.db).where({ id }).del() + } + +export const createRefreshTokenFactory = + (deps: { db: Knex }): CreateRefreshToken => + async ({ token }) => { + const [ret] = await tables.refreshTokens(deps.db).insert(token, '*') + return ret + } diff --git a/packages/server/modules/auth/rest/index.js b/packages/server/modules/auth/rest/index.js index 11a3844af..92b408c0d 100644 --- a/packages/server/modules/auth/rest/index.js +++ b/packages/server/modules/auth/rest/index.js @@ -1,7 +1,12 @@ 'use strict' const cors = require('cors') -const { createAppTokenFromAccessCode, refreshAppToken } = require('../services/apps') -const { validateToken, revokeTokenById } = require(`@/modules/core/services/tokens`) +const { refreshAppToken } = require('../services/apps') +const { + validateToken, + revokeTokenById, + createAppToken, + createBareToken +} = require(`@/modules/core/services/tokens`) const { validateScopes } = require(`@/modules/shared`) const { InvalidAccessCodeRequestError } = require('@/modules/auth/errors') const { Scopes } = require('@speckle/shared') @@ -9,9 +14,15 @@ const { ForbiddenError } = require('@/modules/shared/errors') const { getAppFactory, revokeRefreshTokenFactory, - createAuthorizationCodeFactory + createAuthorizationCodeFactory, + getAuthorizationCodeFactory, + deleteAuthorizationCodeFactory, + createRefreshTokenFactory } = require('@/modules/auth/repositories/apps') const { db } = require('@/db/knex') +const { + createAppTokenFromAccessCodeFactory +} = require('@/modules/auth/services/serverApps') // TODO: Secure these endpoints! module.exports = (app) => { @@ -70,6 +81,15 @@ module.exports = (app) => { app.options('/auth/token', cors()) app.post('/auth/token', cors(), async (req, res) => { try { + const createAppTokenFromAccessCode = createAppTokenFromAccessCodeFactory({ + getAuthorizationCode: getAuthorizationCodeFactory({ db }), + deleteAuthorizationCode: deleteAuthorizationCodeFactory({ db }), + getApp: getAppFactory({ db }), + createRefreshToken: createRefreshTokenFactory({ db }), + createAppToken, + createBareToken + }) + // Token refresh if (req.body.refreshToken) { if (!req.body.appId || !req.body.appSecret) diff --git a/packages/server/modules/auth/services/apps.js b/packages/server/modules/auth/services/apps.js index 70946215d..f0cd8ff30 100644 --- a/packages/server/modules/auth/services/apps.js +++ b/packages/server/modules/auth/services/apps.js @@ -4,62 +4,9 @@ const knex = require(`@/db/knex`) const { createBareToken, createAppToken } = require(`@/modules/core/services/tokens`) const { getAppFactory } = require('@/modules/auth/repositories/apps') -const ServerApps = () => knex('server_apps') -const ServerAppsScopes = () => knex('server_apps_scopes') - -const AuthorizationCodes = () => knex('authorization_codes') const RefreshTokens = () => knex('refresh_tokens') module.exports = { - async createAppTokenFromAccessCode({ appId, appSecret, accessCode, challenge }) { - const code = await AuthorizationCodes().select().where({ id: accessCode }).first() - - if (!code) throw new Error('Access code not found.') - if (code.appId !== appId) - throw new Error('Invalid request: application id does not match.') - - await AuthorizationCodes().where({ id: accessCode }).del() - - const timeDiff = Math.abs(Date.now() - new Date(code.createdAt)) - if (timeDiff > code.lifespan) { - throw new Error('Access code expired') - } - - if (code.challenge !== challenge) throw new Error('Invalid request') - - const app = await ServerApps().select('*').where({ id: appId }).first() - - if (!app) throw new Error('Invalid app') - if (app.secret !== appSecret) throw new Error('Invalid app credentials') - - const scopes = await ServerAppsScopes().select('scopeName').where({ appId }) - - const appScopes = scopes.map((s) => s.scopeName) - - const appToken = await createAppToken({ - userId: code.userId, - name: `${app.name}-token`, - scopes: appScopes, - appId - }) - - const bareToken = await createBareToken() - - const refreshToken = { - id: bareToken.tokenId, - tokenDigest: bareToken.tokenHash, - appId: app.id, - userId: code.userId - } - - await RefreshTokens().insert(refreshToken) - - return { - token: appToken, - refreshToken: bareToken.tokenId + bareToken.tokenString - } - }, - async refreshAppToken({ refreshToken, appId, appSecret }) { const refreshTokenId = refreshToken.slice(0, 10) const refreshTokenContent = refreshToken.slice(10, 42) diff --git a/packages/server/modules/auth/services/serverApps.ts b/packages/server/modules/auth/services/serverApps.ts index 2b36d2f01..51362fb19 100644 --- a/packages/server/modules/auth/services/serverApps.ts +++ b/packages/server/modules/auth/services/serverApps.ts @@ -1,12 +1,17 @@ import { getDefaultApps } from '@/modules/auth/defaultApps' import { + CreateAppTokenFromAccessCode, + CreateRefreshToken, + DeleteAuthorizationCode, GetAllScopes, GetApp, + GetAuthorizationCode, InitializeDefaultApps, RegisterDefaultApp, UpdateDefaultApp } from '@/modules/auth/domain/operations' import { ScopeRecord } from '@/modules/auth/helpers/types' +import { createAppToken, createBareToken } from '@/modules/core/services/tokens' import { ServerScope } from '@speckle/shared' /** @@ -49,3 +54,59 @@ export const initializeDefaultAppsFactory = }) ) } + +export const createAppTokenFromAccessCodeFactory = + (deps: { + getAuthorizationCode: GetAuthorizationCode + deleteAuthorizationCode: DeleteAuthorizationCode + getApp: GetApp + createRefreshToken: CreateRefreshToken + createAppToken: typeof createAppToken + createBareToken: typeof createBareToken + }): CreateAppTokenFromAccessCode => + async ({ appId, appSecret, accessCode, challenge }) => { + const code = await deps.getAuthorizationCode({ id: accessCode }) + + if (!code) throw new Error('Access code not found.') + if (code.appId !== appId) + throw new Error('Invalid request: application id does not match.') + + await deps.deleteAuthorizationCode({ id: accessCode }) + + const timeDiff = Math.abs(Date.now() - new Date(code.createdAt).getTime()) + if (timeDiff > code.lifespan) { + throw new Error('Access code expired') + } + + if (code.challenge !== challenge) throw new Error('Invalid request') + + const app = await deps.getApp({ id: appId }) + + if (!app) throw new Error('Invalid app') + if (app.secret !== appSecret) throw new Error('Invalid app credentials') + + const appScopes = app.scopes.map((s) => s.name) + + const appToken = await deps.createAppToken({ + userId: code.userId, + name: `${app.name}-token`, + scopes: appScopes, + appId + }) + + const bareToken = await deps.createBareToken() + + const refreshToken = { + id: bareToken.tokenId, + tokenDigest: bareToken.tokenHash, + appId: app.id, + userId: code.userId + } + + await deps.createRefreshToken({ token: refreshToken }) + + return { + token: appToken, + refreshToken: bareToken.tokenId + bareToken.tokenString + } + } diff --git a/packages/server/modules/auth/tests/apps.graphql.spec.js b/packages/server/modules/auth/tests/apps.graphql.spec.js index 8aca00427..87f0eb973 100644 --- a/packages/server/modules/auth/tests/apps.graphql.spec.js +++ b/packages/server/modules/auth/tests/apps.graphql.spec.js @@ -5,18 +5,38 @@ const chai = require('chai') const expect = chai.expect const { createUser } = require('@/modules/core/services/users') -const { createPersonalAccessToken } = require('@/modules/core/services/tokens') +const { + createPersonalAccessToken, + createAppToken, + createBareToken +} = require('@/modules/core/services/tokens') const { beforeEachContext, initializeTestServer } = require('@/test/hooks') -const { createAppTokenFromAccessCode } = require('../services/apps') const { Scopes } = require('@speckle/shared') -const { createAuthorizationCodeFactory } = require('@/modules/auth/repositories/apps') +const { + createAuthorizationCodeFactory, + getAuthorizationCodeFactory, + deleteAuthorizationCodeFactory, + getAppFactory, + createRefreshTokenFactory +} = require('@/modules/auth/repositories/apps') const { db } = require('@/db/knex') +const { + createAppTokenFromAccessCodeFactory +} = require('@/modules/auth/services/serverApps') let sendRequest let server let app const createAuthorizationCode = createAuthorizationCodeFactory({ db }) +const createAppTokenFromAccessCode = createAppTokenFromAccessCodeFactory({ + getAuthorizationCode: getAuthorizationCodeFactory({ db }), + deleteAuthorizationCode: deleteAuthorizationCodeFactory({ db }), + getApp: getAppFactory({ db }), + createRefreshToken: createRefreshTokenFactory({ db }), + createAppToken, + createBareToken +}) describe('GraphQL @apps-api', () => { let testUser diff --git a/packages/server/modules/auth/tests/apps.spec.js b/packages/server/modules/auth/tests/apps.spec.js index 3c34f7431..e1472567f 100644 --- a/packages/server/modules/auth/tests/apps.spec.js +++ b/packages/server/modules/auth/tests/apps.spec.js @@ -2,9 +2,13 @@ const expect = require('chai').expect const { createUser } = require(`@/modules/core/services/users`) -const { validateToken } = require(`@/modules/core/services/tokens`) +const { + validateToken, + createAppToken, + createBareToken +} = require(`@/modules/core/services/tokens`) const { beforeEachContext } = require(`@/test/hooks`) -const { createAppTokenFromAccessCode, refreshAppToken } = require('../services/apps') +const { refreshAppToken } = require('../services/apps') const { Scopes } = require('@/modules/core/helpers/mainConstants') const knex = require('@/db/knex') @@ -17,8 +21,14 @@ const { updateAppFactory, deleteAppFactory, revokeExistingAppCredentialsForUserFactory, - createAuthorizationCodeFactory + createAuthorizationCodeFactory, + getAuthorizationCodeFactory, + deleteAuthorizationCodeFactory, + createRefreshTokenFactory } = require('@/modules/auth/repositories/apps') +const { + createAppTokenFromAccessCodeFactory +} = require('@/modules/auth/services/serverApps') const getApp = getAppFactory({ db: knex }) const updateDefaultApp = updateDefaultAppFactory({ db: knex }) @@ -31,6 +41,15 @@ const revokeExistingAppCredentialsForUser = revokeExistingAppCredentialsForUserF }) const createAuthorizationCode = createAuthorizationCodeFactory({ db: knex }) +const createAppTokenFromAccessCode = createAppTokenFromAccessCodeFactory({ + getAuthorizationCode: getAuthorizationCodeFactory({ db: knex }), + deleteAuthorizationCode: deleteAuthorizationCodeFactory({ db: knex }), + getApp, + createRefreshToken: createRefreshTokenFactory({ db: knex }), + createAppToken, + createBareToken +}) + describe('Services @apps-services', () => { const actor = { name: 'Dimitrie Stefanescu',