👻

C#クライアントアプリでGitLabのOAuth2アクセストークンを得る

2024/04/02に公開

C#のデスクトップアプリ・コンソールアプリなど、クライアントPC上で動くアプリケーションでOAuth2のトークンがほしいことがありました。Webアプリケーションでの利用のサンプルや実装などは多く見つかるのですが、クライアント側となると途端に情報が減るようです。

本記事では『Windowsデスクトップで動作する側のクライアントアプリ』として、OAuth2の各種トークンを取得する実装について紹介します。

前提条件

今回はWindows PCで動作するコンソールアプリケーションで、OAuth2トークンを取得します。別PC(仮想マシンでもOK)に用意したGitLabサーバーにアクセスするものとします。
ローカルに立てた都合で、httpsとなっていないので注意してください

準備

GitLab側では、Applicationsからアプリケーションの登録を完了させておきます。このとき使用する以下の情報については、コードから使用するのでコピーしておきます。

  • Application ID
  • Secret (クライアントシークレット)

コールバックURLはC#アプリケーション側で受け取るため、適切なポート番号に変更してください。ここでは http://localhost:5000/callbackで受け取るものとしています。

全体の流れ

OAuth2.0とは

OAuth2.0は詳しく書かれた多くの情報があるのでここでは簡単に流れだけ説明します。

OAuth2.0は認証ではなく、認可を行うためのものです。認可はリソースへのアクセスの権限を与えるものです。誰がという情報はここには無く、アクセスのためのチケット(トークン)があればリソースへのアクセスができるということです。一方で、認証は現在の相手が誰かを確認するものです。
今回の場合では、リソースオーナーはユーザー自身ですが、リソースサーバーはGitLab、認可サーバーはGitLabということで、認証と認可のサーバーが同じなのでピンとこないのかもしれません。

サンプルコードの話

OAuth2による認可を実行し、トークンをもらうまでのサンプルアプリは以下のような感じです。このAuthorize()の中身が本記事の趣旨ですので、順番にみていきます。

var oauth2 = new OAuth2Example(
    "http://192.168.1.1:8080",  // 他所に立てたGitLabサーバー
    applicationID,              // アプリケーションID
    clientSecret,               // クライアントシークレット
    "http://localhost:5000/callback");// コールバックURL(リダイレクトURL)
var token = oauth2.Authorize();
Console.WriteLine($"Get OAuth2 AccessToken: {token.AccessToken}");

認可ページの表示

まずはOAuth2.0の認可ページへのアクセス部分を実装します。
準備で用意したApplication IDやリダイレクトURLの情報などをURLパラメータに含めます。
stateについては、ここでは適当に生成したGUIDから適当な文字列としています。

// url: http://(youraddress)/oauth/authorize
// RedirectUri: http://localhost:5000/callback
var srcState = Guid.NewGuid().ToString("N");
var url = $"{AuthorizationServer}/{AuthorizeUri}";
var reqParams = new Dictionary<string, string>();
reqParams["response_type"] = "code";
reqParams["client_id"] = ApplicationID;
reqParams["redirect_uri"] = RedirectUri;
reqParams["state"] = srcState;

ブラウザを開く

ここで生成したアドレスをブラウザでアクセスします。
URLを文字列で表示して、ユーザーにブラウザへコピペしてもらって表示、というのもできますが、スムーズに処理したいので自動的にブラウザに開くようにします。

以下の実装をすると、デフォルトブラウザで先ほど用意したURLとパラメータ付きになったアドレスでアクセスします。

private void OpenBrowser(string url, Dictionary<string, string> reqParams)
{
    var strParams = new FormUrlEncodedContent(reqParams).ReadAsStringAsync().Result;
    url += $"?{strParams}";
    var urlEscaped = url.ToString().Replace("&", "^&");
    Process.Start(new ProcessStartInfo("cmd", $"/c start {urlEscaped}")
    {
        CreateNoWindow = true
    });
}

これによりブラウザでは以下のような画面が表示されます。
ここでAuthorizeボタンを押すのは「まだ」です。リダイレクトURL側の支度をしてからにします。

コールバックの準備

リダイレクトURLとして設定したアドレスにURLパラメータ付きとなって、結果が返ってきます。
このURLパラメータの中に含まれるデータがほしいので、C#のHttpListenerを使って HTTPサーバーを装いながら待ち受けをします。

private void SetupRecvCallback()
{
    var url = RedirectUri;
    if(!url.EndsWith("/")) { url += "/"; }
    RecvHttpListener = new HttpListener();
    RecvHttpListener.Prefixes.Add(url);
    RecvHttpListener.Start();
}

このアドレスにブラウザからアクセスが来たときに処理する部分が以下の通りです。
ページ内容には特に意味がないのですが、URLパラメータとして渡ってきている code と state に用があります。
この statecode は次のアクセストークン取得に使用します。

private (string? code, string? state) ReceiveWaitCode()
{
    var context = RecvHttpListener.GetContextAsync().Result;
    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;
    responseOutput.WriteAsync(buffer, 0, buffer.Length).Wait();
    responseOutput.Close();
    RecvHttpListener.Stop();

    var code = context.Request.QueryString.Get("code");
    var state = context.Request.QueryString.Get("state");
    return (code, state);
}

アクセストークンの取得

アクセストークンの取得も似たような処理となりますが、直前で送られてきたcode,stateの情報が必要です。これらのパラメータから以下の実装のようにURLを組み立てて、POSTのリクエストを発行します。

成功するとJSONのレスポンスが返ってきて、このなかにアクセストークンやリフレッシュトークンのキーが含まれています。

// url: http://(youraddress)/oauth/token
var url = $"{AuthorizationServer}/{GetAccessTokenUri}";
using (var httpClient = new HttpClient())
{
    httpClient.BaseAddress = new Uri(AuthorizationServer);
    var reqParams = new Dictionary<string, string>
    {
        ["client_id"] = ApplicationID,
        ["client_secret"] = ClientSecret,
        ["grant_type"] = "authorization_code",
        ["redirect_uri"] = RedirectUri,
        ["code"] = code,
        ["state"] = state,
    };
    var response = httpClient.PostAsync(GetAccessTokenUri, new FormUrlEncodedContent(reqParams)).Result;
    if (response.IsSuccessStatusCode == false)
    {
        Console.Error.WriteLine("Failed.");
        return null;
    }
    var body = response.Content.ReadAsStringAsync().Result;
    return MakeInfoFromResponseJson(body);
}

まとめ

これでGitLabへ情報アクセスにおいて、ユーザーのアカウント情報ではなくOAuth2でもらったトークンを使えるようになりました。これまでのコードの断片を掲載してきましたが、クラス全貌は本ページの終わりに記載しています。

参考にした情報

https://msiz.hatenablog.jp/entry/2019/11/30/175446
https://zenn.dev/mryhryki/articles/2020-12-28-oauth2-flow
https://qiita.com/ist-n-m/items/67a5a0fb4f50ac1e30c1
https://docs.gitlab.com/ee/api/oauth2.html

自分がOAuth2.0やらないとなと思ったときに最初に読んだ書籍(の商業版)がこちらです。
雰囲気で使わずきちんと理解する!整理してOAuth2.0を使うためのチュートリアルガイド

サンプルコード

本記事で使用したOAuth2Exampleクラスの全体コードは以下の通りです。

Discussion