#はじめに
WindowsのOwinというミドルウェアを使ってOAuthを学習するメモです。
#3からの続きです。
やっと認可コードフローまでたどり着きました。
#認可コードフローとは
OAuth徹底入門セキュアな認可システムを適用するための原則と実践によると
認可コードによる付与方式(AuthorizationCodeGrantType)はOAuthのすべての構成要素を完全に分離しており、結果としてこの付与方式が本書で扱う付与方式の中で最も基本的でありながらも複雑なものになっているからです。OAuthのほかのすべての付与方式はこの認可コードによる付与方式を特定のユース・ケースや環境に合うように最適化されたものです。
と、いうわけで、
認可コードフローはOAuth2.0のフローの中で最も基本的なんだけれども、最も複雑
です・・・
#環境
Windows10のPC1台でやります。
- Windows10 Pro 1903 (64bit)
- Visual Studio 2019
- C#
- .Net Framework 4.5
- ASP.net Web アプリケーション
- waireshark←HTTPキャプチャ用
- サンプルプログラム
#とりあえずサンプルを実行してみる
#1で使ったサンプルプグラムを改造したものです。
クライアントアプリが認可サーバーからアクセストークンをGETしてリソースサーバーにアクセスする、というシナリオは今までの学習メモと同じです。
認可コードフローのポイントはアクセストークンをGETする前に認可コードをGETするというところと、ステートがあるということです。
※認可コード、ステートについては中盤以降で説明します。
http://localhost:38500/
が入口です。
####①クライアント画面
1.アクセストークンを取得する をクリック
####②ログイン画面
ログインを要求されるのでUserNameとPasswordを入力してログインをクリック、
サンプルなので何を入力してもOK。
####③確認画面
確認画面で承認してログインするをクリック
####④クライアント画面
アクセストークンがGETできれば最初の画面に戻ってきます。
2.リソースサーバーにアクセスする
をクリックしてUserNameがGETできればサンプルプログラムの動作はOKです。
⇒リソースサーバーにアクセストークンを提示してログインしたUserNameをGETしている。
#フロー
処理の大雑把な流れは
- A.認可コード取得
- A-1.クライアント検証(※)
- A-2.ユーザー検証(※)
- A-3.認可確認
- B.アクセストークン取得
- C.リソース取得
※検証について
図にするとこんな感じ
#処理詳細
詳細フロー図
##A.認可コード取得
###A-1.クライアント検証
クライアント - 認可エンドポイントへのリクエスト
GET {認可エンドポイント}
?client_id={クライアントID}
&redirect_uri={リダイレクトURI}
&state={任意文字列}
&response_type=code
HTTP/1.1
HOST: {認可サーバー}
// 認可エンドポイント=http://localhost:11625/OAuth/Authorize
// クライアントID=123456
// リダイレクトURI=http://localhost:38500/
認可サーバー 認可エンドポイント - クライアントIDの検証
認可サーバーではValidateClientRedirectUri()
イベントが発生し、ここでクライアントIDを検証します。
private Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
// ClientIDの検証
if (context.ClientId == "123456")
{
// OKだったらValidated()する
context.Validated();
}
return Task.FromResult(0);
}
その次にOAuthController.Authorize()
が実行されす。
以下コードはかなり省略しています。Challenge()してreturnするところがポイントです
public ActionResult Authorize()
{
…省略
var authentication = HttpContext.GetOwinContext().Authentication;
var ticket = authentication.AuthenticateAsync("Application").Result;
var identity = ticket != null ? ticket.Identity : null;
if (identity == null)
{
// ログインされてない場合は、ここに入ります
authentication.Challenge("Application");
return null;
}
…省略
return View();
}
クライアント - 認可エンドポイントからのレスポンス
HTTP/1.1 302 Found
Location: {リダイレクトURI}
// リダイレクトURI
// http://localhost:11625/Account/Login
// ?ReturnUrl={リクエストで指定されたパラメータそのまま}
###A-2.ユーザー検証
認可サーバーが提供するログイン画面でログインされたタイミングでユーザー検証が行われます。
クライアント - ログインのリクエスト
POST /Account/Login HTTP/1.1
Host: {認可サーバー}
Content-Type: application/x-www-form-urlencoded
username={ユーザー名}
&password={パスワード}
&submit.Signin="ログイン"
// ユーザー名=ログイン画面で入力されたUserName
// パスワード=ログイン画面で入力されたPassword
認可サーバー Login - ユーザー名とパスワードの検証
public ActionResult Login()
{
var authentication = HttpContext.GetOwinContext().Authentication;
if (Request.HttpMethod == "POST")
{
if (!string.IsNullOrEmpty(Request.Form.Get("submit.Signin")))
{
var username = Request.Form["username"];
var pass = Request.Form["password"];
// ここでログインしていいユーザーかどうかチェックします
// OKであればSignIn()
authentication.SignIn(
new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, username) }, "Application"));
}
}
return View();
}
クライアント - ログインのレスポンス
HTTP/1.1 302 Found
Location: {リダイレクトURI}
// リダイレクトURI=http://localhost:11625/Authorize/
###A-3.認可確認
クライアント - 認可エンドポイントへのリクエスト
####認可エンドポイントへのリクエスト
認可サーバーの認可エンドポイントに対してPOSTリクエストします
POST {認可エンドポイント}
?client_id={クライアントID}
&redirect_uri={リダイレクトURI}
&state={任意文字列}
&response_type=code
HTTP/1.1
Host: {認可サーバー}
Content-Type: application/x-www-form-urlencoded
submit.Grant=承認してログインする
認可サーバー 認可エンドポイント - クライアントIDの検証
####クライアントIDの検証
さっきと同じように、認可サーバーでValidateClientRedirectUri()
イベントが発生し、クライアントIDを検証します。
認可サーバー 認可エンドポイント - identity生成
####identity生成
再びOAuthController.Authorize()
が実行されますが、いろいろ条件分岐があって、identity(=アクセストークンのもと)を生成します。
public ActionResult Authorize()
{
…省略
if (Request.HttpMethod == "POST")
{
if (!string.IsNullOrEmpty(Request.Form.Get("submit.Grant")))
{
// identity(=アクセストークンのもと)を生成する
identity = new ClaimsIdentity(identity.Claims, "Bearer", identity.NameClaimType, identity.RoleClaimType);
foreach (var scope in scopes)
{
identity.AddClaim(new Claim("urn:oauth:scope", scope));
}
// これで次のステップに移る
authentication.SignIn(identity);
}
}
return View();
}
認可サーバー 認可エンドポイント - 認可コード生成
####認可コード生成
続けざまにStartup.CreateAuthenticationCode()
が発生します。ここでついに認可コードが生成されるのです。
生成した認可とアクセストークン(context.SerializeTicket())を紐づけてメモリに格納します。
private void CreateAuthenticationCode(AuthenticationTokenCreateContext context)
{
// 認可コード生成
var code = Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n");
context.SetToken(code);
// テーブルに格納(本気で実装する人はDBとかに入れましょう)
_authenticationCodes[context.Token] = context.SerializeTicket();
}
クライアント - 認可エンドポイントからのレスポンス
####認可エンドポイントからのレスポンス
レスポンスは例によって302のリダイレクトです。
このとき生成された認可コードをもってすっ飛んで行きます。
行先は、クライアント画面です
HTTP/1.1 302 Found
Location: {リダイレクトURI}
// リダイレクトURI
// http://localhost:38500/
// ?code={認可コード}
// &state={リクエストで指定したステート}
これでやっと認可コードがクライアントに渡されました。
ちなみに現時点では、認可サーバーが生成した認可コードは認可サーバーのメモリにも格納されているので、クライアントと認可サーバー両者が認可コードを持っています。
###B.アクセストークンの取得
ここまで来たらあとはクライアント・クレデンシャルズフローとあんまり変わりません。
クライアント - トークンエンドポイントへのリクエスト
####トークンエンドポイントへのリクエスト
認可サーバーのトークンエンドポイントに対してPOSTリクエストします
Authorizationヘッダに例のBasicを指定します。
codeで認可コードを指定します!
POST {トークンエンドポイント} HTTP/1.1
Host: {認可サーバー}
Authorization: Basic {クライアントクレデンシャルズ}
Content-Type: application/x-www-form-urlencoded
code={認可コード}
&redirect_uri={リダイレクトURI}
&grant_type=authorization_code
// クライアントクレデンシャルズ=BASE64({クライアントID}:{シークレット})
// 認可コード=さっき取得したもの
// リダイレクトURI=http://localhost:38500/
認可サーバー トークンエンドポイント - クライアントID、シークレットの検証
####クライアントID、シークレットの検証
認可サーバー側はStartup.ValidateClientAuthentication()
イベントが発生してクライアントID、シークレットの検証を行います。これはクライアント・クレデンシャルズフローと同じです。
認可サーバー トークンエンドポイント - アクセストークンの生成
####アクセストークンの生成
続けざまにStartup.ReceiveAuthenticationCode()
イベントが発生します。
ここではアクセストークンの生成、というか、すでに生成されているアクセストークンを取り出す処理になります。
private void ReceiveAuthenticationCode(AuthenticationTokenReceiveContext context)
{
// 認可コード受信したときのイベント
// CreateAuthenticationCode()で格納したテーブルから取り出して、テーブルのレコードを削除する
// つまり、認可コードは1回だけ有効、ということ
string value;
if (_authenticationCodes.TryRemove(context.Token, out value))
{
// value(アクセトークン)を返却する
context.DeserializeTicket(value);
}
}
認可サーバー トークンエンドポイント - リフレッシュトークンの生成
####リフレッシュトークンの生成
Startup.CreateRefreshToken()
イベントが発生します。
#3と同じようにリフレッシュトークンを生成します。
クライアント - トークンエンドポイントからのレスポンス
###C.リソース取得
ほかのGrantと同じなので省略・・・
#ステートについて
一番最初にクライアント→認可エンドポイントにリクエストするときにstateで指定した任意の文字列ですが、これはその後ずっとついて回ります。これ、何なんでしょうか?
OAuth徹底入門セキュアな認可システムを適用するための原則と実践によると
stateパラメータを使ったサイトをまたいだ攻撃に対する保護
・・・
認可サーバーはstateパラメータを持った認可リクエストを受け取ると、認可コードとともにそのstateパラメータを変更することなくクライアントに必ず返さなければなりません。
・・・
もし、stateの値が想定しているものと違う場合、それは何か不穏なこと、たとえば、やり取り内容の改ざんや有効な認可コードに対するフィッシングなど何らかの攻撃が行われていることを示唆しています。
ということで、要するに最初にクライアントが生成したステートなる任意の文字列が変わっていないかチェックしなさい、ということです。
これOwinで勝手にやってくれないようなので、自分でいちいちチェックする必要があります。
すごいめんどくさい!
ちなみにこのサンプルではステートのチェックはやっていません。本気で実装する場合はちゃんとやりましょう。
#おつかれさまでした
もう、フローは、とうぶん、みたくない