Skip to content

Commit

Permalink
implement jwt authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Sep 8, 2024
1 parent 183c79a commit 9bb7196
Show file tree
Hide file tree
Showing 16 changed files with 376 additions and 8 deletions.
11 changes: 9 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,18 @@
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/typeorm": "^10.0.2",
"class-transformer": "catalog:",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pg": "^8.12.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"typeorm": "catalog:",
"class-transformer": "catalog:"
"typeorm": "catalog:"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
Expand All @@ -34,6 +39,8 @@
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
Expand Down
7 changes: 5 additions & 2 deletions api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ApiConfigModule } from '@api/modules/config/app-config.module';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from '@auth/guards/jwt-auth.guard';
import { AuthenticationModule } from '@auth/authentication/authentication.module';

@Module({
imports: [ApiConfigModule],
imports: [ApiConfigModule, AuthenticationModule],
controllers: [AppController],
providers: [AppService],
providers: [AppService, { provide: APP_GUARD, useClass: JwtAuthGuard }],
})
export class AppModule {}
1 change: 1 addition & 0 deletions api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(4000);
}
void bootstrap();
30 changes: 29 additions & 1 deletion api/src/modules/auth/authentication/authentication.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
import { Module } from '@nestjs/common';
import { AuthenticationService } from './authentication.service';
import { AuthenticationController } from './authentication.controller';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ApiConfigModule } from '@api/modules/config/app-config.module';
import { ApiConfigService } from '@api/modules/config/app-config.service';
import { JwtStrategy } from '@auth/strategies/jwt.strategy';
import { UsersService } from '@api/modules/users/users.service';
import { UsersModule } from '@api/modules/users/users.module';

@Module({
providers: [AuthenticationService],
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ApiConfigModule],
inject: [ApiConfigService],
useFactory: (config: ApiConfigService) => ({
secret: config.getJWTConfig().secret,
signOptions: { expiresIn: config.getJWTConfig().expiresIn },
}),
}),
UsersModule,
],
providers: [
AuthenticationService,
{
provide: JwtStrategy,
useFactory: (users: UsersService, config: ApiConfigService) => {
return new JwtStrategy(users, config);
},
inject: [UsersService, ApiConfigService],
},
],
controllers: [AuthenticationController],
})
export class AuthenticationModule {}
9 changes: 9 additions & 0 deletions api/src/modules/auth/decorators/get-user.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '@shared/entities/users/user.entity';

export const GetUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext): User => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
8 changes: 8 additions & 0 deletions api/src/modules/auth/decorators/is-public.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SetMetadata } from '@nestjs/common';

/**
* @description Decorator to inject a IS_PUBLIC_KEY metadata to the handler, which will be read by the JwtAuthGuard to allow public access to the handler.
*/

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
27 changes: 27 additions & 0 deletions api/src/modules/auth/guards/jwt-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { IS_PUBLIC_KEY } from '@auth/decorators/is-public.decorator';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private readonly reflector: Reflector) {
super();
}

canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const isPublic: boolean = this.reflector.get<boolean>(
IS_PUBLIC_KEY,
context.getHandler(),
);

if (isPublic) {
return true;
}

return super.canActivate(context);
}
}
30 changes: 30 additions & 0 deletions api/src/modules/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UsersService } from '@api/modules/users/users.service';
import { ApiConfigService } from '@api/modules/config/app-config.service';

export type JwtPayload = { id: string };

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly userService: UsersService,
private readonly config: ApiConfigService,
) {
const { secret } = config.getJWTConfig();
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: secret,
});
}

async validate(payload: JwtPayload) {
const { id } = payload;
const user = await this.userService.findOneBy(id);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
29 changes: 29 additions & 0 deletions api/src/modules/auth/strategies/local.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// import { Injectable, UnauthorizedException } from '@nestjs/common';
// import { PassportStrategy } from '@nestjs/passport';
//
// import { Strategy } from 'passport-local';
// import { User } from '@shared/dto/users/user.entity';
// import { AuthService } from '@api/modules/auth/auth.service';
//
// /**
// * @description: LocalStrategy is used by passport to authenticate by email and password rather than a token.
// */
//
// @Injectable()
// export class LocalStrategy extends PassportStrategy(Strategy) {
// constructor(private readonly authService: AuthService) {
// super({ usernameField: 'email' });
// }
//
// async validate(email: string, password: string): Promise<User> {
// const user: User | null = await this.authService.validateUser(
// email,
// password,
// );
//
// if (!user) {
// throw new UnauthorizedException();
// }
// return user;
// }
// }
12 changes: 12 additions & 0 deletions api/src/modules/config/app-config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DATABASE_ENTITIES } from '@shared/entities/database.entities';

export type JWTConfig = {
secret: string;
expiresIn: string;
};

@Injectable()
export class ApiConfigService {
constructor(private configService: ConfigService) {}
Expand Down Expand Up @@ -30,4 +35,11 @@ export class ApiConfigService {
private isProduction(): boolean {
return this.configService.get('NODE_ENV') === 'production';
}

getJWTConfig(): JWTConfig {
return {
secret: this.configService.get('JWT_SECRET'),
expiresIn: this.configService.get('JWT_EXPIRES_IN'),
};
}
}
1 change: 1 addition & 0 deletions api/src/modules/users/users.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ import { User } from '@shared/entities/users/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
13 changes: 12 additions & 1 deletion api/src/modules/users/users.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '@shared/entities/users/user.entity';
import { Repository } from 'typeorm';

@Injectable()
export class UsersService {}
export class UsersService {
constructor(@InjectRepository(User) private repo: Repository<User>) {}

async findOneBy(id: string) {
return this.repo.findOne({
where: { id },
});
}
}
2 changes: 1 addition & 1 deletion api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"resolveJsonModule": true
"resolveJsonModule": true,
}
}
Loading

0 comments on commit 9bb7196

Please sign in to comment.