JWTとは

JWT

アプリケーション

2020-09-11 01:40:01

概要

JWTについて調べたことをまとめておく。
OAuthやOpen ID Connectなど実際の利用事例については触れない。

JWTとは

JWT(JSON Web Token)は、JSONデータ構造を用いたURLセーフなClaim(JWTを用いたJSONオブジェクトはClaim Setと呼ばれる)を表現するフォーマット。
JWTでは、デジタル署名またはメッセージ認証コード(MAC)を用いたJWS(JSON Web Signature)、あるいは暗号化を用いたJWE(JSON Web Encryption)が利用される。

JWT、JWS、JWEのそれぞれのRFCは下記の通り。
ietf.org - rfc7519 JSON Web Token
ietf.org - rfc7516 JSON Web Encryption
ietf.orf - rfc7515 JSON Web Signature

その他の関連するRFCとしては、以下のようなものもある。

ietf.org - rfc7517 JSON Web Key

JWSやJWE,JWKの仕様で仕様される暗号化アルゴリズムと識別子に関する仕様。
ietf.org - rfc7518 JSON Web Algorithm

上記の仕様をまとめてJWxと呼ばれることがあるらしい。

JWTのデータ構造

jwt.ioでJWTのエンコードとデコードをUIで体験することができる。

JWTの例が下記。
ピリオドで区切られた3つのセクションはそれぞれ、ヘッダー.ペイロード.署名の役割を担っている。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

ヘッダー、ペイロード、署名の順にデコードしたものが下記。

{
  "alg": "HS256",
  "typ": "JWT",
  "alg": "HS256"
}
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)

ヘッダー

ヘッダーには、署名の検証を行うためのデータ(JSONをBase64エンコードした文字列)が含まれる。

ペイロード

ペイロードはClaim(JSONをBase64エンコードした文字列)を含む。
Claimには以下の3種類がある。

Registered Claim Names

www.iana.org - jwtに登録済みのClaim。
必須ではなく、推奨。
Claimの種類はietf.org - rfc7519 JSON Web Tokenを参照。

Public Claim Names

JWTを使用するユーザーが自由に定義することができるClaimだが、衝突防止のため、www.iana.org - jwtに登録するか、別途対応をする必要がある。

Private Claim Names

JWTを使用する当事者間で自由に定義することができる。Registerd ClaimやPublic Claimで予約されているもの以外に限る。

署名

トークンの改ざん検証のためのデータを含む。

JWTとセキュリティ

JWTの扱いについての注意点は以下の記事がよくまとまっているので一読しておきたい。
auth0.com - JWT の最新ベスト プラクティスに関するドラフトを読み解く

GolangでJWTを使ってみる

JWSを用いたJWTの実装をGolangでやってみる。

コードはgithub.com - bmf-san/go-snippetsにも置いてある。

GolangでJWTを扱うためにgithub.com - dgrijalva/jwt-goというパッケージを利用している。

// Refered to https://github.com/EricLau1/go-api-login
package main

import (
  "encoding/json"
  "fmt"
  "html"
  "log"
  "net/http"
  "strings"
  "time"

  "github.com/dgrijalva/jwt-go"

  "golang.org/x/crypto/bcrypt"
)

const (
  // Actualy, thease values comes from a form or something for getting user infomations.
  //These values are require validation.
  userName  = "bmf"
  userEmail = "foobar@example.com"
  userPass  = "password"
)

var secretKey = []byte("thisisexampleforauthjwt")

// ex.
// curl -X POST -H 'Content-Type:application/json' http://localhost:9999/login
func login(w http.ResponseWriter, r *http.Request) {
  if r.Method == http.MethodPost {
    // Actualy, you need to get values from a form or somthing. ex. name, email, password.
    token, err := signIn(userEmail, userPass)
    if err != nil {
      toJSON(w, err.Error(), http.StatusUnauthorized)
      return
    }

    toJSON(w, token, http.StatusOK)
    return
  }

  toJSON(w, "Method not allowed", http.StatusMethodNotAllowed)
  return
}

func toJSON(w http.ResponseWriter, data interface{}, statusCode int) {
  w.Header().Set("Content-type", "application/json; charset=UTF8")
  w.WriteHeader(statusCode)
  err := json.NewEncoder(w).Encode(data)
  if err != nil {
    log.Fatal(err)
  }
}

func signIn(userEmail string, userPass string) (string, error) {
  // Actualy, this values stored in a something storage so you need to get it from a something storage by using a something key.
  // ex. user := model.GetByEmail(email) → user.password
  // Here, hash a userPass for password verification(bcrypt.VerifyPassword).
  hashedUserPass, err := bcrypt.GenerateFromPassword([]byte(userPass), bcrypt.DefaultCost)
  if err != nil {
    return "", err
  }

  err = bcrypt.CompareHashAndPassword([]byte(hashedUserPass), []byte(userPass))
  if err != nil {
    return "", err
  }

  // If password verification is ok, creates and returns a jwt.
  jwt, err := generateJWT()
  if err != nil {
    return "", err
  }

  return jwt, nil
}

func generateJWT() (string, error) {
  token := jwt.New(jwt.SigningMethodHS256)
  claims := token.Claims.(jwt.MapClaims)
  claims["authorized"] = true
  claims["user_email"] = userEmail
  claims["exp"] = time.Now().Add(time.Minute * 30).Unix()
  return token.SignedString(secretKey)
}

func jwtExtract(r *http.Request) (map[string]interface{}, error) {
  headerAuthorization := r.Header.Get("Authorization")
  bearerToken := strings.Split(headerAuthorization, " ")
  tokenString := html.EscapeString(bearerToken[1])
  claims := jwt.MapClaims{}
  _, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
    return secretKey, nil
  })
  if err != nil {
    return nil, err
  }
  return claims, nil
}

// ex.
// curl -H http://localhost:9999/public
func public(w http.ResponseWriter, r *http.Request) {
  toJSON(w, "public page", http.StatusOK)
  return
}

// ex.
// curl -H 'Content-Type:application/json' -H "Authorization:Bearer <JWT>" http://localhost:9999/private
func private(w http.ResponseWriter, r *http.Request) {
  jwtParams, err := jwtExtract(r)
  if err != nil {
    toJSON(w, err.Error(), http.StatusUnauthorized)
    return
  }
  email, ok := jwtParams["user_email"].(string)
  if !ok {
    toJSON(w, "payload invalid", http.StatusUnauthorized)
    return
  }
  toJSON(w, email, http.StatusOK)
  return
}

func middlewareAuth(next http.HandlerFunc) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    authorizationHeader := r.Header.Get("Authorization")
    if authorizationHeader != "" {
      bearerToken := strings.Split(authorizationHeader, " ")
      if len(bearerToken) == 2 {
        token, err := jwt.Parse(bearerToken[1], func(token *jwt.Token) (interface{}, error) {
          if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unauthorized")
          }
          return secretKey, nil
        })
        if err != nil {
          w.WriteHeader(http.StatusUnauthorized)
          w.Write([]byte(err.Error()))
          return
        }
        if token.Valid {
          next.ServeHTTP(w, r)
        }
      } else {
        w.WriteHeader(http.StatusUnauthorized)
        w.Write([]byte("Unauthorized"))
      }
    }
  }
}

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/login", login)
  mux.HandleFunc("/public", public)
  mux.HandleFunc("/private", middlewareAuth(private))

  if err := http.ListenAndServe(":9999", mux); err != nil {
    fmt.Println(err)
  }
}

参考

About the author

Image

bmf san @bmf_san
A web developer in Japan.