Help us understand the problem. What is going on with this article?

ConnexionのJWT Auth Exampleを読み解く

はじめに

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を叩きます。

  1. GET /auth/{user_id} を実行し、取得したトークンをクリップボードにコピーします。
  2. Authorize ボタンを押し、コピーしたトークンを登録します。
  3. 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

https://github.com/zalando/connexion/blob/master/examples/openapi3/jwt/openapi.yaml

openapi.yaml には、2つのAPIが定義されています。

GET /auth/{user_id}

1つ目のAPIは GET /auth/{user_id} です。
このAPIは user_id を指定すると、対応するトークンを発行します。
認証をイメージしたAPIのようですが、user_idinteger であれば何でもよく、パスワードも必要ありません。

openapi.yaml
  /auth/{user_id}:
    get:
      summary: Return JWT token
      operationId: app.generate_token
      ...

GET /secret

2つ目のAPIは GET /secret です。
このAPIは秘密の情報を返します(という設定です)。
トークンを指定する必要があります。

openapi.yaml
  /secret:
    get:
      summary: Return secret string
      operationId: app.get_secret
      ...
      security:
      - jwt: ['secret']

最後で securityjwt を指定しているところに注目してください。
これにより、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は以下のように定義されています。

openapi.yaml
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

https://github.com/zalando/connexion/blob/master/examples/openapi3/jwt/app.py

app.py には、3つのpublicな関数が定義されています。

generate_token()

1つ目の関数は GET /auth/{user_id} に対応する generate_token() です。
user_id に対応するJWTを生成します。

app,py
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-jose5jose.jwt モジュールを使用しています。

実際の用途では、ここで認証(ユーザIDとパスワードを照合する等)が必要になるでしょう。

decode_token()

2つ目の関数はトークンのデコード(つまり認可)を行う decode_token() です。
openapi.yaml 内の x-bearerInfoFunc に指定されていましたね。

app.py
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-jose5jose.jwt モジュールを使用していますね。
トークンが正しい場合はトークンの情報(ペイロード)を返し、
トークンが正しくない場合は werkzeug.exceptions.Unauthorized 6 をraiseします。

algorithms=[JWT_ALGORITHM] は、JWTの alg クレームに関する脆弱性7への対処と思われます。
デコードに使用するアルゴリズムを制限していますね。

get_secret()

3つ目の関数は GET /secret に対応する get_secret() です。
秘密の情報を返します(という設定です)。

app.py
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 /secretsecurityjwt が指定されていましたね。
これにより、get_secret() が呼び出される前に decode_token() が呼び出されます。
トークンが正しい場合のみ get_secret() が実行されます。

Connexionは、get_secret() の引数に user (sub クレーム、つまり user_id) と token_info (decode_token() の戻り値、つまりJWTのペイロード) を渡します。
APIに対応する関数内でトークンが持つ情報を参照し、ユーザを判別したりできるということですね。

改めて、GET /secret の実行結果を見てみましょう。
usertoken_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だったと思います。

omineyu
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした