【Unity】UnityエディタでGoogle APIのOAuth2認証をする(ライブラリ使わない版)

Unityエディタからライブラリを使わずにGoogle APIのOAuth2認証をする方法をまとめました。

Unity2019.3.5

はじめに

この記事ではUnityエディタからGoogle APIのOAuth2認証をする方法をまとめます。
今回はGoogle公式クライアントライブラリを使わずにAPIを叩いて実装してみます。

また本記事における例として、Spreadsheetからデータを読み込むことを目的とします。
他の用途に使う際にはSpreadsheetの部分を適宜読み替えてください。

ちなみにOAuth認証無しでSpreadSheetを読む方法は以下で紹介しています。

light11.hatenadiary.com

また、クライアントライブラリを使った方法は以下の記事にまとめています。

light11.hatenadiary.com

Google APIの初期設定を行う

OAuth認証を使うには、まず認証情報を取り扱うアプリケーションの情報を登録する必要があります。
登録を行うためにGoogleのDeveloper Consoleにアクセスします。

console.developers.google.com

すると以下のようなページが表示されます。
スクリーンショットは英語表記になっていますが適宜読み替えてください)

f:id:halya_11:20200426213534p:plain:w600
Google Developer Console

プロジェクトを作成

まず左上のSelect a project > NEW PROJECTを選択して新しくプロジェクトを作成します。
以下のようなプロジェクト作成ページが表示されるので適当なプロジェクト名を入力してCREATEボタンを押下します。

f:id:halya_11:20200426220917p:plain:w600
プロジェクトの作成

これでプロジェクトが作成されました。

同意画面を作成

次に画面上部から今作ったプロジェクトを選択し、左側のメニューからOAuth consent screenを選択します。

f:id:halya_11:20200426221826p:plain:w600
プロジェクト選択・同意画面

用途に応じてInternalあるいはExternalを選択してCREATEボタンを押下します。

f:id:halya_11:20200426222207p:plain:w600
用途を選択

次の画面ではApplication Nameに適当な名前を入力して画面最下部のSAVEボタンを押下します。

f:id:halya_11:20200426222707p:plain:w600
同意画面を作成

これで同意画面が作成されました。

クライアントIDを作成

次にクライアントIDを作成します。
左側のメニューからCredentialsを選択します。

f:id:halya_11:20200426223338p:plain:w600
認証情報

続いて画面上部からCREATE CREDENTIALS > OAuth client IDを選択します。

f:id:halya_11:20200426221216p:plain:w600
クライアントIDを生成

Application typeはOtherを選択し、適当な名前を入力してCreateボタンを押下します。

f:id:halya_11:20200426223609p:plain:w600
クライアントIDを生成

Client IDとClient Secretが表示されたら作成完了です。

使用するAPIを有効にする

最後に使用するAPIを有効にしておきます。
左側のメニューからLibraryを選択します。

f:id:halya_11:20200510162141p:plain:w600
Libraryを選択

使いたいAPIを検索してENABLEボタンを押下します。
今回はGoogle Sheets APIを有効にしました。

f:id:halya_11:20200510162403p:plain:w600
APIを有効化

認証用ページを開いて認証コードを得る

さて次に前節で作ったクライアントIDを使って認証を行うプログラムを書いていきます。
認証を行うには、認証用のURLを生成して以下のような認証用ページをブラウザで開くように実装します。

f:id:halya_11:20200426231034p:plain
認証ページ

認証用ページを開くには、URLhttps://accounts.google.com/o/oauth2/v2/authに認証用のクエリパラメータをくっつけたURLをブラウザで開くだけです。
URLはたとえばこんな感じになります。

https://accounts.google.com/o/oauth2/v2/auth?client_id=1234567890-abcdefghijklmnopqrstuvwxyz.apps.googleusercontent.com&redirect_uri=http://localhost:64688&response_type=code&scope=https://www.googleapis.com/auth/spreadsheets.readonly&code_challenge=2EGy7-zr7sd9O09JLrNIIQ8Qf8wxUoJ-UAemn0yKMRk&code_challenge_method=S256&state=54l0lO2fQ71kC4Mas3auikqgkzZShA4ybFVDOLfUQ6U

パラメータについては以下で一覧表にし、複雑なものについては次節から詳しく説明します。

パラメータ名 説明
client_id クライアントID
redirect_uri 認証完了ページからリダイレクトするためのURI(後述)
scope 付与する権限(後述)
response_type codeに設定しておけばOK
code_challenge (optional) 安全にOAuth2認証を行うために必要なもの(後述)
code_challenge_method (optional) Code Challengeの方法
state (optional) リダイレクトしてきた結果が認証の結果であることに対する安全性を高めるための文字列(後述)

このようにして開いたページでユーザが認証を完了すると、リダイレクトURIにアクセスが来ます。
認証コードはこのリクエストのクエリパラメータにcodeとして埋め込まれています。

redirect_uri

redirect_uriパラメータにはユーザが認証し終わった後にリダイレクトするURIを指定します。
Unityエディタで使う場合、localhostの適当なポートを指定しつつ、
HttpListenerでローカルサーバを立ててリダイレクトを待ち受けます。

// 認証完了ページからのリダイレクトを待機する
var httpListener = new HttpListener();
httpListener.Prefixes.Add(redirectUriWithSlash);
httpListener.Start();
var taskCompletionSource = new TaskCompletionSource<IAsyncResult>();
httpListener.BeginGetContext(x => taskCompletionSource.SetResult(x), httpListener);
var asyncResult = await taskCompletionSource.Task;

var context = httpListener.EndGetContext(asyncResult);
var request = context.Request;
var response = context.Response;

// 認証完了ページにメッセージを表示
var message = Encoding.UTF8.GetBytes($"<html><head><meta charset='utf-8'/></head><body>{CompletionMessage}</body></html>");
response.OutputStream.Write(message, 0, message.Length);

response.OutputStream.Close();
httpListener.Close();

// 認証コードを取得
var code= request.QueryString.Get("code");

ローカルホストにアクセスが来たら、そのリクエストパラメータから認証コードを取得します。
上記で取得しているようにcodeとして埋め込まれているパラメータが認証コードになります。

scope

scopeにはGoogleのどのAPIに対する認証を求めるかを指定します。
指定できるスコープは以下のページに一覧化されています。

developers.google.com

たとえばSpreadSheetのRead権限だけを取得したい場合にはhttps://www.googleapis.com/auth/spreadsheets.readonlyを指定します。
また半角スペースで区切ることで複数のscopeを指定できます。

code_challenge

GoogleはOAuth2認証をよりセキュアに行う仕組みとしてProof Key for Code Exchangeを採用しています。
これはoptionalなパラメータですが、使用する場合には以下の手順でcode challengeを生成する必要があります。

  1. ランダムな英数字と「-」「.」「_」「~」で構成されたcode verifierを生成する
  2. code verifierのSHA256ハッシュを求める
  3. 2.のハッシュ値をBase64URLエンコードしたものをcode challengeとする

具体的な実装は以下のようになります。

public string GetCodeChallenge()
{
    var codeVerifier = GetRandomStringForUrl(32);
    var codeChallenge = ConvertToBase64Url(Sha256(codeVerifier));
}

private static string GetRandomStringForUrl(uint length)
{
    var cryptoServiceProvider = new RNGCryptoServiceProvider();
    var bytes = new byte[length];
    cryptoServiceProvider.GetBytes(bytes);
    return ConvertToBase64Url(bytes);
}

private static string ConvertToBase64Url(byte[] bytes)
{
    return Convert.ToBase64String(bytes)
        .Replace("+", "-")
        .Replace("/", "_")
        .Replace("=", "");
}

private static byte[] Sha256(string source)
{
    var bytes = Encoding.ASCII.GetBytes(source);
    return new SHA256Managed().ComputeHash(bytes);
}
state

リダイレクトが確かにリクエストの結果であることを確認するための値です。
これもcode challengeと同様optionalなパラメータとなります。

送信時にはランダムな値などをstateとして送付し、
リダイレクト結果のパラメータのstate値と同一であることを以下のように確認します。

private void HandleCallback(IAsyncResult result)
{
    var httpListener = result.AsyncState as HttpListener;
    var context = httpListener.EndGetContext(result);
    var request = context.Request;
    var state = request.QueryString.Get("state");

    if (state != _state)
    {
        _handle?.Fail(new Exception($"OAuth error: Request has invalid state."));
        return;
    }
}

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

前節のようにして認証コードが得られたら、次にアクセストークンを取得します。
アクセストークンは初回だけ認証コードを使って取得します。
期限が切れたらリフレッシュトークンを使ってリフレッシュします。

初回は認証コードを使って取得

Client IDとClient Secretからアクセストークンを得るには、https://oauth2.googleapis.com/tokenにフォームをPostします。

var form = new WWWForm();
form.AddField("client_id", _clientId);
form.AddField("client_secret", _clientSecret);
form.AddField("code", authorizationCode);
form.AddField("code_verifier", codeVerifier);
form.AddField("grant_type", "authorization_code");
form.AddField("redirect_uri", redirectUri);

var taskCompletionSource = new TaskCompletionSource<AsyncOperation>();
var request = UnityWebRequest.Post("https://oauth2.googleapis.com/token", form);
request.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded");
request.SendWebRequest().completed += x => taskCompletionSource.SetResult(x);

await taskCompletionSource.Task;
var response = request.downloadHandler.text;

codeには前述の方法で取得したアクセストークンを使います。
code_verifierも前述の方法で作成したものをそのまま渡します。

レスポンスとして返ってくるjsonaccess_tokenが含まれるので、これを各サービス使用時に使用します。
また同時にrefresh_tokenも受け取れますが、これは次節のリフレッシュ時に使うので保存しておきます。

期限が切れたらリフレッシュ

次に期限が切れたアクセストークンのリフレッシュを行います。
リフレッシュを行うには、grant_typeをrefresh_tokenに設定しつつ先ほど得られたリフレッシュトークンを送ります。

var form = new WWWForm();
form.AddField("client_id", _clientId);
form.AddField("client_secret", _clientSecret);
form.AddField("grant_type", "refresh_token");
form.AddField("refresh_token", refreshToken);

var taskCompletionSource = new TaskCompletionSource<AsyncOperation>();
var request = UnityWebRequest.Post("https://oauth2.googleapis.com/token", form);
request.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded");
request.SendWebRequest().completed += x => taskCompletionSource.SetResult(x);

await taskCompletionSource.Task;
var response = request.downloadHandler.text;

レスポンスとして返ってくるjsonaccess_tokenに新しいアクセストークンが返されます。

実装例

以上で説明は終わりとなりますが、実際に実装した例を以下のリポジトリに用意しました。

github.com

必要に応じて参照してください。

参考

developers.google.com

github.com

tech-blog.optim.co.jp