#はじめに
WindowsのOwinというミドルウェアを使ってOAuthを学習するメモです。
#2からの続きです。
今回は、リソースオーナー・パスワード・クレデンシャルズフローというやたら長い名前のフローとリフレッシュトークンの話です。
リソースオーナー・パスワード・クレデンシャルズフロー
#1でやったクライアント・クレデンシャルズフローより少しだけ考慮が増えたフローで、アクセストークン発行時にクライアントのチェックに加えてユーザーのチェックをします。
###ちなみに
OAuth徹底入門セキュアな認可システムを適用するための原則と実践によると
このフローは使ってはいけない「アンチ・パターン」として解説されています。
抜粋引用
このパターンを使えば、構成要素間のやり取りをすべてリダイレクトを使って進めていくやり方より、プログラムは明らかにシンプルになります。しかし、このようなシンプルさはセキュリティ・リスクを非常に高くすることになり、柔軟性や機能性が減退してしまいます。
・・・中略・・・
それでは、なぜ、OAuthはこのような問題のあるプラクティスを盛り込んだのでしょうか?もし、別の選択肢が利用できるのなら、この付与方式を選ぶべきではないのですが、ほかの現実的な選択肢がいつもあるとはかぎりません。
・・・中略・・・
認可コードによる付与方式のようなフローを使うのが本当は好ましいことですが、場合によっては、このフローを使うことが保護対象リソースに対してリクエストのたびにパスワードを提示するより良い場合もあります。
というわけで、なんもしないよりはいいけど、おススメできないフローだという事です。
もちろんおススメは「認可コードフロー(めちゃ複雑なやつ)」です。
###AuthorizationServerの実装
1. 起動時に実行されるStartup.Configuration()
イベントで動作パラメータを設定する
`Startup.Configuration()`イベント
using Microsoft.Owin;
using Microsoft.Owin.Security.OAuth;
using Owin;
[assembly: OwinStartup(typeof(AuthorizationServer.Startup))]
namespace AuthorizationServer
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
var option = new OAuthAuthorizationServerOptions {
// アクセストークンエンドポイントの設定
TokenEndpointPath = new PathString("/OAuth/Token"),
// HTTPを許可する(リリース時はHTTPSにしないといけないですが、デバックのときはこうしておきましょう)
AllowInsecureHttp = true,
// イベントコールバックメソッドの設定
Provider = new OAuthAuthorizationServerProvider {
// ClientIdとClientSecretの検証
OnValidateClientAuthentication = ValidateClientAuthentication,
// ResourceOwnerCredentialsのときの処理
OnGrantResourceOwnerCredentials = GrantResourceOwnerCredentials
},
// AccessTokenExpireTimeSpanを10分に設定する(省略した場合のデフォルトは20分)
AccessTokenExpireTimeSpan = new TimeSpan(0, 10, 0)
};
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))
{
// clientId と clientSecret をチェックして接続を許可する場合は
// context.Validated();する
context.Validated();
}
return Task.FromResult(0);
}
}
3. Startup.GrantResourceOwnerCredentials()
イベントでユーザーの正当性をチェックする
- ここでidentityを生成します。
-
context.Validated(identity)
すればチェックOKという事になります。 - そうするとクライアントにアクセストークンが渡されます。
`GrantResourceOwnerCredentials()`イベント
public partial class Startup
{
private Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// usernameとpasswordをGetする
string username = context.UserName;
string password = context.Password;
// username と password をチェックして接続を許可する場合は
// identityを作成して
// context.Validated(identity);する
// identityを生成
var identity = new ClaimsIdentity(new GenericIdentity(username, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
// ここでセットしたidentityがTokenになる
context.Validated(identity);
return Task.FromResult(0);
}
}
###ResourceServerの実装
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にアクセスされたときに実行されるTestController.Get()
イベントを実装する
-
[Authorize]
属性を付けます。 - これをつけるだけでOwinがアクセストークンの検証を勝手にやってくれます。
- アクセストークンが有効な場合だけ
TestController.Get()
イベントが実行されるので、データを返却します。
`TestController.Get()`イベント
namespace ResourceServer
{
// クラスの追加→WebAPIコントローラクラス
// http://localhost:38385/api/Test
// で実行されるWebAPI
// [Authorize]属性がついているので、AccessTokenが有効な場合だけ実行される
// AccessTokenの検証はOwinが勝手にやってくれる
[Authorize]
public class TestController : ApiController
{
public IEnumerable<string> Get()
{
// this.User.Identity が Token をデコードしたもの
string value = $"Your Name is {this.User.Identity.Name}";
return new string[] { "result value1", value };
}
}
}
###Clientの実装
リソースオーナー・パスワード・クレデンシャルズフローはバック・チャネル・コミュニケーションというブラウザを経由しない通信なので、curlコマンドでやってしまいます。
1. 認可サーバのトークンエンドポイントにPOSTしてアクセストークンを取得する
アクセストークンを取得する
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=password&username=gebo&password=gebopass&scope=foo bar"
{"access_token":"edIAKCtVWZNqBT7B16Iuk2ktNKcn858EQSIhRIVHrI0LrBnuN9kn6lHrWLSvrIfxm7AjFGzRNwqAWPn02vRwCBgqj5JmCWP8MC-R3r3gwjpU3bqTBaTIoeS-7fP9B1826albPxT9jf8RTgVcITyWg6cYBduVuefInGdz_SMk1MtCgbiBj-_b6vq_mWYrAcYWDT5g4aenXPjs_6hP1eZwl_KA4SP1_b6RkP8iNnWE49yffOgx","token_type":"bearer","expires_in":599}
// 見やすくすると
// コマンド
curl -XPOST http://localhost:11625/OAuth/Token
-H "Authorization:Basic MTIzNDU2OmFiY2RlZg=="
-H "Content-Type: application/x-www-form-urlencoded"
-d "grant_type=password&username=gebo&password=gebopass&scope=foo bar"
// レスポンス
{
"access_token":"edIAKCtVWZNqBT7B16Iuk2ktNKcn858EQSIhRIVHrI0LrBnuN9kn6lHrWLSvrIfxm7AjFGzRNwqAWPn02vRwCBgqj5JmCWP8MC-R3r3gwjpU3bqTBaTIoeS-7fP9B1826albPxT9jf8RTgVcITyWg6cYBduVuefInGdz_SMk1MtCgbiBj-_b6vq_mWYrAcYWDT5g4aenXPjs_6hP1eZwl_KA4SP1_b6RkP8iNnWE49yffOgx",
"token_type":"bearer",
"expires_in":599
}
2. リソースサーバのWebAPIをたたいてリソースをGETする
リソースをGETする
C:\Users\gebo>curl http://localhost:38385/api/Test -H "Authorization:Bearer edIAKCtVWZNqBT7B16Iuk2ktNKcn858EQSIhRIVHrI0LrBnuN9kn6lHrWLSvrIfxm7AjFGzRNwqAWPn02vRwCBgqj5JmCWP8MC-R3r3gwjpU3bqTBaTIoeS-7fP9B1826albPxT9jf8RTgVcITyWg6cYBduVuefInGdz_SMk1MtCgbiBj-_b6vq_mWYrAcYWDT5g4aenXPjs_6hP1eZwl_KA4SP1_b6RkP8iNnWE49yffOgx"
["result value1","Your Name is gebo"]
// 見やすくすると
// コマンド
curl http://localhost:38385/api/Test
-H "Authorization:Bearer edIAKCtVWZNqBT7B16Iuk2ktNKcn858EQSIhRIVHrI0LrBnuN9kn6lHrWLSvrIfxm7AjFGzRNwqAWPn02vRwCBgqj5JmCWP8MC-R3r3gwjpU3bqTBaTIoeS-7fP9B1826albPxT9jf8RTgVcITyWg6cYBduVuefInGdz_SMk1MtCgbiBj-_b6vq_mWYrAcYWDT5g4aenXPjs_6hP1eZwl_KA4SP1_b6RkP8iNnWE49yffOgx"
// レスポンス
["result value1","Your Name is gebo"]
#リフレッシュトークン
アクセストークンを再取得するためのトークンをリフレッシュトークンといいます。
リフレッシュトークンをTokenEndPointに投げると新しいAccessTokenがGETできるという仕組みです。
アクセストークンを再取得したければ、最初に取得したようにすればいいじゃん、と思うところですが、そうしないところがOAuthなのです。
OAuth徹底入門セキュアな認可システムを適用するための原則と実践によると
リフレッシュ・トークンはアクセス・トークンとともに発行され、ユーザーを介入させずに新しいアクセス・トークンを生成するのに使われる
ということです。
アクセストークンの有効期限が切れたときに、新しいアクセストークンを取得したいんだけど、取得するためには、ユーザー認証(の場合ユーザーID、パスワード)が必要になる。その場合のユーザーの操作(介入)を省略するのが目的みたいです。
アクセストークンの有効期限を無期限にすればいいじゃん、って言ってはいけません。
###フロー図
さっきの図とほぼ一緒です。★のところがリフレッシュトークンに関係します。
###AuthorizationServerの実装
1. 起動時に実行されるStartup.Configuration()
イベントで動作パラメータを設定する
リフレッシュトークンの生成と受信コールバックの設定、ということで
RefreshTokenProvider
を設定します。
`Startup.Configuration()`イベント
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(AuthorizationServer.Startup))]
namespace AuthorizationServer
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
var option = new OAuthAuthorizationServerOptions {
// アクセストークンエンドポイントの設定
TokenEndpointPath = new PathString("/OAuth/Token"),
// HTTPを許可する(リリース時はHTTPSにしないといけないですが、デバックのときはこうしておきましょう)
AllowInsecureHttp = true,
// イベントコールバックメソッドの設定
Provider = new OAuthAuthorizationServerProvider {
// ClientIdとClientSecretの検証
OnValidateClientAuthentication = ValidateClientAuthentication,
// ResourceOwnerCredentialsのときの処理
OnGrantResourceOwnerCredentials = GrantResourceOwnerCredentials
},
// ★リフレッシュトークンの生成と受信コールバックの設定
RefreshTokenProvider = new AuthenticationTokenProvider {
OnCreate = CreateRefreshToken,
OnReceive = ReceiveRefreshToken,
},
// AccessTokenExpireTimeSpanを10分に設定する(省略した場合のデフォルトは20分)
AccessTokenExpireTimeSpan = new TimeSpan(0, 10, 0)
};
app.UseOAuthAuthorizationServer(option);
}
}
}
2. CreateRefreshToken()
イベントでリフレッシュトークンを生成する
- このイベントはアクセストークンを取得する一連のイベントの中で発生します。
- リフレッシュトークンの有効期限はここで設定します。
- リフレッシュトークンの有効期限はアクセストークンのそれより長く設定しましょう。
`Startup.CreateRefreshToken()`イベント
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(AuthorizationServer.Startup))]
namespace AuthorizationServer
{
public partial class Startup
{
private void CreateRefreshToken(AuthenticationTokenCreateContext context)
{
// リフレッシュトークンの有効期限を設定する(1日)
int expire = 24 * 60 * 60;
context.Ticket.Properties.ExpiresUtc = new DateTimeOffset(DateTime.Now.AddSeconds(expire));
context.SetToken(context.SerializeTicket());
}
}
}
3. ReceiveRefreshToken()
イベントを実装する
アクセストークンを再取得する場合は、grant_type=refresh_token
をTokenEndpointにPOSTします。
で、このとき、AuthorizationServer側ではRefreshTokenProviderのOnReceiveに指定したイベントが発生します。
このサンプルではReceiveRefreshToken()
を指定しているのでこのメソッドを実装します。
######なにを実装したらいいの?
DeserializeTicket()
- おまじない、です。
- なんかよくわからないが、これをするとOK。
`Startup.CreateRefreshToken()`イベント
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(AuthorizationServer.Startup))]
namespace AuthorizationServer
{
public partial class Startup
{
private void ReceiveRefreshToken(AuthenticationTokenReceiveContext context)
{
// このおまじないをするとCreateRefreshToken()イベントが発生してアクセストークンとリフレッシュトークンが再生成される
context.DeserializeTicket(context.Token);
}
}
}
###ResourceServerの実装
さっきと一緒です
###Clientの実装
1.アクセストークンとリフレッシュトークンを取得する
アクセストークンとリフレッシュトークンを取得する
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=password&username=gebo&password=gebopass&scope=foo bar"
{"access_token":"fmE12yMnrgrXBOVzQuaS7x2oqR7amOTXP7jvMeLpZ33FXKmKBCvRUUxon86xN1QmK2XWdpFmDTzC38OvZ3ZlYSVqqcEsZFYjSi0WCFEPffIRoXy9Sd1e1TOpTuGHw86qXqlQisy43sqEwHR748Mr57bcbAAH1d3ng2nsHu8gfdrMkBHC59JTNbWxHLVf-80jMDlSf3SPiNuUeNkvYqa6oIL7ochb2_68WX_ZS3-o_Y5To1Dm","token_type":"bearer","expires_in":599,"refresh_token":"GsS7YuYXO38e46z2smqXvOMAes4XoNIS2wTABPH19GT6JY9pNR2QeJqyRZRyIsr7Lnb8sEEozCVmkY6taYBExNKo49wf9QDMy06d8O4tm3Vv4h_lanR2WEHax4wyBDaxENNS-eUTy3KQjc3mMwqilB80X9SreULJ7zYPjglQ3SUMhetDcEjuEhsDxbtvlxJu5zOoBU4TbBAKjUZTKBrHbUdqIUYMnYRq0BW9WB3WxNjuLh7x"}
// 見やすくすると
// コマンド
curl -XPOST http://localhost:11625/OAuth/Token
-H "Authorization:Basic MTIzNDU2OmFiY2RlZg=="
-H "Content-Type: application/x-www-form-urlencoded"
-d "grant_type=password&username=gebo&password=gebopass&scope=foo bar"
// レスポンス
{
"access_token":"fmE12yMnrgrXBOVzQuaS7x2oqR7amOTXP7jvMeLpZ33FXKmKBCvRUUxon86xN1QmK2XWdpFmDTzC38OvZ3ZlYSVqqcEsZFYjSi0WCFEPffIRoXy9Sd1e1TOpTuGHw86qXqlQisy43sqEwHR748Mr57bcbAAH1d3ng2nsHu8gfdrMkBHC59JTNbWxHLVf-80jMDlSf3SPiNuUeNkvYqa6oIL7ochb2_68WX_ZS3-o_Y5To1Dm",
"token_type":"bearer",
"expires_in":599,
"refresh_token":"GsS7YuYXO38e46z2smqXvOMAes4XoNIS2wTABPH19GT6JY9pNR2QeJqyRZRyIsr7Lnb8sEEozCVmkY6taYBExNKo49wf9QDMy06d8O4tm3Vv4h_lanR2WEHax4wyBDaxENNS-eUTy3KQjc3mMwqilB80X9SreULJ7zYPjglQ3SUMhetDcEjuEhsDxbtvlxJu5zOoBU4TbBAKjUZTKBrHbUdqIUYMnYRq0BW9WB3WxNjuLh7x"
}
どんなトークンなのかデコードツールでみてみると
アクセストークンの有効期限は10分でリフレッシュトークンの有効期限は1日ということがわかります。
// アクセストークン
トークン発行日時 : 2019/07/12 12:46:17
トークン失効日時 : 2019/07/12 12:56:17
IdentityName : gebo
Roles(0) :
Claims(3) :
- Type=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name , Value=gebo
- Type=urn:oauth:scope , Value=foo
- Type=urn:oauth:scope , Value=bar
// リフレッシュトークン
トークン発行日時 : 2019/07/12 12:46:17
トークン失効日時 : 2019/07/13 12:46:17
IdentityName : gebo
Roles(0) :
Claims(3) :
- Type=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name , Value=gebo
- Type=urn:oauth:scope , Value=foo
- Type=urn:oauth:scope , Value=bar
1. 認可サーバのトークンエンドポイントにリフレッシュトークンを投げてアクセストークンを再取得する
RefreshTokenをPOSTする
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=refresh_token&refresh_token=GsS7YuYXO38e46z2smqXvOMAes4XoNIS2wTABPH19GT6JY9pNR2QeJqyRZRyIsr7Lnb8sEEozCVmkY6taYBExNKo49wf9QDMy06d8O4tm3Vv4h_lanR2WEHax4wyBDaxENNS-eUTy3KQjc3mMwqilB80X9SreULJ7zYPjglQ3SUMhetDcEjuEhsDxbtvlxJu5zOoBU4TbBAKjUZTKBrHbUdqIUYMnYRq0BW9WB3WxNjuLh7x"
{"access_token":"OGgy0u0kgcBdeixmdwAdNijyHKVGbXVkUyfuoOnBfhHMRsO7pAJ_EK2enrYamMQHwp4Jk8yi5j1Cz4LvvQ58paBvH0aJU_78opn5zCA0g8Y2tHyfQ3MXsFM2ZPYOHaLHYj9WApXTjObDsr2UQ6hcWJk5Wksp1lwrHnirjM31QoZ7UJjdJRZx3yyGfIjBDjTm25n5x0eDrpybMGkjHYIbE1hVGXb4nIEPojWYtsTqJi0doTSS","token_type":"bearer","expires_in":599,"refresh_token":"_22wBL2RBmBIWTOaAoZPG-9xPBi6Z0DE52N44V_r8xVUgR8mir2u_nQP1d7W98WpmhHMv4NJ923nUQ7zzGfFDt4yrYmbCf9sqh8zHrS83btPyNLKlYkU3UjvHnKjqPmaVFHKceNm88tCVkiM_XdmiWpiDA7T10wgAIocSyUozHlTE0WMqdaN9w9NbWiqRCenoyWRqF2_mNzQjTkE3K2rGCtIWLmzx8OWB7wt0C2fR7koJcFE"}
// 見やすくすると
// コマンド
curl -XPOST http://localhost:11625/OAuth/Token
-H "Authorization:Basic MTIzNDU2OmFiY2RlZg=="
-H "Content-Type: application/x-www-form-urlencoded"
-d "grant_type=refresh_token&refresh_token=GsS7YuYXO38e46z2smqXvOMAes4XoNIS2wTABPH19GT6JY9pNR2QeJqyRZRyIsr7Lnb8sEEozCVmkY6taYBExNKo49wf9QDMy06d8O4tm3Vv4h_lanR2WEHax4wyBDaxENNS-eUTy3KQjc3mMwqilB80X9SreULJ7zYPjglQ3SUMhetDcEjuEhsDxbtvlxJu5zOoBU4TbBAKjUZTKBrHbUdqIUYMnYRq0BW9WB3WxNjuLh7x"
// レスポンス
{
"access_token":"OGgy0u0kgcBdeixmdwAdNijyHKVGbXVkUyfuoOnBfhHMRsO7pAJ_EK2enrYamMQHwp4Jk8yi5j1Cz4LvvQ58paBvH0aJU_78opn5zCA0g8Y2tHyfQ3MXsFM2ZPYOHaLHYj9WApXTjObDsr2UQ6hcWJk5Wksp1lwrHnirjM31QoZ7UJjdJRZx3yyGfIjBDjTm25n5x0eDrpybMGkjHYIbE1hVGXb4nIEPojWYtsTqJi0doTSS",
"token_type":"bearer",
"expires_in":599,
"refresh_token":"_22wBL2RBmBIWTOaAoZPG-9xPBi6Z0DE52N44V_r8xVUgR8mir2u_nQP1d7W98WpmhHMv4NJ923nUQ7zzGfFDt4yrYmbCf9sqh8zHrS83btPyNLKlYkU3UjvHnKjqPmaVFHKceNm88tCVkiM_XdmiWpiDA7T10wgAIocSyUozHlTE0WMqdaN9w9NbWiqRCenoyWRqF2_mNzQjTkE3K2rGCtIWLmzx8OWB7wt0C2fR7koJcFE"
}
どんなトークンなのかデコードツールでみてみます。
再取得したアクセストークンと再取得したリフレッシュトークンは発行日時、失効日時が更新されているのがわかります。
// POSTしたリフレッシュトークン
トークン発行日時 : 2019/07/12 12:46:17
トークン失効日時 : 2019/07/13 12:46:17
IdentityName : gebo
Roles(0) :
Claims(3) :
- Type=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name , Value=gebo
- Type=urn:oauth:scope , Value=foo
- Type=urn:oauth:scope , Value=bar
// 再取得したアクセストークン
トークン発行日時 : 2019/07/12 12:50:30
トークン失効日時 : 2019/07/12 13:00:30
IdentityName : gebo
Roles(0) :
Claims(3) :
- Type=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name , Value=gebo
- Type=urn:oauth:scope , Value=foo
- Type=urn:oauth:scope , Value=bar
// 再取得したリフレッシュトークン
トークン発行日時 : 2019/07/12 12:50:30
トークン失効日時 : 2019/07/13 12:50:30
IdentityName : gebo
Roles(0) :
Claims(3) :
- Type=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name , Value=gebo
- Type=urn:oauth:scope , Value=foo
- Type=urn:oauth:scope , Value=bar
#おつかれさまでした
ソースはコチラ
https://github.com/gebogebogebo/OwinOAuthSample
だんだんめんどくさくなってきた。
#4へつづく