본문 바로가기
dev/web

Google 3rd Party oAuth2 device 구현하기

by NCJ 2022. 7. 21.
728x90
반응형
SMALL

Google Oauth2 Device Login 

 

이번에 Smart TV 앱을 React로 개발을 진행하면서 3rd Party 기능을 추가하였습니다. 
3rd Party 기능을 추가하면서 각 Google, Facebook, Amazon Device 3rd Party 구현을 정리해 보도록 하겠습니다. 
 
TV, 게임 콘솔, 프린터와 같은 기기에서 실행되는 애플리케이션을 통해 OAuth 2.0 승인을 구현하여 Google API에 액세스 하는 방법을 설명합니다.

 

STEP 1 - oAuth2.0 클라이언트 ID 발급

클라이언트 ID 발급을 위해 Google API Service 사이트로 이동합니다. 

https://console.cloud.google.com/apis/

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com

왼쪽 메뉴에 보면 사용자 인증 정보 메뉴로 이동 후 사용자 인증 정보 만들기 버튼을 눌러 클라이언트 ID를 발급합니다.

구글 사용자 인증 정보 클라이언트 ID 발급

OAuth 클라이언트 ID 만들기 버튼을 눌러 다음 페이지로 이동합니다. 

구글 사용자 인증 정보 클라이언트 ID 발급

애플리케이션 유형을 TV 및 입력 제한 기기를 선택합니다.

구글 사용자 인증 정보 클라이언트 ID 발급

V 및 입력 제한 기기 선택 후 이름을 설정하고 만들기 버튼을 통해 생성합니다.

생성이 완료되면 아래처럼 팝이 뜹니다.

구글 사용자 인증 정보 클라이언트 정보

팝업에서 나오는 클라이언트 ID 값을 복사합니다.

 

STEP 2 - OAuth 2.0 액세스 토큰 가져오기

Google OAuth2.0 토큰 가져오기 오는 순서

  1. 애플리케이션에서 Google에 액세스 권한을 요청하는 범위를 식별하는 승인 서버에 요청을 보냅니다.
  2. 서버는 후속 단계에서 사용되는 기기 정보, 사용자 코드 등 몇 가지 정보를 통해 응답합니다.
  3. 사용자가 앱을 승인하기 위해 별도의 기기에 입력할 수 있는 정보를 표시합니다.
  4. 애플리케이션에서 Google의 인증 서버를 폴링 하여 사용자가 앱을 승인했는지 확인합니다.
  5. 사용자가 입력 기능이 더 풍부한 기기로 전환한 후 웹브라우저를 실행하고 3단계에 표시된 URL로 이동하여 3단계에도 표시되는 코드를 입력합니다. 그러면 사용자는 애플리케이션에 대한 액세스 권한을 부여 (또는 거부)할 수 있습니다.
  6. 폴링 요청에 대한 다음 응답에는 앱에서 사용자를 대신하여 요청을 승인하는 데 필요한 토큰이 포함됩니다. (사용자가 애플리케이션에 대한 액세스를 거부한 경우 응답에 토큰이 포함되지 않습니다.)

아래의 OAuth2.0  프로세스 참조

Google Device OAuth2.0 Process

1) 기기

사용자를 대신하여 Google 인증서버에 POST 요청을 보냅니다. 인증서버는 클라이언트 ID를 식별하여 Device에서 사용할  코드를 보내 줍니다.

 

[매개변수 정보]

client_id (필수)
- 애플리케이션의 클라이언트 ID입니다. 이 값은 API Console Credentials page에서 확인할 수 있습니다.

 

scope (필수)

- 애플리케이션이 사용자를 대신하여 액세스 할 수 있는 리소스를 식별하는 공백으로 구분된 범위 목록입니다. 이러한 값은 Google에서 사용자에게 표시하는 동의 화면에 알립니다. 설치된 앱 또는 기기는 허용된 범위 목록을 참조하세요.

범위를 사용하면 애플리케이션에서 필요한 리소스에 대한 액세스만 요청할 수 있으며, 사용자가 애플리케이션에 부여하는 액세스 양을 제어할 수 있습니다. 따라서 요청된 범위 수와 사용자 동의 획득 가능성 사이에는 반비례적인 관계가 있습니다.

[Request 정보]

POST /device/code HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded

client_id=client_id&scope=

 

 

해당 요청을 react에서 구현하도록 하겠습니다. 

GOOGLE_CLIENT_ID 값은 앞에서 발급받은 클라이언트 ID 값을 넣어준다. 

/**
 * get Device Google Auth Code
 * @returns {Promise<void>}
 */
const getGoogleAuthCode = async () => {
  const formData = new FormData();
  formData.append("client_id", GOOGLE_CLIENT_ID); // 발급 받은 클라이언트 ID값
  formData.append("scope", "email"); // 엑세스할 리소스 식별 범위
  const response = await axios.post(`${googleAuthServerHost}/device/code`, formData, {headers: {'content-type': 'application/x-www-form-urlencoded'}})
  if (response.status === 200) {
    setAuthCode(response.data.user_code);
    setDeviceLoginDomain(response.data.verification_url);
    doStartGoogleAuthPolling(response.data.interval, response.data.device_code);
  }
}

[성공 응답]

device_code

- 승인을 요청하는 앱을 실행하는 기기를 식별하기 위해 Google에서 고유하게 할당하는 값입니다. 사용자가 더 풍부한 입력 기능을 갖춘 다른 기기에서 기기를 승인합니다. 예를 들어 사용자는 TV에서 실행 중인 앱을 승인하기 위해 노트북이나 휴대전화를 사용할 수 있습니다. 이 경우 device_code은 TV를 식별합니다.
이 코드를 사용하면 앱을 실행하는 기기가 사용자가 액세스 권한을 부여했는지 또는 거부했는지 안전하게 판단할 수 있습니다.

expires_in

- device_code 및 user_code가 유효한 시간(초)입니다. 그렇게 해도 사용자가 승인 절차를 완료하지 않고 기기에서 사용자의 결정 정보를 검색하기 위해 설문조사도 하지 않는 경우 1단계부터 이 프로세스를 다시 시작해야 할 수도 있습니다.

interval

- 기기에서 폴링 요청 간에 대기해야 하는 시간(초)입니다. 예를 들어 값이 5이면 기기에서 5초마다 폴링 요청을 Google 승인 서버에 전송해야 합니다. 자세한 내용은 3단계를 참고하세요

user_code

- 애플리케이션에서 액세스를 요청하는 범위를 Google에 식별하는 대소문자를 구분하는 값입니다. 사용자 인터페이스에서는 사용자에게 입력 기능이 더 풍부한 별도의 기기에 이 값을 입력하도록 지시합니다. 그런 다음 이 값을 사용하여 사용자에게 애플리케이션 액세스 권한을 부여하라는 메시지를 표시할 때 올바른 범위 집합을 표시합니다.

verification_url

- 사용자가 별도의 기기에서 user_code를 입력하고 애플리케이션에 대한 액세스를 부여하거나 거부해야 하는 URL입니다. 사용자 인터페이스에도 이 값이 표시됩니다.

{
  "device_code": "4/4-GMMhmHCXhWEzkobqIHGG_EnNYYsAkukHspeYUk9E8",
  "user_code": "GQVQ-JKEC",
  "verification_url": "https://www.google.com/device",
  "expires_in": 1800,
  "interval": 5
}

응답 값으로 전달 받은 user_code, verification_url은 사용자에게 표현해 준다. 

 

React Device Auth Code Page

이제 사용자는 인증 URL을 접속하여 인증코드를 입력 후 google 로그인을 시도한다.

로그인 후 개발하는 웹에서 확인하는 방법 폴링 방식을 통해 일정 주기마다 구글 인증서버에 확인을 요청한다. 

 

웹에서 사용자 로그인 정보를 폴링하여 로그인 후 사용자 토큰 조회한다. 

[매개변수 정보]

client_id

- 애플리케이션의 클라이언트 ID입니다. 이 값은 API Console Credentials page에서 확인할 수 있습니다.
client_secret

- 제공된 client_id의 클라이언트 보안 비밀 이 값은 API Console Credentials page에서 확인할 수 있습니다.
device_code

- 2단계의 승인 서버에서 반환한 device_code
grant_type

- 이 값을 urn:ietf:params:oauth:grant-type:device_code로 설정합니다.

POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded

client_id=client_id&
client_secret=client_secret&
device_code=device_code&
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code

react에서 일정 주기마다 사용자 정보를 조회하는 코드를 구현해 보자.

client_id는 발급 받은 클라이언트 ID 값을 넣습니다. 

client_secret는 발급 받은 보안 비밀번호를 넣습니다. 

/**
 * Google Device Login Polling Access
 * @param interval 구글 인증서버에 요청 주기
 * @param deviceCode 인증 식별 기기 코드
 */
const doStartGoogleAuthPolling = (interval, deviceCode) => {
  if (googlePolling === null) {
    googlePolling = setInterval(async () => {
      if (deviceCode) {
        const formData = new FormData();
        formData.append("client_id", GOOGLE_CLIENT_ID)
        formData.append("client_secret", GOOGLE_SEC_KEY)
        formData.append("device_code", deviceCode)
        formData.append("grant_type", `urn:ietf:params:oauth:grant-type:device_code`)
        try {
          const response = await axios.post(`${googleAuthServerHost}/token`, formData, {headers: {'content-type': 'application/x-www-form-urlencoded'}})
          if (response.status === 200) {
            clearInterval(doStartGoogleAuthPolling);
            // 사용자 토큰 정보 획득
            const access_token = response.data.access_token;
            const expires_in = response.data.expires_in;
            const refresh_token = response.data.refresh_token;
            const scope = response.data.scope;
            const token_type = response.data.token_type;

            // 획득한 토큰을 통해 Google API 사용이 가능합니다.

          }
        } catch (error) {
          console.log(error)
        }
      }
    }, interval * 1000)
  }
}

처음 코드생성후  interval 값을 통해 일정 주기마다 setInterval을 수행하면서 구글 인증서버에 사용자 인증 확인을 체크합니다. 

 

이제 사용자는 사용자 인증코드를 https://www.google.com/device 링크를 통해 입력 후 합니다. 

Google Device Auth Page
Google Device Auth Page
Google Device Auth Page

각 상태에 따른 응답값 

승인 완료

사용자가 기기의 대한 인증코드 입력 후 승인 완료된 경우 

{
  "access_token": "1/fFAGRNJru1FTz70BzhT3Zg",
  "expires_in": 3920,
  "scope": "openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
  "token_type": "Bearer",
  "refresh_token": "1/xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI"
}

액세스 거부됨
사용자가 기기에 대한 액세스 권한 부여를 거부하는 경우 서버 응답에 403 HTTP 응답 상태 코드(Forbidden)가 있습니다. 응답에 다음 오류가 포함됩니다.

{
  "error": "access_denied",
  "error_description": "Forbidden"
}


승인 대기 중
사용자가 아직 승인 과정을 완료하지 않은 경우 서버에서 428 HTTP 응답 상태 코드(Precondition Required)를 반환합니다. 응답에 다음 오류가 포함되어 있습니다.

{
  "error": "authorization_pending",
  "error_description": "Precondition Required"
}


설문조사가 너무 많음
기기에서 폴링 요청을 너무 자주 전송하면 서버에서 403 HTTP 응답 상태 코드(Forbidden)를 반환합니다. 응답에는 다음 오류가 포함됩니다.

{
  "error": "slow_down",
  "error_description": "Forbidden"
}


기타 오류
또한 조사 요청에 필수 매개변수가 누락되었거나 매개변수 값이 잘못된 경우에도 인증 서버가 오류를 반환합니다. 이러한 요청은 일반적으로 400(Bad Request) 또는 401 (Unauthorized) HTTP 응답 상태 코드를 갖습니다. 이러한 오류의 예는 다음과 같습니다.

 

invalid_client 401 OAuth 클라이언트를 찾을 수 없습니다. 예를 들어 client_id 매개변수 값이 잘못된 경우 이 오류가 발생합니다.
invalid_grant 400 code 매개변수 값이 잘못되었습니다.
unsupported_grant_type 400 grant_type 매개변수 값이 잘못되었습니다.

 

사용자가 정상적으로 로그인 후 폴링을 통해 정보를 획득하여 토큰 정보를 가지고 올 수 있습니다. 

이제 토큰 정보를 통해 각 Google API를 사용할 수 있습니다. 

 

 

허용된 범위

기기의 OAuth 2.0 흐름은 다음 범위에서만 지원됩니다.

OpenID Connect, Google 로그인

  • email
  • openid
  • profile

드라이브 API

YouTube API

마지막으로  react에서 Google Device OAuth 2.0 구현 전체 소스입니다. 

import './App.css'
import {useEffect, useState} from "react";
import axios from "axios";

function App() {
  const googleAuthServerHost = "https://oauth2.googleapis.com";
  const [deviceLoginDomain, setDeviceLoginDomain] = useState("")
  const [authCode, setAuthCode] = useState("");
  const GOOGLE_CLIENT_ID = '발급된 클라이언트 ID';
  const GOOGLE_SEC_KEY = '발급된 보안 비밀 번호';
  let googlePolling = null;

  useEffect(() => {
    getGoogleAuthCode();
  }, [])

  /**
   * get Device Google Auth Code
   * @returns {Promise<void>}
   */
  const getGoogleAuthCode = async () => {
    const formData = new FormData();
    formData.append("client_id", GOOGLE_CLIENT_ID); // 발급 받은 클라이언트 ID값
    formData.append("scope", "email"); // 엑세스할 리소스 식별 범위
    const response = await axios.post(`${googleAuthServerHost}/device/code`, formData, {headers: {'content-type': 'application/x-www-form-urlencoded'}})
    if (response.status === 200) {
      setAuthCode(response.data.user_code);
      setDeviceLoginDomain(response.data.verification_url);
      doStartGoogleAuthPolling(response.data.interval, response.data.device_code);
    }
  }

  /**
   * Google Device Login Polling Access
   * @param interval 구글 인증서버에 요청 주기
   * @param deviceCode 인증 식별 기기 코드
   */
  const doStartGoogleAuthPolling = (interval, deviceCode) => {
    if (googlePolling === null) {
      googlePolling = setInterval(async () => {
        if (deviceCode) {
          const formData = new FormData();
          formData.append("client_id", GOOGLE_CLIENT_ID)
          formData.append("client_secret", GOOGLE_SEC_KEY)
          formData.append("device_code", deviceCode)
          formData.append("grant_type", `urn:ietf:params:oauth:grant-type:device_code`)
          try {
            const response = await axios.post(`${googleAuthServerHost}/token`, formData, {headers: {'content-type': 'application/x-www-form-urlencoded'}})
            if (response.status === 200) {
              clearInterval(doStartGoogleAuthPolling);
              // 사용자 토큰 정보 획득
              const access_token = response.data.access_token;
              const expires_in = response.data.expires_in;
              const refresh_token = response.data.refresh_token;
              const scope = response.data.scope;
              const token_type = response.data.token_type;

              // 획득한 토큰을 통해 Google API 사용이 가능합니다.

            }
          } catch (error) {
            console.log(error)
          }
        }
      }, interval * 1000)
    }
  }

  return (
    <div className="App">
      <h1>인증 URL</h1>
      <p>{deviceLoginDomain}</p>
      <h1>인증 코드</h1>
      <p>{authCode}</p>
    </div>
  );
}

export default App;

다음에 Facebook, Amazon Device 인증도 정리 후 올리도록 하겠습니다. 

해당 인증은 Device(Smart TV, 입력이 제한된 기기)에서 인증이 필요할 경우 사용할 수 있습니다. 

감사합니다. 

 

728x90
반응형
LIST