231 lines
8.1 KiB
TypeScript
231 lines
8.1 KiB
TypeScript
import {
|
|
createBareToken,
|
|
createAppTokenFactory,
|
|
validateTokenFactory
|
|
} from '@/modules/core/services/tokens'
|
|
import { validateScopes } from '@/modules/shared'
|
|
import { InvalidAccessCodeRequestError } from '@/modules/auth/errors'
|
|
import { ensureError, Optional, Scopes } from '@speckle/shared'
|
|
import { BadRequestError, ForbiddenError } from '@/modules/shared/errors'
|
|
import {
|
|
getAppFactory,
|
|
revokeRefreshTokenFactory,
|
|
createAuthorizationCodeFactory,
|
|
getAuthorizationCodeFactory,
|
|
deleteAuthorizationCodeFactory,
|
|
createRefreshTokenFactory,
|
|
getRefreshTokenFactory,
|
|
getTokenAppInfoFactory
|
|
} from '@/modules/auth/repositories/apps'
|
|
import { db } from '@/db/knex'
|
|
import {
|
|
createAppTokenFromAccessCodeFactory,
|
|
refreshAppTokenFactory
|
|
} from '@/modules/auth/services/serverApps'
|
|
import { Express } from 'express'
|
|
import {
|
|
getApiTokenByIdFactory,
|
|
getTokenResourceAccessDefinitionsByIdFactory,
|
|
getTokenScopesByIdFactory,
|
|
revokeTokenByIdFactory,
|
|
revokeUserTokenByIdFactory,
|
|
storeApiTokenFactory,
|
|
storeTokenResourceAccessDefinitionsFactory,
|
|
storeTokenScopesFactory,
|
|
storeUserServerAppTokenFactory,
|
|
updateApiTokenFactory
|
|
} from '@/modules/core/repositories/tokens'
|
|
import { getUserRoleFactory } from '@/modules/core/repositories/users'
|
|
import { corsMiddlewareFactory } from '@/modules/core/configs/cors'
|
|
import { withOperationLogging } from '@/observability/domain/businessLogging'
|
|
|
|
// TODO: Secure these endpoints!
|
|
export default function (app: Express) {
|
|
/*
|
|
Generates an access code for an app.
|
|
TODO: ensure same origin.
|
|
*/
|
|
app.get('/auth/accesscode', async (req, res) => {
|
|
try {
|
|
const getApp = getAppFactory({ db })
|
|
const createAuthorizationCode = createAuthorizationCodeFactory({ db })
|
|
const validateToken = validateTokenFactory({
|
|
revokeUserTokenById: revokeUserTokenByIdFactory({ db }),
|
|
getApiTokenById: getApiTokenByIdFactory({ db }),
|
|
getTokenAppInfo: getTokenAppInfoFactory({ db }),
|
|
getTokenScopesById: getTokenScopesByIdFactory({ db }),
|
|
getUserRole: getUserRoleFactory({ db }),
|
|
getTokenResourceAccessDefinitionsById:
|
|
getTokenResourceAccessDefinitionsByIdFactory({ db }),
|
|
updateApiToken: updateApiTokenFactory({ db })
|
|
})
|
|
|
|
const preventRedirect = !!req.query.preventRedirect
|
|
const appId = req.query.appId as Optional<string>
|
|
if (!appId)
|
|
throw new InvalidAccessCodeRequestError('appId missing from querystring.')
|
|
|
|
const app = await getApp({ id: appId })
|
|
|
|
if (!app) throw new InvalidAccessCodeRequestError('App does not exist.')
|
|
|
|
const challenge = req.query.challenge as Optional<string>
|
|
const userToken = req.query.token as Optional<string>
|
|
if (!challenge) throw new InvalidAccessCodeRequestError('Missing challenge')
|
|
if (!userToken) throw new InvalidAccessCodeRequestError('Missing token')
|
|
|
|
// 1. Validate token
|
|
const tokenValidationResult = await validateToken(userToken)
|
|
const { valid, scopes, userId } =
|
|
'scopes' in tokenValidationResult
|
|
? tokenValidationResult
|
|
: { ...tokenValidationResult, scopes: [], userId: null }
|
|
if (!valid) throw new InvalidAccessCodeRequestError('Invalid token')
|
|
|
|
// 2. Validate token scopes
|
|
await validateScopes(scopes, Scopes.Tokens.Write)
|
|
|
|
const ac = await createAuthorizationCode({ appId, userId, challenge })
|
|
|
|
const redirectUrl = `${app.redirectUrl}?access_code=${ac}`
|
|
return preventRedirect
|
|
? res.status(200).json({ redirectUrl })
|
|
: res.redirect(redirectUrl)
|
|
} catch (err) {
|
|
if (
|
|
err instanceof InvalidAccessCodeRequestError ||
|
|
err instanceof ForbiddenError
|
|
) {
|
|
req.log.info({ err }, 'Invalid access code request error, or Forbidden error.')
|
|
return res.status(400).send(err.message)
|
|
} else {
|
|
req.log.error(err)
|
|
return res
|
|
.status(500)
|
|
.send('Something went wrong while processing your request')
|
|
}
|
|
}
|
|
})
|
|
|
|
/*
|
|
Generates a new api token: (1) either via a valid refresh token or (2) via a valid access token
|
|
*/
|
|
app.options('/auth/token', corsMiddlewareFactory())
|
|
app.post('/auth/token', corsMiddlewareFactory(), async (req, res) => {
|
|
try {
|
|
if (!req.body.appId)
|
|
throw new BadRequestError(
|
|
`Invalid request, insufficient information provided. App Id is required.`
|
|
)
|
|
if (!req.body.appSecret)
|
|
throw new BadRequestError(
|
|
`Invalid request, insufficient information provided. App Secret is required.`
|
|
)
|
|
|
|
const createRefreshToken = createRefreshTokenFactory({ db })
|
|
const getApp = getAppFactory({ db })
|
|
const createAppToken = createAppTokenFactory({
|
|
storeApiToken: storeApiTokenFactory({ db }),
|
|
storeTokenScopes: storeTokenScopesFactory({ db }),
|
|
storeTokenResourceAccessDefinitions: storeTokenResourceAccessDefinitionsFactory(
|
|
{
|
|
db
|
|
}
|
|
),
|
|
storeUserServerAppToken: storeUserServerAppTokenFactory({ db })
|
|
})
|
|
const createAppTokenFromAccessCode = createAppTokenFromAccessCodeFactory({
|
|
getAuthorizationCode: getAuthorizationCodeFactory({ db }),
|
|
deleteAuthorizationCode: deleteAuthorizationCodeFactory({ db }),
|
|
getApp,
|
|
createRefreshToken,
|
|
createAppToken,
|
|
createBareToken
|
|
})
|
|
const refreshAppToken = refreshAppTokenFactory({
|
|
getRefreshToken: getRefreshTokenFactory({ db }),
|
|
revokeRefreshToken: revokeRefreshTokenFactory({ db }),
|
|
createRefreshToken,
|
|
getApp,
|
|
createAppToken,
|
|
createBareToken
|
|
})
|
|
|
|
// Token refresh
|
|
if ('refreshToken' in req.body) {
|
|
if (!req.body.refreshToken)
|
|
throw new BadRequestError(
|
|
'Invalid request, insufficient information provided. A valid refresh token is required.'
|
|
)
|
|
|
|
const authResponse = await withOperationLogging(
|
|
async () =>
|
|
await refreshAppToken({
|
|
refreshToken: req.body.refreshToken,
|
|
appId: req.body.appId,
|
|
appSecret: req.body.appSecret
|
|
}),
|
|
{
|
|
operationName: 'refreshAppToken',
|
|
operationDescription: 'Refresh an app token',
|
|
logger: req.log
|
|
}
|
|
)
|
|
return res.send(authResponse)
|
|
}
|
|
|
|
// Access-code - token exchange
|
|
if (!req.body.accessCode)
|
|
throw new BadRequestError(
|
|
`Invalid request, insufficient information provided. Access Code is required.`
|
|
)
|
|
if (!req.body.challenge)
|
|
throw new BadRequestError(
|
|
`Invalid request, insufficient information provided. Challenge is required.`
|
|
)
|
|
|
|
const authResponse = await withOperationLogging(
|
|
async () =>
|
|
await createAppTokenFromAccessCode({
|
|
appId: req.body.appId,
|
|
appSecret: req.body.appSecret,
|
|
accessCode: req.body.accessCode,
|
|
challenge: req.body.challenge
|
|
}),
|
|
{
|
|
operationName: 'createAppTokenFromAccessCode',
|
|
operationDescription: 'Create an app token from an access code',
|
|
logger: req.log
|
|
}
|
|
)
|
|
return res.send(authResponse)
|
|
} catch (err) {
|
|
req.log.info({ err }, 'Error while trying to generate a new token.')
|
|
return res.status(401).send({ err: ensureError(err).message })
|
|
}
|
|
})
|
|
|
|
/*
|
|
Ensures a user is logged out by invalidating their token and refresh token.
|
|
*/
|
|
app.post('/auth/logout', async (req, res) => {
|
|
try {
|
|
const revokeRefreshToken = revokeRefreshTokenFactory({ db })
|
|
const revokeTokenById = revokeTokenByIdFactory({ db })
|
|
|
|
const token = req.body.token
|
|
const refreshToken = req.body.refreshToken
|
|
|
|
if (!token) throw new BadRequestError('Invalid request. No token provided.')
|
|
await revokeTokenById(token)
|
|
|
|
if (refreshToken) await revokeRefreshToken({ tokenId: refreshToken })
|
|
|
|
return res.status(200).send({ message: 'You have logged out.' })
|
|
} catch (err) {
|
|
req.log.info({ err }, 'Error while trying to logout.')
|
|
return res.status(400).send('Something went wrong while trying to logout.')
|
|
}
|
|
})
|
|
}
|