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

Flask-JWTでトークンによる認証を実装する

More than 1 year has passed since last update.

はじめに

Flaskで作ったアプリケーションにJWTによるトークンでの認証機能を実装したので、
その時に調べたことなどをまとめました。

この記事に書いてあること

  • JWTとは何か
  • JWTによる認証のメリット、注意点
  • Flask-JWTを使って、トークンによる認証機能を実装する方法

JWTとは

JSON Web Tokenの略で、「ジョット」と読みます。
JWTは署名の出来るJSONを含んだURL Safeなトークンで、
主に次の特徴があります。

  • 発行者だけが、秘密鍵を使ってトークンが正しいことを検証できる
  • JSONの中身は誰でも見ることができる
    • トークンはBase64でエンコードされただけの文字列
  • 署名により、JSONの改ざんを検知できる

JWTによる認証のメリット、注意点

メリット

ステートレス

JWTによるトークン認証における大きなメリットは、ステートレスであることです。
このことによって、以下のような恩恵を受けることができます。

  • ユーザーが増えた場合、サーバー側で認証情報を保持しないので、メモリの心配をしなくて済む
  • サーバーが複数台でもログイン状態を保持できる
  • テストの工数が減る(状態があると、状態によってテストケースが増える)
  • サービスをまたいだ認証機構として利用できる

注意点

重要な情報をJSONに含めない

冒頭でも説明した通り、JSONの中身はBase64でエンコードされただけの文字列です。改ざんはできませんが、中身を確認することは誰でもできます

Flask-JWTとは

Flask-JWTはFlaskアプリケーションでJWTの機能を使用できるライブラリです。
ドキュメントはこちら

ここからは、Flask-JWTを使って
トークンを使った認証機能を実装する手順をご紹介します。

最小構成

root
  └ app.py

インストール

まずはFlaskとFlask-JWTをインストールします。

pip install Flask Flask-JWT

アプリケーション

次はアプリケーションの中身ですが、こちらはFlask-JWTのドキュメントから。

app.py
from flask import Flask
from flask_jwt import JWT, jwt_required, current_identity
from werkzeug.security import safe_str_cmp

class User(object):
    def __init__(self, id, username, password):
        self.id = id
        self.username = username
        self.password = password

    def __str__(self):
        return "User(id='%s')" % self.id

users = [
    User(1, 'user1', 'abcxyz'),
    User(2, 'user2', 'abcxyz'),
]

username_table = {u.username: u for u in users}
userid_table = {u.id: u for u in users}

def authenticate(username, password):
    user = username_table.get(username, None)
    if user and safe_str_cmp(user.password.encode('utf-8'), password.encode('utf-8')):
        return user

def identity(payload):
    user_id = payload['identity']
    return userid_table.get(user_id, None)

app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'super-secret'

jwt = JWT(app, authenticate, identity)

@app.route('/protected')
@jwt_required()
def protected():
    return '%s' % current_identity

if __name__ == '__main__':
    app.run()

JWTの初期化

コードのこの部分で初期化しています。

jwt = JWT(app, authenticate, identity)

第1引数 (app)

アプリケーションを渡してあげます。

第2引数 (authentication_handler)

実際に認証を行う関数を渡します。
authentication_handlerは username, password の2つの引数を受け取り、
認証されたIdentityオブジェクトを返却する必要があります。

第3引数 (identity_handler)

current_identityで参照するIdentityを返却するようにします。

APIを叩いて認証してみる

サーバ起動

export FLASK_APP=app.py; flask run

リクエスト

curl http://localhost:5000/auth -X POST -H "Content-Type: application/json" -d '{ "username": "user1", "password": "abcxyz" }'

認証に成功すると、アクセストークンが返ってきます。

レスポンス
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NTE2MDM0NzUsImlhdCI6MTU1MTYwMzE3NSwibmJmIjoxNTUxNjAzMTc1LCJpZGVudGl0eSI6MX0.Cs351X-LvACQBQYnQVIYUh2hFvYDo3YEMI1Ll_Vyo2g"
}

エンドポイントに対して認証を要求する

@jwt_required() というデコレータを使用して、
特定のエンドポイントに対して認証を要求することができます。

AuthorizationヘッダJWT <アクセストークン>の形でトークンを付与することで、
認証を行うことができます。
これはデフォルト設定ですが、プリフィックスはカスタマイズできます。

app.py の /protected を叩いてみます。

curl http://localhost:5000/protected -H "Authorization: JWT <アクセストークン>"
レスポンス
User(id='1')

トークンの有効期限が切れているなど、認証に失敗した場合は以下のようなレスポンスが返ってきます。
ログイン後のユーザーのみ使用できるAPIなどは、
このようにエンドポイントにデコレータを付けるだけで実装できます。

レスポンス
{
  "description": "Signature has expired", 
  "error": "Invalid token", 
  "status_code": 401
}

認証済みユーザーの情報を取得する

/protected では、 レスポンスの内容としてcurrent_identityを返しています。
ここで返されるcurrent_identityの内容は def identity() の返り値です。

@jwt_required() が付与された関数内では、
current_identityを使用してトークンが渡されたユーザーにアクセスすることができます。
トークンに加えて権限チェックもしたい場合などは、
current_identityにroleも持たせておくなどすると良さそうです。

カスタマイズ

さきほど触れたAuthorizationヘッダのプリフィックスや、
authentication_handlerのキー(username, password)などはカスタマイズ可能です。
カスタマイズする場合は、config等で設定してあげるとよいと思います。

カスタマイズ可能な項目は、
ドキュメントConfiguration Optionsに一覧で掲載されています。

おわりに

これまでフロントがメインで、バックエンドはまだ始めたばかりなので、
こうするともっといいよ、などのアドバイス等ありましたら
ご助言頂けますと幸いです。

よろしくお願いいたします。

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
ユーザーは見つかりませんでした