ABEJA Platform の認証についてまとめる

この記事は [ABEJA Platform Advent Calendar](https://qiita.com/advent-calendar/2018/abejaplatform) の 4 日目の記事です。今回は基本的なこととして、API 呼び出しの認証についてまとめます。なお、特に意味はありませんが、コード例はすべて [Elixir](https://elixir-lang.org/) で書いてあります。

## 認証の必要性

### リクエスター

ABEJA Platform の API を呼び出すには、なんらかの手段でリクエストを発行する主体 (リクエスター 英: _Requester_) を認証する必要があります。

ABEJA Platform にはこのリクエスターが複数種類あり、リクエスターの種類に応じてアクセス可能なリソースの種類や権限が制限されています。そのため、リクエスターは用途や目的に応じて使い分ける必要がありますが、主に使われるのは以下のふたつです。

| リクエスターの種類 | 説明                                                                                                                   |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| ユーザー           | プラットフォームの利用ユーザーです。ユーザー個人の権限に紐づくため、プロダクションコードで利用するのには適していません |
| データーソース     | カメラやセンサーなどのデバイスからデータを送信するときに利用します                                                     |

### 認証情報 (クレデンシャル)

また、リクエスターには各々が自分自身であることを証明するための認証情報 (クレデンシャル) を発行することができます。クレデンシャルは共通鍵方式 (一部、公開鍵認証の鍵ペアもあります) で、

- ユーザーには「パーソナル・アクセス・トークン (Personal Access Token)」
- データーソースには「シークレット (Secret)」

という名前で発行される、ランダムな文字列です。

これらは 20 バイトの 16 進数表現になります。暗号論的擬似乱数生成器によって、毎回ランダムな文字列が生成されます。

```
416df9c92b9875037f3909ea473113dc3e509c94
```

余談ですが、同様の文字列は、以下のようなコードで生成できます。

```elixir
iex> :crypto.strong_rand_bytes(20) |> Base.encode16(case: :lower)
"416df9c92b9875037f3909ea473113dc3e509c94"
```

## 認証の方法

共通鍵形式のクレデンシャルを持つリクエスター（ユーザー、またはデーターソース）を認証するには、以下のふたつの方式があります。

1. Basic 認証
2. JWT 認証

それぞれに利点と欠点があるので、目的に応じて使い分けてください。

### Basic 認証

手元で API を呼び出したいだけであれば、Basic 認証がもっとも手軽です。 `curl` などの HTTP ツールでもサポートされているので、簡単に使えます (なお、ABEJA Platform のすべての API は HTTPS で提供されており、通信経路は暗号化されています)。

**ユーザーの場合**

- ユーザー名には `user-{User-ID}`
- パスワードにはパーソナル・アクセス・トークン

**データーソースの場合**

- ユーザー名には `datasource-{DataSource-ID}`
- パスワードにはシークレット

たとえば、`curl` を使う場合は、以下のように指定します。

```bash
$ curl -u user-1234567890123:416df9c92b9875037f3909ea473113dc3e509c94 ...
```

Elixir (HTTP ライブラリは [HTTPoison](https://github.com/edgurgel/httpoison)) では以下のように呼び出します。

```elixir
api_endpoint = "https://api.abeja.io/users/me"
user = "user-1234567890123"
password = "416df9c92b9875037f3909ea473113dc3e509c94"
auth = [hackney: [basic_auth: {user, password}]]

HTTPoison.get(api_endpoint, %{}, auth)
```

### JWT 認証

インストール型のアプリケーションなど、HTTPS 通信をしていても中間者攻撃による通信内容の傍受の可能性がある場合は、クレデンシャルを直接通信経路に流すのは危険です。そういう場合のために、JWT (JSON Web Token) を用いた認証もサポートしています。

JWT 認証を使うことで、以下のような利点があります。

- 通信経路を流れるのは署名 (Signature) だけなので、万が一通信内容が洩れたとしてもクレデンシャル自体は漏洩しない
- 有効期限を指定できる

JWT 認証のためにはまず、以下の Claim を含む JWT を生成します。

| Name  | Type     | Description                                                                   |
| ----- | -------- | ----------------------------------------------------------------------------- |
| `iss` | `string` | 後述                                                                          |
| `aud` | `string` | `"api.abeja.io"`                                                              |
| `exp` | `number` | 有効期限を UNIX time (`1970-01-01T00:00:00Z` からの経過秒数) で指定しています |
| `iat` | `number` | トークンを生成した日時を UNIX time で指定します                               |

**ユーザーの場合**

- `iss` には `user-{User-ID}`
- パーソナル・アクセス・トークンの **バイト列** で HS256 署名をします

**データーソースの場合**

- `iss` には `datasource-{DataSource-ID}`
- シークレットの **バイト列** で HS256 署名をします

ここで生成した JWT を `Authorization` ヘッダーに指定してリクエストを投げます。

```
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```

Elixir (JWT ライブラリは [Joken](https://github.com/joken-elixir/joken)) では以下のようにして JWT を生成できます。

```elixir
import Joken

hexadecimal_secret = "416df9c92b9875037f3909ea473113dc3e509c94"
secret = Base.decode16!(hexadecimal_secret, case: :lower)

my_token =
  token()
  |> with_aud("api.abeja.io")
  |> with_iss("user-1234567890123")
  |> with_iat()
  |> with_signer(hs256(secret))
  |> sign()
  |> get_compact()
```

## まとめ

今回はとりあえず、基本的な認証について解説しました。ABEJA Platform では他にも OpenID Connect に対応してたりもするので、そのうち機会があればこのへんも書いていきたいと思います。