OpenSSLで作成した署名鍵を利用したIDトークン生成/検証エンドポイントの作成方法 メモ


## ①署名鍵生成

* opensslを利用して秘密鍵/署名鍵を生成

  ```
  openssl genrsa -out private-key.pem 2048
  openssl rsa -in private-key.pem -pubout -out public-key.pem
  ```

  

## ②公開鍵をJWK形式に変換

* jsライブラリ`pem-jwk`を利用して、JWK形式に変換する。※もっとスマートな方法があれば...

* `generate_jwk.js`

  ```js
  var fs = require('fs')
  var pem2jwk = require('pem-jwk').pem2jwk
  var str = fs.readFileSync('./public-key.pem', 'ascii')
  var jwk = pem2jwk(str)
  console.log(jwk)
  ```

  ```json
  node generate_jwk.js
  {
    kty: 'RSA',
    n: 'pw5We-LHJAIXSOwNbUxO8T615XgNn8e1J_1auE8Nza9MEkSWqf1ylz1MnOoPWqnnleo0mJfAza2Ro5xm7aKUGtPVwThwGj32SUtYj5mYPd3kfVgO9colc9k_dle8OpcP54CoA-5fVd24jt-jL3SXcITxB2-9-ms2gyTZnPjTaEeXCa6ElyMiHjcw4VApwzmZsu6wkCW4uPnX-_uZvxwjcGEH9RKMyWU0B5m_NOKcgy9eChZMw01rAxlxDugvvEU0iedzuAUu3vv19tn1UOvIUToyXrhUw0PSEr_nUg_zITVf-IjZ6yqKgI_2HU37AypypIe8TsjmgHwSP49X2MnUmQ',
    e: 'AQAB'
  }
  ```

  

## ③エンドポイント作成

#### プロジェクト構成

```
test	--- docker-compose.yml
		 |_ 	app		- 	Dockerfile
		 				|_	requirements.txt
		 				|_	main.py
		 				|_	private-key.pem※前述の手順で生成したもの
```

#### コード・設定ファイル類

##### `main.py`
* FastAPIを使用して各種エンドポイントを作成する。

```python
from fastapi import FastAPI,Form
from starlette.requests import Request
from starlette.responses import RedirectResponse
import jwt
from jwt import PyJWKClient
from pydantic import BaseModel
import datetime
from datetime import datetime, timedelta
import time

# リクエストbody定義
class IdToken(BaseModel):
    id_token: str

# JWK,IDトークン要素
KID = "12345"
USE = "sig"
ALG = "RS256"
ISS = "http://server.example.com"
SUB = "248289761001"
AUD = "s6BhdRkqt3"

app = FastAPI()

# JWKSエンドポイント
@app.get("/auth/jwks")
def jwks():
    # 「公開鍵をJWK形式に変換」の取得結果にkid,use,algを追加
    return {
        "keys":[{
            "kid":KID,
            "use":USE,
            "alg":ALG,
            "kty": "RSA",
            "n": "pw5We-LHJAIXSOwNbUxO8T615XgNn8e1J_1auE8Nza9MEkSWqf1ylz1MnOoPWqnnleo0mJfAza2Ro5xm7aKUGtPVwThwGj32SUtYj5mYPd3kfVgO9colc9k_dle8OpcP54CoA-5fVd24jt-jL3SXcITxB2-9-ms2gyTZnPjTaEeXCa6ElyMiHjcw4VApwzmZsu6wkCW4uPnX-_uZvxwjcGEH9RKMyWU0B5m_NOKcgy9eChZMw01rAxlxDugvvEU0iedzuAUu3vv19tn1UOvIUToyXrhUw0PSEr_nUg_zITVf-IjZ6yqKgI_2HU37AypypIe8TsjmgHwSP49X2MnUmQ",
            "e": "AQAB"
        }]
    }

# IDトークン生成エンドポイント
@app.get("/auth/generate_id_token")
def generate_it_token():
    # 署名用秘密鍵
    with open('./private-key.pem') as f_private:
        private_key = f_private.read()
    # ヘッダー部
    header = {
        "kid":KID,
        "alg":ALG
    }
    # ペイロード部
    payload = {
        "iss":ISS,
        "aud":AUD,
        "sub":SUB,
        "exp":int(time.mktime((datetime.now() + timedelta(days=1)).timetuple())),
        "iat":int(time.mktime(datetime.now().timetuple()))
    }
    id_token = jwt.encode(payload, private_key,
                         algorithm='RS256', headers=header)
    return {
        "id_token":id_token
    }

# IDトークン検証エンドポイント
@app.post("/auth/verify_id_token")
def verify_id_token(idToken: IdToken):
    # ローカルJWKSエンドポイントURL
    url = "http://localhost:8000/auth/jwks"
    jwks_client = PyJWKClient(url)
    public_key = jwks_client.get_signing_key_from_jwt(idToken.id_token)
    payload = jwt.decode(idToken.id_token, public_key.key, algorithms=["RS256"], audience=AUD)
    return payload

if __name__ == "__main__":
    uvicorn.run(app, port=8000, loop="asyncio")
```

##### Dockerfile

```
FROM python:3.8

WORKDIR /usr/src/server
ADD requirements.txt .
ADD private-key.pem .
RUN pip install -r requirements.txt

# uvicornのオプションに--reloadを付与し、
# main.pyの編集と同時に変更内容を反映させる。
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]
```



##### requirements.txt

※依存ライブラリ

```txt
fastapi
python-multipart
requests
uvicorn
cryptography
PyJWT
```

##### docker-compose.yml

```yaml
version: "3.5"
networks:
  container-link:
    name: docker.internal
services:
  app:
    container_name: "app"
    build: ./app
    volumes:
      - ./app:/usr/src/server
    ports:
      - "8000:8000"
    networks:
      - container-link
```



## 動作確認

### 起動

```
docker-compose up -d
```

### IDトークン生成エンドポイント

* リクエスト

  ```http
  GET /auth/generate_id_token HTTP/1.1
  Host: 127.0.0.1:8000
  Cache-Control: no-cache
  Postman-Token: 438dbc81-196c-9983-dce2-8e8875680b18
  ```

* レスポンス

  ```json
  {
      "id_token": "eyJ0eXAiOiJ...Y3fDPOg"
  }
  ```

### IDトークン検証エンドポイント

* リクエスト

  ```http
  POST /auth/verify_id_token HTTP/1.1
  Host: 127.0.0.1:8000
  Content-Type: application/json
  Cache-Control: no-cache
  Postman-Token: 1a357704-50f8-256e-5821-347114829cec
  
  {
      "id_token": "eyJ0eXAiOiJKV1Q...3rHY3fDPOg"
  }
  ```

* レスポンス

  ```json
  {
      "iss": "http://server.example.com",
      "aud": "s6BhdRkqt3",
      "sub": "248289761001",
      "exp": 1648722999,
      "iat": 1648636599
  }
  ```

### JWKSエンドポイント

* リクエスト

  ```http
  GET /auth/jwks HTTP/1.1
  Host: 127.0.0.1:8000
  Cache-Control: no-cache
  Postman-Token: 5902c2a7-0337-f43e-0f71-c448d22ee3c8
  ```

* レスポンス

  ```json
  {
      "keys": [
          {
              "kid": "12345",
              "use": "sig",
              "alg": "RS256",
              "kty": "RSA",
              "n": "pw5We-LHJAIXSOwNbUxO8T615XgNn8e1J_1auE8Nza9MEkSWqf1ylz1MnOoPWqnnleo0mJfAza2Ro5xm7aKUGtPVwThwGj32SUtYj5mYPd3kfVgO9colc9k_dle8OpcP54CoA-5fVd24jt-jL3SXcITxB2-9-ms2gyTZnPjTaEeXCa6ElyMiHjcw4VApwzmZsu6wkCW4uPnX-_uZvxwjcGEH9RKMyWU0B5m_NOKcgy9eChZMw01rAxlxDugvvEU0iedzuAUu3vv19tn1UOvIUToyXrhUw0PSEr_nUg_zITVf-IjZ6yqKgI_2HU37AypypIe8TsjmgHwSP49X2MnUmQ",
              "e": "AQAB"
          }
      ]
  }
  ```

  

## 参考情報

* [pem-jwk](https://www.npmjs.com/package/pem-jwk)