Skip to content

Commit

Permalink
BC-7653 - Add guard decorators and make auth guard module dynamic (#5307
Browse files Browse the repository at this point in the history
)
  • Loading branch information
bischofmax authored Nov 6, 2024
1 parent 1767779 commit afcb384
Show file tree
Hide file tree
Showing 57 changed files with 205 additions and 202 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,3 @@ data:
IDENTITY_MANAGEMENT__TENANT: "{{ IDENTITY_MANAGEMENT__TENANT }}"
IDENTITY_MANAGEMENT__CLIENTID: "{{ IDENTITY_MANAGEMENT__CLIENTID }}"
TLDRAW__WEBSOCKET_URL: "wss://{{ DOMAIN }}/tldraw-server"
JWT_PUBLIC_KEY: "{{ JWT_PUBLIC_KEY }}"
33 changes: 26 additions & 7 deletions apps/server/src/infra/auth-guard/auth-guard.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
import { Module } from '@nestjs/common';
import { DynamicModule, Module, Provider } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtValidationAdapter } from './adapter';
import { JwtStrategy, WsJwtStrategy, XApiKeyStrategy } from './strategy';

@Module({
imports: [PassportModule],
providers: [JwtStrategy, WsJwtStrategy, JwtValidationAdapter, XApiKeyStrategy],
exports: [JwtValidationAdapter],
})
export class AuthGuardModule {}
export enum AuthGuardOptions {
JWT = 'jwt',
WS_JWT = 'ws-jwt',
X_API_KEY = 'x-api-key',
}

@Module({})
export class AuthGuardModule {
static register(options: AuthGuardOptions[]): DynamicModule {
const providers: Provider[] = [JwtValidationAdapter];

if (options.includes(AuthGuardOptions.JWT)) providers.push(JwtStrategy);

if (options.includes(AuthGuardOptions.WS_JWT)) providers.push(WsJwtStrategy);

if (options.includes(AuthGuardOptions.X_API_KEY)) providers.push(XApiKeyStrategy);

return {
module: AuthGuardModule,
imports: [PassportModule],
providers,
exports: [JwtValidationAdapter],
};
}
}
3 changes: 2 additions & 1 deletion apps/server/src/infra/auth-guard/config/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './x-api-key.config';
export * from './jwt-auth-guard.config';
export * from './x-api-key-auth-guard.config';
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Algorithm } from 'jsonwebtoken';

export interface AuthGuardConfig {
ADMIN_API__ALLOWED_API_KEYS: string[];
export interface JwtAuthGuardConfig {
JWT_PUBLIC_KEY: string;
JWT_SIGNING_ALGORITHM: Algorithm;
SC_DOMAIN: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export interface XApiKeyConfig {
export interface XApiKeyAuthGuardConfig {
ADMIN_API__ALLOWED_API_KEYS: string[];
}
2 changes: 2 additions & 0 deletions apps/server/src/infra/auth-guard/decorator/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './jwt-auth.decorator';
export * from './ws-jwt-auth.decorator';
export * from './x-api-key.decorator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { applyDecorators, UseGuards } from '@nestjs/common';
import { WsJwtAuthGuard } from '../guard';

export const WsJwtAuthentication = () => {
const decorator = applyDecorators(UseGuards(WsJwtAuthGuard));

return decorator;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { applyDecorators, UseGuards } from '@nestjs/common';
import { XApiKeyGuard } from '../guard';

export const XApiKeyAuthentication = () => {
const decorator = applyDecorators(UseGuards(XApiKeyGuard));

return decorator;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import { AuthGuard } from '@nestjs/passport';
import { StrategyType } from '../interface';

@Injectable()
export class ApiKeyGuard extends AuthGuard(StrategyType.API_KEY) {}
export class XApiKeyGuard extends AuthGuard(StrategyType.API_KEY) {}
9 changes: 4 additions & 5 deletions apps/server/src/infra/auth-guard/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
export { JwtValidationAdapter } from './adapter';
export { AuthGuardModule } from './auth-guard.module';
export { AuthGuardConfig } from './auth-guard.config';
export { XApiKeyConfig } from './config';
export { CurrentUser, JWT, JwtAuthentication } from './decorator';
export { AuthGuardModule, AuthGuardOptions } from './auth-guard.module';
export { JwtAuthGuardConfig, XApiKeyAuthGuardConfig } from './config';
export { CurrentUser, JWT, JwtAuthentication, WsJwtAuthentication, XApiKeyAuthentication } from './decorator';
// JwtAuthGuard only exported because api tests still overried this guard.
// Use JwtAuthentication decorator for request validation
export { ApiKeyGuard, JwtAuthGuard, WsJwtAuthGuard } from './guard';
export { JwtAuthGuard, WsJwtAuthGuard, XApiKeyGuard } from './guard';
export { CreateJwtPayload, ICurrentUser, JwtPayload, StrategyType } from './interface';
export { CurrentUserBuilder, JwtPayloadFactory } from './mapper';
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ConfigService } from '@nestjs/config';
import { JwtFromRequestFunction, StrategyOptions } from 'passport-jwt';
import { AuthGuardConfig } from '../auth-guard.config';
import { JwtAuthGuardConfig } from '../config';

export class JwtStrategyOptionsFactory {
static build(
jwtFromRequestFunction: JwtFromRequestFunction,
configService: ConfigService<AuthGuardConfig>
configService: ConfigService<JwtAuthGuardConfig>
): StrategyOptions {
const publicKey = configService.getOrThrow<string>('JWT_PUBLIC_KEY');
const algorithm = configService.getOrThrow<string>('JWT_SIGNING_ALGORITHM');
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/infra/auth-guard/strategy/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { PassportStrategy } from '@nestjs/passport';
import { extractJwtFromHeader } from '@shared/common';
import { Strategy } from 'passport-jwt';
import { JwtValidationAdapter } from '../adapter';
import { AuthGuardConfig } from '../auth-guard.config';
import { JwtAuthGuardConfig } from '../config';
import { ICurrentUser, JwtPayload } from '../interface';
import { CurrentUserBuilder, JwtStrategyOptionsFactory } from '../mapper';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly jwtValidationAdapter: JwtValidationAdapter,
configService: ConfigService<AuthGuardConfig>
configService: ConfigService<JwtAuthGuardConfig>
) {
const strategyOptions = JwtStrategyOptionsFactory.build(extractJwtFromHeader, configService);

Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/infra/auth-guard/strategy/ws-jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { WsException } from '@nestjs/websockets';
import { JwtExtractor } from '@shared/common';
import { Strategy } from 'passport-jwt';
import { JwtValidationAdapter } from '../adapter';
import { AuthGuardConfig } from '../auth-guard.config';
import { JwtAuthGuardConfig } from '../config';
import { ICurrentUser, JwtPayload, StrategyType } from '../interface';
import { CurrentUserBuilder, JwtStrategyOptionsFactory } from '../mapper';

@Injectable()
export class WsJwtStrategy extends PassportStrategy(Strategy, StrategyType.WS_JWT) {
constructor(
private readonly jwtValidationAdapter: JwtValidationAdapter,
configService: ConfigService<AuthGuardConfig>
configService: ConfigService<JwtAuthGuardConfig>
) {
const strategyOptions = JwtStrategyOptionsFactory.build(JwtExtractor.fromCookie('jwt'), configService);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createMock } from '@golevelup/ts-jest';
import { UnauthorizedException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { XApiKeyAuthGuardConfig } from '../config/x-api-key-auth-guard.config';
import { XApiKeyStrategy } from './x-api-key.strategy';
import { XApiKeyConfig } from '../config/x-api-key.config';

describe('XApiKeyStrategy', () => {
let module: TestingModule;
let strategy: XApiKeyStrategy;
let configService: ConfigService<XApiKeyConfig, true>;
let configService: ConfigService<XApiKeyAuthGuardConfig, true>;

beforeAll(async () => {
module = await Test.createTestingModule({
Expand All @@ -17,15 +17,15 @@ describe('XApiKeyStrategy', () => {
XApiKeyStrategy,
{
provide: ConfigService,
useValue: createMock<ConfigService<XApiKeyConfig, true>>({
useValue: createMock<ConfigService<XApiKeyAuthGuardConfig, true>>({
get: () => ['7ccd4e11-c6f6-48b0-81eb-cccf7922e7a4'],
}),
},
],
}).compile();

strategy = module.get(XApiKeyStrategy);
configService = module.get(ConfigService<XApiKeyConfig, true>);
configService = module.get(ConfigService<XApiKeyAuthGuardConfig, true>);
});

afterAll(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import Strategy from 'passport-headerapikey';
import { XApiKeyConfig } from '../config';
import { XApiKeyAuthGuardConfig } from '../config';
import { StrategyType } from '../interface';

@Injectable()
export class XApiKeyStrategy extends PassportStrategy(Strategy, StrategyType.API_KEY) {
private readonly allowedApiKeys: string[];

constructor(private readonly configService: ConfigService<XApiKeyConfig, true>) {
constructor(private readonly configService: ConfigService<XApiKeyAuthGuardConfig, true>) {
super({ header: 'X-API-KEY' }, false);
this.allowedApiKeys = this.configService.get<string[]>('ADMIN_API__ALLOWED_API_KEYS');
this.allowedApiKeys = this.configService.getOrThrow<string[]>('ADMIN_API__ALLOWED_API_KEYS');
}

public validate = (apiKey: string, done: (error: Error | null, data: boolean | null) => void) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { XApiKeyConfig } from '@infra/auth-guard';
import { AccountConfig } from '@modules/account';
import { Algorithm } from 'jsonwebtoken';

export interface AuthenticationConfig extends AccountConfig, XApiKeyConfig {
export interface AuthenticationConfig extends AccountConfig {
DISABLED_BRUTE_FORCE_CHECK: boolean;
FEATURE_JWT_EXTENDED_TIMEOUT_ENABLED: boolean;
JWT_PRIVATE_KEY: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Configuration } from '@hpi-schul-cloud/commons/lib';
import { AuthGuardModule } from '@infra/auth-guard';
import { AuthGuardModule, AuthGuardOptions } from '@infra/auth-guard';
import { CacheWrapperModule } from '@infra/cache';
import { IdentityManagementModule } from '@infra/identity-management';
import { AccountModule } from '@modules/account';
Expand Down Expand Up @@ -58,7 +58,7 @@ const createJwtOptions = () => {
RoleModule,
IdentityManagementModule,
CacheWrapperModule,
AuthGuardModule,
AuthGuardModule.register([AuthGuardOptions.JWT]),
],
providers: [
UserRepo,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { AuthGuardModule } from '@infra/auth-guard';
import { CacheWrapperModule } from '@infra/cache';
import { IdentityManagementModule } from '@infra/identity-management';
import { AccountModule } from '@modules/account';
Expand Down Expand Up @@ -58,7 +57,6 @@ const createJwtOptions = (configService: ConfigService<AuthenticationConfig>) =>
RoleModule,
IdentityManagementModule,
CacheWrapperModule,
AuthGuardModule,
],
providers: [
UserRepo,
Expand Down
8 changes: 4 additions & 4 deletions apps/server/src/modules/board/board-collaboration.config.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Configuration } from '@hpi-schul-cloud/commons';
import { JwtAuthGuardConfig } from '@infra/auth-guard';
import { Algorithm } from 'jsonwebtoken';

export interface BoardCollaborationConfig {
export interface BoardCollaborationConfig extends JwtAuthGuardConfig {
NEST_LOG_LEVEL: string;
}

const boardCollaborationConfig = {
const boardCollaborationConfig: BoardCollaborationConfig = {
NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string,

// Node's process.env escapes newlines. We need to reverse it for the keys to work.
// See: https://stackoverflow.com/questions/30400341/environment-variables-containing-newlines-in-node
JWT_PUBLIC_KEY: (Configuration.get('JWT_PUBLIC_KEY') as string).replace(/\\n/g, '\n'),
JWT_SIGNING_ALGORITHM: Configuration.get('JWT_SIGNING_ALGORITHM') as Algorithm,
SC_DOMAIN: Configuration.get('SC_DOMAIN') as string,
ADMIN_API__ALLOWED_API_KEYS: (Configuration.get('ADMIN_API__ALLOWED_API_KEYS') as string).split(','),
};

export const config = () => boardCollaborationConfig;
4 changes: 2 additions & 2 deletions apps/server/src/modules/board/board-collaboration.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AuthGuardModule } from '@infra/auth-guard';
import { AuthGuardModule, AuthGuardOptions } from '@infra/auth-guard';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { defaultMikroOrmOptions } from '@modules/server';
import { Module } from '@nestjs/common';
Expand Down Expand Up @@ -28,7 +28,7 @@ import { BoardModule } from './board.module';
BoardModule,
AuthorizationModule,
BoardWsApiModule,
AuthGuardModule,
AuthGuardModule.register([AuthGuardOptions.WS_JWT]),
],
providers: [],
exports: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { ConfigModule } from '@nestjs/config';
import { ALL_ENTITIES } from '@shared/domain/entity';
import { createConfigModuleOptions } from '@src/config';
import { CoreModule } from '@src/core';
import { AuthGuardModule, AuthGuardOptions } from '@src/infra/auth-guard';
import { MongoMemoryDatabaseModule } from '@src/infra/database';
import { RabbitMQWrapperModule } from '@src/infra/rabbitmq';
import { RabbitMQWrapperTestModule } from '@src/infra/rabbitmq';
import { AuthenticationApiModule } from '../authentication/authentication-api.module';
import { AuthorizationModule } from '../authorization';
import { config as boardCollaborationConfig } from './board-collaboration.config';
Expand All @@ -20,7 +21,7 @@ const config = () => {
imports: [
CoreModule,
ConfigModule.forRoot(createConfigModuleOptions(config)),
RabbitMQWrapperModule,
RabbitMQWrapperTestModule,
MongoMemoryDatabaseModule.forRoot({
...defaultMikroOrmOptions,
entities: ALL_ENTITIES,
Expand All @@ -29,6 +30,7 @@ const config = () => {
AuthorizationModule,
AuthenticationApiModule,
BoardWsApiModule,
AuthGuardModule.register([AuthGuardOptions.WS_JWT]),
],
providers: [],
exports: [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { WsJwtAuthGuard } from '@infra/auth-guard';
import { WsJwtAuthentication } from '@infra/auth-guard';
import { Socket, WsValidationPipe } from '@infra/socketio';
import { MikroORM, UseRequestContext } from '@mikro-orm/core';
import { UseGuards, UsePipes } from '@nestjs/common';
import { UsePipes } from '@nestjs/common';
import {
OnGatewayDisconnect,
SubscribeMessage,
Expand Down Expand Up @@ -43,7 +43,7 @@ import { UpdateContentElementMessageParams } from './dto/update-content-element.

@UsePipes(new WsValidationPipe())
@WebSocketGateway(BoardCollaborationConfiguration.websocket)
@UseGuards(WsJwtAuthGuard)
@WsJwtAuthentication()
export class BoardCollaborationGateway implements OnGatewayDisconnect {
@WebSocketServer()
server!: Server;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiKeyGuard } from '@infra/auth-guard';
import { XApiKeyGuard } from '@infra/auth-guard';
import { AdminApiServerTestModule } from '@modules/server/admin-api.server.module';
import { ExecutionContext, INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
Expand All @@ -15,7 +15,7 @@ describe(`deletionExecution (api)`, () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AdminApiServerTestModule],
})
.overrideGuard(ApiKeyGuard)
.overrideGuard(XApiKeyGuard)
.useValue({
canActivate(context: ExecutionContext) {
const req: Request = context.switchToHttp().getRequest();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiKeyGuard } from '@infra/auth-guard';
import { XApiKeyGuard } from '@infra/auth-guard';
import { EntityManager } from '@mikro-orm/mongodb';
import { AdminApiServerTestModule } from '@modules/server/admin-api.server.module';
import { ExecutionContext, INestApplication } from '@nestjs/common';
Expand Down Expand Up @@ -61,7 +61,7 @@ describe(`deletionRequest create (api)`, () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AdminApiServerTestModule],
})
.overrideGuard(ApiKeyGuard)
.overrideGuard(XApiKeyGuard)
.useValue({
canActivate(context: ExecutionContext) {
const req: Request = context.switchToHttp().getRequest();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiKeyGuard } from '@infra/auth-guard';
import { XApiKeyGuard } from '@infra/auth-guard';
import { EntityManager } from '@mikro-orm/mongodb';
import { AdminApiServerTestModule } from '@modules/server/admin-api.server.module';
import { ExecutionContext, INestApplication } from '@nestjs/common';
Expand All @@ -20,7 +20,7 @@ describe(`deletionRequest delete (api)`, () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AdminApiServerTestModule],
})
.overrideGuard(ApiKeyGuard)
.overrideGuard(XApiKeyGuard)
.useValue({
canActivate(context: ExecutionContext) {
const req: Request = context.switchToHttp().getRequest();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiKeyGuard } from '@infra/auth-guard';
import { XApiKeyGuard } from '@infra/auth-guard';
import { EntityManager } from '@mikro-orm/mongodb';
import { AdminApiServerTestModule } from '@modules/server/admin-api.server.module';
import { ExecutionContext, INestApplication } from '@nestjs/common';
Expand All @@ -20,7 +20,7 @@ describe(`deletionRequest find (api)`, () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AdminApiServerTestModule],
})
.overrideGuard(ApiKeyGuard)
.overrideGuard(XApiKeyGuard)
.useValue({
canActivate(context: ExecutionContext) {
const req: Request = context.switchToHttp().getRequest();
Expand Down
Loading

0 comments on commit afcb384

Please sign in to comment.