OAuth2.0でGoogleのAPIを利用する

2023.01.14
OAuth2.0でGoogleのAPIを利用する

Google Calendar APIを利用する代表的な方法は

  1. OAuth2.0
  2. サービスアカウント

を使う2種類の方法があります。



2のサービスアカウントは特定のカレンダーを利用する場合、

1のOAuth2.0はどのGoogleカレンダーを利用するか分からない場合、例えばカレンダーアプリを作るような場合に向いています。



サービスアカウントを使ってGoogle Calendar APIを使う方法については以前書いた以下の記事を良ければどうぞ!



サービスアカウントの記事を書いた後、

他APIにも応用がきき、汎用性の高いOAuth2.0を使った方法も把握しておきたいなと思いました。





OAuth2.0を使ってGoogleのAPIを利用するシーンは

  • Webサービス
  • モバイルアプリ

辺りが多そうなイメージです。



またOAuth2.0を使用する場合

Googleの認証画面 → アプリケーション

にリダイレクトさせるという手順が発生しWebサービスがイメージしやすそう、と思いました。




そのため、この記事ではNext.jsでOAuth2.0を使ってGoogleのAPIを使用する方法を書きます!



ReactでOAtuh2.0を使用する事も考えたのですが、

後述する秘匿情報のクライアントシークレットがブラウザで読み込むコードに流れてしまい第三者が把握出来てしまう可能性があります。



React/Vueのみで完結させたい場合SSRがある Next.js/Nuxt.js を使用すると良さそうです。

検証した環境

1 next 13.1.1
2 googleapis 110.0.0

1. GCPでの準備

私は今回Googleの提供するAPIの中でGoogle Calendar APIを利用したかったため、

Google Calendar APIを例に進めます。

1-1. プロジェクト作成

GCPのプロジェクト作成ページを開きプロジェクトを作成します

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

  • プロジェクト名・・自分が分かる名前
  • 場所・・自分が判別出来るようにする。「組織なし」で始めて問題なし



1-2. Google Calendar APIを有効にする

1-1.で作成したプロジェクトを選択した状態でAPIライブラリよりGoogle Calendar APIを有効にします

https://console.cloud.google.com/apis/library/calendar-json.googleapis.com



1-3. OAuthの同意画面の設定

同じく作成したプロジェクトが選択された状態で

サイドバー > 認証情報 > 認証情報を作成 > OAuthクライアントID を選択



最初にOAuthの同意画面のための設定を行う必要があります。

同意画面とは良くアプリを使っている際、目にする↓のような画面のことです。



UserType

アプリケーションによって変わってきます

  • 内部・・組織内のユーザーのみ利用可能にする(Google Workspaceを利用して組織のドメインを持っている場合)
  • 外部・・それ以外のケース

今回個人利用だったので私は「外部」を選択



アプリケーション情報を入力

アプリケーションに必要な情報を入力します。


以下の項目が必須

  • アプリ名・・アプリケーションの名前(テスト的に作成する場合は「サンプルアプリ」のように適当な名前を付けて問題なし)
  • サポートメール・・一覧から選択
  • 連絡先メールアドレス・・サポートメールと同じでも問題なし



スコープの設定

「スコープを追加または削除」ボタンを押下


表示されたウィンドウで必要なスコープを選択します

今回カレンダーの一覧を取得するため auth/calendar.readonlyにチェックを入れました。



テストユーザー

開発中は「テストモード」となりテストユーザーのみが選択したAPIにアクセス出来ます


今回のケースでいくと、Google Calendar APIを利用したいGoogleアカウントを入力します。


例えば複数アカウントを使っていてそれら全てのカレンダーにアクセスしたい場合、その複数のアカウント情報を入力する必要があります。



1-4. OAuthクライアントIDを作成

OAuthの同意画面の設定が完了した後、

再度サイドバー > 認証情報 > 認証情報を作成 > OAuthクライアントID を選択



すると今度は「OAuthクライアントIDの作成」画面が表示されます

  • アプリケーションの種類を選択
  • 名前を入力
  • 承認済みのJavaScript生成元・承認済みのリダイレクト URIは分かっていれば入力、後ほど追加も可能
    • 今回私はhttp://localhost:3000/callback にリダイレクトして戻ってくるためこちらを入力しました

クライアントID・クライアントシークレットが作成されます!

これらの値は後ほど使用する重要な値になります。

また、これらの情報も含めたjsonファイルがDL出来るためDLしておきます。


クライアントID・クライアントシークレットは後ほどDL・再発行 可能です

Webアプリケーションの準備

GCPでAPIを利用するための準備が出来たので、

それを利用するためのWebアプリケーションを準備します。



前述した通り、Next.jsのアプリケーションを作成します。


本題からそれるので簡単に

# プロジェクトを作成したいフォルダで以下のコマンドを実行
# my-app という名前でプロジェクトが作成される
$ yarn create next-app
・・・
✔ What is your project named? … my-app
✔ Would you like to use TypeScript with this project? … Yes
✔ Would you like to use ESLint with this project? … Yes

# プロジェクトフォルダに移動
$ cd my-app

# reactの開発環境を起動
$ yarn run dev



Next.jsの開発環境が起動しました!



2. Google Calendar APIをOAuth2.0で利用する

ここではサーバ部分にNext.jsのAPIを利用しますが、他FWのREST APIなどに読み替えて問題ありません。


以下のような流れを作っていきます!


  1. フロントで「認証する」ボタンを押下
  2. サーバでGoogle認証用画面のURLを組み立てフロントに返す
  3. フロントからGoogle認証のページに遷移
  4. Google認証のページから認証コードを付けた状態でフロントにリダイレクト
  5. 認証コードを合わせてサーバに送る
  6. 該当ユーザーのアクセストークン・リフレッシュトークンなどを取得しフロントに返す
  7. アクセストークンを用いてGoogle Calendar APIにアクセスする



道のりは長いですが、

6.のアクセストークン・リフレッシュトークンが取得出来れば、後はよくある認証方式です。

期限が切れてないアクセストークンを使ってGoogleのAPIにアクセスすればOKです!


2-1. 認証ボタンの用意

トップページに認証ボタンを表示します

pages/index.tsx
export default function Home() {
  const handleOnClick = async () => {
    // TODO: 認証ボタン押下時の処理
  }

  return (
    <div>
      <button onClick={handleOnClick}>認証する</button>
    </div>
  )
}



2-2. サーバでGoogle認証用画面のURLを組み立てフロントに返す

Google APIを利用するためのライブラリを追加

$ yarn add googleapis



API側(Next.jsのAPI)にGoogle認証画面のURLを生成し返せるようにします

pages/api/generate-google-oauth-url.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { google } from 'googleapis'

type Response = {
  authorizeUrl: string
}

// Google認証画面のURLを生成しレスポンスに含める
export default function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
  const oauth2Client = new google.auth.OAuth2({
    clientId: '<クライアントID>',
    clientSecret: '<クライアントシークレット>',
    redirectUri: 'http://localhost:3000/callback',
  })

  // Google認証画面のURLを生成する
  const authorizeUrl = oauth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: 'https://www.googleapis.com/auth/calendar.readonly',
  })

  res.status(200).json({ authorizeUrl })
}



フロント側ではNext.jsのAPIをコールする際にaxiosを使いたいので追加

$ yarn add axios



「認証する」ボタン押下時に

先ほどのGoogle認証画面生成APIを呼び出し、受け取ったページのURLを別タブで開きます。

pages/index.tsx
import axios from 'axios'

export default function Home() {
  const handleOnClick = async () => {
    const response = await axios.get('http://localhost:3000/api/generate-google-oauth-url')
    const { authorizeUrl } = response.data

    // Google認証ページを別タブで開く
    window.open(authorizeUrl, '_blank')
  }

  return (
    <div>
      <button onClick={handleOnClick}>認証する</button>
    </div>
  )
}

すると、良く目にする↓のようなGoogle認証画面が表示出来ます!



2-3. Google認証のページから認証コードを付けた状態でフロントにリダイレクト

Google認証画面を進めていくと最終的にredirectUriで設定したページに 「認証コード」 と呼ばれる値がgetパラメータで付与された状態でリダイレクトして戻ってきます


以下のようなURLで戻ってくる

http://localhost:3000/callback?code=<認証コード>&scope=<許可したGoogleのAPI>



認証コードはこの後のトークンを取得する際に使用するため、getパラメータから取り出せるようにしておきます。

pages/callback.tsx
import { useRouter } from 'next/router'
import { useEffect } from 'react'

export default function Callback() {
  const router = useRouter()

  useEffect(() => {
    const fn = async () => {
      // getパラメータの認証コードを取得する
      const code = router.query.code
      if (!code) return

      console.log('code: ', code)
    }

    fn().then()
  }, [router.query.code])

  return <div></div>
}



2-4. 認証コードをサーバに送りトークンを取得する

認証コードを元にアクセストークンなどが取得出来ます。



まず、API側に認証コードからトークンを取得するエンドポイントを作成

pages/api/get-google-auth-token.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { google } from 'googleapis'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { authorizationCode } = req.body

  const oauth2Client = new google.auth.OAuth2({
    clientId: '<クライアントID>',
    clientSecret: '<クライアントシークレット>',
    redirectUri: 'http://localhost:3000/callback',
  })

  const response = await oauth2Client.getToken(authorizationCode)
  res.status(200).json({ tokens: response.tokens })
}



フロント側で↑のAPIを呼び出してトークン関連の値を取得

pages/callback.tsx
import axios from 'axios'
import { useRouter } from 'next/router'
import { useEffect } from 'react'

export default function Callback() {
  const router = useRouter()
  useEffect(() => {
    const fn = async () => {
      const code = router.query.code
      if (!code) return

      const response = await axios.post('http://localhost:3000/api/get-google-auth-token', {
        authorizationCode: code,
      })
      // tokens = {
      //   access_token: "ya29.abcdefghijk...",
      //   expiry_date: 1673545804055,
      //   refresh_token: "1//0e6-abcdefghijk...",
      //   scope: "https://www.googleapis.com/auth/calendar.readonly",
      //   token_type: "Bearer",
      // }
      const { tokens } = response.data
      console.log('tokens: ', tokens)
    }

    fn().then()
  }, [router.query.code])

  return <div></div>
}

トークンが無事取得出来ました!




ここまで出来ればあとは、Webアプリケーションの場合

  • LocalStorage
  • cookie

などにこれらトークンの値を保存。

必要に応じて使用、「リフレッシュトークンがあればログイン出来ている」といった判断も出来るようになりますね!



2-5. カレンダーの情報を取得

最後にGoogleのAPIを試す上でGoogleカレンダーの情報を取得してみます。



API側にアクセストークンを受け取ってカレンダーの一覧を取得し返すエンドポイントを追加

pages/api/get-google-calendar.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { google, calendar_v3 } from 'googleapis'
import Calendar = calendar_v3.Calendar

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { accessToken } = req.body

  const oauth2Client = new google.auth.OAuth2({
    clientId: '<クライアントID>',
    clientSecret: '<クライアントシークレット>',
    redirectUri: 'http://localhost:3000/callback',
  })

  // 認証情報を設定
  // tokens として受け取った access_token・refresh_token・expiry_date などが設定可能
  // access_token が最低限必須
  oauth2Client.setCredentials({ access_token: accessToken })

  // Googleカレンダーの一覧を取得する
  const calendar: Calendar = google.calendar({ version: 'v3', auth: oauth2Client })
  const calendarResponse = await calendar.calendarList.list()

  // カレンダーの一覧をレスポンスに含める
  res.status(200).json({ items: calendarResponse.data.items })
}



重要なのが2-4.で取得したアクセストークンを設定している以下の部分

// 認証情報を設定
// access_token が最低限必須
oauth2Client.setCredentials({ access_token: accessToken })

期限が切れていないアクセストークンさせ設定すれば、scopeで設定したGoogleのAPIへアクセス出来るようになります!




フロント側では

受け取ったレスポンスの値をstateに保存して値があればmapで回してidを表示する、としています。

any[]を使って無理矢理感がありますね 😂

pages/callback.tsx
import axios from 'axios'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'

export default function Callback() {
  const [calendars, setCalendars] = useState<any[]>([])
  const router = useRouter()

  useEffect(() => {
    const fn = async () => {
      const code = router.query.code
      if (!code) return

      const response = await axios.post('http://localhost:3000/api/get-google-auth-token', {
        authorizationCode: code,
      })
      // tokens = {
      //   access_token: "",
      //   expiry_date: 1673545804055,
      //   refresh_token: "",
      //   scope: "https://www.googleapis.com/auth/calendar.readonly",
      //   token_type: "Bearer",
      // }
      const { tokens } = response.data

      // カレンダーの一覧を取得しstateに保存する
      const res = await axios.post('http://localhost:3000/api/get-google-calendar', {
        accessToken: tokens.access_token,
      })
      setCalendars(res.data.items)
    }

    fn().then()
  }, [router.query.code])

  return (
    <div>
      <ul>
        {calendars.map((calendar: any) => {
          return <li key={calendar.id}>{calendar.id}</li>
        })}
      </ul>
    </div>
  )
}

無事Google Calendar APIと接続が出来て、カレンダーの一覧が取得・表示出来るようになりました!




今回はGoogle Calendar APIとの接続でしたが、

Oauth2.0の仕組みさえ理解出来れば他のGoogle APIにも応用が効きます。


またアクセストークン、リフレッシュトークンを用いたOAuthの仕組みの理解、

というところでもGoogleのAPIを使うのはとっつきやすいな、というのが所感でした!

おすすめ