Files
speckle-server/packages/server/modules/shared/middleware/index.ts
T
Gergő Jedlicska 4e3e1de8d2 gergo/invalid token throw (#1444)
* fix(server authz): make sure to forbid access with invalid tokens

fix #927

* test(server authz tests): update tests to reflect the changes in the invalid token forbidden flow
2023-03-13 14:07:49 +01:00

125 lines
4.1 KiB
TypeScript

import {
AuthContext,
authPipelineCreator,
AuthPipelineFunction,
AuthParams,
authHasFailed
} from '@/modules/shared/authz'
import { Request, Response, NextFunction, RequestWithAuthContext } from 'express'
import { ForbiddenError, UnauthorizedError } from '@/modules/shared/errors'
import { ensureError } from '@/modules/shared/helpers/errorHelper'
import { validateToken } from '@/modules/core/services/tokens'
import { TokenValidationResult } from '@/modules/core/helpers/types'
import { buildRequestLoaders } from '@/modules/core/loaders'
import { GraphQLContext } from '@/modules/shared/helpers/typeHelper'
export const authMiddlewareCreator = (steps: AuthPipelineFunction[]) => {
const pipeline = authPipelineCreator(steps)
const middleware = async (
req: RequestWithAuthContext,
res: Response,
next: NextFunction
) => {
const { authResult } = await pipeline({
context: req.context as AuthContext,
params: req.params as AuthParams,
authResult: { authorized: false }
})
if (!authResult.authorized) {
let message = 'Unknown AuthZ error'
let status = 500
if (authHasFailed(authResult)) {
message = authResult.error?.message || message
if (authResult.error instanceof UnauthorizedError) status = 401
if (authResult.error instanceof ForbiddenError) status = 403
}
return res.status(status).json({ error: message })
}
return next()
}
return middleware
}
export const getTokenFromRequest = (req: Request | null | undefined): string | null =>
req?.headers?.authorization ?? null
/**
* Create an AuthContext from a raw token value
* @param rawToken
* @param tokenValidator
* @returns The resulting AuthContext object of the token validator
*/
export async function createAuthContextFromToken(
rawToken: string | null,
tokenValidator: (
tokenString: string
) => Promise<TokenValidationResult> = validateToken
): Promise<AuthContext> {
// null, undefined or empty string tokens can continue without errors and auth: false
// to enable anonymous user access to public resources
if (!rawToken) return { auth: false }
let token = rawToken
if (token.startsWith('Bearer ')) token = token.split(' ')[1]
try {
const tokenValidationResult = await tokenValidator(token)
// invalid tokens however will be rejected.
if (!tokenValidationResult.valid)
return { auth: false, err: new ForbiddenError('Your token is not valid.') }
const { scopes, userId, role } = tokenValidationResult
return { auth: true, userId, role, token, scopes }
} catch (err) {
const surelyError = ensureError(err, 'Unknown error during token validation')
return { auth: false, err: surelyError }
}
}
export async function authContextMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const token = getTokenFromRequest(req)
const authContext = await createAuthContextFromToken(token)
if (!authContext.auth && authContext.err) {
let message = 'Unknown Auth context error'
let status = 500
message = authContext.err?.message || message
if (authContext.err instanceof UnauthorizedError) status = 401
if (authContext.err instanceof ForbiddenError) status = 403
return res.status(status).json({ error: message })
}
;(req as RequestWithAuthContext).context = authContext
next()
}
export function addLoadersToCtx(ctx: AuthContext): GraphQLContext {
const loaders = buildRequestLoaders(ctx)
return { ...ctx, loaders }
}
type MaybeAuthenticatedRequest = Request | RequestWithAuthContext | null | undefined
const isRequestWithAuthContext = (
req: MaybeAuthenticatedRequest
): req is RequestWithAuthContext =>
req !== null && req !== undefined && 'context' in req
/**
* Build context for GQL operations
*/
export async function buildContext({
req,
token
}: {
req: MaybeAuthenticatedRequest
token: string | null
}): Promise<GraphQLContext> {
const ctx = isRequestWithAuthContext(req)
? req.context
: await createAuthContextFromToken(token ?? getTokenFromRequest(req))
// Adding request data loaders
return addLoadersToCtx(ctx)
}