【GoogleCloudPlatform】ASP.NETアプリからGoogleAPIにOAuth2認証でアクセスするとエラー400:redirect_uri_mismatch

# 概要
スマホで使用している「GoogleFit」の歩数データを取得したかったので、ASP.NETアプリからGoogle Fitness APIに問い合わせようと思いました。
認証方法はAPIキー認証、OAuth2認証、サービスアカウント認証の3種類ありますが、今回はOAuth2認証でアクセスします。
その際に発生した問題と、解決方法をトラブルシューティング的にメモ。
**ASP.NET**で**Google.Apis.Auth.OAuth2**というnugetパッケージを使って**GoogleAPIにOAuth2認証**したい方向けです。
# 環境
[ASP.NET Core MVC(.netCore 5.0)](https://docs.microsoft.com/ja-jp/aspnet/core/mvc/overview?view=aspnetcore-5.0)
[Google Console Platform](https://console.cloud.google.com/)
[Google.Apis.Auth.OAuth2](https://developers.google.com/api-client-library/dotnet/apis/oauth2/v2)
# GCP(Google Cloud Platform)の使用方法
GCP認証情報の設定方法は以下の公式ドキュメントを参考にしました。
https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth

途中までは過去のエントリも参考になるかもです。
[GoogleFitのデータをC#で取得してみた！ - Qiita](https://qiita.com/tsumasakky/items/39853ee3680b1ce227e5)
# コード
## Scopes.cs
```c#:Scopes.cs
/// <summary>
/// アクセスするAPIのスコープを定義
/// </summary>
public class Scopes
{
  public static readonly string[] FitnessScopes = new[] {
    FitnessService.Scope.FitnessActivityRead
  };
}
```
## AuthManager.cs
```c#:AuthManager.cs
/// <summary>
/// 認証を管理するクラス
/// </summary>
public class AuthManager
{
  // GCPからダウンロードした認証情報のjsonファイル名
  private const string FileName = "client_secret.json";
  private readonly string[] _scopes;

  public AuthManager( IEnumerable<string> scopes )
  {
    _scopes = scopes.ToArray();
  }

  /// <summary>
  /// OAuth認証を用いてCredentialを取得する。
  /// </summary>
  public Task<UserCredential> GetUserCredential()
  {
    // 今回は認証情報のjsonファイルをプロジェクトルート直下に配置してるので、実行時のディレクトリを取得
    var current = Directory.GetCurrentDirectory();

    using var stream = new FileStream( Path.Combine( current, FileName ), FileMode.Open, FileAccess.Read );
    const string credPath = "token.json";
    return GoogleWebAuthorizationBroker.AuthorizeAsync(
       GoogleClientSecrets.Load( stream ).Secrets,
       _scopes,
       "user", CancellationToken.None, new FileDataStore( credPath, true ) );
  }
}
```
## GoogleFitController.cs
```c#:GoogleFitController.cs
/// <summary>
/// GoogleFitに関するコントローラー
/// </summary>
[ApiController]
[Route( "[controller]" )]
public class GoogleFitController : ControllerBase
{
  /// <summary>
  /// OAuth2認証を実行します
  /// </summary>
  [HttpGet]
  [Route( "auth" )]
  public async void Auth()
  {
    var auth = new AuthManager( Scopes.FitnessScopes );
    await auth.GetUserCredential();
  }

  /// <summary>
  /// OAuth2認証を実行します
  /// </summary>
  [HttpGet]
  [Route( "steps" )]
  public void GetSteps()
  {
    // ここが認証後のリダイレクト先ルート
  }
}
```

# 実行
http://[ホスト・ルート]/GoogleFit/authにアクセス
するとログイン画面すら出てこずに、図のような画面に飛ばされます。

「リダイレクトURIが一致しないよ」
![2021-02-12_15h28_29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147118/d30dc739-d164-76cb-ff65-45a40a235b43.png)
# GCPのリダイレクトURI設定間違えたかな？
と思って、GCPのリダイレクト設定を見直してみます。
ちなみに私の場合リダイレクトURIは`http://localhost:8080/GoogleFit/steps`と設定していました。
ポート番号はASP.NETのWebプロジェクトを右クリックし、「プロパティ＞デバッグ＞アプリURL」から設定できます。
「SSLを有効にする」というチェックボックスにチェックしていると別のポートが自動で割り当てられますので、それをリダイレクトURIに設定してもよいです。
GCPからダウンロードしてきた認証情報のclient_secret.jsonファイル内でも、redirect_urisは一致しています。あれ～？（必要情報以外伏せてます）

```json:client_secret.json
"web":{
  "client_id": "*****",
  "project_id": "*****",
  "auth_uri": "*****",
  "token_uri": "*****",
  "auth_provider_x509_cert_url": "*****",
  "client_secret": "*****",
  "redirect_uris": [
    "http://localhost:8080/GoogleFit/steps"
  ],
  "javascript_origins": [
    "http://localhost:8080"
  ]
}
```
# 解明
[c# - How do I set return_uri for GoogleWebAuthorizationBroker.AuthorizeAsync? - Stack Overflow](https://stackoverflow.com/questions/31209273/how-do-i-set-return-uri-for-googlewebauthorizationbroker-authorizeasync)

どうやらリダイレクトURIが一致していないというのは本当で、この書き方だとトークン要求時に送信しているリダイレクトURIは
Google.Apis.Auth.OAuth2によって自動生成されているようです。

赤枠のURLがリダイレクトURIとして指定されてしまっているようでした。
![2021-02-12_15h28_29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147118/27b47b3c-bf06-91af-c7fe-15f4dcb13e35.png)

コードからリダイレクトURIを設定してトークン要求してあげる必要があるみたいです。
# コード修正
https://stackoverflow.com/a/32119401
の通りにトークン要求処理をカスタムします。
## AuthorizationBroker.cs
```c#:AuthorizationBroker.cs
/// <summary>
/// GoogleWebAuthorizationBrokerをラップし、リダイレクトURIをカスタム可能にします。
/// </summary>
public class AuthorizationBroker : GoogleWebAuthorizationBroker
{
  public static string RedirectUri;

  public new static async Task<UserCredential> AuthorizeAsync(
    ClientSecrets clientSecrets,
    IEnumerable<string> scopes,
    string user,
    CancellationToken taskCancellationToken,
    IDataStore dataStore = null )
  {
    var initializer = new GoogleAuthorizationCodeFlow.Initializer
    {
      ClientSecrets = clientSecrets,
    };
    return await AuthorizeAsyncCore( initializer, scopes, user,
        taskCancellationToken, dataStore ).ConfigureAwait( false );
  }

  private static async Task<UserCredential> AuthorizeAsyncCore(
    GoogleAuthorizationCodeFlow.Initializer initializer,
    IEnumerable<string> scopes,
    string user,
    CancellationToken taskCancellationToken,
    IDataStore dataStore )
    {
      initializer.Scopes = scopes;
      initializer.DataStore = dataStore ?? new FileDataStore( Folder );
      var flow = new AuthorizationCodeFlow( initializer );
      return await new AuthorizationCodeInstalledApp( flow,
          new LocalServerCodeReceiver() )
          .AuthorizeAsync( user, taskCancellationToken ).ConfigureAwait( false );
    }
}

public class AuthorizationCodeFlow : GoogleAuthorizationCodeFlow
{
  public AuthorizationCodeFlow( Initializer initializer ) : base( initializer ) { }

  public override AuthorizationCodeRequestUrl
                  CreateAuthorizationCodeRequest( string redirectUri )
  {
      return base.CreateAuthorizationCodeRequest( AuthorizationBroker.RedirectUri );
  }
}
```
## AuthManager.cs
```c#:AuthManager.cs
/// <summary>
/// 認証を管理するクラス
/// </summary>
public class AuthManager
{
  // GCPからダウンロードした認証情報のjsonファイル名
  private const string FileName = "client_secret.json";
  private readonly string[] _scopes;

  public AuthManager( IEnumerable<string> scopes )
  {
    _scopes = scopes.ToArray();
  }

  /// <summary>
  /// OAuth認証を用いてCredentialを取得する際に、リダイレクトURIをカスタマイズする。
  /// </summary>
  public Task<UserCredential> GetUserCredential( string redirectUri )
  {
    var current = Directory.GetCurrentDirectory();
    // ファイル名は先ほど取得した認証情報のjson
    using var stream = new FileStream( Path.Combine( current, FileName ), FileMode.Open, FileAccess.Read );
    const string credPath = "token.json";
    AuthorizationBroker.RedirectUri = redirectUri;
    return AuthorizationBroker.AuthorizeAsync(
           GoogleClientSecrets.Load( stream ).Secrets,
             _scopes,
             "user", CancellationToken.None, new FileDataStore( credPath, true ) );
  }
}
```
## GoogleFitController.cs
```c#:GoogleFitController.cs
/// <summary>
/// GoogleFitに関するコントローラー
/// </summary>
[ApiController]
[Route( "[controller]" )]
public class GoogleFitController : ControllerBase
{
  /// <summary>
  /// OAuth2認証を実行します
  /// </summary>
  [HttpGet]
  [Route( "auth" )]
  public async void Auth()
  {
    var auth = new AuthManager( Scopes.FitnessScopes );
    await auth.GetUserCredential( "http://localhost:8080/GoogleFit/steps" );
  }

  /// <summary>
  /// OAuth2認証を実行します
  /// </summary>
  [HttpGet]
  [Route( "steps" )]
  public void GetSteps()
  {
    // ここが認証後のリダイレクト先ルート
  }
}
```