게시글
velca
벨바카
10주 (수정됨)
Angular에서 Google OAuth2 인증을 만들었다

이미 이 블로그에도 적용돼있지만, 나는 Google 계정을 이용한 로그인을 좋아한다.

그런데 지금 블로그는 조금 구 버전의 문서인 Sign in with Google for Web 문서를 보고 만들어져서, OAuth2로 마이그레이션 할 겸 만든 것들을 공유하고자 한다.

OAuth2 프로토콜에 대한 설명은 생략하고, 이 문서를 보면 자바스크립트를 이용한 OAuth2.0 액세스 토큰을 발급 받는 방법을 알 수 있다.

방법은 간단하다.

아래 URL로 리디렉트 해주면 끝이다.

https://accounts.google.com/o/oauth2/v2/auth?
  client_id=client_id&
  redirect_uri=https%3A//oauth2.example.com/code&
  response_type=token&
  scope=profile email&
  state=state_parameter_passthrough_value&
  include_granted_scopes=true&
  enable_granular_consent=true&
  login_hint=example@gmail.com&
  prompt=consent

각 파라미터에 대해 간단하게 알아보자.

이름 필수여부 설명
client_id true 사용자 인증화면에서 발급 받은 클라이언트 아이디
redirect_uri true OAuth2.0 토큰 획득 후 리디렉트 할 Uri. 사용자 인증페이지에서 설정 가능
response_type true 자바스크립트 앱에선 token 값으로 고정
scope true 앱이 사용자를 대신해 액세스 할 수 있는 리소스 배열. 공백으로 구분
state false 권장 필드. 상태값을 Google OAuth서버에 제공한 후 리디렉스 Uri에 포함되도록 하여 보안을 강화하기 위함
include_granted_scopes false boolean 값을 사용하며, 앱이 점진적 권한 부여 기능을 사용할 지 말지 설정. 기본 값은 false
enable_granular_consent false 기본 값은 false. true로 설정할 경우 2019년 이전에 생성된 OAuth 클라이언트 ID에 대해 더 세분화 된 Google 계정 권한이 비활성화 됨.
prompt false none, consent, select_account중 하나 사용. 자세한 값은 문서 참고

이제 이 내용들을 반복 사용하기 위해 Angular 서비스를 만들어줬다.

@Injectable({
  providedIn: 'root',
})
export class OauthService {
  redirectToGetGoogleAccessToken({
    clientId,
    redirectUri,
    scope = ['email', 'profile'],
    prompt,
    state,
    includeGrantedScopes,
    enableGranularConsent,
    loginHint,
  }: GoogleRedirectOptions): void {
    const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');

    url.searchParams.set('client_id', clientId);
    url.searchParams.set('redirect_uri', redirectUri);
    url.searchParams.set('response_type', 'token');
    url.searchParams.set('scope', scope.join(' '));

    if (prompt !== undefined) {
      url.searchParams.set('prompt', prompt);
    }

    if (includeGrantedScopes !== undefined) {
      url.searchParams.set('include_granted_scopes', String(includeGrantedScopes));
    }

    if (enableGranularConsent !== undefined) {
      url.searchParams.set('enable_granular_consent', String(enableGranularConsent));
    }

    if (loginHint !== undefined) {
      url.searchParams.set('login_hint', loginHint);
    }

    if (state !== undefined) {
      url.searchParams.set('state', state);
    }

    location.href = url.toString();
  }
}

나는 구글 계정을 이용한 로그인 기능만 사용할 것이기 때문에 scope의 기본 값을 emailprofile로 지정해줬다.

액세스 토큰을 이용해 사용자 정보를 조회할 때 emailprofile은 각각 다음 정보들을 포함한다.

email

  • email: 구글 이메일
  • email_verified: 이메일 인증 여부

profile

  • name: given_namefamily_name을 합친 이름
  • given_name: 구글 계정에 등록된 이름
  • family_name: 구글 계정에 등록된 성
  • picture: 구글 계정 아바타 URL

이렇게 하면 이제 OauthService.redirectToGetGoogleAccessToken()메서드를 호출했을 때 다음과 같이 구글 계정을 선택하는 화면으로 이동한다.

위 화면에서 계정을 선택하면 이제 redirect_uri로 설정한 URL로 이동하게 되는데, 이 때 URL Fragment에 결과값들이 포함되어 아래와 같은 형태로 전달된다.

{your_redirect_url}#access_token={access_token}&token_type=Bearer&expires_in=3599&scope=email%20profile%20https://www.googleapis.com/auth/userinfo.profile%20https://www.googleapis.com/auth/userinfo.email%20openid&authuser=0&prompt=consent

이렇게 전달되는 값을 이용하기 위해, redirect_uri 로 지정된 페이지에서 다음과 같이 처리해줬다.

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-oauth-page',
  standalone: true,
  imports: [],
  templateUrl: './oauth-page.component.html',
  styleUrl: './oauth-page.component.scss',
  host: {
    class: 'flex-col-stretch',
  },
})
export class OauthPageComponent {
  constructor(private readonly _activatedRoute: ActivatedRoute) {
    this._activatedRoute.fragment.pipe(takeUntilDestroyed()).subscribe((fragment) => {
      if (fragment) {
        const urlSearchParams = new URLSearchParams(fragment);

        // Use parsed data.
      }
    });
  }
}

ActivatedRoute를 이용해 fragment를 읽어오고, URLSearchParamsfragment를 사용하기 쉽게 변경해줬다.

하지만 이 상태만으로는 사용자 계정 정보를 알 수 없기 때문에 활용하기에 제한이 있다.

따라서 액세스 토큰과 아래 API를 이용해 사용자 정보를 불러온다.

https://www.googleapis.com/oauth2/v3/userinfo?access_token={your_access_token}

위 API를 호출하면 아래와 같은 결과를 얻을 수 있다.

{
  "sub": "",
  "name": ",
  "given_name": "",
  "family_name": "",
  "picture": "",
  "email": "",
  "email_verified": true
}

개인 정보들은 다 가렸지만, 실제로는 구글 계정 소유자의 정보가 표시될 거다.

이제 이 정보들을 이용해 로그인 처리를 하든, 새 계정을 만들든 하면 된다.


여담인데 Google Authentication, OAuth 관련 API 문서를 가끔 쓸 때마다 문서 위치를 찾는게 너무 헷갈린다.

이번에 이렇게 스니펫 화를 해놨으니 한 동안 문서 찾아 헤맬 일은 없겠지만 문서 위치 찾느라 조금 고생했다.

심지어 사용자 정보를 불러오는 https://www.googleapis.com/oauth2/v3/userinfo API 관련 문서는 찾지도 못하고 다른 사람들이 쓴 글을 통해서 알게됐다.

구글 시스템이 워낙 방대하다보니 이해는 한다만 조금만 더 잘 정리돼있었으면 하는 아쉬움이 있다.