chore(server): auth IoC 11 - createAppTokenFromAccessCodeFactory

This commit is contained in:
Kristaps Fabians Geikins 2024-09-18 12:42:55 +03:00
Родитель 547e6d19b5
Коммит f2caebceda
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 16D20A16730A0111
7 изменённых файлов: 177 добавлений и 63 удалений

Просмотреть файл

@ -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<FullServerApp | null>
@ -56,6 +61,16 @@ export type UpdateApp = (params: {
export type DeleteApp = (params: { id: string }) => Promise<number>
export type GetAuthorizationCode = (params: {
id: string
}) => Promise<Optional<AuthorizationCodeRecord>>
export type DeleteAuthorizationCode = (params: { id: string }) => Promise<number>
export type CreateRefreshToken = (params: {
token: SetOptional<RefreshTokenRecord, 'createdAt' | 'lifespan'>
}) => Promise<RefreshTokenRecord>
export type CreateAuthorizationCode = (params: {
appId: string
userId: string
@ -63,3 +78,13 @@ export type CreateAuthorizationCode = (params: {
}) => Promise<string>
export type InitializeDefaultApps = () => Promise<void>
export type CreateAppTokenFromAccessCode = (params: {
appId: string
appSecret: string
accessCode: string
challenge: string
}) => Promise<{
token: string
refreshToken: string
}>

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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)

Просмотреть файл

@ -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)

Просмотреть файл

@ -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
}
}

Просмотреть файл

@ -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

Просмотреть файл

@ -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',