LoginSignup
23
12

More than 3 years have passed since last update.

GoでJWTを使って認証してみる

Posted at

メリークリスマス!開発が楽しくて充実してますー!:innocent::innocent::innocent:

Ca Tech Dojo/Challenge/JOB Advent Calender 2019の25日目は@yawn_yawn_yawn_が書かせていただきます!
@ta9starにめっちゃハードル上げられた気がしますがマイペースにいきます。

はじめに

やっと忙しさも緩和されて開発できるようになったので。楽しくなってきました。

個人開発でもゼミで始めたプロジェクトでも、個人認証が必須になりました。今のご時世必須機能ですね。
認証周りのことで、cookieにセッションIDとか乗っければいいかーと思っていたのですが、
「インターンでJWTという単語を聞いたな...そういえばまだ手が出せてなかったな...」
ということで、調べて実装!までやってみました。
ついでに改ざん検知をやって感動したので。

JWTとは

JWTについての知識は調べればそこらじゅうに記事があるので要約だけ。

JWT(JsonWebToken)とは、二者間で安全に情報を送信するための、RFC7519で定められた方式です。Jsonオブジェクトでデータを送りつけます。
発行されるトークンの構造は、header.payload.signatureという感じになります。

header

一般的に2つパラメータがあります。

パラメータ 説明
typ JWT JWTトークンであることを示す
alg HMAC,SHA256,... 署名に使うアルゴリズムを指定

payload

予約されているクレームが割とたくさんあるので、3つだけ。サーバ側で独自にクレームを設定できます。

パラメータ 値の例 説明
iss JohnDoe 発行者の識別子
exp 1300819380 有効期限
sub 1234567890 JWTに含まれるクレームのsubject

最初、クレームってなによって思ったので補足。クレームはJsonオブジェクトのメンバだそうです。

signature

署名部分で、メッセージが改ざんされてないかを検証する時に使います。
エンコードしたheader、エンコードしたpayload、シークレットキー、headerに指定されたアルゴリズムから、署名が作成されます。

要約が長い...下手か...

さらに詳しくは JWT IntroductionOpenID Foundation Japan から。

実装

dgrijalva/jwt-goを使って実装していきます。
トークンをリクエストから簡単に読みだせるパッケージdgrijalva/jwt-go/requestもありますが、今回は使ってません。コメントアウトされた場所に実装してた気がします...たしか...。

今回のコードはgithubにあります。
アーキテクチャを多少かじりましたが、そこまで厳密に考えて実装してないので、ものすごくごちゃごちゃです。
いい感じに崩すって難しい。

実装した機能は以下です。

  • signup
  • login
  • 認証が必要な機能(who am i)

signupはまあ、アカウント登録です。
loginはまあ、ログインです。ここでJWTを発行します。
whoamiは、認証してからI am {user_name}ってレスポンスしてくれる機能です。しょぼ。
JWTがリクエストに載ってこない場合、400を吐いてくれます。401ではない...ゆるして...

signup

signupについてはJWT全く関係ないので、どんな機能かだけ。
必要な情報を載せたリクエストを受けて、DBに登録する機能です。

login

loginの全体的な流れは以下です。
リクエストの読み出し -> ユーザ認証 -> JWT発行 -> レスポンスの作成
エラーハンドリングは省略します。

authJWT-go/handler/auth.go
func Login(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // bodyの読み出し
        body, err := ioutil.ReadAll(r.Body)
        if err != nil {
            ...
        }
        var req LoginRequest
        err = json.Unmarshal(body, &req)
        if err != nil {
            ...
        }

        // ユーザ認証
        var hash string
        row := db.QueryRow("SELECT password FROM users WHERE id=?", req.ID)
        if err = row.Scan(&hash); err != nil {
            ...
        }
        err = server.PasswordVerify(hash, req.Password)
        if err != nil {
            ...
        }
        log.Println("login success: userid = ", req.ID)

        // tokenの発行
        token, err := server.CreateToken(req.ID)
        if err != nil {
            ...
        }

        // response
        server.Success(w, &LoginResponse{
            Token: token,
        })
    }
}

type LoginRequest struct {
    ID       string `json:"id"`
    Password string `json:"password"`
}

type LoginResponse struct {
    Token string `json:"token"`
}

CreateToken()にてJWTの発行を行なっています。CreateToken()は以下。

authJWT-go/server/auth.go
func CreateToken(userID string) (string, error) {
    // tokenの作成
    token := jwt.New(jwt.GetSigningMethod("HS256"))

    // claimsの設定
    token.Claims = jwt.MapClaims{
        "user": userID,
        "exp":  time.Now().Add(time.Hour * 1).Unix(), // 有効期限を指定
    }

    // 署名
    var secretKey = "secret" // 任意の文字列
    tokenString, err := token.SignedString([]byte(secretKey))
    if err != nil {
        return "", err
    }
    return tokenString, nil
}

トークン作成 -> クレームの設定 -> 署名 して発行完了です!

who am i

whoamiの全体的な流れは以下です。
リクエストの読み出し -> JWTの検証 -> レスポンスの作成
エラーハンドリングは省略します...。

authJWT-go/handler/service.go
func WhoAmI(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // headerから読み出し
        tokenString := r.Header.Get("Authorization")
        tokenString = strings.TrimPrefix(tokenString, "Bearer ")

        // tokenの認証
        token, err := server.VerifyToken(tokenString)
        if err != nil {
            ...
        }

        // ペイロードの読み出し
        claims := token.Claims.(jwt.MapClaims)
        server.Success(w, &whoAmIResponse{
            Message: fmt.Sprintf("I am %s", claims["user"]),
        })
    }
}

type whoAmIResponse struct {
    Message string `json:"message"`
}

server.VerifyToken()にてトークンの検証を行なっています。VerifyToken()は以下。

authJWT-go/server/auth.go
func VerifyToken(tokenString string) (*jwt.Token, error) {
    // jwtの検証
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        return []byte("secret"), nil // CreateTokenにて指定した文字列を使います
    })
    if err != nil {
        return token, err
    }
    return token, nil
}

検証

ちょっとだけ中身拝見、ということで、以下のことを行いました。

  • 作成されたJWTのデコード
  • 期限切れのトークンを使ったらどうなるか
  • JWTの改竄検知

作成されたJWTのデコード

jwt.ioにてエンコード/デコードが可能です。
作成されたトークンの中身を拝見します。

$ curl -H 'Content-Type:application/json' -d '{"id":"yawn-yawn-yawn", "password":"password"}' http://localhost:8080/login
{
    "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzY3MTUzNjIsInVzZXIiOiJ5YXduLXlhd24teWF3biJ9.DEwXA_Kg3QRfxH6NiwbT0or0NvqFRUAi49UPT9HwLlo"
}

このトークンをデコードするとこんな感じ

スクリーンショット 2019-12-19 8.31.25.png

うんうん。
ちゃんと入ってそう。

認証できている状態で whoami するとこんな感じ。

$ curl -H 'Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzY3MTUzNjIsInVzZXIiOiJ5YXduLXlhd24teWF3biJ9.DEwXA_Kg3QRfxH6NiwbT0or0NvqFRUAi49UPT9HwLlo' http://localhost:8080/whoami
{
    "message":"I am yawn-yawn-yawn"
}

ちゃんと読み出せてますね。

余談。
JWTをAuthorizationヘッダに載せるときは、Bearerにすることが多いらしいです。

期限切れのトークンを使ったらどうなるか

CreateToken()の中のexpクレームで、そのJWTがいつまで有効かを設定しています。
期限切れのJWTを使ってリクエストした結果

{
    "code": 400,
    "message": "Token is expired"
}

ちゃんと使えない!

改竄検知

client -> proxy -> server って感じでリクエストを流して、プロキシで中身をいじります。
Burp Suiteというローカルプロキシツールを使います。

今回やってみるのは、userクレームの改ざんです。
yawn-yawn-yawnからyawn-yawnに差し替えます。

  • requestに使うJWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzY4MTQwMTgsInVzZXIiOiJ5YXduLXlhd24teWF3biJ9.mGn6fs7GPOa6b5g5I6wDaGC_qzSkSqOWmpR09Qt6CEU
  • 改ざんしたJWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzY4MTQwMTgsInVzZXIiOiJ5YXduLXlhd24ifQ.3XM58H8JMFOIUuRO7LMTGde-zNXTQHgymHVfxuJlEP4

違いがわかりづらい。末尾の方とかちょこちょこ変わっていると思います。

それぞれのデコード結果はこんな感じ。

  • 元のJWT
    スクリーンショット 2019-12-20 11.55.14.png

  • 改ざんしたJWT
    スクリーンショット 2019-12-20 11.54.58.png

userクレームが差し替えられてます。

リクエストがBurpSuiteを経由した際に、トークンをちょこちょこーっと書き換えます。

こんなかんじ。

スクリーンショット 2019-12-20 14.28.52.png

結果は...

{
    "code": 400,
    "message": "signature is invalid"
}

改ざん検知してくれましたー!

ただエンコード、デコードが割とサクッとできてしまうため、盗聴はできるし、ハイジャックもできる気が...
ここでSSL/TLSの登場ですね!

認証周りは闇が深い

おわりに

最初に書いてたネタがインフラ寄りだったのですが...(先日投稿した記事です...)
DojoでGoやったんだしGoしたい!と思ってギリギリに書き直していた所存です。
深夜のノリで、Dojoの仲間に、「クリスマスもやるしかないな」みたいなことを言われ、
「おっしゃクリスマスもやるかー!」とか返してしまったので、両方出しました。良い経験になりました...!

本当に最後に

Ca Tech Dojo/Challenge/JOB Advent Calender 2019参加者のみなさまお疲れ様でした!
読んでいただいたみなさま、本当にありがとうございます!

良いお年を!

参考

jwt.io
OpenID Foundation Japan

23
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
12