icon

OAuth 2.0 및 소셜 로그인 통합


 OAuth 2.0은 사용자 인증과 권한 부여를 위한 업계 표준 프로토콜입니다.

 NestJS 애플리케이션에 OAuth 2.0과 소셜 로그인을 통합하면 보안성 향상과 사용자 경험 개선을 동시에 달성할 수 있습니다.

OAuth 2.0 개념 및 NestJS 통합 이점

 OAuth 2.0의 주요 개념

  • 클라이언트 : OAuth 2.0을 사용하여 사용자 데이터에 접근하려는 애플리케이션
  • 리소스 소유자 : 데이터의 소유자인 사용자
  • 인가 서버 : 권한 부여를 관리하는 서버
  • 리소스 서버 : 보호된 데이터를 호스팅하는 서버

 NestJS 통합 이점

  1. 모듈화된 구조로 인한 깔끔한 코드 구성
  2. 내장된 가드와 데코레이터를 통한 쉬운 보안 구현
  3. Passport.js와의 원활한 통합

NestJS에서 OAuth 2.0 구현

  1. 필요한 패키지 설치
npm install @nestjs/passport passport passport-oauth2
  1. OAuth 전략 구현
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-oauth2';
 
@Injectable()
export class OAuth2Strategy extends PassportStrategy(Strategy, 'oauth2') {
  constructor() {
    super({
      authorizationURL: 'https://provider.com/oauth2/authorize',
      tokenURL: 'https://provider.com/oauth2/token',
      clientID: 'YOUR_CLIENT_ID',
      clientSecret: 'YOUR_CLIENT_SECRET',
      callbackURL: 'http://localhost:3000/auth/oauth2/callback',
      scope: ['email', 'profile']
    });
  }
 
  async validate(accessToken: string, refreshToken: string, profile: any) {
    // 사용자 정보 처리 및 반환
    return { userId: profile.id, username: profile.username, email: profile.email };
  }
}
  1. 인증 모듈 및 컨트롤러 구현
@Module({
  imports: [PassportModule],
  controllers: [AuthController],
  providers: [OAuth2Strategy],
})
export class AuthModule {}
 
@Controller('auth')
export class AuthController {
  @Get('oauth2')
  @UseGuards(AuthGuard('oauth2'))
  oauth2Login() {}
 
  @Get('oauth2/callback')
  @UseGuards(AuthGuard('oauth2'))
  oauth2LoginCallback(@Req() req) {
    // 로그인 성공 후 처리
    return req.user;
  }
}

주요 소셜 로그인 제공업체 통합

  1. Google OAuth 통합
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-google-oauth20';
 
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor() {
    super({
      clientID: 'YOUR_GOOGLE_CLIENT_ID',
      clientSecret: 'YOUR_GOOGLE_CLIENT_SECRET',
      callbackURL: 'http://localhost:3000/auth/google/callback',
      scope: ['email', 'profile'],
    });
  }
 
  async validate(accessToken: string, refreshToken: string, profile: any) {
    return { userId: profile.id, email: profile.emails[0].value, name: profile.displayName };
  }
}
  1. Facebook OAuth 통합
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-facebook';
 
@Injectable()
export class FacebookStrategy extends PassportStrategy(Strategy, 'facebook') {
  constructor() {
    super({
      clientID: 'YOUR_FACEBOOK_APP_ID',
      clientSecret: 'YOUR_FACEBOOK_APP_SECRET',
      callbackURL: 'http://localhost:3000/auth/facebook/callback',
      scope: 'email',
      profileFields: ['emails', 'name'],
    });
  }
 
  async validate(accessToken: string, refreshToken: string, profile: any) {
    return { userId: profile.id, email: profile.emails[0].value, name: profile.name.givenName };
  }
}

사용자 정보 처리 및 계정 연동

 사용자 정보 처리 서비스

@Injectable()
export class UserService {
  constructor(@InjectRepository(User) private userRepository: Repository<User>) {}
 
  async findOrCreateUser(profile: any, provider: string): Promise<User> {
    let user = await this.userRepository.findOne({ where: { email: profile.email } });
 
    if (!user) {
      user = this.userRepository.create({
        email: profile.email,
        name: profile.name,
        [provider + 'Id']: profile.userId,
      });
    } else {
      user[provider + 'Id'] = profile.userId;
    }
 
    return this.userRepository.save(user);
  }
}

다중 OAuth 제공자 지원 시스템

 통합 인증 서비스

@Injectable()
export class AuthService {
  constructor(
    private userService: UserService,
    private jwtService: JwtService,
  ) {}
 
  async validateOAuthLogin(profile: any, provider: string): Promise<string> {
    const user = await this.userService.findOrCreateUser(profile, provider);
    const payload = { username: user.email, sub: user.id };
    return this.jwtService.sign(payload);
  }
}

OAuth 토큰 관리

 토큰 저장 및 갱신

@Injectable()
export class TokenService {
  constructor(
    @InjectRepository(Token) private tokenRepository: Repository<Token>,
    private jwtService: JwtService,
  ) {}
 
  async saveToken(userId: number, accessToken: string, refreshToken: string): Promise<void> {
    await this.tokenRepository.save({ userId, accessToken, refreshToken });
  }
 
  async refreshToken(refreshToken: string): Promise<string> {
    const token = await this.tokenRepository.findOne({ where: { refreshToken } });
    if (!token) {
      throw new UnauthorizedException('Invalid refresh token');
    }
    // 새 액세스 토큰 생성 및 반환
    return this.jwtService.sign({ sub: token.userId });
  }
}

OAuth 2.0 그랜트 타입 구현

 1. Authorization Code 그랜트

  • 웹 애플리케이션에 적합
  • 이미 구현된 Google, Facebook 전략이 이 방식 사용

 2. Implicit 그랜트

  • 더 이상 권장되지 않음, 보안상의 이유로 사용 지양

 3. Resource Owner Password Credentials 그랜트

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }
 
  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

 4. Client Credentials 그랜트

@Injectable()
export class ClientCredentialsStrategy extends PassportStrategy(Strategy, 'client-credentials') {
  constructor(private authService: AuthService) {
    super({
      grantType: 'client_credentials',
    });
  }
 
  async validate(clientId: string, clientSecret: string): Promise<any> {
    const client = await this.authService.validateClient(clientId, clientSecret);
    if (!client) {
      throw new UnauthorizedException();
    }
    return client;
  }
}

하이브리드 인증 시스템

 로컬 인증과 OAuth를 결합

@Injectable()
export class AuthService {
  constructor(
    private userService: UserService,
    private jwtService: JwtService,
  ) {}
 
  async validateUser(username: string, password: string): Promise<any> {
    const user = await this.userService.findByUsername(username);
    if (user && await bcrypt.compare(password, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
 
  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
 
  async validateOAuthLogin(profile: any, provider: string) {
    // OAuth 로그인 처리
  }
}

Best Practices 및 보안 강화 전략

  1. 환경 변수 사용 : 클라이언트 ID와 시크릿을 환경 변수로 관리
  2. HTTPS 사용 : 모든 OAuth 관련 통신은 HTTPS로 암호화
  3. 상태 파라미터 사용 : CSRF 공격 방지를 위해 상태 파라미터 구현
  4. 토큰 저장 : 클라이언트 측에서 안전하게 토큰 저장 (HttpOnly 쿠키 등)
  5. 스코프 제한 : 필요한 최소한의 스코프만 요청
  6. 정기적인 토큰 갱신 : 액세스 토큰의 수명을 짧게 유지하고 주기적으로 갱신
  7. 에러 처리 : 상세한 에러 메시지 노출 자제
  8. 로깅 및 모니터링 : 비정상적인 인증 시도 감지 및 로깅
  9. 다중 요소 인증(MFA) 고려 : 중요한 작업에 대해 추가 인증 단계 구현
  10. 정기적인 보안 감사 : 의존성 업데이트 및 보안 설정 검토

 NestJS에서 OAuth 2.0 및 소셜 로그인을 구현할 때는 보안과 사용자 경험의 균형을 잘 맞추는 것이 중요합니다.

 OAuth 2.0의 다양한 그랜트 타입을 이해하고 적절히 활용하는 것도 중요합니다.

 각 그랜트 타입의 특성과 보안 implications을 고려하여 애플리케이션의 요구사항에 가장 적합한 방식을 선택해야 합니다.