DoorkeeperのAuthorization Code Flowが導入されている状態からPKCEを導入する手順

# これは何？

前回[こちら](https://qiita.com/nobuo_hirai/items/8cd8140e7d3970e4e094) の記事でPKCEのフローをまとめました。
今回は実際に`doorkeeper`という`gem`を使って実装していこうと思います。

# doorkeeper gemでPKCEを導入する手順
以前 [こちら](https://qiita.com/nobuo_hirai/items/62260eb505f7085aa872) の記事で`doorkeeper gem`でOAuthプロバイダ機能を作成する記事を投稿しました。

今回は前回の状態からPKCEを導入する手順を記載します。
(※ doorkeeper gem のwikiにもPKCEについて記載があるのでご確認お願いします。)
(https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-PKCE-flow)

**導入手順**

```ruby
# wikiに記載されているように以下のコマンドを実行します。
bundle exec rails generate doorkeeper:pkce

# 実行すると以下のmigrationファイルが作成されます。
db/migrate/20210330224805_enable_pkce.rb
```

migrationファイルの中身は以下のようになっており `oauth_access_grants`テーブルへのカラム追加となっています。

```ruby
# frozen_string_literal: true

class EnablePkce < ActiveRecord::Migration[6.0]
  def change
    add_column :oauth_access_grants, :code_challenge, :string, null: true
    add_column :oauth_access_grants, :code_challenge_method, :string, null: true
  end
end
```

migrateを実行します。

```ruby
rails db:migrate RAILS_ENV=development
```

これで `oauth_access_grants`　テーブルへ `code_challenge、code_challenge_method` を保存することができます。

続いてOAuth Applicationを作成していきます。

`http://localhost:3000/oauth/applications`

へアクセスし[New Application]をクリックします。

![pkce_oauthアプリ作成1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67654/fc889712-a976-766b-421e-ebba4c5d24e0.jpeg)

アプリケーションの情報を入力して登録します。
![pkce_oauthアプリ作成2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67654/ab167e83-2abf-959b-6669-0e70a7d344bf.jpeg)

![pkce_oauthアプリ作成3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67654/1b77e0ea-19af-937d-370a-9ad84884ab35.jpeg)


今回Client側のアプリのURLは `http://localhost:3001` としcallbackURLを `http://localhost:3001/pkce_callback` として準備しました。

画面に表示されている `CLIENT_ID(UID)、CLIENT_SECRET、scopes` をClient側のアプリへ設定しておきます。

ここでPKCEの仕組みを以下へ記載します。
1. `code_verifier` を使って `code_challenge_method` に合った `code_challenge` を生成し、`code_challenge, code_challenge_method` をパラメータへ指定して認可画面をリクエストします。
2. 認可サーバは `code_challenge, code_challenge_method` を保存し、Clientへ認可コードを返します。
3. Clientは認可コードと `code_verifier` を使ってアクセストークンを取得します。
4. 認可サーバで受け取った `code_verifier`を保存してある `code_challenge_method`で変換して `code_challenge`のになるか検証します。

それではPKCEを使うためClientアプリケーションの `OAuth2::Client`の部分を実装していきます。

以下は認可画面のURLを作成する処理のサンプルになります。

```ruby
# 認可画面のURLを作成する処理
def pkce_authorization
  session[:code_verifier] = SecureRandom.alphanumeric(100)

  client = OAuth2::Client.new(
    ENV["CLIENT_ID_PKCE"],
    ENV["CLIENT_SECRET_PKCE"],
    site: ENV["SITE"],
    authorize_url: ENV["AUTHORIZE_URL"],
    token_url: ENV["TOKEN_URL"]
  )
  code_challenge = Base64.urlsafe_encode64(OpenSSL::Digest::SHA256.digest(session[:code_verifier]), padding: false)

  authorize_url = client.auth_code.authorize_url(
    redirect_uri: ENV["CALLBACK_URI_PKCE"],
    scope: ENV["SCOPE"],
    code_challenge: code_challenge,
    code_challenge_method: "S256"
  )
  redirect_to authorize_url
end
```

`code_verifier` の値は、[A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" からなるランダムな文字列であり、最低43文字、最大128文字の長さが必要となります。

今回は 

```ruby
code_verifier = SecureRandom.alphanumeric(100)
```
のように100文字の英数字を設定します。ref：https://tools.ietf.org/html/rfc7636

`code_challenge` は `code_challenge_method` によって値が変わります。
`code_challenge_method` を `plain` にした場合
`code_challenge = code_verifier` となります。
`code_challenge_method` を `S256` にした場合
`code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))` となります。
ref：https://tools.ietf.org/html/rfc7636

これをrubyで実装すると

```ruby
code_challenge = Base64.urlsafe_encode64(OpenSSL::Digest::SHA256.digest(session[:code_verifier]), padding: false)
```
で実装できます。

`code_challenge_method` は 2種類あるのですが `plain`は `code_verifier`と `code_challenge` が同じ値のため特に `plain` を選ぶ理由がなければセキュリティリスクを考えて `S256` を使用しましょう。

続いて認可コードが返ってきてからアクセストークンを取得する部分のサンプルが以下になります。

```ruby
# 認可コードよりアクセストークンを取得
def pkce_callback
  client = OAuth2::Client.new(
    ENV["CLIENT_ID_PKCE"],
    ENV["CLIENT_SECRET_PKCE"],
    site: ENV["SITE"],
    authorize_url: ENV["AUTHORIZE_URL"],
    token_url: ENV["TOKEN_URL"]
  )

  code_verifier = session[:code_verifier]
  session.delete(:code_verifier)
  @access_token = client.auth_code.get_token(
    params[:code],
    redirect_uri: ENV["CALLBACK_URI_PKCE"],
    code_verifier: code_verifier
  )

  render :callback
end
```

実際に認可画面から認可後、アクセストークンを取得できるか確認します。

**認可画面**
![pkce_認可画面.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67654/ce7d31f5-6499-8762-9fb1-38d85dc7ae91.jpeg)

**認可コード取得後、アクセストークン取得**
![pkce_認可コードからアクセストークン取得.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67654/f33e26f9-30fc-08a3-4ad4-11f7dcaabebf.jpeg)

念のため、認可サーバ側で `code_challenge, code_challenge_method` が保存されているか確認しておきます。

```ruby
Doorkeeper::AccessGrant.all

  Doorkeeper::AccessGrant Load (0.3ms)  SELECT "oauth_access_grants".* FROM "oauth_access_grants" LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Doorkeeper::AccessGrant id: 1, resource_owner_id: 1, application_id: 2, token: "ygCPJhHoenE3vfiY1StLqCVBRJWTZfWnZFdOPUcTn7M", expires_in: 600, redirect_uri: "http://localhost:3001/pkce_callback", created_at: "2021-05-03 07:32:25", revoked_at: "2021-05-03 07:32:25", scopes: "public write", code_challenge: "oDJceObZlNuTivuP6EocfJZt2ODCdyjOh5IyUXK_4Rw", code_challenge_method: "S256">]>
```
正常に`code_challenge, code_challenge_method`が保存されていることが確認できました！

# 最後に
`doorkeeper gem`を使用していればPKCEの導入は非常に簡単にできました。
Authorization Code Flowとの共存も可能なため実際の業務でもPKCEは導入しやすいと思います！