Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ APP_URL=localhost
APP_PORT=3000

# redis
REDIS_HOST="redis"
REDIS_PORT="6379"
REDIS_HOST=redis
REDIS_PORT=6379

# postgres
POSTGRES_HOST="localhost"
POSTGRES_PORT="5432"

POSTGRES_DB="docker-nest-postgres"
POSTGRES_USER="username"
POSTGRES_PASSWORD="password"
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres

# jwt
JWT_SECRET=
JWT_ACCESS_TOKEN_SECRET=
JWT_ACCESS_TOKEN_EXPIRATION_TIME=
JWT_REFRESH_TOKEN_SECRET=
JWT_REFRESH_EXPIRATION_TIME=
42 changes: 29 additions & 13 deletions src/app/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { AuthService } from './auth.service';
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiCreatedResponse,
ApiInternalServerErrorResponse,
ApiNotFoundResponse,
ApiOkResponse,
Expand All @@ -28,31 +29,31 @@ import { JwtResponse } from '../../libs/response/auth/jwt.response';
import { UnauthorizedError } from '../../libs/response/status-code/unauthorized.error';
import { AccessTokenResponse } from '../../libs/response/auth/access-token.response';
import { CheckUsernameSuccessResponse } from '../../libs/response/auth/check-username.success.response';
import { JwtRefreshStrategy } from './jwt/jwt-refresh.strategy';
import { UserIdRequest } from '../../libs/request/users/user-id.request';
import { OkSuccess } from '../../libs/response/status-code/ok.success';
import { JwtStrategy } from './jwt/jwt.strategy';

@Controller('auth')
@ApiTags('Auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Post('/login')
@ApiOkResponse({
status: 201,
@ApiCreatedResponse({
description:
'계정 정보가 일치하는 경우 access_token과 user_id를 반환합니다.',
type: AccessTokenResponse,
})
@ApiBadRequestResponse({
status: 400,
description: 'password가 일치하지 않는 경우',
type: BadRequestError,
})
@ApiNotFoundResponse({
status: 404,
description: 'username이 존재하지 않는 경우',
type: NotFoundError,
})
@ApiInternalServerErrorResponse({
status: 500,
description: '서버에 에러가 발생한 경우',
type: InternalServerErrorError,
})
Expand All @@ -61,21 +62,39 @@ export class AuthController {
return this.authService.login(dto);
}

@UseGuards(JwtAuthGuard)
@ApiBearerAuth('access_token')
@UseGuards(JwtRefreshStrategy)
@ApiOkResponse({
description: 'refresh 토큰의 정보가 null로 성공적으로 변경된 경우',
type: OkSuccess,
})
@ApiUnauthorizedResponse({
description: '인증이 되어있지 않은 경우',
type: UnauthorizedError,
})
@ApiInternalServerErrorResponse({
description: '서버에 에러가 발생한 경우',
type: InternalServerErrorError,
})
@ApiOperation({ summary: 'DB에 저장된 refresh token의 정보를 초기화합니다.' })
@Post('logout/:id')
async logOut(@Param() dto: UserIdRequest) {
return this.authService.removeRefreshToken(dto);
}

@UseGuards(JwtStrategy)
// @UseGuards(JwtRefreshStrategy)
@ApiBearerAuth('accessToken')
// @ApiBearerAuth('refreshToken')
@Get('profile')
@ApiOkResponse({
status: 200,
description: 'jwt 토큰의 인증이 성공한 경우 username을 반환합니다.',
type: JwtResponse,
})
@ApiUnauthorizedResponse({
status: 401,
description: '인증이 되어있지 않은 경우',
type: UnauthorizedError,
})
@ApiInternalServerErrorResponse({
status: 500,
description: '서버에 에러가 발생한 경우',
type: InternalServerErrorError,
})
Expand All @@ -86,18 +105,15 @@ export class AuthController {

@Get(':username')
@ApiOkResponse({
status: 200,
description:
'존재하는 username을 입력한 경우 user의 id와 name을 반환합니다.',
type: CheckUsernameSuccessResponse,
})
@ApiNotFoundResponse({
status: 404,
description: '존재하지 않는 username을 입력한 경우',
type: NotFoundError,
})
@ApiInternalServerErrorResponse({
status: 500,
description: '서버 에러가 발생했을 경우',
type: InternalServerErrorError,
})
Expand Down
8 changes: 6 additions & 2 deletions src/app/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@ import { JwtStrategy } from './jwt/jwt.strategy';
import { AuthController } from './auth.controller';
import { jwtConstants } from './jwt/constants';
import { AuthRepository } from './auth.repository';
import * as process from 'process';
import { JwtRefreshStrategy } from './jwt/jwt-refresh.strategy';

@Module({
imports: [
UserModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '3600s' },
signOptions: {
expiresIn: `${process.env.JWT_ACCESS_TOKEN_EXPIRATION_TIME}s`,
},
}),
],
controllers: [AuthController],
providers: [AuthService, AuthRepository, JwtStrategy],
providers: [AuthService, AuthRepository, JwtStrategy, JwtRefreshStrategy],
exports: [AuthService],
})
export class AuthModule {}
40 changes: 39 additions & 1 deletion src/app/auth/auth.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,39 @@ import { UserEntity } from '../../libs/entity/user.entity';
@Injectable()
export class AuthRepository {
constructor(private readonly dataSource: DataSource) {}
async findOne(username: string): Promise<UserEntity> {
async getUserByUsername(username: string): Promise<UserEntity> {
try {
return await this.findOneByUsername(username);
} catch (error) {
throw error;
}
}

async getUserById(userId: number) {
try {
return await this.findOneById(userId);
} catch (error) {
throw error;
}
}

async updateRefreshToken(userId: number, refreshToken: object) {
try {
await this.updateRefreshTokenByUserId(userId, refreshToken);
return refreshToken;
} catch (error) {
throw error;
}
}

private async findOneById(userId: number) {
return await this.dataSource
.createQueryBuilder()
.select()
.from(UserEntity, 'User')
.where(`User.id =: userId`, { userId })
.getRawOne();
}
private async findOneByUsername(username: string): Promise<UserEntity> {
return await this.dataSource
.createQueryBuilder()
Expand All @@ -20,4 +46,16 @@ export class AuthRepository {
.where(`User.username =:username`, { username })
.getRawOne();
}

private async updateRefreshTokenByUserId(
userId: number,
refreshToken: object,
) {
await this.dataSource
.createQueryBuilder()
.update(UserEntity)
.set(refreshToken)
.where(`id =:userId`, { userId })
.execute();
}
}
95 changes: 91 additions & 4 deletions src/app/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { UsernameRequest } from '../../libs/request/users/username.request';
import { UserEntity } from '../../libs/entity/user.entity';
import { LoginRequest } from '../../libs/request/auth/login.request';
import * as argon2 from 'argon2';
import { jwtConstants } from './jwt/constants';
import { UserIdRequest } from '../../libs/request/users/user-id.request';

@Injectable()
export class AuthService {
Expand All @@ -23,10 +25,17 @@ export class AuthService {
async login(dto: LoginRequest): Promise<object> {
try {
const user = await this.validateUser(dto.username, dto.password);
const payload = { username: user.username, password: user.password };
const { accessToken, ...accessOption } = this.getCookieWithJwtAccessToken(
user.id,
);
const { refreshToken, ...refreshOption } =
this.getCookieWithJwtRefreshToken(user.id);

await this.setCurrentRefreshToken(refreshToken, user.id);
return {
user_id: user.id,
access_token: this.jwtService.sign(payload),
accessToken,
refreshToken,
};
} catch (error) {
this.logger.error(error);
Expand All @@ -40,7 +49,7 @@ export class AuthService {

async checkUsername(dto: UsernameRequest): Promise<object> {
try {
const user = await this.authRepository.findOne(dto.username);
const user = await this.authRepository.getUserByUsername(dto.username);
if (!user) throw new NotFoundException('존재하지 않는 username입니다.');
return {
id: user.id,
Expand All @@ -54,13 +63,91 @@ export class AuthService {
}
}

async removeRefreshToken(dto: UserIdRequest): Promise<object> {
try {
await this.authRepository.updateRefreshToken(dto.id, {
currentHashedRefreshToken: null,
});
return {};
} catch (error) {
this.logger.error(error);
throw new BadRequestException(error.getResponse());
}
}

private async validateUser(
username: string,
pass: string,
): Promise<UserEntity> {
const user = await this.authRepository.findOne(username);
const user = await this.authRepository.getUserByUsername(username);
if (!user) throw new NotFoundException('존재하지 않는 username입니다.');
if (await argon2.verify(user.password, pass)) return user;
else throw new BadRequestException('password가 일치하지 않습니다.');
}

getCookieWithJwtAccessToken(id: number) {
const payload = { id };
const token = this.jwtService.sign(payload, {
secret: jwtConstants.access_token_secret,
expiresIn: `${jwtConstants.access_token_expiration}s`,
});

return {
accessToken: token,
domain: 'localhost',
path: '/',
httpOnly: true,
maxAge: Number(jwtConstants.access_token_expiration) * 1000,
};
}

getCookieWithJwtRefreshToken(id: number) {
const payload = { id };
const token = this.jwtService.sign(payload, {
secret: jwtConstants.refresh_token_secret,
expiresIn: `${jwtConstants.refresh_token_expiration}s`,
});

return {
refreshToken: token,
domain: 'localhost',
path: '/',
httpOnly: true,
maxAge: Number(jwtConstants.refresh_token_expiration) * 1000,
};
}

getCookiesForLogOut() {
return {
accessOption: {
domain: 'localhost',
path: '/',
httpOnly: true,
maxAge: 0,
},
refreshOption: {
domain: 'localhost',
path: '/',
httpOnly: true,
maxAge: 0,
},
};
}

async setCurrentRefreshToken(refreshToken: string, id: number) {
const currentHashedRefreshToken = await argon2.hash(refreshToken);
await this.authRepository.updateRefreshToken(id, {
currentHashedRefreshToken,
});
}

async getUserIfRefreshTokenMatches(refreshToken: string, id: number) {
const user = await this.authRepository.getUserById(id);
const isRefreshTokenMatching = await argon2.verify(
refreshToken,
user.currentHashedRefreshToken,
);

if (isRefreshTokenMatching) return user;
}
}
6 changes: 5 additions & 1 deletion src/app/auth/jwt/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ import * as dotenv from 'dotenv';

dotenv.config();
export const jwtConstants = {
secret: process.env.JWT_SECRET,
secret: process.env.JWT_ACCESS_TOKEN_SECRET,
access_token_secret: process.env.JWT_ACCESS_TOKEN_SECRET,
access_token_expiration: process.env.JWT_ACCESS_TOKEN_EXPIRATION_TIME,
refresh_token_secret: process.env.JWT_REFRESH_TOKEN_SECRET,
refresh_token_expiration: process.env.JWT_REFRESH_EXPIRATION_TIME,
};
5 changes: 5 additions & 0 deletions src/app/auth/jwt/jwt-auth-refresh.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 JwtRefreshGuard extends AuthGuard('jwt-refresh-token') {}
Loading