OAuth 2.0 の勉強のために認可サーバーを自作する

# はじめに

OAuth 2.0 の仕様書である [RFC 6749](https://tools.ietf.org/html/rfc6749) は、**認可サーバー**（authorization server）の動作を定めています。この記事は、認可サーバーを簡易的に実装することで、OAuth 2.0 の理解を深めることを目的としています。


# 1. エンドポイント

認可サーバーは Web サーバーの一種で、**[認可エンドポイント](https://tools.ietf.org/html/rfc6749#section-3.1)**（authorization endpoint）と**[トークンエンドポイント](https://tools.ietf.org/html/rfc6749#section-3.2)**（token endpoint）という二つのエンドポイントを提供します。この二つのエンドポイントを実装すれば、最小構成の認可サーバーができます。

<img width="943" alt="authz-ep_and_token-ep.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/0bd316ed-2ec4-905a-f2c5-ac4e20e2ea90.png">

実際は、上図が示すように、認可エンドポイントに送られてきた**認可リクエスト**（authorization request）を処理する過程で、Web ブラウザを介して（まだ済んでいなければ）ユーザー認証および同意取得をインタラクティブに行わなければならないため、認可エンドポイントと連携するその他のエンドポイント群も必要となります。詳細は『[OAuth 2.0 の認可レスポンスとリダイレクトに関する説明](https://qiita.com/TakahikoKawasaki/items/8567c80528da43c7e844)』を参照してください。

なお、サポートする認可フローを限定すれば、どちらか一方のエンドポイントのみの実装で済む場合もあります。具体的には、[インプリシット・フロー](https://tools.ietf.org/html/rfc6749#section-4.2)のみであれば認可エンドポイントの実装だけで済みます。一方、[リソースオーナー・パスワード・クレデンシャルズ・フロー](https://tools.ietf.org/html/rfc6749#section-4.3)もしくは[クライアント・クレデンシャルズ・フロー](https://tools.ietf.org/html/rfc6749#section-4.4)だけであればトークンエンドポイントの実装だけで済みます。

|フロー|認可エンドポイント|トークンエンドポイント|
|:--|:-:|:-:|
|認可コード|使用する|使用する|
|インプリシット|使用する|使用しない|
|リソースオーナー・パスワード・クレデンシャルズ|使用しない|使用する|
|クライアント・クレデンシャルズ|使用しない|使用する|
|リフレッシュ・トークン|使用しない|使用する|

逆に、RFC 6749 以外で定義されている認可フローをサポートする場合、新たに別のエンドポイントの実装が必要になることがあります。例えば **[CIBA](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html)**（Client Initiated Backchannel Authentication）では**[バックチャネル認証エンドポイント](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html#rfc.section.7)**（backchannel authentication endpoint）、**デバイスフロー**（[RFC 8628](https://tools.ietf.org/html/rfc8628)）では**デバイス認可エンドポイント**（device authorization endpoint）の実装が求められます。

この記事では、認可エンドポイントとトークンエンドポイントを実装します。サポートする認可フローは[認可コードフロー](https://tools.ietf.org/html/rfc6749#section-4.1)のみ、サポートする[クライアント・タイプ](https://tools.ietf.org/html/rfc6749#section-2.1)はパブリックのみとします。


# 2. 注意点

下記の理由、および書かれていないその他の理由により、本実装は商用利用には適していません。

- セキュリティー上必須の仕様である PKCE（[RFC 7636](https://tools.ietf.org/html/rfc7636)）をサポートしていない。
- 認可コードフロー以外のフローをサポートしていない。
- コンフィデンシャル・クライアントをサポートしていない。つまり、[クライアント認証](https://qiita.com/TakahikoKawasaki/items/63ed4a9d8d6e5109e401)に関するコードは一切含まれていない。
- [RFC 6749](https://tools.ietf.org/html/rfc6749) では条件により `redirect_uri` リクエストパラメーターを省略できるが、本実装では省略できない。
- RFC 6749 はリダイレクト URI がクエリー部を含むことを許しているが、その場合、この実装は `Location` ヘッダー用の適切な値を生成できない。
- RFC 6749 で _"Request and response parameters MUST NOT be included more than one."_ と書かれているが、本実装では同一リクエストパラメーターが複数回指定されているかどうかのチェックをしていない。
- RFC 6749 の _"Parameters sent without a value MUST be treated as if they were omitted from the request."_ という規定に厳密には従っていない。
- 認可エンドポイントは、使うべきリダイレクト URI を特定できた後に発生したエラーでも `302 Found` を使わない。
- RFC 6749 は `state` リクエストパラメーターの値に使うことができる文字を %x20-7E の範囲に限定しているが、この実装では `state` の値をチェックしていない。
- トークンエンドポイントは常に `client_id` リクエストパラメーターを要求する。（パブリック・クライアントしか存在しないと知っているため）
- トークンエンドポイントは常に `redirect_uri` リクエストパラメーターを要求する。（認可コードフローしかサポートせず、かつ、認可エンドポイントが `redirect_uri` を常に要求することを知っているため）
- 有効期限切れの認可コードとアクセストークンを定期的に削除する仕組みを持たない。
- 認可コードとアクセストークンのエントロピーがとても低い。
- Sinatra のセッション機能をセキュリティー上の配慮なく使用している。
- CSRF に対する保護を実装していない。
- 次のものがハードコーディングされている：クライアント（一つ）、ユーザーアカウント（一つ）、アクセストークンの有効期限

その他、[OpenID Connect](https://openid.net/connect/) 対応も考える場合、このブログで紹介するものとはかなり異なる設計・実装が必要となりますので注意してください。特に、[response_type](https://qiita.com/TakahikoKawasaki/items/185d34814eb9f7ac7ef3#12-%E5%BF%9C%E7%AD%94%E3%82%BF%E3%82%A4%E3%83%97-response_type) と[リクエストオブジェクト](https://qiita.com/TakahikoKawasaki/items/dd4dcf2f5c05b8183169)、[response_mode](https://qiita.com/TakahikoKawasaki/items/8567c80528da43c7e844#response_mode-%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%83%BC) と [JARM](https://qiita.com/TakahikoKawasaki/items/8567c80528da43c7e844#jwt-secured-authorization-response-mode)、署名・暗号アルゴリズムのサポート方法については予め設計に組み込んでおかないと、後でかなり苦労することになります（最悪書き直しになります）。『[クレーム](https://qiita.com/TakahikoKawasaki/items/185d34814eb9f7ac7ef3#15-%E3%82%AF%E3%83%AC%E3%83%BC%E3%83%A0-claim)』の処理も意外と厄介です。

商用レベルの高品質な認可サーバーが必要な場合は **[Authlete](https://www.authlete.com/)**（オースリート）をご検討ください。


# 3. ソースコード

実装には、Ruby の Web フレームワークの一つである [Sinatra](http://sinatrarb.com/) を用います。ソースコードの完成版は GitHub の [authlete/useless-oauth-server](https://github.com/authlete/useless-oauth-server) で公開しています。誤って商用利用することがないよう、名称に useless という単語を含めています。


# 4. データ構造

## 4.1. クライアント

仕様上、クライアントアプリケーションを表現するデータ構造が持つべきデータはクライアント ID とリダイレクト URI 群の二つです。

コンフィデンシャル・クライアントであれば、クライアント・シークレットも必要ですが、本実装ではコンフィデンシャル・クライアントをサポートしないので、クライアント・シークレット用のデータフィールドを用意しないことにします。

クライアント名を表すデータフィールドは仕様上必須ではありませんが、認可ページでクライアント名を表示したいので、今回の実装では含めることにします。

上記をまとめると、クライアントを表現するデータ構造は次のようになります。

```ruby
class Client
    attr_accessor :client_id
    attr_accessor :client_name
    attr_accessor :redirect_uris

    def initialize(client_id, client_name, redirect_uris)
        @client_id     = client_id
        @client_name   = client_name
        @redirect_uris = redirect_uris
    end
end
```

なお、[OpenID Connect Dynamic Client Registration 1.0](https://openid.net/specs/openid-connect-registration-1_0.html) をサポートしようとすると、[クライアントのメタデータ](https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata)の数は爆発的に増えます。その他、クライアントのメタデータは、[CIBA](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html) やデバイスフロー（[RFC 8628](https://tools.ietf.org/html/rfc8628)）、[MTLS](https://datatracker.ietf.org/doc/draft-ietf-oauth-mtls/?include_text=1)（OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens）や [JARM](https://openid.net/specs/openid-financial-api-jarm.html)（JWT Secured Authorization Response Mode for OAuth 2.0） など、様々な仕様で適宜追加されます。


## 4.2. ユーザー

ユーザー認証をどのようにおこなうかは、OAuth 2.0 の仕様の範囲外です。この実装では、ログイン ID とパスワードという古典的な方法でユーザー認証をおこなうことにします。最低限のデータ構造は次のようになります。

```ruby
class User
    attr_accessor :user_id
    attr_accessor :login_id
    attr_accessor :password

    def initialize(user_id, login_id, password)
        @user_id  = user_id
        @login_id = login_id
        @password = password
    end
end
```

OpenID Connect をサポートする場合、[OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) の「[5.1. Standard Claims](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims)」に挙げられている項目を参考にデータフィールドを追加するのが望ましいです。


## 4.3. 認可コード

**認可コード**（authorization code）は、認可エンドポイントから発行される短命のトークンです。クライアントは、受け取った認可コードをすみやかにトークンエンドポイントに提示し、アクセストークンと交換します。

認可コードは、どのユーザーがどのクライアントに何の権限を与えたかを表現しているので、認可コードを表すデータ構造には、最低限、ユーザー、クライアント、権限（スコープ）を表すデータフィールドが必要になります。

認可リクエストに `redirect_uri` リクエストパラメーターが含まれていた場合、同じ値がトークンリクエストにも含まれていなければならないという規定があります。そのため、認可コード発行の際に指定されたリダイレクト URI を、その認可コードに紐づけて覚えておく必要があります。のちほどトークンエンドポイントの実装でチェックするためです。このような理由により、認可コードを表すデータ構造には、リダイレクト URI を表現するデータフィールドが必要です。

認可コードは短命であり、RFC 6749 に _"A maximum authorization code lifetime of 10 minutes is RECOMMENDED."_ と書かれています。必然的に有効期限を表すデータフィールドも必要です。

上記をまとめると、認可コードを表すデータ構造は次のようになります。

```ruby
class AuthorizationCode
    attr_accessor :value
    attr_accessor :user_id
    attr_accessor :client_id
    attr_accessor :scopes
    attr_accessor :redirect_uri
    attr_accessor :expires_at

    def initialize(user_id, client_id, scopes, redirect_uri, expires_at)
        @value        = SecureRandom.urlsafe_base64(6)
        @user_id      = user_id
        @client_id    = client_id
        @scopes       = scopes
        @redirect_uri = redirect_uri
        @expires_at   = expires_at
    end
end
```

インスタンス生成時にランダム文字列を生成して `value` フィールドに設定しています。この値は、外部からみた認可コードの値として使用します。

本実装では PKCE（RFC 7636）をサポートしていませんが（そのため商用利用はできませんが）、PKCE をサポートする場合、`code_challenge` と `code_challenge_method` を表すデータフィールドも必要になります。詳細は『[PKCE: 認可コード横取り攻撃対策のために OAuth サーバーとクライアントが実装すべきこと](https://qiita.com/TakahikoKawasaki/items/00f333c72ed96c4da659)』を参照してください。


## 4.4. アクセストークン

**アクセストークン**（access token）は、どのユーザーがどのクライアントに何の権限を与えたかを表現しているので、アクセストークンを表すデータ構造には、最低限、ユーザー、クライアント、権限（スコープ）を表すデータフィールドが必要になります。また、通常アクセストークンには有効期限を設けます。

上記をまとめると、アクセストークンを表すデータ構造は次のようになります。

```ruby
class AccessToken
    attr_accessor :value
    attr_accessor :user_id
    attr_accessor :client_id
    attr_accessor :scopes
    attr_accessor :expires_at

    def initialize(user_id, client_id, scopes, expires_at)
        @value      = SecureRandom.urlsafe_base64(6)
        @user_id    = user_id
        @client_id  = client_id
        @scopes     = scopes
        @expires_at = expires_at
    end
end
```

認可コードと同様、インスタンス生成時にランダム文字列を生成して `value` フィールドに設定しています。この値は、外部からみたアクセストークンの値として使用します。

アクセストークンの実装を [JWT](https://tools.ietf.org/html/rfc7519) とし、認可サーバー側にアクセストークンのデータを持たないようにすることもできます。世の中にそのような実装が多いことは知っていますが、あまりお勧めしません。詳細は『[OAuth アクセストークンの実装に関する考察](https://qiita.com/TakahikoKawasaki/items/970548727761f9e02bcd)』を参照してください。

実装にもよりますが、アクセストークンのデータ構造にリフレッシュトークンに関する情報、もしくはリフレッシュトークンそのものが含まれる場合があります。[リフレッシュトークン・フロー](https://tools.ietf.org/html/rfc6749#section-6)を実装する場合はデータ構造の検討が必要となります。

Mutual-TLS Client Certificate-Bound Access Tokens（[MTLS](https://datatracker.ietf.org/doc/draft-ietf-oauth-mtls/?include_text=1), Section 3）をサポートする場合は、アクセストークン発行時に用いられたクライアント証明書のフィンガープリントを覚えておくデータフィールドも必要となります。


# 5. データストア

この簡易実装では、全てのデータをメモリ上に保存します（永続記憶領域には書き込みません）。また、クライアントとユーザーをそれぞれ一つずつ、ハードコーディングしておきます。

結果として、データストアは次のような実装になります。

```ruby
# 登録済みクライアントアプリケーション
$ClientStore = [
    # クライアント ID, クライアント名, リダイレクト URI 群
    Client.new('1', 'My Client', ['http://example.com/'])
]

# 登録済みユーザー
$UserStore = [
    # ユーザー ID, ログイン ID, パスワード
    User.new('1', 'john', 'john')
]

# 発行された認可コード
# キーは AuthorizationCode.value、値は AuthorizationCode インスタンス
$AuthorizationCodeStore = {}

# 発行されたアクセストークン
# キーは AccessToken.value、値は AccessToken インスタンス
$AccessTokenStore = {}
```


# 6. 定数

サポートするスコープの種類、認可コードの有効期限秒数、アクセストークンの有効期限秒数は、次のようにハードコーディングすることにします。

```ruby
$SUPPORTED_SCOPES            = %w(read write)
$AUTHORIZATION_CODE_DURATION = 600
$ACCESS_TOKEN_DURATION       = 86400
```

# 7. 認可エンドポイント

認可エンドポイントの簡易実装は次のようになります。

```ruby
# 認可エンドポイント
get '/authorization' do
    # --- client_id ---
    # client_id リクエストパラメーターからクライアントを検索する。
    client = look_up_client(params['client_id'])
    if client.nil?
        halt 400, 'client_id is wrong.'
    end

    # --- redirect_uri ---
    # この実装は redirect_uri リクエストパラメーターを必ず要求する。
    redirect_uri = params['redirect_uri']
    if redirect_uri.nil? || !client.redirect_uris.include?(redirect_uri)
        halt 400, 'redirect_uri is wrong.'
    end

    # --- response_type ---
    # サポートするのは認可コードフローのみ。
    if params['response_type'] != 'code'
        halt 400, 'response_type is wrong.'
    end

    # --- state ---
    state = params['state'].nil? ? '' : params['state']

    # --- scope ---
    # scope リクエストパラメーターで指定されたスコープ群のうち、
    # サポートされているものだけを取り出す。
    scopes = filter_scopes(params['scope'])

    # 後で使用するので、幾つかのパラメーターをセッションに保存する。
    session[:client]       = client
    session[:state]        = state
    session[:scopes]       = scopes
    session[:redirect_uri] = redirect_uri

    # 認可ページを描画する。
    erb :authorization_page, :locals => {
        'client_name' => client.client_name,
        'scopes'      => scopes
    }
end
```

一つずつ見ていきましょう。

## 7.1. client_id

```ruby
    # --- client_id ---
    # client_id リクエストパラメーターからクライアントを検索する。
    client = look_up_client(params['client_id'])
    if client.nil?
        halt 400, 'client_id is wrong.'
    end
```

このコードでは、認可リクエストに含まれる `client_id` リクエストパラメーターの値を取り出し、その値をクライアント ID として持つ `Client` インスタンスを検索しています。仕様上、`client_id` リクエストパラメーターは必須です。

`Client` インスタンスの検索に用いられている `look_up_client()` 関数の実装は次の通りです。

```ruby
def look_up_client(value)
    $ClientStore.find { |client| client.client_id == value }
end
```

`Client` インスタンスが見つからない場合、`400 Bad Request` というエラーレスポンスを返します。


## 7.2. redirect_uri

```ruby
    # --- redirect_uri ---
    # この実装は redirect_uri リクエストパラメーターを必ず要求する。
    redirect_uri = params['redirect_uri']
    if redirect_uri.nil? || !client.redirect_uris.include?(redirect_uri)
        halt 400, 'redirect_uri is wrong.'
    end
```

仕様上、ある条件下では `redirect_uri` リクエストパラメーターは省略可能です（一方 OpenID Connect では常に省略不可）。しかし、単純化のため、この実装では `redirect_uri` を必ず要求するようにしています。

`redirect_uri` が指定されている場合、その値が登録済みかどうかをチェックしなければなりません。指定された値と登録されている値との比較は、後者が full URI でない場合は [RFC 3986](https://tools.ietf.org/html/rfc3986) [Section 6](https://tools.ietf.org/html/rfc3986#section-6) で規定されている方法で比較すべきとされています。しかし、単純化のため、この実装では単純な文字列比較（[Section 6.2.1](https://tools.ietf.org/html/rfc3986#section-6.2.1)）としています（OpenID Connect では常に単純文字列比較）。

本来、リダイレクト URI の検証はもっと複雑です。詳細は「[リダイレクト URI](https://qiita.com/TakahikoKawasaki/items/f2a0d25a4f05790b3baa#%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88-uri)」を参照してください。


## 7.3. response_type

```ruby
    # --- response_type ---
    # サポートするのは認可コードフローのみ。
    if params['response_type'] != 'code'
        halt 400, 'response_type is wrong.'
    end
```

RFC 6749 の場合、実質的に `response_type` が取りうる値は `code` もしくは `token` のどちらかで、それを前提にコーディングしていれば大丈夫です。しかし、OpenID Connect は、ここに `id_token` という値を加えた上で、これらを複数同時に指定できるようにしました。このため、OpenID Connect をサポートしようとすると、`response_type` の処理はかなり複雑になります。詳細は「[応答タイプ (response_type)](https://qiita.com/TakahikoKawasaki/items/f2a0d25a4f05790b3baa#%E5%BF%9C%E7%AD%94%E3%82%BF%E3%82%A4%E3%83%97-response_type)」を参照してください。

`response_type` が決まると、レスポンスパラメーター群をクエリー部に置くのか、それともフラグメント部に置くのかを決定できます。RFC 6749 だけの世界では、認可コードフローならクエリー部、インプリシット・フローならフラグメント部となります。一方、[OAuth 2.0 Multiple Response Type Encoding Practices](http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html)、[OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html)、[JWT Secured Authorization Response Mode for OAuth 2.0 (JARM)](https://openid.net/specs/openid-financial-api-jarm.html)、まで考慮に入れると、レスポンスパラメーター群の置き場所とそのフォーマットの決定は、かなり複雑な処理になります。

しかしながら、本実装では認可コードフローしかサポートしないと決めており、その他の拡張仕様もサポートしないので、`response_type` リクエストパラメーターの値が `code` かどうかを決め打ちで調べています。


## 7.4. state

```ruby
    # --- state ---
    state = params['state'].nil? ? '' : params['state']
```

認可リクエストに含まれる `state` の値は、最終的なレスポンスの中に含める必要があるので、認可リクエストからその値を取り出しておきます。仕様上、`state` の値を構成する文字は `%x20-7E` の範囲でなければなりませんが、この実装ではそこまでチェックしていません。


## 7.5. scope

```ruby
    # --- scope ---
    # scope リクエストパラメーターで指定されたスコープ群のうち、
    # サポートされているものだけを取り出す。
    scopes = filter_scopes(params['scope'])
```

`scope` リクエストパラメーターには、スコープ群が空白文字区切りで列挙されています。カンマ区切りを要求する認可サーバーの実装もありますが、それは仕様違反です。

`filter_scopes()` 関数は次の通りで、指定されたスコープ群の中から、本実装がサポートするスコープのみを取り出し、配列として返します。

```ruby
def filter_scopes(value)
    return [] if value.nil?
    scopes = value.split(/\s+/)
    scopes.find_all { |scope| $SUPPORTED_SCOPES.include?(scope) }
end
```


## 7.6. 後で利用する情報

```ruby
    # 後で使用するので、幾つかのパラメーターをセッションに保存する。
    session[:client]       = client
    session[:state]        = state
    session[:scopes]       = scopes
    session[:redirect_uri] = redirect_uri
```

`state` の値は、後ほど認可レスポンスに含めなければならないので覚えておきます。それ以外の情報は、後ほど認可コードを生成する際に必要となるので覚えておきます。

気付きにくいかも知れませんが、「認可リクエストの際に `redirect_uri` を指定した場合は、同じ値をトークンリクエストに含めなければならない」という要求事項があるので、認可コード発行時に用いられた `redirect_uri` を覚えておく必要があることに注意してください。


## 7.7. 認可ページ

```ruby
    # 認可ページを描画する。
    erb :authorization_page, :locals => {
        'client_name' => client.client_name,
        'scopes'      => scopes
    }
```

最後に HTML の認可ページを生成して返しています。クライアント名とスコープ群に関する情報は、認可ページ内に埋め込むので、`:locals` を用いて認可ページのテンプレート（`:authorization_page`）に渡しています。本実装では、テンプレートをソースコードの末尾に埋め込んでいます。テンプレート内の文法については、Sinatra のドキュメントを参照してください。

```html
__END__
@@ authorization_page
<html>
<head>
  <title>Authorization Page</title>
</head>
<body class="font">
  <h2>Client Application</h2>
    <%= client_name %>

  <h2>Requested Permissions</h2>
    <% if scopes != nil %>
    <ol>
      <% scopes.each do |scope| %>
      <li><%= scope %></li>
      <% end %>
    </ol>
    <% end %>

  <h2>Approve?</h2>
    <form method="post" action="/decision">
      <input  type="text"     name="login_id" placeholder="Login ID"><br>
      <input  type="password" name="password" placeholder="Password"><br>
      <button type="submit" name="approved" value="true">Approve</button>
      <button type="submit" name="denied"   value="true">Deny</button>
    </form>
  </body>
</html>
```


# 8. 認可決定エンドポイント

認可ページでは、認可リクエストの情報（クライアント名とスコープ群）、ログインフォーム、Approve ボタンと Deny ボタンを表示します。ユーザーは、認可リクエストを承認する場合、ログイン ID とパスワードを入力し、Approve ボタンを押します。認可リクエストを承認しない場合は Deny ボタンを押します。

いずれの場合でも、本実装では、submit された情報は `/decision` エンドポイントに送信されます。当該エンドポイントの実装は次のようになります。

```ruby
# Decision Endpoint
post '/decision' do
    client       = session[:client]
    state        = session[:state]
    scopes       = session[:scopes]
    redirect_uri = session[:redirect_uri]
    session.clear

    # レスポンスは、リダイレクト URI がさす場所に、state 付きで返される。
    location = "#{redirect_uri}?state=#{state}"

    # Approve ボタンが押されなかった場合
    if params['approved'] != 'true'
        redirect location + '&error=access_denied' +
          '&error_description=The+request+was+not+approved.', 302
    end

    # ログイン ID とパスワードを用いて、ユーザーを検索する。
    user = find_user(params['login_id'], params['password'])
    if user.nil?
        redirect location + '&error=access_denied' +
          '&error_description=End-user+authentication-failed.', 302
    end

    # 認可コードを生成して保存する。
    expires_at = Time.now.to_i + $AUTHORIZATION_CODE_DURATION
    code = AuthorizationCode.new(
        user.user_id, client.client_id, scopes, redirect_uri, expires_at)
    $AuthorizationCodeStore[code.value] = code

    # 認可コードを含む成功応答
    redirect location + '&code=' + code.value, 302
end
```

一つずつ見ていきましょう。


## 8.1. 認可リクエストの情報

```ruby
    client       = session[:client]
    state        = session[:state]
    scopes       = session[:scopes]
    redirect_uri = session[:redirect_uri]
    session.clear
```

`/authorization` エンドポイントで保存した情報を取り出します。


## 8.2. Location

```ruby
    # レスポンスは、リダイレクト URI がさす場所に、state 付きで返される。
    location = "#{redirect_uri}?state=#{state}"
```

最終的な応答は次の形式になるので（『[OAuth 2.0 の認可レスポンスとリダイレクトに関する説明](https://qiita.com/TakahikoKawasaki/items/8567c80528da43c7e844)』）、

```http
HTTP/1.1 302 Found
Location: {リダイレクトURI}?{レスポンスパラメーター群}
```

ここでは、`Location` ヘッダーの値として用いる URI のベース部分を `location` という変数に設定しています。

認可コードフローしかサポートせず、`response_mode=form_post` もサポートしないので、レスポンスパラメーター群を置く場所はクエリー部で固定です。そのため、レスポンスパラメーター群は `?` に続けて配置します。なお、フラグメント部に置く場合は `?` のかわりに `#` を使うことになります。

成功応答の場合もエラー応答の場合も、認可リクエストに含まれていた `state` の値をレスポンスに含める必要があるため、`location` には `state=#{state}` というレスポンスパラメーターを含めています。

RFC 6749 は、リダイレクト URI がクエリー部を含むことを許しています。しかし、その場合、この簡易実装は `Location` ヘッダー用の適切な URI を構築することができないので注意してください。（リダイレクト URI がクエリー部を含むと、`location` が `?` を二回含むことになってしまう）


## 8.3. 認可リクエストに対する認否

```ruby
    # Approve ボタンが押されなかった場合
    if params['approved'] != 'true'
        redirect location + '&error=access_denied' +
          '&error_description=The+request+was+not+approved.', 302
    end
```

ユーザーが認可リクエストを承認しなかった場合、`error=access_denied` というエラーコードを `location` の末尾に追加してエラーレスポンスを返します。エラーレスポンスの HTTP ステータスコードは `302 Found` になります。

任意ですが、仕様上 `error_description` パラメーターでエラーに関する詳細情報を追加することができます。また、`error_uri` というパラメーターも存在しますが、ここでは用いていません。


## 8.4. ユーザー認証

```ruby
    # ログイン ID とパスワードを用いて、ユーザーを検索する。
    user = find_user(params['login_id'], params['password'])
    if user.nil?
        redirect location + '&error=access_denied' +
          '&error_description=End-user+authentication+failed.', 302
    end
```

ログインフォームに入力されたログイン ID とパスワードを用いて、ユーザーを検索します。ユーザー検索に使う `find_user()` 関数の実装は次の通りです。

```ruby
def find_user(login_id, password)
    $UserStore.find do |user|
        user.login_id == login_id && user.password == password
    end
end
```

ユーザーが見つからなければ `error=access_denied` というエラーコードを返します。`error` の値は先ほどと同じですが、エラーの理由が異なるので、`error_description` に「ユーザー認証が失敗した」という情報を書いています。

この簡易実装は、ログイン ID とパスワードの組が誤っている場合に再入力を促す処理は実装していません。


## 8.5. 認可コード生成

```ruby
    # 認可コードを生成して保存する。
    expires_at = Time.now.to_i + $AUTHORIZATION_CODE_DURATION
    code = AuthorizationCode.new(
        user.user_id, client.client_id, scopes, redirect_uri, expires_at)
    $AuthorizationCodeStore[code.value] = code
```

メインの処理となる認可コード生成部分です。認可コードの有効秒数には `$AUTHORIZATION_CODE_DURATION` という固定値を用いています。


## 8.6. 成功応答

```ruby
    # 認可コードを含む成功応答
    redirect location + '&code=' + code.value, 302
```

生成したばかりの認可コードの値を `code` パラメーターの値として用いて、成功応答を返します。


# 9. トークンエンドポイント

トークンエンドポイントの実装は次の通りです。

```ruby
# Token Endpoint
post '/token' do
    content_type :json

    # --- grant_type ---
    # サポートするのは認可コードフロー（grant_type=authorization_code）のみ
    grant_type_value = extract_mandatory_parameter(params, 'grant_type')
    if grant_type_value != 'authorization_code'
        halt 400, {'error'=>'unsupported_grant_type'}.to_json
    end

    # --- code ---
    # 認可コードフローでは code パラメーターは必須。
    code_value = extract_mandatory_parameter(params, 'code')
    code = $AuthorizationCodeStore[code_value]
    if code.nil?
        halt 400, {'error'=>'invalid_grant','error_description'=>
                   'The authorization code is not found.'}.to_json
    end
    if code.expires_at < Time.now.to_i
        $AuthorizationCodeStore.delete(code_value)
        halt 400, {'error'=>'invalid_grant','error_description'=>
                   'The authorization code has expired.'}.to_json
    end

    # --- redirect_uri --
    # 認可リクエストに redirect_uri が含まれていれば、トークンリクエストでも
    # redirect_uri が必要。この簡易実装は、redirect_uri を含まない認可
    # リクエストを全て拒絶するように実装されている。
    redirect_uri_value = extract_mandatory_parameter(params, 'redirect_uri')
    if redirect_uri_value != code.redirect_uri
        halt 400, {'error'=>'invalid_grant','error_description'=>
                   'redirect_uri is wrong.'}.to_json
    end

    # --- client_id ---
    # コンフィデンシャルクライアントかつクライアント認証が client_id を必要としない、
    # という場合を除き、client_id パラメーターは必須。
    client_id_value = extract_mandatory_parameter(params, 'client_id')
    if client_id_value != code.client_id
        halt 400, {'error'=>'invalid_grant','error_description'=>
                   'client_id is wrong.'}.to_json
    end

    # アクセストークンを生成して保存する。
    expires_at = Time.now.to_i + $ACCESS_TOKEN_DURATION
    token = AccessToken.new(
        code.user_id, code.client_id, code.scopes, expires_at)
    $AccessTokenStore[token.value] = token

    # 使用済みの認可コードを削除する。
    $AuthorizationCodeStore.delete(code_value)

    # アクセストークンを含む成功応答
    {
        'access_token' => token.value,
        'token_type'   => 'Bearer',
        'expires_in'   => $ACCESS_TOKEN_DURATION,
        'scope'        => token.scopes.join(' ')
    }.to_json
end
```

一つずつ見ていきましょう。


## 9.1. application/json

```ruby
    content_type :json
```

トークンエンドポイントからの応答の Content-Type は常に `application/json` になります。


## 9.2. grant_type

```ruby
    # --- grant_type ---
    # サポートするのは認可コードフロー（grant_type=authorization_code）のみ
    grant_type_value = extract_mandatory_parameter(params, 'grant_type')
    if grant_type_value != 'authorization_code'
        halt 400, {'error'=>'unsupported_grant_type'}.to_json
    end
```

フローの種別に関わらず、`grant_type` リクエストパラメーターは必須です。

`params['grant_type']` と書けば `grant_type` リクエストパラメーターの値を取り出すことができますが、ここではわざわざ `extract_mandatory_parameter()` 関数を用いて値を取り出しています。この関数の実装は次のようになっており、指定されたリクエストパラメーターが存在しないときに、`error=invalid_request` で `400 Bad Request` を返すようになっています。

```ruby
def extract_mandatory_parameter(params, key)
    value = params[key]

    if value.nil? || value == ''
        halt 400, {'error'=>'invalid_request','error_description'=>
                   "#{key} is missing."}.to_json
    end

    return value
end
```

`grant_type` の値を取り出したあとは、それが `authorization_code` と等しいかどうかをチェックしています。この実装では認可コードフローしかサポートしないので、このような決め打ちのチェックになっています。もしも `grant_type` の値が `authorization_code` ではない場合、`error=unsupported_grant_type` で `400 Bad Request` を返します。

参考までに、`grant_type` が取りうる値としては、例えば次のようなものがあります。

|フロー|`grant_type` の値|
|:--|:--|
|認可コード|`authorization_code`|
|リソースオーナー・パスワード・クレデンシャルズ|`password`|
|クライアント・クレデンシャルズ|`client_credentials`|
|リフレッシュ・トークン|`refresh_token`|
|CIBA|`urn:openid:params:grant-type:ciba`|
|デバイス|`urn:ietf:params:oauth:grant-type:device_code`|


## 9.3. code

```ruby
    # --- code ---
    # 認可コードフローでは code パラメーターは必須。
    code_value = extract_mandatory_parameter(params, 'code')
    code = $AuthorizationCodeStore[code_value]
```

認可コードフローの場合（`grant_type=authorization_code` の場合）、`code` リクエストパラメーターは必須です。`code` パラメーターの値は認可コードを表します。

上記のコードでは、まず、`extract_mandatory_parameter()` で `code` の値を取り出しています。それから、その値に対応する `AuthorizationCode` インスタンスを `$AuthorizationCodeStore` から取り出しています。

```ruby
    if code.nil?
        halt 400, {'error'=>'invalid_grant','error_description'=>
                   'The authorization code is not found.'}.to_json
    end
```

もしも `AuthorizationCode` インスタンスが存在しなければ、それは、`code` パラメーターで指定された認可コードが存在しないことを意味しているので、`error=invalid_grant` で `400 Bad Request` を返します。

```ruby
    if code.expires_at < Time.now.to_i
        $AuthorizationCodeStore.delete(code_value)
        halt 400, {'error'=>'invalid_grant','error_description'=>
                   'The authorization code has expired.'}.to_json
    end
```

`AuthorizationCode` インスタンスが存在していても、その有効期限が既に切れている場合は、`error=invalid_grant` で `400 Bad Request` を返します。


## 9.4. redirect_uri

```ruby
    # --- redirect_uri --
    # 認可リクエストに redirect_uri が含まれていれば、トークンリクエストでも
    # redirect_uri が必要。この簡易実装は、redirect_uri を含まない認可
    # リクエストを全て拒絶するように実装されている。
    redirect_uri_value = extract_mandatory_parameter(params, 'redirect_uri')
    if redirect_uri_value != code.redirect_uri
        halt 400, {'error'=>'invalid_grant','error_description'=>
                   'redirect_uri is wrong.'}.to_json
    end
```

認可コードフローにおいて、認可リクエストに `redirect_uri` パラメーターが明示的に渡されていた場合、トークンリクエストにも同じ値を持つ `redirect_uri` パラメーターを指定しなければなりません。

この簡易実装は、認可コードフローしかサポートせず、かつ、認可エンドポイントが `redirect_uri` を必ず要求するように実装されているので、結果として、この簡易実装のトークンエンドポイントに対するトークンリクエストは常に `redirect_uri` を含んでいなければなりません。

上記のような理由により、このコードでは、何の条件もチェックせずに、`redirect_uri` を必須パラメーターとして扱っています。

`redirect_uri` の値を取得した後、それが、認可コードに紐付いているリダイレクト URI の値（＝認可リクエストの `redirect_uri` の値）と等しいかどうかを調べています。


## 9.5. client_id

```ruby
    # --- client_id ---
    # コンフィデンシャルクライアントかつクライアント認証が client_id を必要としない、
    # という場合を除き、client_id パラメーターは必須。
    client_id_value = extract_mandatory_parameter(params, 'client_id')
    if client_id_value != code.client_id
        halt 400, {'error'=>'invalid_grant','error_description'=>
                   'client_id is wrong.'}.to_json
    end
```

トークンリクエストの `client_id` が必須となるのは、次のケースです。

- クライアントタイプがパブリックである。
- クライアントタイプがコンフィデンシャルで、使用する[クライアント認証](https://qiita.com/TakahikoKawasaki/items/63ed4a9d8d6e5109e401)方式が `client_id` パラメーターを必要とする。`client_secret_post`、`tls_client_auth`、`self_signed_tls_client_auth` が該当します。

この簡易実装がサポートするのはパブリッククライアントのみなので、決め打ちで `client_id` を必須パラメーターとして扱っています。

大抵の実装では、認可コードのみからクライアントを特定することは可能ですが、そうであったとしても、セキュリティー上の理由から、パブリッククライアントの場合は `client_id` リクエストパラメーターを必ず渡さなければならないとされています。


## 9.6. アクセストークン生成

```ruby
    # アクセストークンを生成して保存する。
    expires_at = Time.now.to_i + $ACCESS_TOKEN_DURATION
    token = AccessToken.new(
        code.user_id, code.client_id, code.scopes, expires_at)
    $AccessTokenStore[token.value] = token
```

アクセストークンの生成です。アクセストークンの属性の多くは、認可コードの属性からのコピーです。アクセストークンの有効秒数には、`$ACCESS_TOKEN_DURATION` という固定値を使用しています。


## 9.7. 認可コード削除

```ruby
    # 使用済みの認可コードを削除する。
    $AuthorizationCodeStore.delete(code_value)
```

認可コードを複数回使用することは禁止されているので、ここで削除しておきます。


### 9.8. 成功応答

```ruby
    # アクセストークンを含む成功応答
    {
        'access_token' => token.value,
        'token_type'   => 'Bearer',
        'expires_in'   => $ACCESS_TOKEN_DURATION,
        'scope'        => token.scopes.join(' ')
    }.to_json
```

生成したばかりのアクセストークンを含む JSON を返します。

応答のうち、`access_token` と `token_type` は必須のパラメーターです。

`token_type` の値が `Bearer` となっていますが、これは [RFC 6750](https://tools.ietf.org/html/rfc6750) 由来のものです。通常、リソースサーバーの API 群は RFC 6750 で定義されている方法でアクセストークンを受け取るように実装されると思うので、`token_type` の値は `Bearer` でよいでしょう。

`expires_in` はアクセストークンの有効期限秒数を表すオプショナルパラメーターです。

認可リクエストで要求された `scope` と、実際に許可された `scope` が異なる場合、トークンレスポンスの `scope` パラメーターは必須となります。


# 10. 実行

## 10.1. 認可サーバー起動

[Sinatra](http://sinatrarb.com/) をまだインストールしていなければインストールします。

```shell-session
$ gem install sinatra
```

ソースコードをダウンロードし、

```shell-session
$ git clone https://github.com/authlete/useless-oauth-server
$ cd useless-oauth-server
```

プログラムを実行します。

```shell-session
$ ruby server.rb
```

これにより、`http://localhost:4567` で認可サーバーが動き始めます。


## 10.2. 認可リクエストと認可レスポンス

認可リクエストを表す次の URL を Web ブラウザのアドレスバーに入力します（もしくは単にクリックします）。

<code>http://localhost:4567/authorization?response_type=code&client_id=1&redirect_uri=http://example.com/&scope=read+write&state=mystate</code>

すると、次のような認可ページが表示されます。

![authorization_page.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/81764ebf-1abf-6dea-db59-0db859610b43.png)

ログインフォームの Login ID と Password にそれぞれ `john`、`john` と入力し、Approve ボタンを押します。

その結果、リダイレクト先（`http://example.com/`）に遷移し、次のような画面が表示されます。

![redirection_page.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/0461842b-b25c-2aac-2394-302d7c78fc47.png)

ここで重要なのは、アドレスバーに表示されている URL の `code` パラメーターの値です。この値が、発行された認可コードです。この例では `SBxFEHaX` が認可コードです。

学習用の認可サーバー実装なので、コピペしやすいように、認可コードの長さは短くしてあり、8 桁しかありません。認可コードのエントロピーが極端に低いので、セキュリティー上問題となります。

認可コードの有効期限は 10 分しかないので、すみやかにトークンリクエストを投げましょう。


## 10.3. トークンリクエストとトークンレスポンス

シェルターミナルを開いて、次のコマンドを実行します。

```shell-session
$ CODE={発行された認可コード}
$ curl http://localhost:4567/token \
  -d grant_type=authorization_code \
  -d code=$CODE \
  -d client_id=1 \
  -d redirect_uri=http://example.com/
```

次のような応答が返ってきたら成功です。`access_token` の値が発行されたアクセストークンです。この例では `mvL_sRy3` がアクセストークンです。

```json
{
  "access_token":"mvL_sRy3",
  "token_type":"Bearer",
  "expires_in":86400,
  "scope":"read write"
}
```

なお、見やすいようにここでは出力を整形していますが、実際の出力は 1 行です。


# おわりに

この記事で実装したのは [RFC 6749](https://tools.ietf.org/html/rfc6749) の一部のみであり、厳密に仕様に準拠していない箇所や、セキュリティー上問題になる箇所もあるので、とても商用利用できるものではありません。しかしながら、認可サーバーの実装のイメージを掴むことはできたと思います。

[Authlete](https://www.authlete.com/)（オースリート）のバージョン 2.1 では、次の仕様をサポートしています。

- [RFC 6749: The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749)
- [RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://tools.ietf.org/html/rfc6750)
- [RFC 7009: OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009)
- [RFC 7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://tools.ietf.org/html/rfc7523)
- [RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol](https://tools.ietf.org/html/rfc7591)
- [RFC 7592: OAuth 2.0 Dynamic Client Registration Management Protocol](https://tools.ietf.org/html/rfc7592)
- [RFC 7636: Proof Key for Code Exchange by OAuth Public Clients](https://tools.ietf.org/html/rfc7636)
- [RFC 7662: OAuth 2.0 Token Introspection](https://tools.ietf.org/html/rfc7662)
- [RFC 8628: OAuth 2.0 Device Authorization Grant](https://tools.ietf.org/html/rfc8628)
- [OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html)
- [OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html)
- [OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-mtls)
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
- [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html)
- [OpenID Connect Dynamic Client Registration 1.0](https://openid.net/specs/openid-connect-registration-1_0.html)
- [OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html)
- [Financial-grade API - Part 1: Read-Only API Security Profile](https://openid.net/specs/openid-financial-api-part-1.html)
- [Financial-grade API - Part 2: Read and Write API Security Profile](https://openid.net/specs/openid-financial-api-part-2.html)
- [Financial-grade API: JWT Secured Authorization Response Mode for OAuth 2.0 (JARM)](https://openid.net/specs/openid-financial-api-jarm-ID1.html)
- [Financial-grade API: Client Initiated Backchannel Authentication Profile](https://openid.net/specs/openid-financial-api-ciba.html)
- [UK Open Banking Security Profile](https://standards.openbanking.org.uk/)

Authlete は次の項目で [OpenID Certification](https://openid.net/certification/) を取得しており、

- Basic OP
- Implicit OP
- Hybrid OP
- Config OP
- Dynamic OP
- Form Post OP
- FAPI R/W OP w/ MTLS
- FAPI R/W OP w/ Private Key
- FAPI-CIBA OP poll w/ MTLS
- FAPI-CIBA OP poll w/ Private Key
- FAPI-CIBA OP Ping w/ MTLS
- FAPI-CIBA OP Ping w/ Private Key

[FAPI-CIBA プロファイル](https://qiita.com/TakahikoKawasaki/items/f478e2fd07b7236d6fbe)に至っては、現時点（2019 年 10 月 17 日）で世界で唯一、OpenID Certification を取得している実装です。

![FAPI-CIBA_OPs_20190920.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/84db5ba6-c70c-118e-4d18-8ef98ea1d86d.png)


商用の認可サーバーには、是非 [Authlete](https://www.authlete.com/) をご利用ください！

・・・

なお、簡易実装と商用利用に耐えうる実装の間には天と地ほどの差があるので、簡易実装のソースコードを公開したところで、Authlete のビジネスには全く影響ありません。ーーーこう考えるんだ。この簡易実装と解説記事を 24 時間以内に仕上げてしまう人が、世界からエキスパートを集めてチームを作り、5 年以上かけて（もうすぐ 6 年）開発し続けているのが Authlete なんだ。だから、Authlete の機能が多く、品質が高いのは当然なんだ。