Skip to content

Commit

Permalink
Merge pull request #211 from innovationacademy-kr/feat/refreshToken
Browse files Browse the repository at this point in the history
✨ feat: refresh token 추가
  • Loading branch information
niamu01 authored Jul 26, 2024
2 parents 67cda1a + c747d1a commit 550c347
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 44 deletions.
14 changes: 12 additions & 2 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ CLIENT_SECRET = *42인트라넷에서 발급받은 API SECRET KEY
CLIENT_CALLBACK = *callback url, 보통 /user/login/callback/42 로 고정

JWT_OR_SESSION_SECRET = *JWT 혹은 세션에 사용할 private key
JWT_EXPIREIN = *JWT 토큰의 만료 기간 (예 : 28일 -> 28d)
JWT_ACCESS_EXPIREIN = *JWT 엑세스 토큰의 만료 기간 (예 : 28일 -> 28d)
JWT_REFRESH_EXPIREIN = *JWT 리프레시 토큰의 만료 기간 (예 : 28일 -> 28d)

LOG_DEBUG = true일 경우 debug로그의 레벨을 debug로 false일 경우, info

Expand All @@ -24,9 +25,18 @@ GOOGLEAPI_SERVICE_ACCOUNT_PRIVATE_KEY = google API를 이용하기 위한 PEM
GOOGLEAPI_SHEET_ID = 변경하고자 하는 google 스프레드시트 ID
GOOGLEAPI_SHEET_RANGE = 변경하고자 하는 google 스프레드시트의 범위

GOOGLEAPI_CARD_ACCOUNT_EMAIL = google API를 이용하기 위한 이메일 (credential)
GOOGLEAPI_CARD_ACCOUNT_PRIVATE_KEY = google API를 이용하기 위한 PEM 개인키 (credential)
GOOGLEAPI_CARD_SHEET_ID = 변경하고자 하는 google 스프레드시트 ID
GOOGLEAPI_CARD_SHEET_RANGE = 변경하고자 하는 google 스프레드시트의 범위

JANDI_WEBHOOK_URL = 잔디 URL

URI_MONEY_GUIDELINE = `/redirect/money_guidelines` 으로 리다이렉트 시킬 URI
URI_QUESTION = `/redirect/question` 으로 리다이렉트 시킬 URI
URI_USAGE = `/redirect/usage` 으로 리다이렉트 시킬 URI
URI_FEEDBACK = `/redirect/feedback` 으로 리다이렉트 시킬 URI
URI_TERMS = `/redirect/terms` 으로 리다이렉트 시킬 URI
URI_REISSUANCE_GUIDELINE = `/redirect/reissuance_guidelines` 으로 리다이렉트 시킬 URI
URI_REISSUANCE_GUIDELINE = `/redirect/reissuance_guidelines` 으로 리다이렉트 시킬 URI

package_version = 패키지 버전
82 changes: 79 additions & 3 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import {
Controller,
Get,
HttpCode,
Inject,
Logger,
Post,
Req,
Res,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import {
ApiBearerAuth,
ApiOperation,
Expand All @@ -25,7 +30,12 @@ import { User } from './user.decorator';
export class Auth42Controller {
private logger = new Logger(Auth42Controller.name);

constructor(private googleApi: GoogleApi) {}
constructor(
private googleApi: GoogleApi,
private jwtService: JwtService,
@Inject(ConfigService)
private configService: ConfigService,
) {}

@ApiOperation({
summary: '42 계정 로그인 링크',
Expand Down Expand Up @@ -64,8 +74,13 @@ export class Auth42Controller {
@UseGuards(FtOAuthGuard, JWTSignGuard)
async ftcallback(@Req() req, @Res() res, @User() user) {
this.logger.verbose(`@ftcallback) login callback : ${user.login}`);
if (req.cookies['redirect']) {
res.status(302).redirect(req.cookies['redirect']);

const redirectUrl = req.cookies['redirect'];

res.clearCookie('redirect');

if (redirectUrl) {
res.status(302).redirect(redirectUrl);
} else {
if (user && user.is_staff) {
/**
Expand Down Expand Up @@ -98,4 +113,65 @@ export class Auth42Controller {
async islogin() {
this.logger.debug(`@islogin)`);
}

@ApiOperation({
summary: 'Access 토큰 갱신',
description: 'Refresh 토큰을 사용하여 새로운 Access 토큰을 발급합니다.',
})
@ApiResponse({
status: 200,
description: '새로운 Access 토큰이 발급되었습니다.',
})
@ApiResponse({
status: 401,
description: 'Refresh 토큰이 유효하지 않거나 만료되었습니다.',
})
@ApiBearerAuth()
@Post('refresh')
async refreshToken(@Req() req, @Res() res) {
const refreshToken = req.cookies['refreshToken'];

if (!refreshToken) {
throw new UnauthorizedException('Refresh 토큰을 찾을 수 없습니다.');
}

const accessExpiresIn = this.configService.getOrThrow<string>(
'jwt.accessExpiresIn',
);

const jwtSecret = this.configService.getOrThrow<string>('jwt.secret');

this.logger.debug(`@refreshToken) refreshToken : ${refreshToken}`);

try {
const payload = await this.jwtService.verifyAsync(refreshToken);

const newAccessToken = await this.jwtService.signAsync(
{ login: payload.login, is_staff: payload.is_staff },
{ expiresIn: accessExpiresIn, secret: jwtSecret },
);

const decodedAccessTokenForExpires =
await this.jwtService.verifyAsync(newAccessToken);

const accessTokenExpires = new Date(
decodedAccessTokenForExpires['exp'] * 1000,
);

res.cookie('accessToken', newAccessToken, {
expires: accessTokenExpires,
httpOnly: false,
domain: req.headers.host.split('.').slice(1).join('.'),
});

return res.json({
accessToken: newAccessToken,
message: 'Access 토큰이 성공적으로 갱신되었습니다.',
});
} catch (e) {
throw new UnauthorizedException(
'Refresh 토큰이 유효하지 않거나 만료되었습니다.',
);
}
}
}
3 changes: 0 additions & 3 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ const repo = {
JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => ({
secret: configService.getOrThrow<string>('jwt.secret'),
signOptions: {
expiresIn: configService.getOrThrow<string>('jwt.expiresIn'),
},
}),
inject: [ConfigService],
}),
Expand Down
67 changes: 59 additions & 8 deletions src/auth/guard/jwt-sign.guard.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
Logger,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { Request, Response } from 'express';
import { Observable } from 'rxjs';
Expand All @@ -16,7 +18,11 @@ import { UserSessionDto } from 'src/auth/dto/user.session.dto';
export class JWTSignGuard implements CanActivate {
private logger = new Logger(JWTSignGuard.name);

constructor(private jwtService: JwtService) {}
constructor(
private jwtService: JwtService,
@Inject(ConfigService)
private configService: ConfigService,
) {}

canActivate(
context: ExecutionContext,
Expand All @@ -26,27 +32,72 @@ export class JWTSignGuard implements CanActivate {
return this.generateJWTToken(req, res);
}

private generateJWTToken(request: Request, response: Response): boolean {
private async generateJWTToken(
request: Request,
response: Response,
): Promise<boolean> {
const user = request.user as UserSessionDto | undefined;
if (user === undefined) {
this.logger.error(`can't generate JWTToken`);
return false;
}
const token = this.jwtService.sign(user);

const accessExpiresIn = this.configService.getOrThrow<string>(
'jwt.accessExpiresIn',
);

const refreshExpiresIn = this.configService.getOrThrow<string>(
'jwt.refreshExpiresIn',
);

const jwtSecret = this.configService.getOrThrow<string>('jwt.secret');

const accessToken = await this.jwtService.signAsync(user, {
expiresIn: accessExpiresIn,
secret: jwtSecret,
});

const refreshToken = await this.jwtService.signAsync(
{ ...user, type: 'refresh' },
{ expiresIn: refreshExpiresIn, secret: jwtSecret },
);

const host = request.headers.host;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ...words] = host.split('.');
const domain = words.join('.');

// NOTE: JWT token의 만료시간을 직접 가져옴.
const expires = new Date(this.jwtService.decode(token)['exp'] * 1000);
const decodedAccessTokenForExpires =
await this.jwtService.verifyAsync(accessToken);
const decodedRefreshTokenForExpires =
await this.jwtService.verifyAsync(refreshToken);

const accessTokenexpires = new Date(
decodedAccessTokenForExpires['exp'] * 1000,
);
const refreshTokenexpires = new Date(
decodedRefreshTokenForExpires['exp'] * 1000,
);

const cookieOptions = {
expires,
expires: accessTokenexpires,
httpOnly: false,
domain,
};
this.logger.debug(`token : ${token}`);
this.logger.debug(`cookieOptions : ${cookieOptions}`);
response.cookie('accessToken', token, cookieOptions);

const refreshcookieOptions = {
expires: refreshTokenexpires,
httpOnly: true,
domain,
};

this.logger.debug(`accessToken : ${accessToken}`);
this.logger.debug(`refreshToken : ${refreshToken}`);

response.cookie('accessToken', accessToken, cookieOptions);
response.cookie('refreshToken', refreshToken, refreshcookieOptions);

return true;
}
}
11 changes: 6 additions & 5 deletions src/configs/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export default () => ({
version: process.env.package_version,
port: parseInt(process.env.PORT, 10),
database: {
host: process.env.DATABASE_HOST,
Expand All @@ -15,9 +14,13 @@ export default () => ({
},
jwt: {
secret: process.env.JWT_OR_SESSION_SECRET,
expiresIn: process.env.JWT_EXPIREIN,
accessExpiresIn: process.env.JWT_ACCESS_EXPIREIN,
refreshExpiresIn: process.env.JWT_REFRESH_EXPIREIN,
},
log: process.env.LOG_DEBUG === 'true' ? true : false,
frontend: {
uri: process.env.URL_FOR_CORS,
},
googleApi: {
email: process.env.GOOGLEAPI_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLEAPI_SERVICE_ACCOUNT_PRIVATE_KEY,
Expand All @@ -33,9 +36,6 @@ export default () => ({
jandi: {
webhook: process.env.JANDI_WEBHOOK_URL,
},
frontend: {
uri: process.env.URL_FOR_CORS,
},
redirect: {
money_guidelines: process.env.URI_MONEY_GUIDELINE,
question: process.env.URI_QUESTION,
Expand All @@ -44,4 +44,5 @@ export default () => ({
terms: process.env.URI_TERMS,
reissuance_guidelines: process.env.URI_REISSUANCE_GUIDELINE,
},
version: process.env.package_version,
});
2 changes: 1 addition & 1 deletion src/configs/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default class TypeOrmConfigService implements TypeOrmOptionsFactory {
password: this.configService.getOrThrow<string>('database.password'),
database: this.configService.getOrThrow<string>('database.database'),
entities: [`${__dirname}/../**/entities/*.entity.{js,ts}`],
logging: this.configService.getOrThrow<boolean>('log'),
logging: this.configService.get<boolean>('log'),
charset: 'utf8mb4_general_ci',
timezone: '+09:00',
// cache: true, TODO: 추후에 캐시 전략 구상할 때 사용 예정
Expand Down
12 changes: 9 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: log_level,
});

const configService = app.get(ConfigService);
const cors_uri = configService.getOrThrow<string>('frontend.uri');
const version = configService.getOrThrow<string>('version');
const cors_uri = configService.get<string>('frontend.uri');
const version = configService.get<string>('version');

// enable CORS if exists
if (cors_uri) {
app.enableCors({
Expand All @@ -34,9 +36,13 @@ async function bootstrap() {
.setVersion(version)
.addBearerAuth()
.build();

const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('docs', app, swaggerDocument);

await app.listen(parseInt(process.env.PORT, 10));
const port = configService.getOrThrow<number>('port');

await app.listen(port);
}

bootstrap();
14 changes: 6 additions & 8 deletions src/redirect/redirect.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,26 @@ export class RedirectService {
constructor(private configService: ConfigService) {}

async moneyGuidelines(): Promise<string> {
return this.configService.getOrThrow<string>('redirect.money_guidelines');
return this.configService.get<string>('redirect.money_guidelines');
}

async question(): Promise<string> {
return this.configService.getOrThrow<string>('redirect.question');
return this.configService.get<string>('redirect.question');
}

async usage(): Promise<string> {
return this.configService.getOrThrow<string>('redirect.usage');
return this.configService.get<string>('redirect.usage');
}

async feedback(): Promise<string> {
return this.configService.getOrThrow<string>('redirect.feedback');
return this.configService.get<string>('redirect.feedback');
}

async terms(): Promise<string> {
return this.configService.getOrThrow<string>('redirect.terms');
return this.configService.get<string>('redirect.terms');
}

async reissuance_guidelines(): Promise<string> {
return this.configService.getOrThrow<string>(
'redirect.reissuance_guidelines',
);
return this.configService.get<string>('redirect.reissuance_guidelines');
}
}
2 changes: 1 addition & 1 deletion src/reissue/reissue.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class ReissueService {
) {}

private logger = new Logger(ReissueService.name);
private jandiWebhook = this.configService.getOrThrow<string>('jandi.webhook');
private jandiWebhook = this.configService.get<string>('jandi.webhook');

getTimeNowKST(): string {
const KR_TIME_DIFF = 9 * 60 * 60 * 1000;
Expand Down
8 changes: 4 additions & 4 deletions src/reissue/repository/googleApi/card-reissue.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ export class CardReissueRepository implements ICardReissueRepository {

constructor(@Inject(ConfigService) private configService: ConfigService) {
this.gsScope = ['https://www.googleapis.com/auth/spreadsheets'];
this.gsKey = this.configService.getOrThrow<string>('googleCardApi.key');
this.gsEmail = this.configService.getOrThrow<string>('googleCardApi.email');
this.gsRange = this.configService.getOrThrow<string>('googleCardApi.range');
this.gsSheetId = this.configService.getOrThrow<string>(
this.gsKey = this.configService.get<string>('googleCardApi.key');
this.gsEmail = this.configService.get<string>('googleCardApi.email');
this.gsRange = this.configService.get<string>('googleCardApi.range');
this.gsSheetId = this.configService.get<string>(
'googleCardApi.spreadsheetId',
);
}
Expand Down
Loading

0 comments on commit 550c347

Please sign in to comment.