본문 바로가기
NestJS

Authentication - 로직 구현

by Programmer.Junny 2025. 2. 26.

1. Auth Resource 생성

nest g resource
// 다음 'Auth' 로 생성

2. JWT 패키지 설치

yarn add @nestjs/jwt bcrypt
// 혹은
npm i @nestjs/jwt bcrypt

jwt와 bcrypt 패키지를 설치한다.

3. Token 발급 메서드

signToken(user: Pick<UsersModel, 'email' | 'id'>, isRefreshToken: boolean) {
const payload = {
email: user.email,
sub: user.id,
type: isRefreshToken ? 'refresh' : 'access',
};
return this.jwtService.sign(payload, {
secret: JWT_SECRET,
//seconds
expiresIn: isRefreshToken ? 3600 : 300,
})
}

참고로 auth.service.ts 에 작성해야하며, 해당 로직에는 비밀번호가 포함되지 않아야 한다.

4. loginUser 메서드

loginUser(user: Pick<UsersModel, 'email' | 'id'>) {
return {
accessToken: this.signToken(user, false),
refreshToken: this.signToken(user, true),
}
}

loginUser 메서드는 사용자가 로그인을 하거나 회원가입을 할 때, AccessToken과 RefreshToken을 발급받는 공통적인 과정을 하나의 메서드로 작성한 것이다.

5. 회원가입

@Post('register/email')
postRegisterEmail(
@Body('nickname') nickname: string,
@Body('email') email: string,
@Body('password') password: string,
) {
return this.authService.registerWithEmail({
nickname,
email,
password,
});
}

auth.controller.ts 에 위와 같이 라우터를 구현한다.

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModel } from './entities/users.entity';
@Module({
imports: [TypeOrmModule.forFeature([UsersModel])],
exports: [UsersService],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
exports: [UsersService],

우선 로직을 작성하기에 앞서 UsersModel에 접근해야 한다.

그러나 Auth에서 UsersModel에 직접 접근하기보다는 이미 만들어둔 메서드를 사용하기 위해 users.service.ts 에 있는 메서드를 가져오도록 한다.

그러기 위해선 users.module.ts 내에 exports로 UsersService를 등록한다.

@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly usersService: UsersService,
) {}

 

이후 auth.service.ts 내의 생성자에 UsersService를 등록하여 사용할 수 있다.

async registerWithEmail(user: Pick<UsersModel, 'nickname' | 'email' | 'password'>) {
const hash = await bcrypt.hash(
user.password,
HASH_ROUNDS,
);
const newUser = await this.usersService.createUser({
...user,
password: hash,
});
return this.loginUser(newUser);
}

registerWithEmail 메서드는 클라이언트로부터 받은 nickname, email, password를 받아온 뒤 password와 HASH_ROUNTDS(salt)를 이용해 bcrypt 해쉬를 만들고, 이것을 usersService 내에 만들어져있는 createUser 메서드를 호출하여 유저를 DB에 등록한다.

그리고 loginUser 메서드를 호출하여 AccessToken과 RefreshToken을 생성하여 클라이언트에 반환한다.

6. 이메일 로그인

@Post('login/email')
postLoginEmail(
@Headers('authorization') rawToken: string,
) {
// email:password -> base64
// 디코딩 후 email과 password를 나눠야함
const token = this.authService.extractTokenFromHeader(rawToken, false);
const credentials = this.authService.decodeBasicToken(token);
return this.authService.loginWithEmail(credentials);
}

auth.controller.ts 에 위와 같이 라우터를 구현한다. rawToken은 클라이언트에서 'email:password' 로 만든 후 base64 인코딩한 문자열이다.

/**
* Header로 부터 토큰을 받을 때
*
* {authorization: 'Basic {token}'}
* {authorization: 'Bearer {token}'}
*/
extractTokenFromHeader(header: string, isBearer: boolean) {
const splitToken = header.split(' ');
const prefix = isBearer ? 'Bearer' : 'Basic';
if(splitToken.length !== 2 || splitToken[0] !== prefix) {
throw new UnauthorizedException('잘못된 토큰입니다.');
}
const token = splitToken[1];
return token;
}

extractTokenFromHeader는 클라이언트에서 보낸 헤더를 기반으로 Basic (토큰생성), Bearer (토큰사용)을 구분하여 토큰을 반환한다.

/**
* Basic asdlkfjadlfkjadflkjasdf
*
* 1) asdlkfjadlfkjadflkjasdf -> email:password (디코딩)
* 2) email:password -> [email, password]
* 3) {email: email, password: password}
*/
decodeBasicToken(base64String: string) {
const decoded = Buffer.from(base64String, 'base64').toString('utf8');
const split = decoded.split(':');
if(split.length !== 2) {
throw new UnauthorizedException('잘못된 유형의 토큰입니다.');
}
const email = split[0];
const password = split[1];
return {
email,
password,
}
}

decodedBasicToken은 받아온 token(email:password)을 디코딩하는 과정이다. 객체로 만들어 email, password을 반환한다.

async loginWithEmail(user: Pick<UsersModel, 'email' | 'password'>) {
const existingUser = await this.authenticateWithEmailAndPassword(user);
return this.loginUser(existingUser);
}
async authenticateWithEmailAndPassword(user: Pick<UsersModel, 'email' | 'password'>) {
const existingUser = await this.usersService.getUserByEmail(user.email);
if (!existingUser) {
throw new UnauthorizedException('존재하지 않는 사용자입니다.');
}
/**
* 파라미터
*
* 1) 입력된 비밀번호
* 2) 기존 해시(hash) -> 사용자 정보에 저장되어있는 hash
*/
const passOk = await bcrypt.compare(user.password, existingUser.password);
if (!passOk) {
throw new UnauthorizedException('비밀번호가 틀렸습니다.');
}
return existingUser;
}

사용자가 이메일과 비밀번호로 로그인을 진행하면 입력된 이메일로 유저를 DB에서 가져온 뒤, DB에 저장된 패스워드(base64 암호화)입력된 비밀번호를 비교하여 일치하는 경우 유저를 반환하고, 이 유저를 가지고 loginUser 메서드를 실행하여 AccessToken과 RefreshToken을 클라이언트에 반환한다.

7. AccessToken 갱신

@Post('token/access')
postTokenAccess(
@Headers('authorization') rawToken: string,
) {
const token = this.authService.extractTokenFromHeader(rawToken, true);
const newToken = this.authService.rotateToken(token, false);
/**
* {accessToken: {token}}
*/
return {
accessToken: newToken,
}
}

auth.controller.ts 에 postTokenAccess 메서드를 라우팅한다.

/**
* Header로 부터 토큰을 받을 때
*
* {authorization: 'Basic {token}'}
* {authorization: 'Bearer {token}'}
*/
extractTokenFromHeader(header: string, isBearer: boolean) {
const splitToken = header.split(' ');
const prefix = isBearer ? 'Bearer' : 'Basic';
if(splitToken.length !== 2 || splitToken[0] !== prefix) {
throw new UnauthorizedException('잘못된 토큰입니다.');
}
const token = splitToken[1];
return token;
}

로그인때와 마찬가지로 클라이언트에서 보낸 헤더를 분해하여 토큰을 반환한다.

rotateToken(token: string, isRefreshToken: boolean) {
const decoded = this.verifyToken(token);
/**
* sub: id
* email: email,
* type: 'access' | 'refresh'
*/
if(decoded.type !== 'refresh') {
throw new UnauthorizedException('토큰 재발급은 Refresh 토큰으로만 가능합니다!');
}
return this.signToken({
...decoded
}, isRefreshToken);
}

이후 토큰을 검증 후 signToken을 통해 토큰을 재발급한다.

/**
* 토큰 검증
*/
verifyToken(token: string) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return this.jwtService.verify(token, {
secret: JWT_SECRET,
});
}

verifyTokenjwtService.verify 를 통해 AccessTokenRefreshToken을 검증할 수 있다.

8. RefreshToken 갱신

@Post('token/refresh')
postTokenRefresh(
@Headers('authorization') rawToken: string,
) {
const token = this.authService.extractTokenFromHeader(rawToken, true);
const newToken = this.authService.rotateToken(token, true);
/**
* {refreshToken: {token}}
*/
return {
refreshToken: newToken,
}
}

AccessToken 갱신과 다른 부분은 rotateToken에 두 번째 매개변수 isRefreshToken: boolean차이 뿐이다.

'NestJS' 카테고리의 다른 글

NestJS - Pipe  (0) 2025.02.26
VSC 디버거 사용하기  (0) 2025.02.26
Authentication - Encryption (암호화)  (0) 2025.02.26
Authentication - JWT  (0) 2025.02.26
TypeORM - Repository 메서드 종류  (0) 2025.02.25

최근댓글

최근글

skin by © 2024 ttuttak