#はじめに
WindowsのOwinというミドルウェアを使ってOAuthを学習するメモです。
#環境
Windows10のPC1台でやります。
- Windows10 Pro 1903 (64bit)
- Visual Studio 2019
- C#
- .Net Framework 4.5
- ASP.net Web アプリケーション
- コンソールアプリ
- curl 7.65.1 for Windows
#OAuthとは
以下のデータをインプットすれば完全に理解できます。
#Owinとは
Open Web Interface for .NET
なんだがよくわからないのですが、.NETで使うミドルウェアです。nugetで落とします。
OAuthとかWindowsの頭文字ではないようです・・・
- http://owin.org/
- OAuth2.0の機能だけでなくいろいろあるっぽい
- OAuth使う場合はnugetから以下をインストールしてください
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.OAuth
- Microsoft.AspNet.WebApi.Owin
- Microsoft.AspNet.Identity.Owin
#環境構築
サンプルソースを作りました。
マイクロソフトの全く理解できない解説もあります。(参考までに)
##サンプルソリューション構成
- Clients(クライアントアプリ)
- AuthorizationCodeGrant
- ClientCredentialGrant
- ImplicitGrant
- ResourceOwnerPasswordCredentialGrant
- AuthorizationServer(認可サーバ)
- ※ASP.net Web アプリケーション
- Constants(共通クラス)
- ※クラスライブラリ(dll)
- ResourceServer(リソースサーバ)
- ※ASP.net Web アプリケーション
#クライアント・クレデンシャルズフロー
一番簡単そうなクライアント・クレデンシャルズフローをやってみます。
##とりあえず実行してみる
- AuthorizationServer(Webアプリ)を起動
- ResourceServer(Webアプリ)を起動
- ClientCredentialGrant.exe(コンソールアプリ)を実行
でClientCredentialGrant.exeの実行結果が以下のように出てきます。
C:\work\Sample\C#\ClientCredentialGrant\bin\Debug>ClientCredentialGrant.exe
Requesting Token...
Access Token: bC1ajTMu4q38RUEdYjyRpxkIgKn7voHZdBj_8bZtgkX83Wz7c0zwXsEOWl05k9bOHQsNEVEGITYBQmvazz5-la1k3ljifuw8pnASBTmZsHjg5wqdVF5BXE1lpSdJ0-2ykeed_bxj-FNIbKTWKfgSvQB1Qg44BQRA-At7zqtdb5nBKK1fd5mnJU63o1oLZ_pLQqZBoJElGbJgwb7wNF7YbKpTuh-4Pl6ukfLtn8bntVVCABpR
Access Protected Resource
"123456"
Access Token:のところでAuthorizationServerから取得したアクセストークンが出力され、最後の"12345"がResourceServerからGetしたデータです。
##フローで見てみる
実行しただけでは何が何なのかさっぱりわかりません。
フロー図にしてみます。
####ClientCredentialGrant.exe
- クライアント。
- 保護対象リソースを参照したい人です。
- クライアント・クレデンシャルズフローはバック・チャネル・コミュニケーションというブラウザを経由しない通信でいけるんで、このサンプルはコンソールアプリで作られています。
- ClientId=123456,ClientSecret=abcdefが割り当てられている。
####AuthorizationServer
- 認可サーバ。
- アクセストークンを発行する人。
-
http://localhost:11625/OAuth/Token
でトークンを発行する。
####ResourceServer
- 保護対象リソース。
- アクセストークンがあれば、リソースを出してあげる人。
-
http://localhost:38385/api/Me
でリソースを出す。 - このサンプルでは保護しているリソースを返すのではなく、接続してきたUserNameを返す仕様になっています。なのでClientIdの123456を返しています。
##Owinを使ったOAuth
Owinはアクセストークンの生成、検証を勝手にやってくれるので、かなり簡単に実装できます。
とはいえ、何をどうすればいいのか、解りやすいマニュアルはないので、サンプルプログラムを読んで、ググって、理解していく必要があります。
ここから少し詳しくサンプルプログラムの中身を見ていきます。
- AuthorizationServer
- ResourceServer
- ClientCredentialGrant.exe
の順に見ていきます。
###AuthorizationServer(認可サーバの実装)
クライアント・クレデンシャルズフローでのAuthorizationServerはクライアントの提示するClientIdとClientSecretをチェックして、パスすればアクセストークンを発行します。
Owinを利用してプログラムを作成する人はClientIdとClientSecretをチェックするロジックを書けば、アクセストークンの発行はOwinが勝手にやってくれます、簡単です。
OwinのロジックはStartupクラスに全部まとめて書きます。
具体的には以下の通り
1. 起動時に実行されるStartup.Configuration()
イベントで動作パラメータを設定する
`Startup.Configuration()`イベント
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(AuthorizationServer.Startup))]
namespace AuthorizationServer
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
// ここで初期設定
ConfigureAuth(app);
}
public void ConfigureAuth(IAppBuilder app)
{
// Setup Authorization Server
var option = new OAuthAuthorizationServerOptions
{
// アクセストークンエンドポイントの設定
TokenEndpointPath = new PathString(Paths.TokenPath),
// HTTPを許可する(リリース時はHTTPSにしないといけないですが、デバックのときはこうしておきましょう)
AllowInsecureHttp = true,
// イベントコールバックメソッドの設定
Provider = new OAuthAuthorizationServerProvider
{
// ClientIdとClientSecretの検証
OnValidateClientAuthentication = ValidateClientAuthentication,
// ClientCredetailsのときの処理
OnGrantClientCredentials = GrantClientCredetails
},
// AccessTokenExpireTimeSpanはデフォルトで20:00
};
app.UseOAuthAuthorizationServer(option);
}
}
}
2. TokenEndpointにアクセスされたときに実行されるStartup.ValidateClientAuthentication()
イベントでクライアントの正当性をチェックする
context.Validated()
すればチェックOKという事になります。
`ValidateClientAuthentication()`イベント
public partial class Startup
{
private Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId;
string clientSecret;
// ClientIDとClientSecretをヘッダまたはフォームからGetする
if (context.TryGetBasicCredentials(out clientId, out clientSecret) ||
context.TryGetFormCredentials(out clientId, out clientSecret))
{
if (clientId == Clients.Client1.Id && clientSecret == Clients.Client1.Secret)
{
// Client1のとき->OK
context.Validated();
}
else if (clientId == Clients.Client2.Id && clientSecret == Clients.Client2.Secret)
{
// Client2のとき->OK
context.Validated();
}
}
return Task.FromResult(0);
}
}
3. Startup.GrantClientCredetails()
イベントの実装
- ここでidentityを生成します。
-
context.Validated(identity)
すればチェックOKという事になります。 - そうするとクライアントにアクセストークンが渡されます。
`GrantClientCredetails()`イベント
public partial class Startup
{
private Task GrantClientCredetails(OAuthGrantClientCredentialsContext context)
{
// identityを生成
var identity = new ClaimsIdentity(new GenericIdentity(context.ClientId, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
context.Validated(identity);
return Task.FromResult(0);
}
}
###ResourceServer(保護対象リソースの実装)
保護対象リソースを引き渡すためのWebAPIで/API/Me
を提供しています。
ここにアクセスしてくるクライアントが提示するアクセストークンを検証し、有効なアクセストークンであれば、リソースを返却します。
このサンプルで返却するリソースはアクセスしてきたユーザー名です。厳密にはアクセストークンの中に含まれるUserNameです。
アクセストークンの検証は簡単です。
1. WebAPIの初期設定をする
WebAPIの初期設定
using System.Web.Http;
namespace ResourceServer
{
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
// Microsoft.AspNet.WebApi.WebHostをnugetから追加すること
GlobalConfiguration.Configure(WebApiConfig.Register);
}
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
}
}
2. 起動時に実行されるStartup.Configuration()
イベントで動作パラメータを設定する
`Configuration()`イベント
[assembly: OwinStartup(typeof(ResourceServer.Startup))]
namespace ResourceServer
{
// クラスの追加→OWIN Startupクラス
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions());
}
}
}
3.WebAPIにアクセスされたときに実行されるMeController.Get()
イベントを実装する
-
[Authorize]
属性を付けます。 - これをつけるだけでOwinがアクセストークンの検証を勝手にやってくれます。
- アクセストークンが有効な場合だけ
MeController.Get()
イベントが実行されるので、データを返却します。
`MeController.Get()`イベント
namespace ResourceServer
{
// http://localhost:38385/api/Me
// で実行されるWebAPI
// [Authorize]属性がついているので、AccessTokenが有効な場合だけ実行される
// AccessTokenの検証はOwinが勝手にやってくれる
[Authorize]
public class MeController : ApiController
{
public string Get()
{
// User.IdentityはApiControllerクラスのメンバ
// AccessTokenを復号したクラス
// Nameを返すと呼び出しAppにNameの内容がResponseとして返る
return this.User.Identity.Name;
}
}
}
###ClientCredentialGrant.exe(クライアントの実装)
サンプルプログラムだと何やっているかいまいちわからないのでcurlで同じことをやります。
(最近のWindowsはcurlが標準で入っています)
1. 認可サーバのトークンエンドポイントにPOSTしてTokenをGETする
TokenをGETする
C:\Users\gebo>curl -XPOST http://localhost:11625/OAuth/Token -H "Authorization:Basic MTIzNDU2OmFiY2RlZg==" -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=client_credentials"
{"access_token":"GlArJSuFC8dqXCJT6dXEKTBeuedcyI_239qsC4HJpdlqJssztctPV9hEQ6jzGAtRveqv5Olo_Tru-QwkZH3ksbZlSh2DHRDI0zOpIYk27kf5cQGaRl6Znx_5LKnqgIJUjQj1Mmb_G246lgE8_Vxapbb5fhzBOenGdMujbREAJpp819hKk8VDXPMnYBndPg0LXERPoRHS48aBKvAy5xO_x8kvXGPFuCT89yCht6CMqh4","token_type":"bearer","expires_in":1199}
// 見やすくすると
// コマンド
curl -XPOST http://localhost:11625/OAuth/Token
// Authorizationヘッダ Basic半角スペースのあとClientIdとClientSecretをコロンでつないでBASE64変換したものを指定する
-H "Authorization:Basic MTIzNDU2OmFiY2RlZg=="
// Content-Typeヘッダ Bodyあるよという意味でこの指定
-H "Content-Type: application/x-www-form-urlencoded"
// Body grant_typeの指定はclient_credentialsにすること
-d "grant_type=client_credentials"
// レスポンス
{
// 取得したアクセストークン
"access_token":"GlArJSuFC8dqXCJT6dXEKTBeuedcyI_239qsC4HJpdlqJssztctPV9hEQ6jzGAtRveqv5Olo_Tru-QwkZH3ksbZlSh2DHRDI0zOpIYk27kf5cQGaRl6Znx_5LKnqgIJUjQj1Mmb_G246lgE8_Vxapbb5fhzBOenGdMujbREAJpp819hKk8VDXPMnYBndPg0LXERPoRHS48aBKvAy5xO_x8kvXGPFuCT89yCht6CMqh4",
// トークンタイプ
"token_type":"bearer",
// アクセストークンの有効期限(秒) 1199秒≒20分
"expires_in":1199
}
2. リソースサーバのWebAPIをたたいてリソースをGETする
さっき取得したアクセストークンはここで使います。
リソースをGETする
C:\Users\gebo>curl http://localhost:38385/api/Me -H "Authorization:Bearer GlArJSuFC8dqXCJT6dXEKTBeuedcyI_239qsC4HJpdlqJssztctPV9hEQ6jzGAtRveqv5Olo_Tru-QwkZH3ksbZlSh2DHRDI0zOpIYk27kf5cQGaRl6Znx_5LKnqgIJUjQj1Mmb_G246lgE8_Vxapbb5fhzBOenGdMujbREAJpp819hKk8VDXPMnYBndPg0LXERPoRHS48aBKvAy5xO_x8kvXGPFuCT89yCht6CMqh4"
"123456"
// 見やすくすると
// コマンド
curl http://localhost:38385/api/Me
// Authorizationヘッダ Bearer半角スペースのあと取得したアクセストークンを指定する
-H "Authorization:Bearer GlArJSuFC8dqXCJT6dXEKTBeuedcyI_239qsC4HJpdlqJssztctPV9hEQ6jzGAtRveqv5Olo_Tru-QwkZH3ksbZlSh2DHRDI0zOpIYk27kf5cQGaRl6Znx_5LKnqgIJUjQj1Mmb_G246lgE8_Vxapbb5fhzBOenGdMujbREAJpp819hKk8VDXPMnYBndPg0LXERPoRHS48aBKvAy5xO_x8kvXGPFuCT89yCht6CMqh4"
// レスポンス
"123456"
#おつかれさまでした
Owin学習コスト高いな・・・
MSのサンプルを参考に作成したソース
https://github.com/gebogebogebo/OwinOAuthSample/tree/master/ClientCredentialGrant
#2につづく