コントローラからOAuthの処理のべた書きを締め出して、Dependency Injectionで入れたらかなり納得する形になった

 ASP.NET Core MVCでOAuth(Githubの)を実装したのだが、実装を急いだので処理をコントローラにべた書き+グローバルな静的クラスに任せていた。それをリファクタリングして、なおかつOAuthは他でも使いまわせるようにしたかった。できればOAuthの処理の普遍な部分は抽象クラスにして、個別のWebアプリで実装すべき部分は派生クラスで実装するような。
 そんなクラスにOAuthを切り出して、それをコントローラにDependency Injectionしたらいい形になったなぁと思ったハナシ。

 まずOAuthの、Webアプリサーバサイドでの処理。
事前準備: OAuthを使わせてくれるサイトに必要情報を登録し、ClientIDとClientSecretをもらっておく。
以下OAuth処理
OAuthを使わせてくれるサイトへのリンクをクライアントに渡す

クライアントはリンクを踏む。一旦自サイトから外に出るが、リダイレクトで自分のサイトへ戻ってくる。戻ってくるときにコードを持っている

自分のサイトのサーバで、コードを使って、OAuthをやらせてくれるサイトからトークンをもらう

取得したトークンでOAuthをやらせてくれるサイトからユーザ情報をもらう

あとはユーザ情報を自サイトで判別

 問題のOAuth処理べた書きコントローラ。
[Route("OAuth")]

public async Task<IActionResult> OAuthAsync()
{
var clientId = GithubOAuth.ClientId;
var clientSecret = GithubOAuth.ClientSecret;


if (!Request.Query.ContainsKey("code"))
{
ViewData["OAuthLink"] = $"https://github.com/login/oauth/authorize?client_id={clientId}";
return View("OAuthAsync");
}

var code = Request.Query["code"];

var token = await GetAcessTokenAsync(clientId, clientSecret, code);

// 取得したトークンを使ってGithubにユーザ情報を要求する
var id = await GetUserId(token);
var loginSuccess = GithubOAuth.Login(id, Response.Cookies);

return Redirect(loginSuccess ? "/Master/" : "/Auth/OAuth");
}

private async Task<string> GetAcessTokenAsync(string clientId, string clientSecret, string code)
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

// 持って帰ってきた認証コードを使ってトークンを取得する
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "client_id", clientId },
{ "client_secret", clientSecret },
{ "code", code },
});
var response = await httpClient.PostAsync("https://github.com/login/oauth/access_token", content);
var responseBody = await response.Content.ReadAsStringAsync();
var jsonObj = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseBody);
var token = jsonObj["access_token"];

return token;
}

private async Task<string> GetUserId(string token)
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

var uri = $"https://api.github.com/user?access_token={token}";
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Zenigata");
var response = await httpClient.GetAsync(uri);
var responseBody = await response.Content.ReadAsStringAsync();
var userInfo = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseBody);
var id = userInfo["id"];

return id;
}


 ビフォーアフターのアフターなコントローラ。OAuth処理を、専用のオブジェクト(インスタンス)に頼っている。専用のオブジェクトは、Dependency Injectionでコントローラにて使えるようになっている。
public class AuthorizationController : Controller

{
private readonly GithubOAuth _githubOAuth;

public AuthorizationController(GithubOAuth githubOAuth)
{
this._githubOAuth = githubOAuth;
}

[Route("OAuth")]
public async Task<IActionResult> OAuthAsync()
{
if (!Request.Query.ContainsKey("code"))
{
ViewData["OAuthLink"] = this._githubOAuth.GetRedirectUri();
return View("OAuthAsync");
}

var id = await this._githubOAuth.GetIdAsync(Request.Query["code"]);
var loginSuccess = this._githubOAuth.Login(id, Response.Cookies);

return Redirect(loginSuccess ? "/Master/" : "/Auth/OAuth");
}
}


 OAuth処理のベースをまとめた抽象クラス。ユーザにコードをもらうためにアクセスしてもらうURLや、サーバ側でコードを使ってトークン取得、ユーザ情報取得などがOAuth処理として普遍な機能なので実装されている。
using Microsoft.AspNetCore.Http;

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;


namespace OAuthProvider
{
abstract public class GithubOAuthBase
{
public GithubOAuthBase(string clientId, string clientSecret)
{
this._clientId = clientId;
this._clientSecret = clientSecret;
}

private string _clientId = "";

public string ClientId
{
get
{
if (String.IsNullOrEmpty(_clientId))
{
throw new ArgumentNullException(
"Github OAuth client ID isn't given."
);
}
return _clientId;
}
}

private string _clientSecret = "";

public string ClientSecret
{
set => _clientSecret = value;
get
{
if (String.IsNullOrEmpty(_clientSecret))
{
throw new ArgumentNullException(
"Github OAuth client secret isn't given."
);
}
return _clientSecret;
}
}

public string GetRedirectUri()
{
var uri = $"https://github.com/login/oauth/authorize?client_id={this.ClientId}";
return uri;

}


public async Task<string> GetIdAsync(string code)
{
// 取得したトークンを使ってGithubにユーザ情報を要求する
var token = await GetAccessTokenAsync(code);

var id = await GetUserId(token);

return id;
}

virtual public bool Login(string id, IResponseCookies cookies)
{
throw new NotImplementedException();
}

virtual public async Task<bool> LoginAsync(string id, IResponseCookies cookies)
{
throw new NotImplementedException();
}

private async Task<string> GetAccessTokenAsync(string code)
{
// 認証コードを使ってトークンを取得する
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "client_id", this.ClientId },
{ "client_secret", this.ClientSecret },
{ "code", code },
});
var response = await httpClient.PostAsync("https://github.com/login/oauth/access_token", content);
var responseBody = await response.Content.ReadAsStringAsync();
var jsonObj = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseBody);
var token = jsonObj["access_token"];

return token;
}

private async Task<string> GetUserId(string token)
{
// 取得したトークンを使ってGithubにユーザ情報を要求する
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

var uri = $"https://api.github.com/user?access_token={token}";
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Zenigata");
var response = await httpClient.GetAsync(uri);
var responseBody = await response.Content.ReadAsStringAsync();
var userInfo = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseBody);
var id = userInfo["id"];

return id;
}
}
}


 上記のクラスは抽象クラス。ユーザ情報をOAuthをやらせてくれるサイトから取ってきたら、その後の具体的な処理(認可を与えるべきか判断する、認可を与える)はサイトごとにことなる。それをLoginメソッドとして、↑の抽象クラスを継承した派生クラスで実装する。
using Microsoft.AspNetCore.Http;

using MongoDB.Driver;
using OAuthProvider;
using System;
using System.Security.Cryptography;
using tetsujin.Models;

namespace tetsujin
{
public class GithubOAuth : GithubOAuthBase
{
public GithubOAuth(string clientId, string clientSecret) : base(clientId, clientSecret) { }

/// <summary>
/// ログインを実行する
/// </summary>
/// <param name="id">ユーザID</param>
/// <param name="cookies">クッキー</param>
/// <returns>ログインの成否</returns>
override public bool Login(string id, IResponseCookies cookies)
{
var userCollection = DbConnection.Db.GetCollection<OAuthUser>(OAuthUser.CollectionName);
var filter = Builders<OAuthUser>.Filter.Eq("_id", id);
var master = userCollection.Find(filter).FirstOrDefault<OAuthUser>();
if (master == null) // ユーザが登録されていない場合
{
return false;
}
else // ユーザが登録されていた場合
{
// トークンを使ってセッションを開始
var token = GetToken();
var collection = DbConnection.Db.GetCollection<Session>(Session.CollectionName);
collection.InsertOne(new Session
{
Id = token,
CreatedAt = DateTime.Now
});

// cookieにsecure属性を付与
var cookieOption = new CookieOptions()
{
Secure = true
};
cookies.Append(OAuthUser.SessionCookie, token, cookieOption);

return true;
}
}

/// <summary>
/// ランダムトークンを取得
/// </summary>
/// <returns>ランダムトークン</returns>
private static string GetToken()
{
var rng = RandomNumberGenerator.Create();
var buff = new byte[25];
rng.GetBytes(buff);
return Convert.ToBase64String(buff);
}
}
}

 これでOAuth処理部分を、コントローラから任意のクラスに、うまく切り離すことができた。で、このOAuth処理を実装したクラスGithubOAuthのインスタンスをどうコントローラ内で参照できるようにするかという話である。クラスGithubOAuthはコンストラクタでClientIDとClientSecretを設定するので、静的クラスとしては使えず、インスタンス化する必要がある。
 インスタンスはいつ作る?リクエスト毎にコントローラのアクション内で…やるのは効率が悪い。インスタンスにはClientIDとClientSecretを保持してほしいが、これらはリクエスト毎に変えるものではない。アプリ立ち上げ時にあたえておくだけのもの。じゃあそれをやるために、ASP.NET Core MVCにあるDependency Injectionを使わせてもらう。
 ASP.NET Core MVCのDependency Injectionでコントローラにオブジェクトを渡すことができる。オブジェクトはクラスでもインスタンスでもいいし、インスタンスはその寿命を任意に与えられる。リクエスト毎か、アプリと同等の寿命を与えるかなど。今回はインスタンスにアプリと同等の寿命を与えるDependency Injectionにしている。
Startup.cs

public void ConfigureServices(IServiceCollection services)
{
services.Configure<GzipCompressionProviderOptions>
(options => options.Level = CompressionLevel.Fastest);
services.AddResponseCompression(options =>
{
options.Providers.Add<GzipCompressionProvider>();
});
services.AddMvc();

services.AddSingleton<HtmlEncoder>(
HtmlEncoder.Create(allowedRanges: new[] {
UnicodeRanges.BasicLatin,
UnicodeRanges.CjkSymbolsandPunctuation,
UnicodeRanges.Hiragana,
UnicodeRanges.Katakana,
UnicodeRanges.CjkUnifiedIdeographs
})
);

var githubClientId = Configuration.GetValue<string>("GITHUB_CLIENT_ID");
var githubClientSecret = Configuration.GetValue<string>("GITHUB_CLIENT_SECRET");
var githubOAuth = new GithubOAuth(githubClientId, githubClientSecret);
services.AddSingleton<GithubOAuth>(githubOAuth);
}

 これでコントローラにOAuth処理を提供してくれるインスタンスを渡すことができた。コントローラ作成時にしかインスタンス作成は行われないので不合理もない。Dependency Injectionで、コントローラにインスタンスを渡す満足な形にコードを持っていくことができた。

参考===============================
抽象クラス: http://ufcpp.net/study/csharp/oo_abstract.html
仮想メソッド: http://ufcpp.net/study/csharp/oo_polymorphism.html#virtual

Dependency Injection(オブジェクトの注入): https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/dependency-injection


comment: 0