概要
スマホで使用している「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)
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
/// <summary>
/// アクセスするAPIのスコープを定義
/// </summary>
public class Scopes
{
public static readonly string[] FitnessScopes = new[] {
FitnessService.Scope.FitnessActivityRead
};
}
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
/// <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にアクセス
するとログイン画面すら出てこずに、図のような画面に飛ばされます。
GCPのリダイレクトURI設定間違えたかな?
と思って、GCPのリダイレクト設定を見直してみます。
ちなみに私の場合リダイレクトURIはhttp://localhost:8080/GoogleFit/steps
と設定していました。
ポート番号はASP.NETのWebプロジェクトを右クリックし、「プロパティ>デバッグ>アプリURL」から設定できます。
「SSLを有効にする」というチェックボックスにチェックしていると別のポートが自動で割り当てられますので、それをリダイレクトURIに設定してもよいです。
GCPからダウンロードしてきた認証情報のclient_secret.jsonファイル内でも、redirect_urisは一致しています。あれ~?(必要情報以外伏せてます)
"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として指定されてしまっているようでした。
コードからリダイレクトURIを設定してトークン要求してあげる必要があるみたいです。
コード修正
https://stackoverflow.com/a/32119401
の通りにトークン要求処理をカスタムします。
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
/// <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
/// <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()
{
// ここが認証後のリダイレクト先ルート
}
}