Skip to content

Commit 44c7c8f

Browse files
authored
Merge branch 'dev' into 10-be-oauth-구현체-구현
2 parents 82aca93 + b76e0df commit 44c7c8f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1132
-71
lines changed

backend/.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ module.exports = {
1717
'@typescript-eslint/interface-name-prefix': 'off',
1818
'@typescript-eslint/explicit-function-return-type': 'off',
1919
'@typescript-eslint/explicit-module-boundary-types': 'off',
20-
'@typescript-eslint/no-explicit-any': 'off'
20+
'@typescript-eslint/no-explicit-any': 'off',
2121
},
2222
};

backend/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,23 @@
2424
"@nestjs/common": "^9.0.0",
2525
"@nestjs/config": "^2.2.0",
2626
"@nestjs/core": "^9.0.0",
27+
"@nestjs/jwt": "^9.0.0",
28+
"@nestjs/passport": "^9.0.0",
2729
"@nestjs/platform-express": "^9.0.0",
2830
"@nestjs/swagger": "^6.1.3",
2931
"@nestjs/typeorm": "^9.0.1",
3032
"@types/cookie-parser": "^1.4.3",
3133
"@types/helmet": "^4.0.0",
3234
"axios": "^1.1.3",
35+
"class-transformer": "^0.5.1",
36+
"class-validator": "^0.13.2",
3337
"cookie-parser": "^1.4.6",
3438
"helmet": "^6.0.0",
39+
"joi": "^17.7.0",
3540
"mysql2": "^2.3.3",
41+
"passport": "^0.6.0",
42+
"passport-jwt": "^4.0.0",
43+
"passport-local": "^1.0.0",
3644
"reflect-metadata": "^0.1.13",
3745
"rimraf": "^3.0.2",
3846
"rxjs": "^7.2.0",
@@ -46,6 +54,8 @@
4654
"@types/express": "^4.17.13",
4755
"@types/jest": "28.1.8",
4856
"@types/node": "^16.0.0",
57+
"@types/passport-jwt": "^3.0.7",
58+
"@types/passport-local": "^1.0.34",
4959
"@types/supertest": "^2.0.11",
5060
"@typescript-eslint/eslint-plugin": "^5.0.0",
5161
"@typescript-eslint/parser": "^5.0.0",

backend/src/auth/auth.module.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { UserModule } from 'src/user/user.module';
77
import { OauthNaverService } from './service/oauth/naver-oauth.service';
88
import { TypeOrmModule } from '@nestjs/typeorm';
99
import { OauthKakaoService } from './service/oauth/kakao-oauth.service';
10+
import { JwtService } from '@nestjs/jwt';
11+
import { ConfigService } from '@nestjs/config';
12+
import { JwtAccessStrategy } from './jwt/access-jwt.strategy';
13+
import { JwtRefreshStrategy } from './jwt/refresh-jwt.strategy';
1014

1115
export const UserRepository: ClassProvider = {
1216
provide: USER_REPOSITORY_INTERFACE,
@@ -17,5 +21,15 @@ export const UserRepository: ClassProvider = {
1721
imports: [UserModule, TypeOrmModule.forFeature([TypeormUserRepository])],
1822
controllers: [AuthController],
1923
providers: [AuthService, UserRepository, OauthKakaoService, OauthNaverService],
24+
providers: [
25+
AuthService,
26+
userRepository,
27+
OauthGoogleService,
28+
OauthNaverService,
29+
JwtService,
30+
ConfigService,
31+
JwtAccessStrategy,
32+
JwtRefreshStrategy,
33+
],
2034
})
2135
export class AuthModule {}

backend/src/auth/controller/auth.controller.ts

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
1-
import { Controller, Get, HttpCode, Param, Query, Res } from '@nestjs/common';
21
import { Response } from 'express';
2+
import {
3+
Controller,
4+
Get,
5+
HttpCode,
6+
Param,
7+
Query,
8+
Redirect,
9+
Req,
10+
Res,
11+
UseGuards,
12+
} from '@nestjs/common';
313
import { AuthService } from '../service/auth.service';
14+
import { Request, Response } from 'express';
15+
import {
16+
ACCESS_TOKEN,
17+
JWT_ACCESS_TOKEN_EXPIRATION_TIME,
18+
JWT_ACCESS_TOKEN_SECRET,
19+
JWT_REFRESH_TOKEN_EXPIRATION_TIME,
20+
JWT_REFRESH_TOKEN_SECRET,
21+
OK,
22+
REFRESH_TOKEN,
23+
tokenCookieOptions,
24+
} from '@constant';
25+
import { JwtAuthGuard } from '../guard/jwt.guard';
426

527
@Controller('auth')
628
export class AuthController {
@@ -14,8 +36,43 @@ export class AuthController {
1436
}
1537

1638
@Get('oauth/callback/:type')
17-
async socialStart(@Query('code') authorizationCode: string, @Param('type') type: string) {
18-
const userId = await this.authService.socialStart({ type, authorizationCode });
19-
return userId;
39+
async socialStart(
40+
@Query('code') authorizationCode: string,
41+
@Param('type') type: string,
42+
@Res({ passthrough: true }) res: Response
43+
) {
44+
const user = await this.authService.socialStart({ type, authorizationCode });
45+
const accessToken = this.authService.createJwt({
46+
payload: { nickname: 'user.nickname', email: 'user.email' },
47+
secret: JWT_ACCESS_TOKEN_SECRET,
48+
expirationTime: JWT_ACCESS_TOKEN_EXPIRATION_TIME,
49+
});
50+
const refreshToken = this.authService.createJwt({
51+
payload: { nickname: 'user.nickname', email: 'user.email' },
52+
secret: JWT_REFRESH_TOKEN_SECRET,
53+
expirationTime: JWT_REFRESH_TOKEN_EXPIRATION_TIME,
54+
});
55+
56+
res.cookie(ACCESS_TOKEN, accessToken, tokenCookieOptions);
57+
res.cookie(REFRESH_TOKEN, refreshToken, tokenCookieOptions);
58+
}
59+
60+
@UseGuards(JwtAuthGuard)
61+
@Get('login')
62+
@HttpCode(OK)
63+
loginValidate(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
64+
const { accessToken, refreshToken } = req.cookies;
65+
res.cookie(ACCESS_TOKEN, accessToken, tokenCookieOptions).cookie(
66+
REFRESH_TOKEN,
67+
refreshToken,
68+
tokenCookieOptions
69+
);
70+
}
71+
72+
@UseGuards(JwtAuthGuard)
73+
@Get('logout')
74+
logout(@Res({ passthrough: true }) res: Response) {
75+
res.clearCookie(ACCESS_TOKEN);
76+
res.clearCookie(REFRESH_TOKEN);
2077
}
2178
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { IsEmail, IsString } from 'class-validator';
2+
3+
export class CreateJwtDto {
4+
@IsString()
5+
nickname: string;
6+
7+
@IsEmail()
8+
email: string;
9+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { AuthGuard } from '@nestjs/passport';
3+
4+
@Injectable()
5+
export class JwtAuthGuard extends AuthGuard(['jwt-access', 'jwt-refresh']) {}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { JWT_ACCESS_TOKEN_SECRET } from '@constant';
2+
import { Injectable, UnauthorizedException } from '@nestjs/common';
3+
import { ConfigService } from '@nestjs/config';
4+
import { PassportStrategy } from '@nestjs/passport';
5+
import { ExtractJwt, Strategy } from 'passport-jwt';
6+
import { Request } from 'express';
7+
import { Payload } from 'src/types/auth.type';
8+
9+
@Injectable()
10+
export class JwtAccessStrategy extends PassportStrategy(Strategy, 'jwt-access') {
11+
constructor(private readonly configService: ConfigService) {
12+
super({
13+
ignoreExpiration: false,
14+
jwtFromRequest: ExtractJwt.fromExtractors([
15+
(req) => {
16+
const token = req?.cookies.accessToken;
17+
return token ?? null;
18+
},
19+
]),
20+
secretOrKey: configService.get(JWT_ACCESS_TOKEN_SECRET),
21+
passReqToCallback: true,
22+
});
23+
}
24+
25+
async validate(req: Request, payload: Payload) {
26+
return payload;
27+
}
28+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
JWT_ACCESS_TOKEN_EXPIRATION_TIME,
3+
JWT_ACCESS_TOKEN_SECRET,
4+
JWT_REFRESH_TOKEN_SECRET,
5+
} from '@constant';
6+
import { Injectable, UnauthorizedException } from '@nestjs/common';
7+
import { ConfigService } from '@nestjs/config';
8+
import { PassportStrategy } from '@nestjs/passport';
9+
import { ExtractJwt, Strategy } from 'passport-jwt';
10+
import { Request } from 'express';
11+
import { AuthService } from '../service/auth.service';
12+
import { Payload } from 'src/types/auth.type';
13+
14+
@Injectable()
15+
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
16+
constructor(
17+
private readonly configService: ConfigService,
18+
private readonly authService: AuthService
19+
) {
20+
super({
21+
ignoreExpiration: false,
22+
jwtFromRequest: ExtractJwt.fromExtractors([
23+
(req) => {
24+
const token = req?.cookies.accessToken;
25+
return token ?? null;
26+
},
27+
]),
28+
secretOrKey: configService.get(JWT_REFRESH_TOKEN_SECRET),
29+
passReqToCallback: true,
30+
});
31+
}
32+
33+
async validate(req: Request, payload: Payload) {
34+
const accessToken = this.authService.createJwt({
35+
payload: { nickname: payload.nickname, email: payload.email },
36+
secret: JWT_ACCESS_TOKEN_SECRET,
37+
expirationTime: JWT_ACCESS_TOKEN_EXPIRATION_TIME,
38+
});
39+
40+
req.cookies = { ...req.cookies, accessToken };
41+
42+
return payload;
43+
}
44+
}

backend/src/auth/service/auth.service.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { UserRepository } from 'src/user/repository/interface-user.repository';
66
import { OauthNaverService } from './oauth/naver-oauth.service';
77
import { OauthService } from './oauth/interface-oauth.service';
88
import { OauthKakaoService } from './oauth/kakao-oauth.service';
9+
import { JwtService } from '@nestjs/jwt';
10+
import { ConfigService } from '@nestjs/config';
11+
import { CreateJwtDto } from '../dto/create-jwt.dto';
912

1013
@Injectable()
1114
export class AuthService {
@@ -17,6 +20,9 @@ export class AuthService {
1720

1821
private readonly oauthKakaoService: OauthKakaoService,
1922
private readonly oauthNaverService: OauthNaverService
23+
24+
private jwtService: JwtService,
25+
private configService: ConfigService
2026
) {}
2127

2228
/**
@@ -47,8 +53,32 @@ export class AuthService {
4753
);
4854
const userSocialInfo = await this.oauthInstance.getSocialInfoByAccessToken(accessToken);
4955

50-
const userId = await this.userRepository.saveUser(userSocialInfo as UserInfo);
51-
return userId;
56+
const user = await this.userRepository.saveUser(userSocialInfo as UserInfo);
57+
return user;
58+
}
59+
60+
/**
61+
* 비밀키와 만료 시간을 기반으로 access token 또는 refresh token을 발급합니다.
62+
* @param payload jwt payload에 입력될 값
63+
* @param secret 서명에 사용될 비밀키
64+
* @param expirationTime token의 만료시간
65+
* @returns access token 또는 refresh token
66+
*/
67+
createJwt({
68+
payload,
69+
secret,
70+
expirationTime,
71+
}: {
72+
payload: CreateJwtDto;
73+
secret: string;
74+
expirationTime: string;
75+
}) {
76+
const token = this.jwtService.sign(payload, {
77+
secret: this.configService.get(secret),
78+
expiresIn: `${this.configService.get(expirationTime)}s`,
79+
});
80+
81+
return token;
5282
}
5383

5484
/**

backend/src/config/env.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
import { ConfigModuleOptions } from '@nestjs/config';
2+
import Joi from 'joi';
23

34
export const envConfig: ConfigModuleOptions = {
45
isGlobal: true, // 환경 변수를 전역으로 사용
56
envFilePath: '.env',
67
// 루트 경로에서 .env 사용 (cross-env로 환경에 따른 .env 적용도 가능)
8+
validationSchema: Joi.object({
9+
DB_USER: Joi.string().required(),
10+
DB_PASSWORD: Joi.string().required(),
11+
DB_NAME: Joi.string().required(),
12+
JWT_ACCESS_TOKEN_SECRET: Joi.string().required(),
13+
JWT_ACCESS_TOKEN_EXPIRATION_TIME: Joi.string().required(),
14+
JWT_REFRESH_TOKEN_SECRET: Joi.string().required(),
15+
JWT_REFRESH_TOKEN_EXPIRATION_TIME: Joi.string().required(),
16+
}),
717
};

0 commit comments

Comments
 (0)