LoginSignup
7
3

More than 3 years have passed since last update.

Owinを使ったOAuth2.0お勉強メモ#4 - 認可コードフロー

Last updated at Posted at 2019-07-27

はじめに

WindowsのOwinというミドルウェアを使ってOAuthを学習するメモです。
#3からの続きです。

やっと認可コードフローまでたどり着きました。

認可コードフローとは

OAuth徹底入門セキュアな認可システムを適用するための原則と実践によると

認可コードによる付与方式(AuthorizationCodeGrantType)はOAuthのすべての構成要素を完全に分離しており、結果としてこの付与方式が本書で扱う付与方式の中で最も基本的でありながらも複雑なものになっているからです。OAuthのほかのすべての付与方式はこの認可コードによる付与方式を特定のユース・ケースや環境に合うように最適化されたものです。

と、いうわけで、
認可コードフローはOAuth2.0のフローの中で最も基本的なんだけれども、最も複雑
です・・・

環境

Windows10のPC1台でやります。

とりあえずサンプルを実行してみる

#1で使ったサンプルプグラムを改造したものです。

クライアントアプリが認可サーバーからアクセストークンをGETしてリソースサーバーにアクセスする、というシナリオは今までの学習メモと同じです。

認可コードフローのポイントはアクセストークンをGETする前に認可コードをGETするというところと、ステートがあるということです。
※認可コード、ステートについては中盤以降で説明します。

http://localhost:38500/ が入口です。

①クライアント画面

image.png
1.アクセストークンを取得する をクリック

②ログイン画面

image.png
ログインを要求されるのでUserNameとPasswordを入力してログインをクリック、
サンプルなので何を入力してもOK。

③確認画面

image.png
確認画面で承認してログインするをクリック

④クライアント画面

image.png
アクセストークンがGETできれば最初の画面に戻ってきます。
2.リソースサーバーにアクセスする
をクリックしてUserNameがGETできればサンプルプログラムの動作はOKです。
⇒リソースサーバーにアクセストークンを提示してログインしたUserNameをGETしている。

フロー

処理の大雑把な流れは

  • A.認可コード取得
    • A-1.クライアント検証(※)
    • A-2.ユーザー検証(※)
    • A-3.認可確認
  • B.アクセストークン取得
  • C.リソース取得

※検証について

クライアント検証、ユーザー検証と書いていますが、これは認可サーバーがクライアント/ユーザーが正しいかどうか確認する、という意味で、Verifyのことです。認証とはちょっと違うので言葉を変えています。
OAuthは認証の仕組みではない、という事らしいので、認証という言葉は入れないようにしてます。

図にするとこんな感じ

概要フロー図

Flow_AuthorizationCodeGrant_Simple.png

処理詳細

詳細フロー図

詳細フロー図

Flow_AuthorizationCodeGrant.png

A.認可コード取得

A-1.クライアント検証

クライアント - 認可エンドポイントへのリクエスト

認可エンドポイントへのリクエスト

クライアントから認可サーバーの認可エンドポイントに対してGETリクエストします

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の検証

クライアント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();
}

クライアント - 認可エンドポイントからのレスポンス

認可エンドポイントからのレスポンス

クライアントIDがOKとなると、リダイレクトされて、Login画面にすっ飛んでいきます。

HTTP/1.1 302 Found
Location: {リダイレクトURI}

// リダイレクトURI
// http://localhost:11625/Account/Login
//   ?ReturnUrl={リクエストで指定されたパラメータそのまま}

A-2.ユーザー検証

認可サーバーが提供するログイン画面でログインされたタイミングでユーザー検証が行われます。

クライアント - ログインのリクエスト

ログインのリクエスト

認可サーバーの/Account/Loginに対してPOSTリクエストします

POST /Account/Login HTTP/1.1
Host: {認可サーバー}
Content-Type: application/x-www-form-urlencoded

username={ユーザー名}
&password={パスワード}
&submit.Signin="ログイン"

// ユーザー名=ログイン画面で入力されたUserName
// パスワード=ログイン画面で入力されたPassword

認可サーバー Login - ユーザー名とパスワードの検証

ユーザー名とパスワードの検証

認可サーバーではAccountController.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();
}

クライアント - ログインのレスポンス

ログインのレスポンス

ユーザー検証がOKとなると、リダイレクトされて、認可確認画面にすっ飛んでいきます。

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と同じようにリフレッシュトークンを生成します。

クライアント - トークンエンドポイントからのレスポンス

トークンエンドポイントからのレスポンス

ついにアクセストークンがGETできます。
アクセストークンの細かい説明→#2
リフレッシュトークン→#3

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-cashe
Pragma: no-cache

{
  "access_token":"{アクセストークン}",
  "token_type":"bearer",
  "expires_in":{有効秒数},
  "refresh_token":"{リフレッシュトークン}"
}

C.リソース取得

ほかのGrantと同じなので省略・・・

ステートについて

一番最初にクライアント→認可エンドポイントにリクエストするときにstateで指定した任意の文字列ですが、これはその後ずっとついて回ります。これ、何なんでしょうか?

OAuth徹底入門セキュアな認可システムを適用するための原則と実践によると

stateパラメータを使ったサイトをまたいだ攻撃に対する保護
・・・
認可サーバーはstateパラメータを持った認可リクエストを受け取ると、認可コードとともにそのstateパラメータを変更することなくクライアントに必ず返さなければなりません。
・・・
もし、stateの値が想定しているものと違う場合、それは何か不穏なこと、たとえば、やり取り内容の改ざんや有効な認可コードに対するフィッシングなど何らかの攻撃が行われていることを示唆しています。

ということで、要するに最初にクライアントが生成したステートなる任意の文字列が変わっていないかチェックしなさい、ということです。
これOwinで勝手にやってくれないようなので、自分でいちいちチェックする必要があります。
すごいめんどくさい!

ちなみにこのサンプルではステートのチェックはやっていません。本気で実装する場合はちゃんとやりましょう。

おつかれさまでした

もう、フローは、とうぶん、みたくない

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3