デスクトップアプリで OAuth 2.0 のAuthorization Code Flow に対応する冴えたやりかた

アプリが Web API を呼び出すためのアクセストークンを取得するとき、OAuth 2.0 の Authorization Code Flow に対応することになると思う。

Web アプリなら、普通にリダイレクト先を用意すればいい。モバイルアプリでも、Custom URL Scheme によってアプリを起動できるので、それで認証コードを取り出せる。

問題はデスクトップアプリ。それもWinForms や WPF。一応、WebView を使い、ナビゲーションイベントを捕まえて認証コードを取り出す方法はある。ただ、ログインは内部ブラウザでは無く、外部ブラウザを使いたい。

あと、コンソールアプリも同様。リダイレクト先を http://localhost/ とかにしておいて、ブラウザのアドレスバーに表示される URL から認証コードを取り出す方法で、お茶を濁していた。

そんな中、Google の OAuth サンプルで、HttpListener を使ってリダイレクトを待ち受け、認証コードを取り出す方法を知った。何それ賢い。

github.com

GitHubAPI でやってみた。

using System.Diagnostics;
using System.Net;
using System.Text;
using Octokit;

const string ClientId = "<Your Client ID>";
const string ClientSecret = "<Your Client Secret>";
const string RedirectUri = "http://localhost:8081/";

var github = new GitHubClient(new ProductHeaderValue("OAuthSample"));

// リダイレクトで認証コードを受け取るために、
// HttpListener を使って待ち受ける。
var http = new HttpListener();
http.Prefixes.Add(RedirectUri);
Console.WriteLine("Listening..");
http.Start();

// GitHub のログインページを表示。
var loginUrl = github.Oauth.GetGitHubLoginUrl(
    new OauthLoginRequest(ClientId)
    {
        RedirectUri = new Uri(RedirectUri),
        State = Guid.NewGuid().ToString("N"),
        Scopes =
        {
            "repo",
        },
    });
var escapedUrl = loginUrl.ToString().Replace("&", "^&");
Process.Start(new ProcessStartInfo("cmd", $"/c start {escapedUrl}")
{
    CreateNoWindow = true,
});

// OAuth 認証のレスポンスを返すために待機
var context = await http.GetContextAsync();

// ブラウザに HTTP レスポンスを送る
var response = context.Response;
var responseString = "<html><body>Please return to the app.</body></html>";
var buffer = Encoding.UTF8.GetBytes(responseString);
response.ContentLength64 = buffer.Length;
var responseOutput = response.OutputStream;
await responseOutput.WriteAsync(buffer, 0, buffer.Length);
responseOutput.Close();
http.Stop();

// 認証コードを使ってアクセストークンを取得
var code = context.Request.QueryString.Get("code");
var oauthToken = await github.Oauth.CreateAccessToken(
    new OauthTokenRequest(
        ClientId,
        ClientSecret,
        code));

// アクセストークンを設定して、GitHub の API を呼び出す
github.Credentials = new Credentials(oauthToken.AccessToken);
var repositories = await github.Repository.GetAllForCurrent();
foreach (var repository in repositories)
{
    Console.WriteLine(repository.FullName);
}

Azure CLI なんかも、ソースコードを見たら az login では HTTP で待ち受けてるっぽかった。自分が知らなかっただけで、実はデファクトスタンダードだったりするんだろうか。