0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

概要

スマホで使用している「GoogleFit」の歩数データを取得したかったので、ASP.NETアプリからGoogle Fitness APIに問い合わせようと思いました。
認証方法はAPIキー認証、OAuth2認証、サービスアカウント認証の3種類ありますが、今回はOAuth2認証でアクセスします。
その際に発生した問題と、解決方法をトラブルシューティング的にメモ。
ASP.NETGoogle.Apis.Auth.OAuth2というnugetパッケージを使ってGoogleAPIにOAuth2認証したい方向けです。

環境

ASP.NET Core MVC(.netCore 5.0)
Google Console Platform
Google.Apis.Auth.OAuth2

GCP(Google Cloud Platform)の使用方法

GCP認証情報の設定方法は以下の公式ドキュメントを参考にしました。
https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth

途中までは過去のエントリも参考になるかもです。
GoogleFitのデータをC#で取得してみた! - Qiita

コード

Scopes.cs

Scopes.cs
/// <summary>
/// アクセスするAPIのスコープを定義
/// </summary>
public class Scopes
{
  public static readonly string[] FitnessScopes = new[] {
    FitnessService.Scope.FitnessActivityRead
  };
}

AuthManager.cs

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

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

GCPのリダイレクトURI設定間違えたかな?

と思って、GCPのリダイレクト設定を見直してみます。
ちなみに私の場合リダイレクトURIはhttp://localhost:8080/GoogleFit/stepsと設定していました。
ポート番号はASP.NETのWebプロジェクトを右クリックし、「プロパティ>デバッグ>アプリURL」から設定できます。
「SSLを有効にする」というチェックボックスにチェックしていると別のポートが自動で割り当てられますので、それをリダイレクトURIに設定してもよいです。
GCPからダウンロードしてきた認証情報のclient_secret.jsonファイル内でも、redirect_urisは一致しています。あれ~?(必要情報以外伏せてます)

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

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

赤枠のURLがリダイレクトURIとして指定されてしまっているようでした。
2021-02-12_15h28_29.png

コードからリダイレクトURIを設定してトークン要求してあげる必要があるみたいです。

コード修正

https://stackoverflow.com/a/32119401
の通りにトークン要求処理をカスタムします。

AuthorizationBroker.cs

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

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

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()
  {
    // ここが認証後のリダイレクト先ルート
  }
}
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?