コントローラから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

ブラウザにOneDriveAPIのJSONを渡して画像を表示

 OneDriveのAPIを使うとJSONで任意の情報を取得できる。前回に任意のディレクトリの子アイテム一覧をJSONで取得したので、今回はブラウザでそのJSONを受け取って、JSONに含まれているURLから画像ファイルを表示する…というJavaScript。

(function () {

document.getElementById("imageButton").addEventListener("click", function () {
var xhr = new XMLHttpRequest();
xhr.open("get", "/Master/OneDrive/Get");
xhr.responseType = "json";
xhr.addEventListener("load", function () {
var obj = JSON.parse(xhr.response);
var values = obj["value"];
for (var v in values) {
(function () {
var img = new Image();
img.src = values[v]["@@content.downloadUrl"];
img.width = 250;
document.getElementById("images").appendChild(img);
})();
}
});
xhr.send();
});
})();
comment: 0

OneDrive APIについて

前回

 前回にOneDriveでトークンの取得までをやったので、今回はそれを使ったディレクトリやファイルの取得までをちょっとつついておく。

 基本的な形は下記。
https://api.onedrive.com/{api-version}/drive/root/children?access_token={access token}

 ぼくは任意のディレクトリに入っているアイテム一覧がほしいので下記のようにカスタマイズ。https://api.onedrive.com/{api-version}/drive/items/{id}/children?access_token={access token}
https://dev.onedrive.com/resources/item.htm
 ディレクトリのIDは自分でブラウザを使ってOneDriveにログインして、IDを知りたいディレクトリに入ればURLのクエリにそれを確認することができる。

 あとはパラメータでオーダーやフィルターが使える。
https://api.onedrive.com/{api-version}/drive/items/{id}/children?access_token={access token}&orderby=name%20desc
https://dev.onedrive.com/odata/optional-query-parameters.htm
comment: 0

HTTPアクセスをするアプリのデバッグにFiddlerを使う

"Oh, my friend. How did you come to trade the fiddle for the drum?"
- Joni Mitchell


 アプリ、それがデスクトップでもWebアプリでもHTTPリクエストを含む場合、そのリクエストのデバッグがちょっとやっかいになることがある。レスポンスはどのコードとともに返ってきている?そもそも意図したとおりのリクエストを投げられているか?などなど。そのあたりの情報をどう表示する?コンソールに?アプリのウィンドウにデバッグ時にだけ?ブラウザに渡して表示させる?などなど。
 そういうケースではぼくはFiddlerを使っている。これは通信を監視してくれるもので、リクエストやレスポンスがどういうデータで来ているかを表示してくれる。
http://www.telerik.com/fiddler

 ぼくは昨晩にASP.NET Core MVCアプリからOneDriveのOAuthを通そうとしているときにFiddlerを使った。↓がそのキャプチャである。


 ウィンドウ左半分にどこへのリクエストでレスポンスコードはなんだったかという浅めの情報が表示され、右半分に具体的なリクエストデータやレスポンスデータをテキストやらバイナリやらで表示してくれるようになっている。これでPOSTデータは自分の意図通りになっているかということや、エラーがあった場合にサーバから送られてきた生のメッセージを見ることができる。

 というわけでHTTPが絡むようなアプリを作るならFiddlerは便利。
comment: 0

C#: ASP.NET Core MVCのアプリでOneDriveのOAuthを使う

 ASP.NET Core MVCアプリでOneDriveのAPIを使いたいので、OAuthでトークンを取ってくるという使い始めの部分までをまとめる。

 まずアプリをOneDriveのDevCenterに登録する。
https://apps.dev.microsoft.com/

 サインインするとその先の登録アプリ一覧画面の右上にアプリ追加がある。



 必要情報の入力と取得。ClienIDとClientSecret(PasswordとPrivateKeyがあるがPasswordのほう)、入力したリダイレクトURIをメモ。認証コード取得時にリダイレクトURIも渡さないといけないのだが、それが間違っているとコードが降りてこない。


 DevCenterへのアプリ登録ができたらあとはC#コード。OneDriveでアプリの仕様を許諾して認証コードを持って帰ってきたユーザの処理。
[Route("OAuth")]

public async Task<string> AuthUserAsync()
{
var client_id = xxx;
var redirect_uri = xxx;
var client_secret = xxx;
var code = Request.Query["code"];

var httpClient = new System.Net.Http.HttpClient();

// まず持って帰ってきた認証コードを使ってトークンを取得する
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "client_id", client_id },
// ↓スラッシュとか入ってるだろうけどエスケープせずに入れてOK
{ "redirect_uri", redirect_uri },
{ "client_secret", client_secret },
{ "code", code },
{ "grant_type", "authorization_code" },
});
var codeResponse = await httpClient.PostAsync("https://login.live.com/oauth20_token.srf", content);
var codeResponseBody = await codeResponse.Content.ReadAsStringAsync();
var jsonObj = JsonConvert.DeserializeObject<Dictionary<string, string>>(codeResponseBody);
var token = jsonObj["access_token"];

// 取得したトークンを使ってOneDriveにユーザ情報を要求する
var uri = $"https://api.onedrive.com/v1.0/drive?access_token={token}";
var tokenResponse = await httpClient.GetAsync(uri);
var tokenResponseBody = await tokenResponse.Content.ReadAsStringAsync();

// ユーザを確認できる情報が得られたのでごにょごにょする
// Foo(tokenResponseBody);

// return tokenResponseBody;

}
comment: 0