ロゴテキスト ロゴ

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

    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でOAuth2.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

    作成したプロジェクトでGoogle Calendar APIを有効にする


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

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

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

    OAuthクライアントIDを選択する


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

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

    Google認証の同意画面


    UserType

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

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

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



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

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


    以下の項目が必須

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


    スコープの設定

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

    スコープを追加または削除ボタンを押下しスコープの設定画面に

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

    使用するAPIに合わせてスコープを選択する

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



    テストユーザー

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

    テストユーザーを追加する

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


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



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

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

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

    OAuthクライアントIDを選択する


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

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

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

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

    クライアント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の開発環境が起動しました!

    Reactの開発環境が起動された


    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認証画面が表示出来ます!

    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>
      )
    }

    カレンダーのID一覧が表示出来るようになった

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




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

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


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

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

    プロフィールの背景画像 プロフィール画像
    Yuki Takara
    都内でフリーランスのエンジニアをやってます。フロントとアプリ開発メインに幅広くやってます。