メインコンテンツまでスキップ

ReactでGoogle認証

· 約20分

本記事の内容

本記事はReactでユーザのGoogleアカウントでGoogleにログインする方法をご紹介します。

Googleにログインすると、例えば以下のことが出来るようになります。

  • Google APIを使ってユーザーの情報を取得できるようになる。
  • 本人確認ができるため自前の認証の仕組みを実装しなくてよくなる。 (注1)

今回はGoogleにログインし、その認証情報を使ってユーザの基本的な情報をGoole APIで取得するまでをご紹介します。

記事の内容
  • GCPで認証情報設定
  • ReactでユーザのGoogleアカウントでGoogleにログインしてもらう
  • ReactでGoogle APIの呼び出し
注意

(注1) サーバ側でGoogle APIを使って本人確認する必要があります。

GCPで認証情報設定

まずは、あなたのアプリをGoogleに登録して、Client IDClient Secretを取得する必要があります。

Client IDClient Secretは、Google OAuth2

ヒント

開発者(あなた)はGoogleアカウントを持っている必要があります。以下ではGoogleアカウントを持っている事を前提に話を進めます。

  1. まず、Google Cloud Platformに行きます。

  2. 左上のメニューをクリック

    gcp-auth-settings-1

  3. 「APIとサービス」をクリック

    gcp-auth-settings-1

  4. 「OAuthの同意画面」をクリック

    gcp-auth-settings-1

  5. 「アプリ名」、「ユーザーサポートメール」、「デベロッパーの連絡先情報」に入力して「保存して次へ」をクリック

    gcp-auth-settings-1

    gcp-auth-settings-1

  6. 「スコープを追加または削除」をクリックして、使用するGoogle APIを選択する

    gcp-auth-settings-1

  7. 「ADD USERS」をクリックして、テストユーザーを追加する

    gcp-auth-settings-1

  8. 「認証情報」をクリック

    gcp-auth-settings-1

  9. 「認証情報作成」をクリック

    gcp-auth-settings-1

  10. 「OAuthクライアントID」をクリック

    gcp-auth-settings-1

  11. 「アプリケーションの種類」で「ウェブアプリケーション」を選択

    gcp-auth-settings-1

  12. 「名前」で任意のアプリ名を入力する

    gcp-auth-settings-1

  13. 「承認済みリダイレクトURI」で「URIを追加」をクリック

    gcp-auth-settings-1

  14. 「http://localhost:3000/auth_code」を入力

    gcp-auth-settings-1

  15. ダイアログに記載されている「クライアントID」と「クライアントシークレット」をメモる

    gcp-auth-settings-1

これでClient IDClient Secretの取得は完了です。

OAuth2の認可の流れ

アプリの実装にはいる前にOAuth2の認可のフローを簡単に確認しておきましょう。

Googleの認可フローはOAuth2の認可フローに従っています。この認可フローでアクセストークンを取得するには、以下の2つのステップを踏む必要があります。

ヒント

アクセストークンを取得する手順

  1. codeを取得
  2. アクセストークンやJWTを取得する

それぞれ詳しくみてみましょう。

1. codeを取得

ユーザのGoogleアカウントでGoogleにログインしてもらい、ユーザの情報にアクセスする許可をユーザからもらうステップです。アプリがGoolgeの認可用のURLにリダイレクトし、Googleのログイン画面を表示させます。

login-screen.jpg

ログインが完了するとGoogleがアプリにリダイレクトします。ここでは上記で設定した http://localhost:3000/auth_code に戻ってきます。このときのURLのパラメータにcodeが付いてくるので、これを取得しておきます。後でcodeとアクセストークンを交換する際に使用します。

2. アクセストークンを取得する

codeとアクセストークンを交換するステップです。アクセストークン取得用のURLにcodeを付けてリクエストするとアクセストークンを取得することが出来ます。

まずはブラウザとCurlでやってみよう

ReactでGoogleのアクセストークンを取得する前に、認可の流れを簡単に理解するためにブラウザとcurlを使ってアクセストークンを取得してみましょう。

codeの取得

codeを取得するにはブラウザ相当のものが必要です。ここではブラウザのURL欄に直接URLを入力してcodeを取得します。

アクセス先URLは以下です。

https://accounts.google.com/o/oauth2/auth?client_id={clientId}&redirect_uri={redirectUri}&scope={scope}&response_type={responseType}&approval_prompt={approvalPrompt}&access_type={accessType}

それぞれのパラメータには以下の表の値を代入してください。Client IDClient SecretはGCPで取得した値です。

名前
clientIdあなたが取得したClient ID
clientSecretあなたが取得したClient Secret
scope'profile email'
responseType'code'
approvalPrompt'force'
accessType'offline'
redirectUri'http://localhost:3000/auth-code'

ログインが完了すると http://localhost:3000/auth-code にリダイレクトされます。ブラウザのURL欄にはcodeパラメータが入っているので、この値をどこかにコピペをしてください。

get-code

アクセストークンの取得

次のステップはアクセストークンの取得です。先ほど取得したcodeを使用してcurlコマンドで取得することが出来ます。以下のようにシェルスクリプト化しておくと便利でしょう。

code=$1 // 先ほど取得したcode
clientId='???'
clientSecret='???'
scope='https://www.googleapis.com/auth/userinfo.email'
responseType='code'
approvalPrompt='force'
accessType='offline'
redirectUri='http://localhost:3000/auth-code'

curl \
--data "code=${code}" \
--data "client_id=${clientId}" \
--data "client_secret=${clientSecret}" \
--data "redirect_uri=${redirectUri}" \
--data "grant_type=authorization_code" \
--data "access_type=offline" \
https://www.googleapis.com/oauth2/v4/token > token.json

このスクリプトを実行すると、token.jsonというファイルが生成されます。このファイルの中にJSON形式で認可情報が保存されいます。

アクセストークンはaccess_tokenというキーで保存されています。

{
"access_token": "...",
"expires_in": 3599,
"refresh_token": "...",
"scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email openid",
"token_type": "Bearer",
"id_token": "..."
}%

ユーザ情報の取得

アクセストークンを取得できたので、これを使ってユーザー情報を取得してみましょう。ユーザ情報を取得するGoolge APIのエンドポイントにパラメータとしてアクセストークンをくっつけてリクエストするだけです。

access_token=$(cat token.json | jq -r '.access_token')
url="https://www.googleapis.com/oauth2/v1/userinfo?access_token=${access_token}"
curl -v -H 'Content-Type: application/json' -X GET ${url}

レスポンスはJSON形式で以下のような情報が返って来れば成功です。

{
"id": "???",
"email": "???@gmail.com",
"verified_email": true,
"name": "???",
"given_name": "???",
"family_name": "???",
"picture": "???",
"locale": "ja"
}

Reactで実装してみよう

アクセストークンの取得の方法を理解したところで、Reactでこの機能を実装してみましょう。Googleにログインし、ユーザー情報を表示するという簡単なウェブアプリを作成していきます。

処理の流れ

以下の図は処理の流れを簡単に表したものです。

screen-flow

まずログインしているかどうかを画面表示時に判断し、それによって表示する画面を変えます。ログインしていれば、アクセストークンを保持しているということなので、それを使ってGoogleからユーザー情報を取得し表示します。その画面にはログアウトボタンも設置し、そのボタンを押下した場合には、ログアウト処理をします。そうすると最初のログインしていない時の画面に切り替わります。そこにはログインボタンが設置しており、そのボタンの押下によってログイン後の画面表示に切り替わります。

用意する画面は以下の2つのみです。ログインしていない時の画面とログインしている時の画面です。

ログインしていない時の画面 screen1

ログインしている時の画面 screen2

ヘルパー関数の準備

画面を実装する前に、ログインやログアウトなどを実行するヘルパー関数を準備します。その後画面を構成するコンポーネントを作成することにしましょう。

認可に必要なパラメータ

アクセストークンを取得するAPIを呼ぶ時に必要になるパラメータを準備します。それらのパラメータをどこからでも使いやすいように1つのオブジェクトにまとめます。

const authParams = {
clientId: "???",
clientSecret: "???",
scope: "profile email",
responseType: "code",
approvalPrompt: "force",
accessType: "offline",
redirectUri: "http://localhost:3000/auth-code",
grantType: "authorization_code",
} as const;

認可情報をLocalStorageに保存する関数

このアプリの認可情報はブラウザのLocalStorageに保存することにします。こうすることでブラウザをリロードしてもログイン状態を維持できるようにします。

以下はAuthInfoを受け取り、それをLocalStorageに保存する関数の定義です。

type AuthInfo = Readonly<{
access_token: string;
refresh_token: string;
scope: string;
token_type: string;
id_token: string;
expires_in: number;
}>;

function setAuthInfo(a: AuthInfo): void {
window.localStorage.setItem(authInfoKey, JSON.stringify(a));
}

認可情報をlocalStorageから取得する関数

同様にLocalStorageから認可情報を取得する関数を定義します。

function getAuthInfo(): AuthInfo | null {
const item = window.localStorage.getItem(authInfoKey);
if (item !== null) {
return JSON.parse(item) as AuthInfo;
} else {
return null;
}
}

認可情報をlocalStorageから削除する関数

ログアウト時にLocalStorageから認可情報を削除します。その時に実行する関数を定義します。

function deleteAuthInfo(): void {
window.localStorage.removeItem(authInfoKey);
}

Googleのログイン画面に遷移する関数

Googleのログイン画面に遷移する関数を定義します。

エンドポイントは https://accounts.google.com/o/oauth2/auth です。

function requestCodeFlow(): void {
const params = {
client_id: authParams.clientId,
redirect_uri: authParams.redirectUri,
scope: authParams.scope,
response_type: authParams.responseType,
approval_prompt: authParams.approvalPrompt,
access_type: authParams.accessType,
};
const query = new URLSearchParams(params).toString();
window.location.href = `https://accounts.google.com/o/oauth2/auth?${query}`;
}

このrequestCodeFlow関数を実行するとGoogleのログイン画面が表示されますが、その後ユーザーのログインが完了すると、自動的にアプリにリダイレクトされます。そのリダイレクトURLにcodeが含まれているので、その値を取得するための関数も必要です。以下の関数はURLをパースし、codeを取得する関数です。

function getCode(): string | null {
const params = new URLSearchParams(window.location.search);
return params.get("code");
}

トークンを取得する関数

アクセストークンやJWTなどを取得するGoogle APIを実行する関数です。

エンドポイントは https://www.googleapis.com/oauth2/v4/token です。

async function getAuthToken(code: string): Promise<AuthInfo> {
const params = {
code,
client_id: authParams.clientId,
client_secret: authParams.clientSecret,
redirect_uri: authParams.redirectUri,
grant_type: authParams.grantType,
access_type: authParams.accessType,
};
const res = await axios.post(
`https://www.googleapis.com/oauth2/v4/token`,
params
);
return res.data as Promise<AuthInfo>;
}

ログアウトする関数

ログアウトするGoogle APIを実行し、その結果にかかわらず認可情報をLocalStorageから削除する関数です。

エンドポイントは https://accounts.google.com/o/oauth2/revoke です。

async function signOut(authInfo: AuthInfo | undefined): Promise<void> {
try {
if (authInfo !== undefined) {
const res = await axios.get(
`https://accounts.google.com/o/oauth2/revoke`,
{
params: {
token: authInfo.access_token,
},
}
);
if (res.status !== 200) {
return Promise.reject(Error(`Failed to sign out`));
}
}
} finally {
deleteAuthInfo();
window.location.href = "/";
}
return;
}

ユーザー情報を取得する関数

Google APIを使ってユーザ情報を取得する関数です。リクエストパラメータにアクセストークンを乗っける必要があります。

エンドポイントは https://www.googleapis.com/oauth2/v1/userinfo です。

async function getUserInfo(
authInfo: AuthInfo
): Promise<Record<string, string>> {
const res = await axios.get("https://www.googleapis.com/oauth2/v1/userinfo", {
params: {
access_token: authInfo.access_token,
},
});
return res.data;
}

画面構成の概要

では、ここからReactの画面を組み立ていきます。まず最初にコンポーネントの構成が最終的にどのようになるのかを確認します。

最終的にアプリのエントリーポイントのコンポーネント構成は以下のようになります。

function App() {
return (
<QueryClientProvider client={queryClient}>
<Authorized unauthorized={<Unauthorized />}>
<UserInfo />
<SignOut />
</Authorized>
</QueryClientProvider>
);
}

最初のタグのQueryClientProviderReact Queryを使うためのProviderです。このアプリでは非同期処理はReact Queryを使います。React QueryはReactでの非同期処理を便利にしてくれるライブラリです。ここでは詳細は説明しませんが、後で使っているところが出て来ます。

QueryClientProviderの子コンポーネントのAuthorizeタグがこのアプリの肝です。Authorizeタグはログイン済みかどうかを判定します。そしてログイン済みであれば子コンポーネントを表示し、未ログインであればunauthorizedプロパティで指定したコンポーネントを表示します。

unauthorizedプロパティで表示するコンポーネントにはログインボタンがあり、そのボタンをクリックするとログイン処理が走るようになっています。

Authorizedコンポーネントの実装

Authorizedコンポーネントは以下の2つの機能を持っています。

  • ログイン済みかどうか判定し、それに応じて表示する画面を切り替える機能
  • Googleのログイン画面からアプリにリダイレクトした時の処理

ログイン済みかどうかは、LocalStorageに認可情報が保存されているかどうかで判断します。認可情報の期限も判定した方がより丁寧かもしれませんが、これは課題として残しておきましょう。

Googleのログイン画面からアプリにリダイレクトした時は、このアプリの場合はURLのパスが/auth-codeになっています。そのパスになっている場合は、アクセストークンを取得し、LocalStorageに保存し、アプリのトップページ遷移します。

また取得したアクセストークンを子コンポーネントからでも使用できるようにContextに保存します。

type AuthorizedContext = Readonly<{
authInfo?: AuthInfo;
}>;

// 認可情報を保存するContextの生成
const AuthorizedContext = React.createContext<AuthorizedContext>({});

function Authorized({
unauthorized,
children,
}: Readonly<{
unauthorized?: React.ReactElement;
children?: React.ReactNode;
}>): React.ReactElement | null {
// LocalStorageから認可情報取得
const authInfo = getAuthInfo();

React.useEffect(() => {
// Googleのログイン画面からアプリにリダイレクトした時の処理
if (window.location.pathname === "/auth-code") {
// codeの取得
const code = getCode();
if (code != null) {
// アクセストークンの取得
getAuthToken(code)
.then((token) => {
// LocalStorageに保存
setAuthInfo(token);
// トップページ移動
window.location.href = "/";
})
.catch((err) => console.log(err));
}
}
});

if (authInfo === null) {
// 未ログインの場合の画面を表示する
return unauthorized ?? null;
} else {
// ログイン済みの場合の画面を表示する
// Contextを使って認可情報を子コンポーネントでも使用できるようにする
return (
<AuthorizedContext.Provider value={{ authInfo }}>
{children}
</AuthorizedContext.Provider>
);
}
}

未ログイン時の画面を作成する

未ログイン時の画面は簡単です。ユーザにログインされてない旨を伝え、ログインボタンをクリックするとログインフローが開始されるようにします。

function Unauthorized(): React.ReactElement {
return (
<div>
<div>ログインしていません。</div>
<button onClick={() => requestCodeFlow()}>ログイン</button>
</div>
);
}

ユーザー情報を表示する

続いてユーザ情報を表示するコンポーネントの作成です。Authorizedの子コンポーネントとしてログイン済みの場合に表示されます。

ユーザー情報取得するために先ほど定義したgetUserInfo関数を実行する必要があります。これは非同期処理なので、上記で言及したようにReact Queryを使います。React Queryを使えば、useEffectを使わずに非同期処理をすることができます。本記事はReact Queryを説明することが目的ではないため、詳しくは説明しませんが、以下のコードのuseQueryの部分を見れば、だいたい使い方は理解できるのではないかと思います。また、React Queryを使う場合は、Suspenseを使った方が可読性の高いコードなるかもしれません。今回はSuspenseは使用していませんが、これも課題として残しておきましょう。

ユーザー情報取得を取得した後は、レスポンスのJSONのキーと値を単に表示しているだけです。

function UserInfo(): React.ReactElement {
// Contextから認可情報を取得する
const { authInfo } = React.useContext(AuthorizedContext);

// useQueryを使ってユーザ情報を取得するGoogle APIを実行する
const query = useQuery("email", () =>
authInfo == null
? Promise.reject(Error(`No AuthInfo`))
: getUserInfo(authInfo)
);

if (query.isError) {
// APIがエラーの場合
return <div>Failed to get the email</div>;
} else if (query.isSuccess) {
// ユーザー情報取得に成功した場合
return (
<div>
{Object.keys(query.data).map((k: string) => (
<div>
{k}: {query.data[k]}
</div>
))}
</div>
);
} else {
return <div>Loading</div>;
}
}

ログアウトボタン

ログイン済みの場合に、ログアウトするボタンも準備します。ボタン押下時に先ほど定義したsignOut関数呼び出すだけです。

function SignOut(): React.ReactElement {
const { authInfo } = React.useContext(AuthorizedContext);
return (
<div
onClick={() => {
signOut(authInfo);
}}
>
Sign Out
</div>
);
}

最後に作成したコンポーネントを組み合わせる

最後に今まで作成コンポーネントを組み合わせ終わりです。上記でもコードを紹介しましたが、再掲しておきます。

function App() {
return (
<QueryClientProvider client={queryClient}>
<Authorized unauthorized={<Unauthorized />}>
<UserInfo />
<SignOut />
</Authorized>
</QueryClientProvider>
);
}

まとめ

本記事では、ユーザのGoogleアカウントでログインし、ユーザー情報を表示するReactアプリの作成方法をご紹介しました。

この記事を読んでいただいた方の助けに少しでもなれば幸いです。

最後までお読みいただきありがとうございました。