기존 https://github.com/do0ori/login-with-OAuth에서 REST API를 기반으로 소셜 로그인을 구현했었다. 이번에는 이전 글의 마지막에서 이야기했듯이 provider 별 passport를 사용해 기존 repo의 passport branch에 추가로 구현해보았다.
로그인 과정은 기존과 동일하다.
참고로 이 글에서 환경변수를process.env
로 가져왔으나 이는 코드 설명을 위한 것이고 실제로는ConfigService
를 활용했다.
또한 코드 구조는 다 비슷하기 때문에 google 위주로 설명한다.
passport-google을 검색해보니 여러 개였는데 가장 다운로드 수가 많은 strategy로 선택했다.
npm i passport-google-oauth20
npm i -D @types/passport-google-oauth20
google.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from 'passport-google-oauth20';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor() {
super({
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: process.env.REDIRECT_URI,
scope: ['email', 'profile'],
});
}
async validate(accessToken: string, refreshToken: string, profile: Profile): Promise<any> {
const { id, displayName, emails, photos, provider } = profile;
const user = {
id,
name: displayName,
email: emails[0].value,
profileImageUrl: photos[0].value,
provider,
};
return user;
}
}
pasport-google의 Strategy
를 extends해서 작성한다.
'google'
이라는 이름이 passport의 AuthGuard
에 추가되어 추후 사용할 수 있다.
super에 넘겨주는 객체에 환경변수 값을 넣어준다.
validate
method의 profile
매개변수에 사용자 정보가 모두 담겨있다.
보통은 done
함수까지 매개변수로 받아서 done(null, user);
이런 식으로 반환하기는 하지만 바로 user 객체를 반환해도 문제없다.
kakao, naver도 마찬가지이다.
request.user
에 반환된 user 객체 값이 담긴다.
auth-google.controller.ts
import { Controller, Get, Res, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express';
import { AuthService } from 'src/auth/auth.service';
import { SocialData } from '../auth/interfaces/social-data.interface';
import { CookieSettingHelper } from '../helpers/cookie-setting.helper';
import { SocialProfile } from '../users/decorators/user.decorator';
@Controller('auth/google')
export class AuthGoogleController {
constructor(
private readonly authService: AuthService,
private readonly cookieSettingHelper: CookieSettingHelper,
) {}
@Get('login')
@UseGuards(AuthGuard('google'))
async googleAuthorize(): Promise<void> { // 로그인 페이지로 redirect }
@Get('callback')
@UseGuards(AuthGuard('google'))
async googleCallback(
@SocialProfile() socialData: SocialData,
@Res({ passthrough: true }) response: Response,
): Promise<void> {
const loginData = await this.authService.validateSocialLogin(socialData);
this.cookieSettingHelper.setCookies(response, loginData);
response.redirect('http://localhost:3000/me');
}
}
AuthGuard('google')
을 사용해 앞서 작성한 GoogleStrategy
를 사용하도록 해준다.
@SocialProfile()
는 request.user
에 담긴 값을 가져오는 decorator로 GoogleStrategy의 validate method에서 반환한 user 객체를 꺼내준다. (user.decorator.ts
참고)
기존에는 직접 REST API로 authorizeCode를 access 및 refresh token으로 교환하고 이 token으로 사용자 정보를 가져왔는데, GoogleStrategy가 이 부분까지 맡아서 사용자 정보까지 가져와준다.
kakao, naver도 마찬가지이다.
그 이후의 로직은 동일하다.
auth-google.module.ts
에서 providers에 GoogleStrategy
를 추가해준다.
npm i passport-kakao
npm i -D @types/passport-kakao
kakao.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from 'passport-kakao';
@Injectable()
export class KakaoStrategy extends PassportStrategy(Strategy, 'kakao') {
constructor() {
super({
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: process.env.REDIRECT_URI,
});
}
async validate(accessToken: string, refreshToken: string, profile: Profile): Promise<any> {
const { id, displayName, _json, provider } = profile;
const user = {
id: id.toString(),
name: displayName,
email: _json.kakao_account.email,
profileImageUrl: _json.properties.profile_image,
provider,
};
return user;
}
}
GoogleStrategy
에서와는 달리 super에 넘겨주는 객체에 scope
는 없다.
validate
method의 profile
매개변수에 사용자 정보가 모두 담겨있다.
email과 프로필 사진은 _json
에서 꺼내야 한다.
Kakao의 OAuth ID는 number type이지만 다른 provider들의 OAuth ID와 맞추기 위해 string type으로 변환해주었다.
request.user
에 반환된 user 객체 값이 담긴다.
auth-kakao.controller.ts
import { Controller, Get, Res, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express';
import { AuthService } from 'src/auth/auth.service';
import { SocialData } from '../auth/interfaces/social-data.interface';
import { CookieSettingHelper } from '../helpers/cookie-setting.helper';
import { SocialProfile } from '../users/decorators/user.decorator';
@Controller('auth/kakao')
export class AuthKakaoController {
constructor(
private readonly authService: AuthService,
private readonly cookieSettingHelper: CookieSettingHelper,
) {}
@Get('login')
@UseGuards(AuthGuard('kakao'))
async googleAuthorize(): Promise<void> { // 로그인 페이지로 redirect }
@Get('callback')
@UseGuards(AuthGuard('kakao'))
async googleCallback(
@SocialProfile() socialData: SocialData,
@Res({ passthrough: true }) response: Response,
): Promise<void> {
const loginData = await this.authService.validateSocialLogin(socialData);
this.cookieSettingHelper.setCookies(response, loginData);
response.redirect('http://localhost:3000/me');
}
}
AuthGuard('kakao')
를 사용해 앞서 작성한 KakaoStrategy
를 사용하도록 해준다.auth-kakao.module.ts
에서 providers에 KakaoStrategy
를 추가해준다.
github를 찾아보니 네이버에서 만들었다.
npm i passport-naver
npm i -D @types/passport-naver
naver.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from 'passport-naver';
@Injectable()
export class NaverStrategy extends PassportStrategy(Strategy, 'naver') {
constructor() {
super({
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: process.env.REDIRECT_URI,
});
}
async validate(accessToken: string, refreshToken: string, profile: Profile): Promise<any> {
const { id, _json, provider } = profile;
const user = {
id,
name: _json.nickname,
email: _json.email,
profileImageUrl: _json.profile_image,
provider,
};
return user;
}
}
GoogleStrategy
에서와는 달리 super에 넘겨주는 객체에 scope
는 없다.
validate
method의 profile
매개변수에 사용자 정보가 모두 담겨있다.
id와 provider를 제외한 값들은 _json
에서 꺼내야 한다.
request.user
에 반환된 user 객체 값이 담긴다.
auth-naver.controller.ts
import { Controller, Get, Res, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express';
import { AuthService } from 'src/auth/auth.service';
import { SocialData } from '../auth/interfaces/social-data.interface';
import { CookieSettingHelper } from '../helpers/cookie-setting.helper';
import { SocialProfile } from '../users/decorators/user.decorator';
@Controller('auth/naver')
export class AuthNaverController {
constructor(
private readonly authService: AuthService,
private readonly cookieSettingHelper: CookieSettingHelper,
) {}
@Get('login')
@UseGuards(AuthGuard('naver'))
async googleAuthorize(): Promise<void> { // 로그인 페이지로 redirect }
@Get('callback')
@UseGuards(AuthGuard('naver'))
async googleCallback(
@SocialProfile() socialData: SocialData,
@Res({ passthrough: true }) response: Response,
): Promise<void> {
const loginData = await this.authService.validateSocialLogin(socialData);
this.cookieSettingHelper.setCookies(response, loginData);
response.redirect('http://localhost:3000/me');
}
}
AuthGuard('naver')
를 사용해 앞서 작성한 NaverStrategy
를 사용하도록 해준다.auth-naver.module.ts
에서 providers에 NaverStrategy
를 추가해준다.
Frontend(Client)에서 provider 별 로그인 API url로 "이동"하면 각각 소셜 로그인을 사용할 수 있다. (GET 요청 X)
REST API 기반으로 소셜 로그인을 구현하면서 로그인 과정을 모두 이해한 후에 provider 별 passport를 사용해보니 적용하기도 쉽고 간편해서 좋았다. 😁