-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
442 additions
and
435 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,59 +1,73 @@ | ||
import { CanActivate, ExecutionContext, HttpException, Injectable, mixin } from "@nestjs/common"; | ||
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from "@nestjs/common"; | ||
import { Reflector } from "@nestjs/core"; | ||
import { GqlContextType, GqlExecutionContext } from "@nestjs/graphql"; | ||
import { AuthGuard, IAuthGuard, Type } from "@nestjs/passport"; | ||
import { Request } from "express"; | ||
import { isObservable, lastValueFrom } from "rxjs"; | ||
|
||
export function createCometAuthGuard(type?: string | string[]): Type<IAuthGuard> { | ||
@Injectable() | ||
class CometAuthGuard extends AuthGuard(type) implements CanActivate { | ||
constructor(private reflector: Reflector) { | ||
super(); | ||
import { CurrentUser } from "../../user-permissions/dto/current-user"; | ||
import { UserPermissionsService } from "../../user-permissions/user-permissions.service"; | ||
import { AuthServiceInterface } from "../util/auth-service.interface"; | ||
|
||
@Injectable() | ||
export class CometAuthGuard implements CanActivate { | ||
constructor( | ||
private reflector: Reflector, | ||
private readonly service: UserPermissionsService, | ||
@Inject("COMET_AUTH_SERVICES") private readonly authServices: AuthServiceInterface[], | ||
) {} | ||
|
||
private getRequest(context: ExecutionContext): Request & { user: CurrentUser } { | ||
return context.getType().toString() === "graphql" | ||
? GqlExecutionContext.create(context).getContext().req | ||
: context.switchToHttp().getRequest(); | ||
} | ||
|
||
async canActivate(context: ExecutionContext): Promise<boolean> { | ||
const request = this.getRequest(context); | ||
|
||
const disableCometGuard = this.reflector.getAllAndOverride("disableCometGuards", [context.getHandler(), context.getClass()]); | ||
const hasIncludeInvisibleContentHeader = !!request.headers["x-include-invisible-content"]; | ||
if (disableCometGuard && !hasIncludeInvisibleContentHeader) { | ||
return true; | ||
} | ||
|
||
getRequest(context: ExecutionContext): Request { | ||
return context.getType().toString() === "graphql" | ||
? GqlExecutionContext.create(context).getContext().req | ||
: context.switchToHttp().getRequest(); | ||
if (this.isResolvingGraphQLField(context)) { | ||
return true; | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any | ||
handleRequest<CurrentUser>(err: unknown, user: any): CurrentUser { | ||
if (err) { | ||
throw err; | ||
} | ||
if (user) { | ||
return user; | ||
let user = await this.getAuthenticatedUser(request); | ||
if (!user) return false; | ||
|
||
if (typeof user === "string") { | ||
const userId = user; | ||
const userService = this.service.getUserService(); | ||
if (!userService) throw new UnauthorizedException(`User authenticated by ID but no user service given: ${userId}`); | ||
try { | ||
user = await userService.getUser(userId); // TODO Cache this call | ||
} catch (e) { | ||
throw new UnauthorizedException(`Could not get user from UserService: ${userId}`); | ||
} | ||
throw new HttpException("UNAUTHENTICATED", 401); | ||
} | ||
|
||
async canActivate(context: ExecutionContext): Promise<boolean> { | ||
const disableCometGuard = this.reflector.getAllAndOverride("disableCometGuards", [context.getHandler(), context.getClass()]); | ||
const hasIncludeInvisibleContentHeader = !!this.getRequest(context).headers["x-include-invisible-content"]; | ||
if (disableCometGuard && !hasIncludeInvisibleContentHeader) { | ||
return true; | ||
} | ||
request["user"] = await this.service.createCurrentUser(user); | ||
|
||
if (this.isResolvingGraphQLField(context)) { | ||
return true; | ||
} | ||
return true; | ||
} | ||
|
||
const canActivate = await super.canActivate(context); | ||
return isObservable(canActivate) ? lastValueFrom(canActivate) : canActivate; | ||
private async getAuthenticatedUser(request: Request) { | ||
for (const authService of this.authServices) { | ||
const user = await authService.authenticateUser(request); | ||
if (user) return user; | ||
} | ||
} | ||
|
||
// See https://docs.nestjs.com/graphql/other-features#execute-enhancers-at-the-field-resolver-level | ||
private isResolvingGraphQLField(context: ExecutionContext): boolean { | ||
if (context.getType<GqlContextType>() === "graphql") { | ||
const gqlContext = GqlExecutionContext.create(context); | ||
const info = gqlContext.getInfo(); | ||
const parentType = info.parentType.name; | ||
return parentType !== "Query" && parentType !== "Mutation"; | ||
} | ||
return false; | ||
// See https://docs.nestjs.com/graphql/other-features#execute-enhancers-at-the-field-resolver-level | ||
private isResolvingGraphQLField(context: ExecutionContext): boolean { | ||
if (context.getType<GqlContextType>() === "graphql") { | ||
const gqlContext = GqlExecutionContext.create(context); | ||
const info = gqlContext.getInfo(); | ||
const parentType = info.parentType.name; | ||
return parentType !== "Query" && parentType !== "Mutation"; | ||
} | ||
return false; | ||
} | ||
return mixin(CometAuthGuard); | ||
} |
30 changes: 30 additions & 0 deletions
30
packages/api/cms-api/src/auth/services/basic.auth-service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { Injectable, UnauthorizedException } from "@nestjs/common"; | ||
import { Request } from "express"; | ||
|
||
import { AuthServiceInterface } from "../util/auth-service.interface"; | ||
|
||
interface BasicAuthServiceConfig { | ||
username: string; | ||
password: string; | ||
} | ||
|
||
export function createBasicAuthService({ username: requiredUsername, password: requiredPassword }: BasicAuthServiceConfig) { | ||
if (requiredUsername === "") throw new Error(`username for BasicAuthService must no be empty`); | ||
if (requiredPassword === "") throw new Error(`password for BasicAuthService (username "${requiredUsername}") must no be empty`); | ||
|
||
@Injectable() | ||
class BasicAuthService implements AuthServiceInterface { | ||
authenticateUser(request: Request) { | ||
const [type, token] = request.header("authorization")?.split(" ") ?? []; | ||
if (type !== "Basic") return; | ||
|
||
const [username, password] = Buffer.from(token, "base64").toString("ascii").split(":"); | ||
if (username !== requiredUsername) return; | ||
|
||
if (password !== requiredPassword) throw new UnauthorizedException(`Wrong password for Basic Auth user "${username}".`); | ||
|
||
return username; | ||
} | ||
} | ||
return BasicAuthService; | ||
} |
67 changes: 67 additions & 0 deletions
67
packages/api/cms-api/src/auth/services/jwt.auth-service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { Injectable, Type, UnauthorizedException } from "@nestjs/common"; | ||
import { JwtService, JwtVerifyOptions } from "@nestjs/jwt"; | ||
import { Request } from "express"; | ||
import JwksRsa, { JwksClient } from "jwks-rsa"; | ||
|
||
import { User } from "../../user-permissions/interfaces/user"; | ||
import { AuthServiceInterface } from "../util/auth-service.interface"; | ||
|
||
type JwtPayload = { [key: string]: unknown }; | ||
|
||
interface JwtAuthServiceOptions { | ||
jwksOptions?: JwksRsa.Options; | ||
verifyOptions?: JwtVerifyOptions; | ||
tokenHeaderName?: string; | ||
convertJwtToUser?: (jwt: JwtPayload) => Promise<User> | User; | ||
} | ||
|
||
export function createJwtAuthService({ jwksOptions, verifyOptions, ...options }: JwtAuthServiceOptions): Type<AuthServiceInterface> { | ||
@Injectable() | ||
class JwtAuthService implements AuthServiceInterface { | ||
private jwksClient?: JwksClient; | ||
|
||
constructor(private jwtService: JwtService) { | ||
if (jwksOptions) this.jwksClient = new JwksClient(jwksOptions); | ||
} | ||
|
||
async authenticateUser(request: Request) { | ||
const token = this.extractTokenFromRequest(request); | ||
if (!token) return; | ||
|
||
if (this.jwksClient) { | ||
if (!verifyOptions) verifyOptions = {}; | ||
verifyOptions.secret = await this.loadSecretFromJwks(token); | ||
} | ||
let jwt: JwtPayload; | ||
try { | ||
jwt = await this.jwtService.verifyAsync(token, verifyOptions); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
} catch (e: any) { | ||
if (e.name === "JsonWebTokenError") { | ||
throw new UnauthorizedException(e.message); | ||
} | ||
throw e; | ||
} | ||
|
||
if (options.convertJwtToUser) return options.convertJwtToUser(jwt); | ||
|
||
if (typeof jwt.sub !== "string") throw new UnauthorizedException("No sub found in JWT. Please implement `convertJwtToUser`"); | ||
return jwt.sub; | ||
} | ||
|
||
private extractTokenFromRequest(request: Request): string | undefined { | ||
if (options.tokenHeaderName) { | ||
return request.header(options.tokenHeaderName); | ||
} | ||
const [type, token] = request.header("authorization")?.split(" ") ?? []; | ||
return type === "Bearer" ? token : undefined; | ||
} | ||
|
||
private async loadSecretFromJwks(token: string): Promise<string> { | ||
if (!this.jwksClient) throw new Error("jwksOptions.jwksUri not set"); | ||
const jwt = this.jwtService.decode(token, { complete: true }) as { header: { kid: string } }; | ||
return (await this.jwksClient.getSigningKey(jwt.header.kid)).getPublicKey(); | ||
} | ||
} | ||
return JwtAuthService; | ||
} |
21 changes: 21 additions & 0 deletions
21
packages/api/cms-api/src/auth/services/static-authed-user.auth-service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { Injectable } from "@nestjs/common"; | ||
|
||
import { User } from "../../user-permissions/interfaces/user"; | ||
import { AuthServiceInterface } from "../util/auth-service.interface"; | ||
|
||
interface StaticUserAuthServiceConfig { | ||
staticUser: User | string; | ||
} | ||
|
||
export function createStaticUserAuthService(config: StaticUserAuthServiceConfig) { | ||
@Injectable() | ||
class StaticUserAuthService implements AuthServiceInterface { | ||
async authenticateUser() { | ||
if (typeof config.staticUser === "string") { | ||
return config.staticUser; | ||
} | ||
return config.staticUser; | ||
} | ||
} | ||
return StaticUserAuthService; | ||
} |
43 changes: 0 additions & 43 deletions
43
packages/api/cms-api/src/auth/strategies/auth-proxy-jwt.strategy.ts
This file was deleted.
Oops, something went wrong.
31 changes: 0 additions & 31 deletions
31
packages/api/cms-api/src/auth/strategies/static-authed-user.strategy.ts
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.