Skip to content

Commit

Permalink
Remove passport
Browse files Browse the repository at this point in the history
  • Loading branch information
fraxachun committed Nov 22, 2024
1 parent 8552e1b commit bfb4c4d
Show file tree
Hide file tree
Showing 16 changed files with 442 additions and 435 deletions.
3 changes: 0 additions & 3 deletions demo/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"@nestjs/config": "^2.0.0",
"@nestjs/core": "^9.0.0",
"@nestjs/graphql": "^10.0.0",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.50.0",
Expand Down Expand Up @@ -70,7 +69,6 @@
"multer": "^1.0.0",
"nestjs-console": "^8.0.0",
"node-fetch": "^2.0.0",
"passport": "^0.4.0",
"reflect-metadata": "^0.1.13",
"response-time": "^2.3.2",
"rimraf": "^3.0.0",
Expand All @@ -94,7 +92,6 @@
"@types/mime": "^2.0.0",
"@types/multer": "^1.0.0",
"@types/node": "^22.0.0",
"@types/passport": "^1.0.0",
"@types/pg": "^8.0.0",
"@types/response-time": "^2.3.8",
"@types/rimraf": "^3.0.0",
Expand Down
19 changes: 9 additions & 10 deletions demo/api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createAuthResolver, createCometAuthGuard, createStaticAuthedUserStrategy, createStaticCredentialsBasicStrategy } from "@comet/cms-api";
import { CometAuthGuard, createAuthGuardProviders, createAuthResolver, createBasicAuthService, createStaticUserAuthService } from "@comet/cms-api";
import { DynamicModule, Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { Config } from "@src/config/config";
Expand All @@ -15,18 +15,17 @@ export class AuthModule {
return {
module: AuthModule,
providers: [
createStaticCredentialsBasicStrategy({
username: SYSTEM_USER_NAME,
password: config.auth.systemUserPassword,
strategyName: "system-user",
}),
createStaticAuthedUserStrategy({
staticAuthedUser: staticUsers[0],
}),
...createAuthGuardProviders(
createBasicAuthService({
username: SYSTEM_USER_NAME,
password: config.auth.systemUserPassword,
}),
createStaticUserAuthService({ staticUser: staticUsers[0] }),
),
createAuthResolver(),
{
provide: APP_GUARD,
useClass: createCometAuthGuard(["system-user", "static-authed-user"]),
useClass: CometAuthGuard,
},
UserService,
AccessControlService,
Expand Down
7 changes: 0 additions & 7 deletions packages/api/cms-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"@hapi/accept": "^5.0.2",
"@nestjs/jwt": "^9.0.0",
"@nestjs/mapped-types": "^1.2.2",
"@nestjs/passport": "^9.0.0",
"@opentelemetry/api": "^1.9.0",
"@smithy/node-http-handler": "3.1.4",
"@types/get-image-colors": "^4.0.0",
Expand All @@ -68,10 +67,6 @@
"mime-db": "^1.0.0",
"multer": "^1.4.4",
"node-fetch": "^2.0.0",
"passport": "^0.6.0",
"passport-custom": "^1.1.1",
"passport-http": "^0.3.0",
"passport-jwt": "^4.0.0",
"pluralize": "^8.0.0",
"probe-image-size": "^7.0.0",
"reflect-metadata": "^0.1.0",
Expand Down Expand Up @@ -107,8 +102,6 @@
"@types/multer": "^1.4.4",
"@types/node": "^22.0.0",
"@types/node-fetch": "^2.6.2",
"@types/passport-http": "^0.3.9",
"@types/passport-jwt": "^3.0.7",
"@types/pluralize": "^0.0.29",
"@types/probe-image-size": "^7.0.0",
"@types/request-ip": "^0.0.41",
Expand Down
96 changes: 55 additions & 41 deletions packages/api/cms-api/src/auth/guards/comet.guard.ts
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 packages/api/cms-api/src/auth/services/basic.auth-service.ts
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 packages/api/cms-api/src/auth/services/jwt.auth-service.ts
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;
}
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;
}

This file was deleted.

This file was deleted.

Loading

0 comments on commit bfb4c4d

Please sign in to comment.