たかぎとねこの忘備録

プログラミングに関する忘備録を自分用に残しときます。マサカリ怖い。

Google OAuth2.0認証時に取得したアクセストークンの有効期限が切れてしまい、Google APIにアクセスできなくなったときの対処法

remix-authremix-auth-googleを使ってRemixアプリケーションでGoogleアカウントでのログインを実装するとaccessTokenrefreshTokenを取得することができる。

今まではaccessTokenのみにしか注目しておらず、refreshTokenについては全く触らずに開発を行なってきた。

しかし、そのaccessTokenを使用してGoogle APIを操作しようとする際に、アクセストークンの取得から1時間以上経過したことによりGoogle APIを操作することができなくなってしまった。

この問題を調べていたときに、Remixでアクセストークンの更新処理を説明している記事をほとんど見かけなかったので、今回はTypeScriptを使ってその処理の実装方法について説明する。

リフレッシュトークンとは

リフレッシュトークンは有効期限が切れたaccessTokenを再発行するときに使用するトークンのことです。

リフレッシュトークンを利用するとユーザーに再ログインをしてもらわなくても新しいトークンを取得できます。 ユーザーに再ログインを促すことなく、アクセストークンの有効期限以上の長期間にわたってアクセストークンを利用したい場合などに活用できると思います。

【Auth0】リフレッシュトークンの取得方法と使い方 - Qiita

リフレッシュトークンを取得する際の注意点

リフレッシュトークンは初回の認証時にしか受け取ることができない。

The refresh_token is only provided on the first authorization from the user. Subsequent authorizations, such as the kind you make while testing an OAuth2 integration, will not return the refresh_token again.

gdata - Not receiving Google OAuth refresh token - Stack Overflow

remix-auth-googleを使ってストラテジーを登録する際には、accessTypeofflineに設定しないとリフレッシュトークンを取得することができない。そして、初回の認証以降もrefreshTokenが必要な場合はpromptconsentに設定しないといけない。

online accessの場合はrefresh tokenは発行されない。

OAuthのoffline_accessについて - OAuth.jp

authenticatorに対してストラテジーを登録する処理は次のようになる。

// app/auth.server.ts

import { Authenticator } from "remix-auth";
import { GoogleStrategy } from "remix-auth-google";
import { sessionStorage } from "./session.server";

export type User = {
  ...
  accessToken: string;
  refreshToken: string;
};

export let authenticator = new Authenticator<User>(sessionStorage);

const googleClientId = process.env.GOOGLE_CLIENT_ID ?? "";
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET ?? "";
const googleCallbackURL = process.env.GOOGLE_CALLBACK_URL ?? "";
if (!googleClientId || !googleClientSecret || !googleCallbackURL) {
  throw new Error(
    "GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET and GOOGLE_CALLBACK_URL must be provided"
  );
}

const scope = ["OAuth2.0スコープ"].join(" ");
authenticator.use(
  new GoogleStrategy(
    {
      clientID: googleClientId,
      clientSecret: googleClientSecret,
      callbackURL: googleCallbackURL,
      scope,
      includeGrantedScopes: true,
      accessType: "offline",
      prompt: "consent",
    },
    async ({ accessToken, refreshToken, extraParams, profile }) => {
    // 処理を記述
 return {
      ...
      "accessToken": accessToken,
      "refreshToken": refreshToken ?? "",
    };
  }), 
  "google"
);

このとき、コールバック関数の返り値のフィールドに必ずaccessTokenrefreshTokenを返すようにして、セッションに保存するようにしてください。

後でアクセストークンを再発行する処理を実装する際にUserオブジェクトからrefreshTokenを取得する必要があるからです。

sessionStoragecreateCookieSessionStorageを使用して各自で実装することをおすすめします。

リフレッシュトークンを再発行するための関数を実装

アクセストークンを再発行する際に使用するリクエストエンドポイントはhttps://www.googleapis.com/oauth2/v4/tokenになります。

こちらのエンドポイントに対してPOSTメソッドでリクエストを送信します。

www.tate-blog.com

実装は次のようになります。

// app/utils/google/retrieveAccessTokenFromRefreshToken.ts

export async function retrieveAccessTokenFromRefreshToken(
  refreshToken: string
): Promise<
  | {
      access_token: string;
      expires_in: number;
      scope: string;
      token_type: string;
      id_token: string;
    }
  | {
      error: string;
      error_description: string;
    }
> {
  const bodyObj: { [key: string]: string } = {
    refresh_token: refreshToken,
    client_id: process.env.GOOGLE_CLIENT_ID ?? "",
    client_secret: process.env.GOOGLE_CLIENT_SECRET ?? "",
    redirect_uri: process.env.GOOGLE_CALLBACK_URL ?? "",
    grant_type: "refresh_token",
    access_type: "offline",
  };
  const body = Object.keys(bodyObj)
    .map((key) => key + "=" + encodeURIComponent(bodyObj[key]))
    .join("&");

  const response = await fetch(
    `https://www.googleapis.com/oauth2/v4/token`,
    {
      method: "post",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body,
    }
  );
  const data = await response.json();
  return data;
}

bodyに渡す内容として、認証に使用するOAuth2.0 クライアントIDクライアントIDクライアントシークレット、そしてリダイレクトURIを指定してください。

それ以外には、gran_typeとしてrefresh_tokenを、access_typeofflineを指定します。

developers.google.com

この関数を呼び出す際に、先ほどセッションに保存したrefreshTokenを取り出して呼び出してください。

const authUser = await authenticator.isAuthenticated(request);
if (!authUser || !authUser.refreshToken) {
  // 認証できなかったときの処理
}
const res = await retrieveAccessTokenFromRefreshToken(
  authUser.refreshToken
);

まとめ

remix-auth-googleのREADME.mdにaccessTokenrefreshTokenなどの使い方や取得方法が書いていなかったのと、オンライン上で探してもJavaPHPPythonなどでの実装方法しか見かけることがなかった。 なので、もしTypeScriptやRemixを利用していて、同じような問題にぶつかった人の一助になればと思う。

その他の参考にした記事やドキュメント

zudoh.com

qiita.com

qiita.com

github.com