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

#はじめに
WindowsのOwinというミドルウェアを使ってOAuthを学習するメモです。
[#3](https://qiita.com/gebo/items/7ee9386676084d83bd5b)からの続きです。

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

#認可コードフローとは
[OAuth徹底入門セキュアな認可システムを適用するための原則と実践](https://www.amazon.co.jp/dp/B07L5M7DXS)によると

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

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

#環境
Windows10のPC1台でやります。

- Windows10 Pro 1903 (64bit)
- Visual Studio 2019
	- C#
	- .Net Framework 4.5
	- ASP.net Web アプリケーション
- [waireshark](https://www.wireshark.org/)←HTTPキャプチャ用
- [サンプルプログラム](https://github.com/gebogebogebo/OwinOAuthSample/tree/master/AuthorizationCodeGrant)

#とりあえずサンプルを実行してみる
[#1](https://qiita.com/gebo/items/0dab824892af7699f08d)で使ったサンプルプグラムを改造したものです。

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

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

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

####①クライアント画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/a356cfed-be2d-7532-51a3-6a5e1f1790e3.png)
**1.アクセストークンを取得する** をクリック

####②ログイン画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/1e5082c9-e172-e229-1350-7c11411ad2e9.png)
ログインを要求されるのでUserNameとPasswordを入力して**ログイン**をクリック、
サンプルなので何を入力してもOK。

####③確認画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/18db8621-5588-0818-bd3b-ec7156900591.png)
確認画面で**承認してログインする**をクリック

####④クライアント画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/32acf620-d219-a7c7-8b10-218f33e10012.png)
アクセストークンがGETできれば最初の画面に戻ってきます。
**2.リソースサーバーにアクセスする**
をクリックしてUserNameがGETできればサンプルプログラムの動作はOKです。
⇒リソースサーバーにアクセストークンを提示してログインしたUserNameをGETしている。

#フロー
処理の大雑把な流れは

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

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

図にするとこんな感じ

####概要フロー図
![Flow_AuthorizationCodeGrant_Simple.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/0c6fd2d7-54ff-60f6-c4ad-77626a726b6e.png)


#処理詳細
<details><summary>詳細フロー図</summary><div>
####詳細フロー図
![Flow_AuthorizationCodeGrant.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/3163c0ab-1071-2c54-98cd-0d20f5843060.png)

</div></details>

##A.認可コード取得
###A-1.クライアント検証

<details><summary>クライアント - 認可エンドポイントへのリクエスト</summary><div>
####認可エンドポイントへのリクエスト
クライアントから認可サーバーの認可エンドポイントに対して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/
```

</div></details>

<details><summary>認可サーバー 認可エンドポイント - クライアントIDの検証</summary><div>
####クライアントIDの検証

認可サーバーでは`ValidateClientRedirectUri()`イベントが発生し、ここでクライアントIDを検証します。

```CS:
private Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
    // ClientIDの検証
    if (context.ClientId == "123456")
    {
        // OKだったらValidated()する
        context.Validated();
    }
    return Task.FromResult(0);
}
```

その次に`OAuthController.Authorize()`が実行されす。
以下コードはかなり省略しています。Challenge()してreturnするところがポイントです

```CS:
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();
}
```
</div></details>

<details><summary>クライアント - 認可エンドポイントからのレスポンス</summary><div>
####認可エンドポイントからのレスポンス
クライアントIDがOKとなると、リダイレクトされて、Login画面にすっ飛んでいきます。

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

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

</div></details>

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

<details><summary>クライアント - ログインのリクエスト</summary><div>
####ログインのリクエスト
認可サーバーの`/Account/Login`に対してPOSTリクエストします

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

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

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

</div></details>

<details><summary>認可サーバー Login - ユーザー名とパスワードの検証</summary><div>
####ユーザー名とパスワードの検証
認可サーバーでは`AccountController.Login()`が実行されます。

```CS:
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();
}
```

</div></details>

<details><summary>クライアント - ログインのレスポンス</summary><div>
####ログインのレスポンス
ユーザー検証がOKとなると、リダイレクトされて、認可確認画面にすっ飛んでいきます。

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

// リダイレクトURI=http://localhost:11625/Authorize/
```

</div></details>

###A-3.認可確認

<details><summary>クライアント - 認可エンドポイントへのリクエスト</summary><div>

####認可エンドポイントへのリクエスト
認可サーバーの認可エンドポイントに対して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=承認してログインする
```

</div></details>

<details><summary>認可サーバー 認可エンドポイント - クライアントIDの検証</summary><div>

####クライアントIDの検証
[さっきと同じ](#クライアントidの検証)ように、認可サーバーで`ValidateClientRedirectUri()`イベントが発生し、クライアントIDを検証します。

</div></details>

<details><summary>認可サーバー 認可エンドポイント - identity生成</summary><div>

####identity生成
再び`OAuthController.Authorize()`が実行されますが、いろいろ条件分岐があって、**identity(＝アクセストークンのもと)を生成**します。

```CS:
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();
}
```

</div></details>

<details><summary>認可サーバー 認可エンドポイント - 認可コード生成</summary><div>

####認可コード生成
続けざまに`Startup.CreateAuthenticationCode()`が発生します。ここでついに認可コードが生成されるのです。
生成した認可とアクセストークン(context.SerializeTicket())を紐づけてメモリに格納します。

```CS:
private void CreateAuthenticationCode(AuthenticationTokenCreateContext context)
{
    // 認可コード生成
    var code = Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n");
    context.SetToken(code);
    // テーブルに格納（本気で実装する人はDBとかに入れましょう）
    _authenticationCodes[context.Token] = context.SerializeTicket();
}
```

</div></details>

<details><summary>クライアント - 認可エンドポイントからのレスポンス</summary><div>

####認可エンドポイントからのレスポンス
レスポンスは例によって302のリダイレクトです。
このとき生成された認可コードをもってすっ飛んで行きます。
行先は、クライアント画面です

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

// リダイレクトURI
// http://localhost:38500/
//   ?code={認可コード}
//   &state={リクエストで指定したステート}
```

これでやっと**認可コードがクライアントに渡されました**。
ちなみに現時点では、認可サーバーが生成した認可コードは認可サーバーのメモリにも格納されているので、クライアントと認可サーバー両者が認可コードを持っています。

</div></details>

###B.アクセストークンの取得
ここまで来たらあとはクライアント・クレデンシャルズフローとあんまり変わりません。

<details><summary>クライアント - トークンエンドポイントへのリクエスト</summary><div>

####トークンエンドポイントへのリクエスト
認可サーバーのトークンエンドポイントに対して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/
```

</div></details>

<details><summary>認可サーバー トークンエンドポイント - クライアントID、シークレットの検証</summary><div>

####クライアントID、シークレットの検証
認可サーバー側は`Startup.ValidateClientAuthentication()`イベントが発生してクライアントID、シークレットの検証を行います。これはクライアント・クレデンシャルズフローと同じです。

</div></details>

<details><summary>認可サーバー トークンエンドポイント - アクセストークンの生成</summary><div>

####アクセストークンの生成
続けざまに`Startup.ReceiveAuthenticationCode()`イベントが発生します。
ここではアクセストークンの生成、というか、すでに生成されているアクセストークンを取り出す処理になります。

```CS:
private void ReceiveAuthenticationCode(AuthenticationTokenReceiveContext context)
{
    // 認可コード受信したときのイベント
    // CreateAuthenticationCode()で格納したテーブルから取り出して、テーブルのレコードを削除する
    // つまり、認可コードは1回だけ有効、ということ
    string value;
    if (_authenticationCodes.TryRemove(context.Token, out value))
    {
        // value（アクセトークン）を返却する
        context.DeserializeTicket(value);
    }
}
```

</div></details>

<details><summary>認可サーバー トークンエンドポイント - リフレッシュトークンの生成</summary><div>

####リフレッシュトークンの生成
`Startup.CreateRefreshToken()`イベントが発生します。
[#3](https://qiita.com/gebo/items/7ee9386676084d83bd5b#2-createrefreshtoken%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%A7%E3%83%AA%E3%83%95%E3%83%AC%E3%83%83%E3%82%B7%E3%83%A5%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B)と同じようにリフレッシュトークンを生成します。

</div></details>

<details><summary>クライアント - トークンエンドポイントからのレスポンス</summary><div>

####トークンエンドポイントからのレスポンス
ついにアクセストークンがGETできます。
アクセストークンの細かい説明→[#2](https://qiita.com/gebo/items/5b1c6da774c73fc6bd25)
リフレッシュトークン→[#3](https://qiita.com/gebo/items/7ee9386676084d83bd5b#%E3%83%AA%E3%83%95%E3%83%AC%E3%83%83%E3%82%B7%E3%83%A5%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3)

```:
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":"{リフレッシュトークン}"
}
```

</div></details>

###C.リソース取得
ほかのGrantと同じなので省略・・・

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

[OAuth徹底入門セキュアな認可システムを適用するための原則と実践](https://www.amazon.co.jp/dp/B07L5M7DXS)によると

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

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

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

#おつかれさまでした
もう、フローは、とうぶん、みたくない