Skip to content

Commit

Permalink
Merge pull request #145 from conceptadev/feature/global-guards
Browse files Browse the repository at this point in the history
feat(authentication): add global guard pattern and supporting setting…
  • Loading branch information
MrMaz authored Dec 21, 2023
2 parents fddb4c7 + f5e67c4 commit eebaf72
Show file tree
Hide file tree
Showing 36 changed files with 346 additions and 81 deletions.
1 change: 1 addition & 0 deletions packages/nestjs-auth-github/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"passport-github": "^1.1.0"
},
"devDependencies": {
"@concepta/nestjs-auth-jwt": "^4.0.0-alpha.35",
"@concepta/nestjs-crud": "^4.0.0-alpha.35",
"@concepta/nestjs-jwt": "^4.0.0-alpha.35",
"@concepta/nestjs-password": "^4.0.0-alpha.35",
Expand Down
8 changes: 4 additions & 4 deletions packages/nestjs-auth-github/src/auth-github.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import {
AuthenticationResponseInterface,
} from '@concepta/ts-common';
import {
AuthGuard,
AuthUser,
IssueTokenServiceInterface,
AuthenticationJwtResponseDto,
AuthPublic,
} from '@concepta/nestjs-authentication';
import { AUTH_GITHUB_ISSUE_TOKEN_SERVICE_TOKEN } from './auth-github.constants';
import { AUTH_GITHUB_STRATEGY_NAME } from './auth-github.constants';
import { AuthGithubGuard } from './auth-github.guard';

// TODO: improve documentation
/**
Expand All @@ -30,6 +30,8 @@ import { AUTH_GITHUB_STRATEGY_NAME } from './auth-github.constants';
*
*/
@Controller('auth/github')
@UseGuards(AuthGithubGuard)
@AuthPublic()
@ApiTags('auth')
export class AuthGithubController {
constructor(
Expand All @@ -43,7 +45,6 @@ export class AuthGithubController {
@ApiOkResponse({
description: 'Users are redirected to request their GitHub identity.',
})
@UseGuards(AuthGuard(AUTH_GITHUB_STRATEGY_NAME))
@Get('login')
login(): void {
// TODO: no code needed, Decorator will redirect to github
Expand All @@ -55,7 +56,6 @@ export class AuthGithubController {
type: AuthenticationJwtResponseDto,
description: 'DTO containing an access token and a refresh token.',
})
@UseGuards(AuthGuard(AUTH_GITHUB_STRATEGY_NAME))
@Get('callback')
async get(
@AuthUser() user: AuthenticatedUserInterface,
Expand Down
8 changes: 8 additions & 0 deletions packages/nestjs-auth-github/src/auth-github.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@concepta/nestjs-authentication';
import { AUTH_GITHUB_STRATEGY_NAME } from './auth-github.constants';

@Injectable()
export class AuthGithubGuard extends AuthGuard(AUTH_GITHUB_STRATEGY_NAME, {
canDisable: false,
}) {}
7 changes: 7 additions & 0 deletions packages/nestjs-auth-github/src/auth-github.module.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext';
import { AuthenticationModule } from '@concepta/nestjs-authentication';
import { AuthJwtModule } from '@concepta/nestjs-auth-jwt';
import { JwtModule } from '@concepta/nestjs-jwt';
import { CrudModule } from '@concepta/nestjs-crud';
import {
Expand Down Expand Up @@ -32,6 +33,12 @@ describe(AuthGithubModule, () => {
JwtModule.forRoot({}),
AuthGithubModule.forRoot({}),
AuthenticationModule.forRoot({}),
AuthJwtModule.forRootAsync({
inject: [UserLookupService],
useFactory: (userLookupService) => ({
userLookupService,
}),
}),
FederatedModule.forRootAsync({
inject: [UserLookupService, UserMutateService],
useFactory: (userLookupService, userMutateService) => ({
Expand Down
4 changes: 4 additions & 0 deletions packages/nestjs-auth-github/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export * from './auth-github.module';
export * from './auth-github.controller';
export * from './dto/auth-github-login.dto';
export {
AuthGithubGuard,
AuthGithubGuard as GithubAuthGuard,
} from './auth-github.guard';
3 changes: 1 addition & 2 deletions packages/nestjs-auth-jwt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
"@concepta/typeorm-common": "^4.0.0-alpha.35",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.0.0",
"@nestjs/passport": "^9.0.0"
"@nestjs/core": "^9.0.0"
},
"devDependencies": {
"@nestjs/testing": "^9.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../../decorator/auth-jwt.guard';
import { AuthJwtGuard } from '../../auth-jwt.guard';

@Controller('user')
@UseGuards(AuthJwtGuard)
export class UserControllerFixtures {
/**
* Status
*/
@UseGuards(JwtAuthGuard)
@Get('status')
getStatus(): boolean {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { randomUUID } from 'crypto';
import { mock } from 'jest-mock-extended';
import { UserFixture } from '../__fixtures__/user/user.entity.fixture';
import { UserModuleFixture } from '../__fixtures__/user/user.module.fixture';
import { JwtAuthGuard } from './auth-jwt.guard';
import { UserFixture } from './__fixtures__/user/user.entity.fixture';
import { UserModuleFixture } from './__fixtures__/user/user.module.fixture';
import { AuthJwtGuard } from './auth-jwt.guard';

describe(JwtAuthGuard, () => {
describe(AuthJwtGuard, () => {
let context: ExecutionContext;
let jwtAuthGuard: JwtAuthGuard;
let authJwtGuard: AuthJwtGuard;
let spyCanActivate: jest.SpyInstance;
let user: UserFixture;

Expand All @@ -18,37 +18,37 @@ describe(JwtAuthGuard, () => {
const moduleRef = await Test.createTestingModule({
imports: [UserModuleFixture],
}).compile();
jwtAuthGuard = moduleRef.get<JwtAuthGuard>(JwtAuthGuard);
authJwtGuard = moduleRef.get<AuthJwtGuard>(AuthJwtGuard);
spyCanActivate = jest
.spyOn(JwtAuthGuard.prototype, 'canActivate')
.spyOn(AuthJwtGuard.prototype, 'canActivate')
.mockImplementation(() => true);
user = new UserFixture();
user.id = randomUUID();
});

describe(JwtAuthGuard.prototype.canActivate, () => {
describe(AuthJwtGuard.prototype.canActivate, () => {
it('should be success', async () => {
await jwtAuthGuard.canActivate(context);
await authJwtGuard.canActivate(context);
expect(spyCanActivate).toBeCalled();
expect(spyCanActivate).toBeCalledWith(context);
});
});

describe(JwtAuthGuard.prototype.handleRequest, () => {
describe(AuthJwtGuard.prototype.handleRequest, () => {
it('should return user', () => {
const response = jwtAuthGuard.handleRequest<UserFixture>(undefined, user);
const response = authJwtGuard.handleRequest<UserFixture>(undefined, user);
expect(response?.id).toBe(user.id);
});
it('should throw error', () => {
const error = new Error();
const t = () => {
jwtAuthGuard.handleRequest<UserFixture>(error, user);
authJwtGuard.handleRequest<UserFixture>(error, user);
};
expect(t).toThrow();
});
it('should throw error unauthorized', () => {
const t = () => {
jwtAuthGuard.handleRequest(undefined, undefined);
authJwtGuard.handleRequest(undefined, undefined);
};
expect(t).toThrow(UnauthorizedException);
});
Expand Down
22 changes: 22 additions & 0 deletions packages/nestjs-auth-jwt/src/auth-jwt.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { AuthGuard } from '@concepta/nestjs-authentication';
import { ReferenceIdInterface } from '@concepta/ts-core';
import { Injectable, UnauthorizedException } from '@nestjs/common';

import { AUTH_JWT_STRATEGY_NAME } from './auth-jwt.constants';

@Injectable()
export class AuthJwtGuard extends AuthGuard(AUTH_JWT_STRATEGY_NAME, {
canDisable: true,
}) {
handleRequest<T = ReferenceIdInterface>(
err: Error | undefined,
user: T,
info?: Error,
) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw new UnauthorizedException(null, { cause: err ?? info });
}
return user;
}
}
31 changes: 30 additions & 1 deletion packages/nestjs-auth-jwt/src/auth-jwt.module-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import {
Provider,
} from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';

import {
VerifyTokenService,
VerifyTokenServiceInterface,
} from '@concepta/nestjs-authentication';

import { createSettingsProvider } from '@concepta/nestjs-common';

import { AuthJwtOptionsInterface } from './interfaces/auth-jwt-options.interface';
Expand All @@ -22,6 +22,7 @@ import {
} from './auth-jwt.constants';
import { authJwtDefaultConfig } from './config/auth-jwt-default.config';
import { AuthJwtStrategy } from './auth-jwt.strategy';
import { AuthJwtGuard } from './auth-jwt.guard';

const RAW_OPTIONS_TOKEN = Symbol('__AUTH_JWT_MODULE_RAW_OPTIONS_TOKEN__');

Expand Down Expand Up @@ -71,6 +72,7 @@ export function createAuthJwtExports() {
AUTH_JWT_MODULE_USER_LOOKUP_SERVICE_TOKEN,
AUTH_JWT_MODULE_VERIFY_TOKEN_SERVICE_TOKEN,
AuthJwtStrategy,
AuthJwtGuard,
];
}

Expand All @@ -81,10 +83,12 @@ export function createAuthJwtProviders(options: {
return [
...(options.providers ?? []),
AuthJwtStrategy,
AuthJwtGuard,
VerifyTokenService,
createAuthJwtOptionsProvider(options.overrides),
createAuthJwtVerifyTokenServiceProvider(options.overrides),
createAuthJwtUserLookupServiceProvider(options.overrides),
createAuthJwtAppGuardProvider(options.overrides),
];
}

Expand Down Expand Up @@ -128,3 +132,28 @@ export function createAuthJwtUserLookupServiceProvider(
optionsOverrides?.userLookupService ?? options.userLookupService,
};
}

export function createAuthJwtAppGuardProvider(
optionsOverrides?: AuthJwtOptions,
): Provider {
return {
provide: APP_GUARD,
inject: [RAW_OPTIONS_TOKEN, AuthJwtGuard],
useFactory: async (
options: AuthJwtOptionsInterface,
defaultGuard: AuthJwtGuard,
) => {
// get app guard from the options
const appGuard = optionsOverrides?.appGuard ?? options?.appGuard;

// is app guard explicitly false?
if (appGuard === false) {
// yes, don't set a guard
return null;
} else {
// return app guard if set, or fall back to default
return appGuard ?? defaultGuard;
}
},
};
}
32 changes: 0 additions & 32 deletions packages/nestjs-auth-jwt/src/decorator/auth-jwt.guard.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/nestjs-auth-jwt/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './auth-jwt.module';
export * from './auth-jwt.strategy';
export * from './decorator/auth-jwt.guard';
export { AuthJwtGuard, AuthJwtGuard as JwtAuthGuard } from './auth-jwt.guard';
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CanActivate } from '@nestjs/common';
import { VerifyTokenServiceInterface } from '@concepta/nestjs-authentication';
import { AuthJwtSettingsInterface } from './auth-jwt-settings.interface';
import { AuthJwtUserLookupServiceInterface } from './auth-jwt-user-lookup-service.interface';
Expand All @@ -6,4 +7,5 @@ export interface AuthJwtOptionsInterface {
settings?: AuthJwtSettingsInterface;
userLookupService: AuthJwtUserLookupServiceInterface;
verifyTokenService?: VerifyTokenServiceInterface;
appGuard?: CanActivate | false;
}
1 change: 1 addition & 0 deletions packages/nestjs-auth-local/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"passport-local": "^1.0.0"
},
"devDependencies": {
"@concepta/nestjs-auth-jwt": "^4.0.0-alpha.35",
"@concepta/nestjs-jwt": "^4.0.0-alpha.35",
"@nestjs/jwt": "^9.0.0",
"@nestjs/testing": "^9.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { JwtModule } from '@concepta/nestjs-jwt';
import { Module } from '@nestjs/common';
import { AuthenticationModule } from '@concepta/nestjs-authentication';
import { JwtModule } from '@concepta/nestjs-jwt';
import { AuthJwtModule } from '@concepta/nestjs-auth-jwt';

// import { default as ormConfig } from './ormconfig.fixture';
import { AuthLocalModule } from '../auth-local.module';
Expand All @@ -9,6 +11,13 @@ import { UserModuleFixture } from './user/user.module.fixture';
@Module({
imports: [
JwtModule.forRoot({}),
AuthenticationModule.forRoot({}),
AuthJwtModule.forRootAsync({
inject: [UserLookupServiceFixture],
useFactory: (userLookupService: UserLookupServiceFixture) => ({
userLookupService,
}),
}),
AuthLocalModule.forRootAsync({
inject: [UserLookupServiceFixture],
useFactory: (userLookupService) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Injectable } from '@nestjs/common';
import { ReferenceUsername } from '@concepta/ts-core';
import { ReferenceSubject, ReferenceUsername } from '@concepta/ts-core';
import { AuthLocalCredentialsInterface } from '../../interfaces/auth-local-credentials.interface';
import { AuthLocalUserLookupServiceInterface } from '../../interfaces/auth-local-user-lookup-service.interface';
import { LOGIN_SUCCESS, USER_SUCCESS } from './constants';

@Injectable()
export class UserLookupServiceFixture
implements AuthLocalUserLookupServiceInterface
Expand All @@ -13,4 +14,10 @@ export class UserLookupServiceFixture
if (LOGIN_SUCCESS.username === username) return USER_SUCCESS;
else return null;
}

async bySubject(
subject: ReferenceSubject,
): Promise<AuthLocalCredentialsInterface | null> {
throw new Error(`Method not implemented, can't get ${subject}.`);
}
}
Loading

0 comments on commit eebaf72

Please sign in to comment.