Edited at
Fringe81Day 15

認証におけるJWTの利用について

More than 1 year has passed since last update.

記事はFringe81 アドベントカレンダー2017の15日目です。

2日目担当のk315k1010さんからplay2-authの移行話を振られましたが、それは別の機会にとっておき、本記事ではJWT(JSON Web Token)を利用したWebアプリケーションの認証について記載したいと思います。


JWT(JSON Web Token)とは?

JWTの詳細については世に多くの記事が既にある為、ここでは簡易に説明したいと思います。

一般的にセッションIDとCookieを利用した認証のフローは下記のようなイメージになるかと思います。

cookie.png

サーバサイド側でSession IDをKVS等に保存する必要が出てきますが、JWTを利用すれば認証した情報について、クライアントサイドだけで完結することができます。

JSONの形で表した情報をURLEncodeし、tokenとしてクライアントサイドにおくり、認証が完了したクライアントは次回のリクエストからそのtokenをHTTP Header等に含めてサーバサイドに送ります。サーバサイド側ではそのtokenについて検証し、正しいものであるかを判定します。

JWT.png

以下にJWTがどのような形式か具体的に説明していきます。

JWTはJSON Web Tokenの略で、特徴は下記の2点です。(jwt.ioより参照)


  • コンパクト:tokenという形でURLのクエリパラメーターやPOSTのパラメーター、HTTP Headerに含む事ができる等、非常に小さいサイズで情報をやり取りすることができます。

  • 自己完結:必要な情報を全て、tokenという形で含めることができます。

JWTは3つの要素から成り立ちます。


  • Header

  • Payload

  • Signature

この要素を.で繋ぐことにより1つのtokenとして扱います。

xxxx[header].yyyy[Payload].zzzz[Signature]

下記にそれぞれの要素について説明します。


Header

Headerは最低限下記の2つの要素から成り立ちます。


  • alg:Signatureにて詳細に説明しますが、Signatureをhash化する際に利用するアルゴリズムを指定します。

  • typ:トークンのタイプを指定します。通常はJWTで問題ないです。

{

"alg": "HS256",
"typ": "JWT"
}

上記をBase64UrlEncodeしたものがHeaderとなります。


Payload

Payloadがサーバサイドとクライアントサイドで共有したい情報になります。Headerと同様にkeyとvalueで表します(通常このセットをclaimsと呼びます)。claimsには既に定義されているものや、独自にclaimsを設定することができます。既に定義されているものとしてはiss, exp等があります。

{

"iss": "Hoge Publisher",
"sub": "Hoge User"
}

こちらもBase64UrlEncodeされます。


Signature

下記の形式でHeaderとPlayloadをHash化したものがSignatureになります。

<ハッシュアルゴリズム>(

base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

サーバサイド側でしか知らないsecret keyでハッシュ化する為、HeaderとPayloadが例え推測できたとしても、Signatureを第三者が生成することが不可能になります。サーバサイド側ではクライアントから送られてきたtokenについてSignatureを検証することで、そのtokenが正しいものかどうかを判定することができます。

最終的に生成されるtokenは以下のようなものになります。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJIb2dlIFB1Ymxpc2hlciIsInN1YiI6IkhvZ2UgVXNlciJ9.zs2WOqs2M-Aah5417-YfoCg6MdUaCsz5qTLCdbO7aEc


ScalaでのJWTの利用

JWTについては複数のライブラリが存在します。今回はAtlassianがメンテナンスをしているAtlassian JWTを利用してtokenを作成してみます。

利用に際し、以下のようにbuild.sbtを編集します。


build.sbt

import Dependencies._

lazy val root = (project in file(".")).
settings(
inThisBuild(List(
organization := "com.example",
scalaVersion := "2.12.3",
version := "0.1.0-SNAPSHOT"
)),
name := "JWTSample",
resolvers += "atlassian" at "https://maven.atlassian.com/content/repositories/atlassian-public/",
libraryDependencies += scalaTest % Test,
libraryDependencies += "com.atlassian.jwt" % "jwt-core" % "1.6.2",
libraryDependencies += "com.atlassian.jwt" % "jwt-api" % "1.6.2"
)


sbtを起動しconsoleで動作を確認しましょう。

% sbt

[info] Loading project definition from ~/jwt-test/project
[info] Loading settings from build.sbt ...
[info] Set current project to JWTSample (in build file:~/jwt-test/)
[info] sbt server started at 127.0.0.1:4965
sbt:JWTSample> console
[info] Starting scala interpreter...
Welcome to Scala 2.12.3 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_144).
Type in expressions for evaluation. Or try :help.

scala> import com.atlassian.jwt.core.writer.JsonSmartJwtJsonBuilder
import com.atlassian.jwt.core.writer.JsonSmartJwtJsonBuilder

scala> import com.atlassian.jwt.core.writer.NimbusJwtWriterFactory
import com.atlassian.jwt.core.writer.NimbusJwtWriterFactory

scala> import com.atlassian.jwt.SigningAlgorithm
import com.atlassian.jwt.SigningAlgorithm

scala> val jwtBuilder = new JsonSmartJwtJsonBuilder().issuer("Hoge Publisher").subject("Hoge User")
jwtBuilder: com.atlassian.jwt.writer.JwtJsonBuilder = {"sub":"Hoge User","iss":"Hoge Publisher","exp":1513267070,"iat":1513266890}

scala> new NimbusJwtWriterFactory().macSigningWriter(SigningAlgorithm.HS256, "my secret strings").jsonToJwt(jwtBuilder.build())
res0: String = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJIb2dlIFVzZXIiLCJpc3MiOiJIb2dlIFB1Ymxpc2hlciIsImV4cCI6MTUxMzI2NzA3MCwiaWF0IjoxNTEzMjY2ODkwfQ.DDjEtuPd7ZqNRyKWChRvn9qHRlpTZdQC88-LAY4hVBs

生成したtokenはjwt.ioのdebuggerで簡単に確認することができます。

jwt-debug.png


利用に関しての注意点

JWTの利用については以下のような問題が挙げられています。利用するライブラリに注意して、安全にJWTを実装する必要があります。


まとめ

JWTを利用することでサーバサイドにSession ID等を保存せずに、クライアントにtokenを保存するだけで、認証を実現することができます。WebFrameworkによっては、標準で機能が実装されているので比較的簡単に利用できると思います。

実プロダクトにおいてCookie + Session IDからJWTに切り替えを試みた際には、サーバサイドの変更よりもクライアントサイドの変更(tokenをlocal storageに保存、Request Headerにtokenを埋め込む)の方が手間がかかる印象を持ちました。

とは言え、利用するMiddleWareを減らすことは、運用の観点からも望ましいことだと思うので、JWTの利用は十分に検討の価値があるものだと思います。

それではより良い認証ライフを!