Django で API の Token Authenticationトークンベースの認証を JWT で行なう

http://qiita.com/seizans/items/05a909960820dd92a80a

 

API 作成に django-rest-framework を使います。
JWT でトークンベースの認証のために django-rest-framework-jwt というプラグインを使います。

django-rest-framework を使う場合の認証情報の保持には選択肢があり、JWT はその1つです。
JWT はトークンベースの認証で、トークンを永続化する必要が無いのが楽です。
ブラウザでの webアプリなら session_id を Cookie で持つことで認証情報を保持できますが、
その他のクライアント(iOSアプリなど)で使う API では認証情報をトークンなどで保持する必要があります。

略称

JW: JSON Web

  • JWT: Token
  • JWS: Signature
  • JWE: Encryption

django-rest-framework-jwt の使い方

省略。README を読めばすぐわかる。

理屈

  • ログインAPI は id/pass を受けて、これが正しければ、トークンを発行する
  • トークンは次の情報を(base64encodeされた文字列として)含む:

    • JWTヘッダ: 続く中身が JWT 形式だということを示すなど
    • クレームセット(要は中身): user_id, expire を含む
    • HMAC値: 上2つを連結したものの HMAC値 (これで上2つがクライアント側で書き換えられてないか検証している)
  • APIへのリクエストにはトークンを含める。これをサーバー側でデコードして user_id を得て認証となる (中身が改ざんされていないことも検証している)

設定すること

  • トークンの有効期間 (無期限も可能)

運用に関して

  • サーバーが HMAC 生成に使うシークレットキー (デフォルトは settings.SECRET_KEY) の運用: これが漏れると「なりすまし」攻撃される

正常系フロー

  • ログインAPI に POST で id/pass を送り、JWTトークンをレスポンスで受け取る
  • 認証が必要なAPI へのリクエストに "Authorization: JWT [トークン中身]" ヘッダを含める


  • ログインAPI への POST
$ http POST :8000/api-token-auth/ username=ichiro@example.com1 password=password -vvv
POST /api-token-auth/ HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate, compress
Content-Length: 59
Content-Type: application/json; charset=utf-8
Host: localhost:8000
User-Agent: HTTPie/0.8.0

{
    "password": "password",
    "username": "ichiro@example.com1"
}

HTTP/1.0 200 OK
Allow: POST, OPTIONS
Content-Type: application/json
Date: Thu, 05 Jun 2014 07:30:40 GMT
Server: WSGIServer/0.1 Python/2.7.5
X-Frame-Options: SAMEORIGIN

{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImljaGlyb0BleGFtcGxlLmNvbTEiLCJ1c2VyX2lkIjoxLCJlbWFpbCI6ImljaGlyb0BleGFtcGxlLmNvbTEiLCJleHAiOjE0MDE5NTM3NDB9.VITFxMzF8RUgUVwKoTcIFcZS9pFGdCkLqhWNzbBoscY"
}
  • 認証が必要なAPI への GET
$ http GET :8000/api/snippets/ Authorization:"JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImljaGlyb0BleGFtcGxlLmNvbTEiLCJ1c2VyX2lkIjoxLCJlbWFpbCI6ImljaGlyb0BleGFtcGxlLmNvbTEiLCJleHAiOjE0MDE5NTUxNzJ9.tawmWS4X0WlOaQ36_K-z9UwA5sBzGVPHOi_uGRPTgwk" -vvv                       ⏎
GET /api/snippets/ HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImljaGlyb0BleGFtcGxlLmNvbTEiLCJ1c2VyX2lkIjoxLCJlbWFpbCI6ImljaGlyb0BleGFtcGxlLmNvbTEiLCJleHAiOjE0MDE5NTUxNzJ9.tawmWS4X0WlOaQ36_K-z9UwA5sBzGVPHOi_uGRPTgwk
Host: localhost:8000
User-Agent: HTTPie/0.8.0



HTTP/1.0 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Date: Thu, 05 Jun 2014 07:55:28 GMT
Server: WSGIServer/0.1 Python/2.7.5
Vary: Accept
X-Frame-Options: SAMEORIGIN

{
    "count": 0,
    "next": null,
    "previous": null,
    "results": []
}

認証失敗パターン (401 UNAUTHORIZED)

  • トークンが期限切れ
http GET :8000/api/snippets/ Authorization:"JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImljaGlyb0BleGFtcGxlLmNvbTEiLCJ1c2VyX2lkIjoxLCJlbWFpbCI6ImljaGlyb0BleGFtcGxlLmNvbTEiLCJleHAiOjE0MDE5NTM3NDB9.VITFxMzF8RUgUVwKoTcIFcZS9pFGdCkLqhWNzbBoscY" -vvv
GET /api/snippets/ HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImljaGlyb0BleGFtcGxlLmNvbTEiLCJ1c2VyX2lkIjoxLCJlbWFpbCI6ImljaGlyb0BleGFtcGxlLmNvbTEiLCJleHAiOjE0MDE5NTM3NDB9.VITFxMzF8RUgUVwKoTcIFcZS9pFGdCkLqhWNzbBoscY
Host: localhost:8000
User-Agent: HTTPie/0.8.0



HTTP/1.0 401 UNAUTHORIZED
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Date: Thu, 05 Jun 2014 07:54:49 GMT
Server: WSGIServer/0.1 Python/2.7.5
Vary: Accept
WWW-Authenticate: JWT realm="api"
X-Frame-Options: SAMEORIGIN

{
    "detail": "Signature has expired."
}
  • トークンの形式がおかしい: { "detail": "Error decoding signature." }
  • 認証されているけど権限を持っていない: (あとで追記する)

参考

Leave a Reply

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です