// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
@Module({
imports: [UsersModule, PassportModule],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
공식문서 상에서 AuthModule은 다음과 같은 형태로 구현된다. 보다시피, AuthModule에 UsersModule을 import한 형태로 작성된다. 하지만 개인적인 소감으로는, 이 방식보다는 AuthModule이 직접 User를 다루는 게 맞다고 봤다.
따라서 나라면, 코드를 아래처럼 고칠 것이다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../models/tables/user';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
@Module({
imports: [
PassportModule.register({ session: false }),
TypeOrmModule.forFeature([User]),
],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
즉, AuthModule에 User를 넣는 것이 아닌, 또는 다른 모듈들을 import 하는 것이 아닌, AuthModule 자체가 인증에 필요한 TypeORM Module을 상속받아 직접 다루게 하는 것이다. 논리적으로 볼 때 AuthModule이 AppModule과 UsersModule 사이에 있는 것은 어색하지 않다.
하지만 Express 때의 코드와 마찬가지로, passport.js를 사용하는 것은 우리가 예상하는 로직 흐름과 다른 경우가 조금 있다. 그래서 더 직관적으로 만들기 위해서, 나는 반대로 UserModule이 AuthModule을 import하게 한다.
나는 이게 더 이해하기 쉬운 방식이라고 생각한다.
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../models/tables/user';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { KakaoStrategy } from './strategies/kakao.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
@Module({
imports: [
PassportModule.register({ session: false }),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
return {
secret: configService.get('ACCESS_KEY'),
signOptions: { algorithm: 'HS256', expiresIn: '1y' },
};
},
}),
TypeOrmModule.forFeature([User]),
],
providers: [AuthService, KakaoStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
나는 공식문서에 있는 것과는 약간 다르게, AuthModule에서 직접 User Entity를 다루게 한다. 그리고 내용을 더 보충하여 JwtModule이나 KakaoStrategy JwtStrategy 등을 넣어보았다. 이러한 형태로 완성하게 된다면 이제 이 AuthModule을 UserModule에 import 해주면 된다.
UserModule 내부에서는 이제 export된 AuthService를 가져다 사용할 것이기 때문이다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../models/tables/user';
import { UsersController } from '../controllers/users.controller';
import { UsersService } from '../providers/users.service';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [TypeOrmModule.forFeature([User]), AuthModule],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
UserController는 UsersService를 사용한다.
그리고 마찬가지로 UserController는 AuthModule에서 export된 AuthService를 사용한다.
// user.controller.ts
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { User as UserEntity } from '../models/tables/user';
import { CreateUserDto } from '../dtos/create-user.dto';
import { UsersService } from '../providers/users.service';
import { KaKaoGuard } from '../auth/guards/kakao.guard';
import { User } from '../common/decorators/user.decorator';
import { Profile } from 'passport-kakao';
import { AuthService } from '../auth/auth.service';
import { JwtGuard } from '../auth/guards/jwt.guard';
import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
@ApiTags('유저 / User')
@Controller('api/users')
export class UsersController {
constructor(
private readonly authService: AuthService,
private readonly usersService: UsersService,
) {}
@ApiOperation({ summary: 'MVP : 카카오톡을 이용한 로그인' })
@UseGuards(KaKaoGuard)
@Get('kakao/sign-in')
async kakaoSignIn() {}
@ApiOperation({ summary: 'MVP : 카카오톡 로그인 후 Redirect 되는 경로' })
@UseGuards(KaKaoGuard)
@Get('kakao/callback')
async kakaoCallback(@User() profile: Profile): Promise<{ token: string }> {
const { id: oauthId, username: name } = profile;
let user = await this.usersService.findOne({ oauthId, name });
if (!user) {
user = await this.usersService.create({ oauthId, name, nickname: name });
}
return this.authService.userLogin(user);
}
@ApiOperation({ summary: 'MVP : 유저 프로필 조회 & 토큰에 담긴 값 Parsing.' })
@ApiHeader({ name: 'token' })
@UseGuards(JwtGuard)
@Get('profile')
async getProfile(@User() user: UserEntity) {
return user;
}
}
급조된 코드들이지만, authModule이 어느 시점에 사용된 것인지 확인할 수 있다. 유저가 UseGuard를 통해 guard를 접하게 되고, 인증이 완료되면 authService로 가게 된다. 이 때 authService는 토큰을 발급한다.
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User } from '../models/tables/user';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
userLogin(user: User) {
const token = this.jwtService.sign({ ...user });
return { token };
}
}
인증을 더욱 편리하게 하기 위해 나는 JwtModule을 이용해 jwtService를 사용한다. 토큰을 만들어서 클라이언트에게 돌려주자. 지금까지의 경로를 한 번 다시 요약해보자.
이렇게 5단계가 LocalStrategy 혹은 Kakao와 같은 OAuth의 로직이다. 이렇게만 가지고도 간단하게 코드를 구현해볼 수 있다. 로컬을 이용해 문서를 작성하는 게 더 좋았겠지만, 이해하는 데에는 무리가 없을 것이다.