はじめに
Connexion1 の JWT Auth Example が、個人的に面白いと思ったのでメモします。
JWT Auth Example
https://github.com/zalando/connexion/tree/master/examples/openapi3/jwt
Connexionとは:
- Pythonのバックエンドのフレームワークです。
- OpenAPI Specification2 (OAS)の形式で記述されたAPI仕様のYAMLを読み込み、HTTPリクエストをハンドリングします。
- 各APIをPythonの各関数に紐づける役割を果たします。
JWT Auth Exampleは、ConnexionによるJWT認証のexampleです。
シンプルな中にも色々な要素が詰まっているところが面白いと思ったので、解説のメモを残しておきます。
JWT Auth Example
ダウンロード
JWT Auth Exampleを含む zalando/connexion をクローン(もしくはZIPでダウンロード)します。
$ git clone https://github.com/zalando/connexion.git
実行手順
基本的に README の通りにやっていきます。
$ cd connexion/examples/openapi3/jwt/
- ここで
requirements.txt
内にconnexion>=2.0.0rc3
という記載がある場合はconnexion>=2.0.0
に修正しておいてください。
修正しないと、想定外のバージョンのconnexion
がインストールされてハマることがあります。
$ sudo pip3 install -r requirements.txt
$ sudo pip3 install connexion[swagger-ui]
$ ./app.py
- READMEには記載されていませんが、Swagger UI を使用するため
connexion[swagger-ui]
をインストールしました。
ターミナルの最後に以下のように表示されれば起動成功です。
* Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
READMEに従い、Swagger UI (http://localhost:8080/ui/) からAPIを叩きます。
-
GET /auth/{user_id}
を実行し、取得したトークンをクリップボードにコピーします。 -
Authorize
ボタンを押し、コピーしたトークンを登録します。 -
GET /secret
を実行します。
以下は、Swagger UIで生成したcurlコマンドと、その実行結果です。
(これらを直接実行しても構いませんが、一度はSwagger UIを使ってみることをおすすめします)
$ curl -X GET "http://localhost:8080/auth/12" -H "accept: text/plain"
eyJ0e...(中略)...MpgLI
$ curl -X GET "http://localhost:8080/secret" -H "accept: text/plain" -H "Authorization: Bearer eyJ0e...(中略)...MpgLI"
You are user_id 12 and the secret is 'wbevuec'.
Decoded token claims: {'iss': 'com.zalando.connexion', 'iat': 1570964455, 'exp': 1570965055, 'sub': '12'}.
解説
JWT Auth Exampleを、ファイルごとに解説します。
openapi.yaml
openapi.yaml
には、2つのAPIが定義されています。
GET /auth/{user_id}
1つ目のAPIは GET /auth/{user_id}
です。
このAPIは user_id
を指定すると、対応するトークンを発行します。
認証をイメージしたAPIのようですが、user_id
は integer
であれば何でもよく、パスワードも必要ありません。
/auth/{user_id}:
get:
summary: Return JWT token
operationId: app.generate_token
...
GET /secret
2つ目のAPIは GET /secret
です。
このAPIは秘密の情報を返します(という設定です)。
トークンを指定する必要があります。
/secret:
get:
summary: Return secret string
operationId: app.get_secret
...
security:
- jwt: ['secret']
最後で security
に jwt
を指定しているところに注目してください。
これにより、APIの実行にトークンが必要となります。
- 余談ですが、Bearer Authenticationの説明3 によると、
jwt: ['secret']
はjwt: []
としても同じです。[]
contain a list of security scopes required for API calls. The list is empty because scopes are only used with OAuth 2 and OpenID Connect.
jwt
というsecurity schemeは以下のように定義されています。
components:
securitySchemes:
jwt:
type: http
scheme: bearer
bearerFormat: JWT
x-bearerInfoFunc: app.decode_token
ざっくりいうと、jwt
とはJWTを使うBearer認証(トークン認証)3であり、トークンのデコード(つまり認可)は app.decode_token
に実装する、と書いてあります。
なお、x-bearerInfoFunc
はOASではなくConnexionで規定されているプロパティです。4
app.py
app.py
には、3つのpublicな関数が定義されています。
generate_token()
1つ目の関数は GET /auth/{user_id}
に対応する generate_token()
です。
user_id
に対応するJWTを生成します。
def generate_token(user_id):
timestamp = _current_timestamp()
payload = {
"iss": JWT_ISSUER,
"iat": int(timestamp),
"exp": int(timestamp + JWT_LIFETIME_SECONDS),
"sub": str(user_id),
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
JWTの生成には、python-jose5 の jose.jwt
モジュールを使用しています。
実際の用途では、ここで認証(ユーザIDとパスワードを照合する等)が必要になるでしょう。
decode_token()
2つ目の関数はトークンのデコード(つまり認可)を行う decode_token()
です。
openapi.yaml
内の x-bearerInfoFunc
に指定されていましたね。
def decode_token(token):
try:
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
except JWTError as e:
six.raise_from(Unauthorized, e)
この関数は、jwt.decode()
を呼び出しているだけです。
ここでも python-jose5 の jose.jwt
モジュールを使用していますね。
トークンが正しい場合はトークンの情報(ペイロード)を返し、
トークンが正しくない場合は werkzeug.exceptions.Unauthorized
6 をraiseします。
algorithms=[JWT_ALGORITHM]
は、JWTの alg
クレームに関する脆弱性7への対処と思われます。
デコードに使用するアルゴリズムを制限していますね。
get_secret()
3つ目の関数は GET /secret
に対応する get_secret()
です。
秘密の情報を返します(という設定です)。
def get_secret(user, token_info) -> str:
return '''
You are user_id {user} and the secret is 'wbevuec'.
Decoded token claims: {token_info}.
'''.format(user=user, token_info=token_info)
openapi.yaml
で、GET /secret
の security
に jwt
が指定されていましたね。
これにより、get_secret()
が呼び出される前に decode_token()
が呼び出されます。
トークンが正しい場合のみ get_secret()
が実行されます。
Connexionは、get_secret()
の引数に user
(sub
クレーム、つまり user_id
) と token_info
(decode_token()
の戻り値、つまりJWTのペイロード) を渡します。
APIに対応する関数内でトークンが持つ情報を参照し、ユーザを判別したりできるということですね。
改めて、GET /secret
の実行結果を見てみましょう。
user
と token_info
が埋め込まれていることがわかります。
You are user_id 12 and the secret is 'wbevuec'.
Decoded token claims: {'iss': 'com.zalando.connexion', 'iat': 1570964455, 'exp': 1570965055, 'sub': '12'}.
the secret is 'wbevuec'
が何かは、よくわかりません。
秘密の情報ということでしょう。
まとめ
この記事では、ConnexionのJWT Auth Exampleを見てみました。
トークン認証の基本が、OAS、Connexion、そしてpython-joseのおかげで楽に実装できることがわかりました。
シンプルな中に色々な要素が詰まっている、読み応えのあるexampleだったと思います。
-
https://swagger.io/docs/specification/authentication/bearer-authentication/ ↩ ↩2
-
https://connexion.readthedocs.io/en/latest/security.html#bearer-authentication-jwt ↩
-
https://werkzeug.palletsprojects.com/en/0.16.x/exceptions/#werkzeug.exceptions.Unauthorized ↩
-
https://www.chosenplaintext.ca/2015/03/31/jwt-algorithm-confusion.html ↩