Skip to content

Commit

Permalink
feat(auth): 인증 기능 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
WhiteKiwi committed Mar 27, 2022
1 parent 82eada7 commit e3b74c0
Show file tree
Hide file tree
Showing 28 changed files with 500 additions and 10 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

## 사용방법
1. /configs/{config-group}.yml 파일 생성
2. cloud-config.whitekiwi.link/{config-group} 에서 컨피그 조회
- dev: cloud-config.dev.whitekiwi.link/{config-group}
- sample: https://cloud-config.dev.whitekiwi.link/sample
2. cloud-config.whitekiwi.link/api/v1/{config-group} 에서 컨피그 조회
- dev: cloud-config.dev.whitekiwi.link/api/v1/{config-group}
- sample: https://cloud-config.dev.whitekiwi.link/api/v1/sample

### 암복호화
1. POST /encrypt { value: "..." }
1. POST /decrypt { value: "..." }
1. POST /api/v1/encrypt { value: "..." }
1. POST /api/v1/decrypt { value: "..." }
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^1.2.0",
"@nestjs/core": "^8.0.0",
"@nestjs/jwt": "^8.0.0",
"@nestjs/passport": "^8.2.1",
"@nestjs/platform-express": "^8.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"crypto-js": "^4.1.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"passport": "^0.5.2",
"passport-custom": "^1.1.1",
"passport-jwt": "^4.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"
Expand All @@ -43,7 +49,9 @@
"@types/express": "^4.17.13",
"@types/jest": "27.4.1",
"@types/js-yaml": "^4.0.5",
"@types/lodash": "^4.14.180",
"@types/node": "^16.0.0",
"@types/passport-jwt": "^3.0.6",
"@types/supertest": "^2.0.11",
"jest": "^27.2.5",
"source-map-support": "^0.5.20",
Expand Down
3 changes: 2 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { HealthCheckModule } from '@kiwi-lib/nestjs';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

import { CloudConfigModule } from './modules';
import { AuthModule, CloudConfigModule } from './modules';

@Module({
imports: [
Expand All @@ -13,6 +13,7 @@ import { CloudConfigModule } from './modules';
}),
HealthCheckModule,
CloudConfigModule,
AuthModule,
],
})
export class AppModule {}
1 change: 1 addition & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface Env {
PORT: string;
ENCRYPT_KEY: string;
JWT_SECRET: string;
}
16 changes: 16 additions & 0 deletions src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Body, Controller, Post } from '@nestjs/common';

import { AuthService } from './auth.service';

@Controller({
version: '1',
path: 'auth',
})
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Post('login')
async login(@Body() dto: any): Promise<any> {
return await this.authService.login(dto);
}
}
14 changes: 14 additions & 0 deletions src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';

import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { AuthStrategy } from './auth.strategy';
import { JwtAuthenticatorModule } from './authenticators';

@Module({
imports: [PassportModule, JwtAuthenticatorModule],
controllers: [AuthController],
providers: [AuthService, AuthStrategy],
})
export class AuthModule {}
15 changes: 15 additions & 0 deletions src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Inject, Injectable } from '@nestjs/common';

import { Authenticator, AUTHENTICATOR_KEY } from './authenticators';

@Injectable()
export class AuthService {
constructor(
@Inject(AUTHENTICATOR_KEY)
private readonly authenticator: Authenticator,
) {}

async login(dto: any) {
return await this.authenticator.auth(dto);
}
}
20 changes: 20 additions & 0 deletions src/modules/auth/auth.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Inject, Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { Strategy } from 'passport-custom';

import { Authenticator, AUTHENTICATOR_KEY } from './authenticators';

@Injectable()
export class AuthStrategy extends PassportStrategy(Strategy) {
constructor(
@Inject(AUTHENTICATOR_KEY)
private readonly authenticator: Authenticator,
) {
super();
}

async validate(req: Request) {
return await this.authenticator.validate(req);
}
}
21 changes: 21 additions & 0 deletions src/modules/auth/authenticators/authenticator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Request } from 'express';

/**
* AuthenticatorModule은 AUTHENTICATOR_KEY exports 해야함
*/
export const AUTHENTICATOR_KEY = Symbol('AUTHENTICATOR_KEY');

export interface Authenticator {
/**
* dto 인증 후 response data를 반환합니다
* @throws UnauthorizedException 인증 실패
* @returns response data
*/
auth(dto: any): Promise<any>;

/**
* 요청을 validate 한 후 payload를 반환합니다
* @throws UnauthorizedException 인증 실패
*/
validate(req: Request): any;
}
2 changes: 2 additions & 0 deletions src/modules/auth/authenticators/impl/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './jwt';
export * from './no';
4 changes: 4 additions & 0 deletions src/modules/auth/authenticators/impl/jwt/auth.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class AuthDto {
id!: string;
password!: string;
}
1 change: 1 addition & 0 deletions src/modules/auth/authenticators/impl/jwt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './jwt.authenticator.module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Env } from '@config/env';
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';

import { AuthDataStorageModule } from '../../../data-storage';
import { AUTHENTICATOR_KEY } from '../../authenticator';
import { JwtAuthenticator } from './jwt.authenticator';

@Module({
imports: [
AuthDataStorageModule,
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService<Env>) => {
return {
secret: configService.get('JWT_SECRET'),
signOptions: { expiresIn: '90d' },
};
},
}),
],
providers: [
{
provide: AUTHENTICATOR_KEY,
useClass: JwtAuthenticator,
},
],
exports: [AUTHENTICATOR_KEY],
})
export class JwtAuthenticatorModule {}
63 changes: 63 additions & 0 deletions src/modules/auth/authenticators/impl/jwt/jwt.authenticator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { omit } from 'lodash';
import { ExtractJwt, JwtFromRequestFunction } from 'passport-jwt';

import { AUTH_DATA_STORAGE_KEY, AuthDataStorage } from '../../../data-storage';
import { Authenticator } from '../../authenticator';
import { AuthDto } from './auth.dto';

type User = {
id: string;
password: string;
};

@Injectable()
export class JwtAuthenticator implements Authenticator {
private readonly jwtFromRequestFunction: JwtFromRequestFunction =
ExtractJwt.fromAuthHeaderAsBearerToken();

constructor(
@Inject(AUTH_DATA_STORAGE_KEY)
private readonly dataStorage: AuthDataStorage<User>,
private readonly jwtService: JwtService,
) {}

async auth(dto: any): Promise<any> {
this.validateDto(dto);

const user = await this.dataStorage.load(dto.id);

this.validateUser(dto, user);

const accessToken = await this.jwtService.signAsync(omit(user, 'password'));
return { accessToken };
}

private validateDto(dto: any): asserts dto is AuthDto {
if (typeof dto.id !== 'string') throw new UnauthorizedException();
if (typeof dto.password !== 'string') throw new UnauthorizedException();
}

private validateUser(dto: AuthDto, user?: User): asserts user is User {
if (!user) {
throw new UnauthorizedException();
}

// TODO: dto.password + salt의 해시랑 user.password 비교
if (dto.password !== user.password) {
throw new UnauthorizedException();
}
}

async validate(req: Request) {
const token = this.jwtFromRequestFunction(req);

if (!token) {
throw new UnauthorizedException();
}

return await this.jwtService.verifyAsync(token);
}
}
1 change: 1 addition & 0 deletions src/modules/auth/authenticators/impl/no/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './no.authenticator.module';
15 changes: 15 additions & 0 deletions src/modules/auth/authenticators/impl/no/no.authenticator.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';

import { AUTHENTICATOR_KEY } from '../../authenticator';
import { NoAuthenticator } from './no.authenticator';

@Module({
providers: [
{
provide: AUTHENTICATOR_KEY,
useClass: NoAuthenticator,
},
],
exports: [AUTHENTICATOR_KEY],
})
export class NoAuthenticatorModule {}
14 changes: 14 additions & 0 deletions src/modules/auth/authenticators/impl/no/no.authenticator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Injectable, NotFoundException } from '@nestjs/common';

import { Authenticator } from '../../authenticator';

@Injectable()
export class NoAuthenticator implements Authenticator {
async auth(): Promise<never> {
throw new NotFoundException();
}

async validate() {
return {};
}
}
2 changes: 2 additions & 0 deletions src/modules/auth/authenticators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './authenticator';
export * from './impl';
5 changes: 5 additions & 0 deletions src/modules/auth/config.auth-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class ConfigAuthGuard extends AuthGuard('custom') {}
15 changes: 15 additions & 0 deletions src/modules/auth/data-storage/auth.data-storage.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';

import { AUTH_DATA_STORAGE_KEY } from './auth.data-storage';
import { SampleDataStorage } from './impl';

@Module({
providers: [
{
provide: AUTH_DATA_STORAGE_KEY,
useClass: SampleDataStorage,
},
],
exports: [AUTH_DATA_STORAGE_KEY],
})
export class AuthDataStorageModule {}
10 changes: 10 additions & 0 deletions src/modules/auth/data-storage/auth.data-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DataStorage } from '@modules/data-storage';

export const AUTH_DATA_STORAGE_KEY = Symbol('AUTH_DATA_STORAGE_KEY');

export type AuthDataStorage<User = unknown> = DataStorage<{
[userId: UserId]: User;
}>;

// `user_id:${userId}`
export type UserId = string;
1 change: 1 addition & 0 deletions src/modules/auth/data-storage/impl/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './sample.data-storage';
22 changes: 22 additions & 0 deletions src/modules/auth/data-storage/impl/sample.data-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { AuthDataStorage } from '../auth.data-storage';

export class SampleDataStorage
implements AuthDataStorage<{ id: string; password: string }>
{
/**
*
* @param key user id
*/
async load(key: string) {
void key;

return {
id: 'sample',
password: 'sample',
};
}

async save(): Promise<never> {
throw new Error('Method not implemented.');
}
}
3 changes: 3 additions & 0 deletions src/modules/auth/data-storage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './auth.data-storage';
export * from './auth.data-storage.module';
export * from './impl';
2 changes: 2 additions & 0 deletions src/modules/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './auth.module';
export * from './config.auth-guard';
7 changes: 6 additions & 1 deletion src/modules/cloud-config/cloud-config.controller.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ConfigAuthGuard } from '@modules/auth';
import {
Body,
Controller,
Expand All @@ -6,11 +7,15 @@ import {
HttpStatus,
Param,
Post,
UseGuards,
} from '@nestjs/common';

import { CloudConfigService } from './cloud-config.service';

@Controller()
@UseGuards(ConfigAuthGuard)
@Controller({
version: '1',
})
export class CloudConfigController {
constructor(private readonly cloudConfigService: CloudConfigService) {}

Expand Down
1 change: 1 addition & 0 deletions src/modules/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './auth';
export * from './cloud-config';
Loading

0 comments on commit e3b74c0

Please sign in to comment.