// local.strategy.ts
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({ usernameField: 'email', passwordField: 'password' });
}
async validate(email: string, password: string, done: CallableFunction) {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException();
}
return done(null, user); // serializer의 user로 감
}
}
// auth.service.ts
async validateUser(email: string, password: string) {
const user = await this.usersRepository.findOne({
where: { email },
select: ['id', 'email', 'password', 'nickname'],
});
if (!user) {
return null;
}
const result = await bcrypt.compare(password, user.password);
if (result) {
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
}
return null;
}
// local.serializer.ts
@Injectable()
export class LocalSerializer extends PassportSerializer {
constructor(
private readonly authService: AuthService,
@InjectRepository(Users) private usersRepository: Repository<Users>,
) {
super();
}
serializeUser(user: Users, done: CallableFunction) {
done(null, user.id);
}
async deserializeUser(userId: string, done: CallableFunction) {
return await this.usersRepository
.findOneOrFail(
{
id: +userId,
},
{
select: ['id', 'email', 'nickname'],
},
)
.then((user) => {
console.log('user', user);
done(null, user);
})
.catch((error) => done(error));
}
}
// local.auth.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
async canActivate(context: ExecutionContext): Promise<boolean> {
const can = await super.canActivate(context);
if (can) {
const request = context.switchToHttp().getRequest();
await super.logIn(request);
}
return true;
}
}
local Auth guard를 만들어 준다.
// auth.controller.ts
@UseGuards(LocalAuthGuard)
@Post('/login')
login(@Req() req) {
return req.user;
}
@Post('/logout')
logout(@Req() req, @Res() res) {
req.logOut();
res.clearCookie('connect.sid', { httpOnly: true });
res.send('로그아웃 되었습니다');
}
google developer console에서 새 프로젝트를 만들고 Oauth Client ID를 만든다. 다 만들면 Client ID와 Secret을 발급받는다.
// google.strategy.ts
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(private authService: AuthService) {
super({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: 'http://localhost:3000/api/auth/google/redirect',
scope: ['email', 'profile'],
});
}
async validate(
_accessToken: string,
_refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { emails, photos } = profile;
// 구글에서 받아온 profile에서 필요한정보를 뽑아 새로운 객체 만듬
const googleUser: GoogleLoginUserDto = {
googleId: +profile.id,
email: emails[0].value,
imageUrl: photos[0].value,
};
const user = await this.authService.googleSignUp(googleUser);
// google에서 받아온 정보로 새로운 user만들고 serializer에 user 보냄
done(null, user);
}
}
// auth.service.ts
async googleSignUp(user: GoogleLoginUserDto) {
const foundGoogle = await this.usersRepository.findOne({
where: { email: user.email, googleId: user.googleId },
});
if (foundGoogle) {
return foundGoogle;
// 이미 해당 email로 google로그인 한 적이 있는 user
}
const found = await this.usersRepository.findOne({
where: { email: user.email },
});
if (found) {
const googleConnected = {
...found,
googleId: user.googleId,
};
return this.usersRepository.save(googleConnected);
// google과 똑같은 email로 local 회원가입을 한 유저에겐 googleId 속성에 googleId 붙여줌.
}
return this.usersRepository.save(user);
// 처음 구글로 로그인 하는 user는 DB에 해당 구글 email로 user생성
}
// google.auth.guard
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class GoogleAuthGuard extends AuthGuard('google') {
async canActivate(context: ExecutionContext): Promise<boolean> {
const can = await super.canActivate(context);
if (can) {
const request = context.switchToHttp().getRequest();
await super.logIn(request);
}
return true;
}
}
// auth.controller.ts
@UseGuards(GoogleAuthGuard)
@Get('/google')
async googleAuth() {}
@UseGuards(GoogleAuthGuard)
@Redirect(process.env.CLIENT_URL, 302) // 로그인 성공시 리다이렉트 할 프론트 주소
@Get('/google/redirect')
googleAuthRedirect(@CurrentUser() user: CurrentUserDto) {
if (user && !user.nickname) {
// 로그인 성공시 유저의 닉네임이 없으면 프로필생성 페이지로 리다이렉팅 시킴
return { url: `${process.env.CLIENT_URL}/profileset` };
}
}
github-settings-developer settings에서 OAuth Apps을 생성한다. Client ID와 Secret을 발급 받을 수 있다.
@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy, 'github') {
constructor(private authService: AuthService) {
super({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_SECRET,
callbackURL: 'http://localhost:3000/api/auth/github/callback',
scope: ['user:email'], // user의 email에 접근가능하는 권한
});
}
async validate(
_accessToken: string,
_refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { id, avatar_url, name, email } = profile._json;
// 깃헙에서 반환하는 profile._json에서 필요한 정보 빼서 새로운 객체 만듬
const githubUser: GithubLoginUserDto = {
githubId: +id,
email,
imageUrl: avatar_url,
};
const user = await this.authService.githubSignUp(githubUser);
// 새로운 만든 객체를 바탕으로 새로운 user 만들고 해당 user를 done해서 serialize한다
done(null, user);
}
}
// auth.service.ts
async githubSignUp(user: GithubLoginUserDto) {
const foundGithub = await this.usersRepository.findOne({
where: { email: user.email, githubId: user.githubId },
});
if (foundGithub) {
return foundGithub;
// 해당 email로 github로그인을 한적 있는 user
}
const found = await this.usersRepository.findOne({
where: { email: user.email },
});
if (found) {
const githubConnected = {
...found,
githubId: user.githubId,
};
return this.usersRepository.save(githubConnected);
// 해당 email로 local회원가입을 한적 있는 user에게는 githubId 속성 추가
}
return this.usersRepository.save(user);
// 처음 github으로 로그인 하는 user는 DB에 새로운 user만듬
}
// github.auth.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class GithubAuthGuard extends AuthGuard('github') {
async canActivate(context: ExecutionContext): Promise<boolean> {
const can = await super.canActivate(context);
if (can) {
const request = context.switchToHttp().getRequest();
await super.logIn(request);
}
return true;
}
}
// auth.controller.ts
@UseGuards(AuthGuard('github'))
@UseGuards(GithubAuthGuard)
async githubAuth() {}
@UseGuards(GithubAuthGuard)
@Redirect(process.env.CLIENT_URL, 302) // 깃헙 로그인 성공시 리다이렉팅 되는 프론트 주소
@Get('/github/callback')
githubAuthCallback(@CurrentUser() user: CurrentUserDto) {
if (user && !user.nickname) {
// 로그인 성공시 유저의 닉네임이 없으면 프로필 설정 페이지로 리다이렉팅
return { url: `${process.env.CLIENT_URL}/profileset` };
}
}
가장 중요한 auth module에서 provider에 다 만든 strategy를 추가 해주는 것을 잊지말자.. 이거 안해서 한참을 바보짓 했다 ㅠㅠ
// auth.module.ts
@Module({
imports: [
PassportModule.register({ session: true }),
TypeOrmModule.forFeature([Users]),
],
controllers: [AuthController],
providers: [
AuthService,
GoogleStrategy,
GithubStrategy,
LocalStrategy,
LocalSerializer,
],
})
export class AuthModule {}