Ruby, RailsにおけるJWT

JWTについてまっっっっっっっっっっっっっっっっっったくわからず調べたので備忘録としてアウトプットします。
※随時更新予定

:::note warn
誤った内容があればマサカリお願いします。血まみれになりたいｓです。
:::

# JWTとは

Json Web Tokenを略して`JWT`

https://ja.wikipedia.org/wiki/JSON_Web_Token

> JWTでは、トークン内に任意の情報（クレーム）を保持することが可能であり、例えば[サーバ](https://ja.wikipedia.org/wiki/%E3%82%B5%E3%83%BC%E3%83%90)はクライアントに対して「管理者としてログイン済」という情報を含んだトークンを生成することができる。クライアントはそのトークンを、自身が管理者としてログイン済であることの証明に使用することができる。トークンは当事者の一方（通常はサーバ）または両方（もう一方は[公開鍵](https://ja.wikipedia.org/wiki/%E9%8D%B5_(%E6%9A%97%E5%8F%B7))を提供する）の[秘密鍵](https://ja.wikipedia.org/wiki/%E9%8D%B5_(%E6%9A%97%E5%8F%B7))により署名されており、発行されたトークンが正規のものか確認することができる。
> 

`トークン` ：JSONオブジェクトをエンコードした文字列

```ruby
eyJhbGciOiJIUzI1NiJ9.　# ヘッダー
eyJleHAiOjE2MDA1OTY0MzEsInN1YiI6MSwibmFtZSI6InVzZXIwIn0. # ペイロード
lXcwASyLX5GEsMvPYDVhe0ovJj631fUiC0q2ojK-yK0 # 署名
```

### JWT（トークン）の構造
JWT（トークン）は３つの構造からなる

### 1. ヘッダー
トークンのタイプと使用する署名アルゴリズム
```ruby
eyJhbGciOiJIUzI1NiJ9. # ヘッダー
=> { "alg": "HS256", "typ": "JWT" }
```

### 2. ペイロード

実際のデータを含むクレーム（任意の情報）

```ruby
eyJleHAiOjE2MDA1OTY0MzEsInN1YiI6MSwibmFtZSI6InVzZXIwIn0. # ペイロード
=> {"exp"=>1600596431, "sub"=>1, "name"=>"user0"}
```

`exp` や`sub` などのそれぞれの値を**クレーム**と呼ぶ

デフォルトで指定されている値を**「予約済クレーム」**、使用者が任意に指定した値を**「公開（パブリック）クレーム」**、当事者間で合意された非公開のカスタムクレームを**「プライベートクレーム」**と呼ぶ

- **`exp`(Expiration Time)**
    - トークンの有効期限（失効時間）
    - UNIXタイムスタンプ形式
    - 例: `"exp": 1622505600` (2021年6月1日)
- **`sub`(Subject)**
    - トークンの主題（通常はユーザーID）
    - 文字列形式
    - 例: `"sub": "1234567890"` または `"sub": "user@example.com"`
- **`iss` (Issuer)**
    - トークンの発行者
    - 文字列形式
    - 例: `"iss": "https://api.myapp.com"`

[その他のクレーム](https://openid-foundation-japan.github.io/draft-ietf-oauth-json-web-token-11.ja.html#PrivateClaimName)

### 3. 署名

トークンの完全性を検証するのに使用（トークンが変更されていないか確認するために使用）

```ruby
// エンコード => デコード
lXcwASyLX5GEsMvPYDVhe0ovJj631fUiC0q2ojK-yK0
=> HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)
```

## 主な用途

### 認証

ユーザーがログインしたあと、以降のリクエストにJWTが含まれ、そのトークンにより保護されたルートやリソースへのアクセスが許可される

### 情報交換

署名されているため、送信者が本人であることを確認でき、情報が改ざんされていないことを確認できる

# RubyにおけるJWT

https://github.com/jwt/ruby-jwt

Gemとして提供されている`jwt` はJSON Web Tokenの実装を提供している。このgemを使用することで、JWTの生成と検証が簡単に行える。

```ruby
require "jwt"

# トークンのエンコード（生成）
payload = { data: "test" }
token = JWT.encode payload, "my_secret", "HS256"

# トークンのデコード
decoded_token = JWT.decode token, "my_secret", true, { algorithm: "HS256" }
```

```ruby
# トークン生成
def generate_token(user_id)
	pay_load = { user_id: user_id, exp: 24.hours.from_now.to_i }
	JWT.encode payload, Rails.application.secrets.secret_ley_base, "HS256"
end

# トークン検証
def authenticate_user
	token = request.headers["Authorization"]&.split(" ")&.last
	begin
		decoded = JWT.decode token, Rails.application.secrets.secret_key_base, true, { algorithm: "HS256" }
		@current_user = User.find(decoded[0]["user_id"])
	rescue JWT::DecodeError
		render json: { errors: ["Invalid token"] }, status: unauthrized
	rescue JWT::ExpiredSignature
    render json: { errors: ['Token has expired'] }, status: :unauthorized
  end
end
```

# RailsにおけるJWTの役割

### 1. APIにおける認証の実装

Railsでは特にフロントエンドとバックエンドが分離されたSPAやモバイルアプリとの連携において、JWTが重要な役割を果たす。

```ruby
def login
	user = User.find_by(email: params[:email])
	if user&.authenticate(params[:password])
		token = 
		JWT.encode({user_id: user.id, exp: 24.hours.from_now.to_i},
			Rails.application.credentials.secret.secret_key_base)
		render json: { token: token, user: user.as_json(expect: :password_digest) }
	else
		render json: { error: "Invalid credentials" }, status: unauthorized
	end
end
```

### 2.セッションレス認証の実現

従来Railsではcookieベースのセッション管理が一般的だったが、JWTを使用することで
- サーバー側でセッション状態を保持する必要がなくなる（ステートレス）
- スケーラビリティ向上
- クロスドメイン認証の容易さ

というメリットが生まれる。

### 3.マイクロサービスアーキテクチャでの認証

Railsアプリケーションが複数のマイクロサービスと連携する場合、JWTは各サービスの間の認証に使われる

```ruby
# サービスA（ユーザー認証）
token = JWT.encode({user_id: user.id, permissions: user.permissions}, secret_key)

# サービスB（リソースサーバー）
begin
  payload = JWT.decode(token, secret_key, true, algorithm: 'HS256').first
  user_permissions = payload['permissions']
  # 権限に基づいた処理...
rescue JWT::DecodeError
  # 認証エラー処理
end
```

## JWTのいいところ

### 情報改ざんができない

トークンが正しいものか検証する際に署名アルゴリズムと一致したトークンでなければエラーが発生する。

### Userテーブルにトークンを一時的に保有する絡むを作成しなくてもよくなる

### 署名をした鍵を持つものしかトークンを検証することができない

## セキュリティ上の考慮点

- トークンはエンコードされているだけで暗号化されているわけではない
- トークンの有効期限を適切に設定(expクレーム)
- 機密情報をペイロードに含めない
- HTTPS通信
- 定期的な鍵のローテーション

# 参考記事

https://qiita.com/shu1124/items/e8c37b73e015afc63074

https://qiita.com/yami-yami/items/9bd433d717ad26e84c4b

https://blog.cloud-acct.com/posts/u-rails-whats-jwt/

### 特に関係ないけど出てきたわからないコード

`HashWithIndifferentAccess.new(decoded)`

`HashWithIndifferentAccess`

Rails（activesupport）で提供されているHashのサブクラス

SybolとStringを区別せず同一のキーとして取り扱う

```ruby
hogehoge(dev)> normal_hash = { "user_id" => 1 }
=> {"user_id"=>1}
hogehoge(dev)> normal_hash["user_id"]
=> 1
hogehoge(dev)> normal_hash[:user_id]
=> nil
hogehoge(dev)> indifferent_hash = HashWithIndifferentAccess.new({ "user_id" => 1 })
=> {"user_id"=>1}
hogehoge(dev)> indifferent_hash["user_id"]
=> 1
hogehoge(dev)> indifferent_hash[:user_id]
=> 1
```