chore(server): auth IoC 11 - createAppTokenFromAccessCodeFactory
This commit is contained in:
Родитель
547e6d19b5
Коммит
f2caebceda
|
@ -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',
|
||||
|
|
Загрузка…
Ссылка в новой задаче