[Dimelo Project] passport local, google, github 로그인 구현 (nest.js)

Suyeon Pi·2022년 1월 10일
0

Dimelo

목록 보기
5/22

작업내용

  • local login, logout 함수 구현
  • local auth guard, strategy, serialize 모듈 생성 후 auth module provider에 주입.
  • logged, not logged guard 생성
  • google strategy 생성, google auth guard생성
  • google login시 기존에 회원가입된 이메일로 로그인할 시 user의 googleId 추가
  • github strategy 추가, github auth guard생성
  • github login시 기존 회원가입된 이메일로 로그인할 시 user의 githubId에 추가

작업코드

passport-local

// 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('로그아웃 되었습니다');
  }

passport-google

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` };
    }
  }

passport-github

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 {}
profile
Stay hungry, Stay foolish!

0개의 댓글