OAuth 2.0のscopeを考える

※ OAuth 2.0の規程に則って「認可サーバーを作成する」側の話です。

OAuth 2.0では、 `scope` というパラメーターでアクセス範囲を表します。

> クライアントは scope リクエストパラメーターを用いて要求するアクセス範囲を明示することができる.
同様に, 認可サーバーは発行されたアクセストークンの範囲をクライアントに通知するために scope レスポンスパラメーターを使用する.

このscopeですが、どんな形式にするかは実装者に委ねられています。RFCで定められている制約といえば下記くらいのものです。

* 大文字と小文字は区別する
* スペースは使えない（複数のscopeをスペース区切りで表すため）
* scopeを省略しても良いことにしても良い（その場合デフォルト値を定義する）

ref: https://openid-foundation-japan.github.io/rfc6749.ja.html#scope

**めっちゃ自由。** 自由すぎてどんな形式にするのが良いかちょっと悩みます。
この記事では他社事例を見た後、採用するscopeの形式を考えます。

# 他社事例を見てみる
まずは先人の事例を見てみます。

## GitHub OAuth Apps
### scope一覧
* (no scope)
* repo
* repo:status
* repo_deployment
* public_repo
* repo:invite
* security_events
* admin:repo_hook
* write:repo_hook
* read:repo_hook
* admin:org
* write:org
* read:org
* admin:public_key
* write:public_key
* read:public_key
* admin:org_hook
* gist
* notifications
* user
* read:user
* user:email
* user:follow
* delete_repo
* write:discussion
* read:discussion
* write:packages
* read:packages
* delete:packages
* admin:gpg_key
* write:gpg_key
* read:gpg_key
* workflow

ref: https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/#available-scopes

### 特徴
* コロン(`:`)区切りになっている。
* `write:discussion`や`read:discussion`のように、「操作(権限):リソース」の形をしているものが散見される。その視点で見ると、「操作(権限)」にあたるものは`read`, `write`, `admin`, `delete`の4つ。
* scopeなし(空欄)でパブリックな情報へのread only権限が与えられる。

「操作:リソース」のフォーマット良さそう。

## YouTube Data API
### scope一覧
* https://www.googleapis.com/auth/youtube
* https://www.googleapis.com/auth/youtube.readonly
* https://www.googleapis.com/auth/youtube.upload
* https://www.googleapis.com/auth/youtubepartner-channel-audit

ref: https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps?hl=ja#Obtaining_Access_Tokens

### 特徴
* URLの形をしている。
* URLを叩くと、`text/plain`でscope名が返ってくる。
* scopeなし(空欄)はサポートされていない。

URLをどのように使っているのか気になる。リソースサーバー側で管理しやすくするためなのかな。実際にURLであるうまみはあったんだろうか。

## LINE Social API
### scope一覧
* profile
* profile%20openid
* profile%20openid%20email
* openid
* openid%20email

ref: https://developers.line.biz/ja/docs/line-login/integrate-line-login/#scopes

### 特徴
* あらかじめ空白文字(`%20`)で区切られた状態で列挙されている。`%20` を空白文字として捉えると、`email`は単体でscopeにならないので、RFCに準拠してないと言っても良いかもしれない。
* scopeなし(空欄)はサポートされていない。

## Twitter
scopeはなし。
OAuth 1.0aでの認可がまだ主流のようで、OAuth 2.0ではClient Credentials Grantでのpublicな情報へのアクセスしかサポートしていないため、scopeでアクセス範囲を決める必要がないのだろう。

ref: https://developer.twitter.com/en/docs/authentication/api-reference/token

# ぼくのかんがえたさいきょうのscope
先人たちのscopeを見てみましたが、三者三様でした。

どんなAPIを提供するのかに依ってscopeのフォーマットも変わると思いますが、ここではRESTfulなAPIへの認可を考えることにします。

さて、ぼくのかんがえたさいきょうのscopeは、これだっ！！

* `read:RESTのリソース名`
* `write:RESTのリソース名`

## 理由

このscopeを考えるにあたっての、基本的な指針は下記2つです。

* [最小権限の原則](https://ja.wikipedia.org/wiki/%E6%9C%80%E5%B0%8F%E6%A8%A9%E9%99%90%E3%81%AE%E5%8E%9F%E5%89%87)に則って必要最低限の権限のみを認可できるように、scopeのアクセス範囲はなるべく小さくしたい。
* scope名を見ただけでアクセス範囲が分かるような明快なものが良い。

この思想をもとに、下記のように考えました。

* RESTの「リソース」という概念をそのまま利用して、リソース単位で切ると、明快で小さくscopeを分けられるだろう。
* リソースに対しての操作を分けたいが、リソースの新規作成ができるclientには、作成したリソースの更新・削除する権限も与えたいという考え・要件で、readとwriteだけにした。

## オマケ 〜Railsでの実装〜
「リソース名」は単数形にしても複数形にしても良いですが、Railsでは`controller_name`でコントローラー名(基本的にリソースの複数形)が取れ、私はこれを利用するために複数形にしました。

```ruby
# こんなアクセスのとき... http://hoge.jp/articles
controller_name
# => “articles”
# こんなのが取れる！
```

これを利用すれば、scopeの検証は下記のような簡単なコードで実装できます。controller concernなどに書くと良いと思います。

```ruby
def required_read_scope
  scope_name = "read:#{controller_name}"
  render_unauthorized unless current_access_token.scopes.include?(scope_name)
end

def required_write_scope
  scope_name = "write:#{controller_name}"
  render_unauthorized unless current_access_token.scopes.include?(scope_name)
end
```

あとはこのメソッドを各controllerのbefore_actionで、指定したアクションの前に呼び出すだけです。

```ruby
class ArticlesController < ApplicationController
  before_action :required_read_scope, only: %i[index show]
  before_action :required_write_scope, only: %i[create update destroy]

  ...
end
```

# OAuth 2.0の他の仕様で迷ったら
私がOAuth 2.0の自由な仕様で迷ったときに、各社の仕様をこちらにまとめました。他の仕様で迷っているときは参考になるかもしれません。
https://qiita.com/murs313/items/078025d671e937a09285