omuronの備忘録

個人的な備忘録

Auth0 の JWT 検証デバッグ

JWT 検証の大まかな動きは 昨日 の調べたのですが、もうちょっとちゃんと理解したい。
オーソライザーのサンプルデバッグして、もうちょっと理解する。 テスト用の Auth0 アカウントを omuron で取得して試してみる。

環境構築

Fork したサンプルソースを取得。

$ git clone https://github.com/omuron/auth0-golang-api-samples.git

環境変数設定。

始めから準備されている APIs を利用して設定。

f:id:omron:20200619193129p:plain

この設定をクローンしたソースで設定する。

$ cd 01-Authorization-RS256/
$ cp .env.example .env
$ vi .env

先の情報から、 .env の内容は以下の通りとなる。

AUTH0_DOMAIN=omuron.us.auth0.com
AUTH0_AUDIENCE=https://omuron.us.auth0.com/api/v2/

サンプル起動

docker で動かすシェルが準備されいるので起動する。

$ chmod +x exec.sh
$ ./exec.sh 

サンプル呼び出し

curl コマンドで試す。

public API

パブリックエンドポイントはそのまま呼べる。

$ curl -X GET http://localhost:3010/api/public
{"message":"Hello from a public endpoint! You don't need to be authenticated to see this."

private API

プライベートエンドポイントは、そのまま呼ぶと認証エラーになる

$ curl -X GET http://localhost:3010/api/private
Required authorization token not found

JWT を取得するために Auth0 Management APIAPI Explorer からアプリを作成して Token を簡単に取れるようにする。

f:id:omron:20200619193947p:plain

この TokenAuthorization: Bearer ヘッダーに付与して呼び出すと、プライベート API が呼び出せる。

$ curl -X GET http://localhost:3010/api/private -H "Authorization: Bearer ey..."
{"message":"Hello from a private endpoint! You need to be authenticated to see this."}

試しに適当に署名を改ざん(後ろ2byte削る)して渡すと認証エラーになる。

$ curl -X GET http://localhost:3010/api/private -H "Authorization: Bearer ey..."
crypto/rsa: verification error

認証処理

認証部分のソース。
jwtmiddleware というライブラリで、署名の検証を行っている。

 jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{
        ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
            // Verify 'aud' claim
            aud := os.Getenv("AUTH0_AUDIENCE")
            checkAud := token.Claims.(jwt.MapClaims).VerifyAudience(aud, false)
            if !checkAud {
                return token, errors.New("Invalid audience.")
            }
            // Verify 'iss' claim
            iss := "https://" + os.Getenv("AUTH0_DOMAIN") + "/"
            checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false)
            if !checkIss {
                return token, errors.New("Invalid issuer.")
            }

            cert, err := getPemCert(token)
            if err != nil {
                panic(err.Error())
            }

            result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
            return result, nil
        },
        SigningMethod: jwt.SigningMethodRS256,
    })

クレームの検証

環境変数で指定した値と同一かチェックを行っている。

         // Verify 'aud' claim
            aud := os.Getenv("AUTH0_AUDIENCE")
            checkAud := token.Claims.(jwt.MapClaims).VerifyAudience(aud, false)
            if !checkAud {
                return token, errors.New("Invalid audience.")
            }
            // Verify 'iss' claim
            iss := "https://" + os.Getenv("AUTH0_DOMAIN") + "/"
            checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false)
            if !checkIss {
                return token, errors.New("Invalid issuer.")
            }

公開鍵取得

この getPemCert 内で、公開鍵を取得している。

         cert, err := getPemCert(token)
            if err != nil {
                panic(err.Error())
            }

getPemCert 内で、 jwks.json にアクセスして、公開鍵を生成する元ネタを取得。

 resp, err := http.Get("https://" + os.Getenv("AUTH0_DOMAIN") + "/.well-known/jwks.json")

cert に公開鍵がちゃんと生成されていることを確認できる。

-----BEGIN CERTIFICATE-----
MIIDATCCAemgAwIBAgIJbOX8RffC+u+LMA0GCSqGSIb3DQEBCwUAMB4xHDAaBgNVBAMTE29tdXJvbi51cy5hdXRoMC5jb20wHhcNMjAwNjE5MDgxMjAwWhcNMzQwMjI2MDgxMjAwWjAeMRwwGgYDVQQDExNvbXVyb24udXMuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5HgBZMeT0iwu24jS9KsDQX85GLwU9kdbLYnuq/IT8LFENrUH7WxUhRaIaS3FvA4T/OCVNdVcJ+nPH++Gq2AlW4rkv+BBp963IICj9dwYbwZFFKeSlUMp2/QYdg2HeBZLJpqM8z6MhC8tejvWfAZ7Pblu3hICP5wyByDzjF9BiDDfrxdugfUGP0RtRe9EK1+51ZhByO8Hss4mIgoyrDOaw6R0vxJYxJCivOpvhhaKuSvEr+rA4tvOEkd3yeC5JqMyzLb/wouF9Md+0foxpzOrgoYnlIR02zcgnzlMHyRbV5Soaj1nokeXhMmIxU5d8BGNgSn+KX3lIexEHj5XchUUUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSSScOdNOvjZ/oiJTaKoRk21MXjVzAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAIKHXiXfdzB7IS2N2oeI2519aoFukgx5gzfeG7lecjFDGo4CVxt8ez1/oF9ZZeIopZ150zEtkWOuU4C0IvkKNqT5MyKBfW38ED23SPXuuuPdqXcmlsL7AaKpNYuFTAE7escc1yJ2ddVGWRh+F+jeyAii+cP+5LxDyz39p3wBTeB9Am0RiXnNG+nXkPtT/y/MJ7H7jqjoK5zUHcveLRvxfp2K2RDpZtzDV2u0L+SzsPPgi27VCaCKchZpih2I9SDvhXVMKXLWm5mir8triXBjfYIYcSOz/UqzgpxJp6H9wzyaIfrxBMnMWsMF82movhwINV9bSXl475FpLVvEcUvMwp0=
-----END CERTIFICATE-----

RSA 暗号化処理

公開鍵を使って、暗号化(実際は復号化?)している

         result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
            return result, nil

先程の公開鍵から、暗号化に必要な ModulusExponent の値を取得。

28841510422262814191184117115823758213752086907513605263219924877674141560262837182643343501219794671606638745267994212071107711888444383578941981655498917109003582367242857637556293418425232866320959921520192272011454483771082546734210512600409639950379314126836230221764126757761049713194259208036603196548828825386256538323924518953230503729593844469047641753946128749417423643395982894203570868421081354315530158085347299958369526501387655729392972982576739942821990361267465203802890701650287459043817568473076772711628632764276314805392871972678852782737089350470516910831041844758005478192083860668206781109331 65537

この値を利用して、「電子署名」を復号化する。
復号化した値が、「ヘッダー + . + ペイロード」と同じか確認して、改ざんされてないかチェックを行う。
暗号化については このサイト が参考になった。

github.com/auth0/go-jwt-middleware を利用すれば、あまり深くわかってなくても処理ができる。

以上

最終的に署名を検証しているミドルウェアの中まではデバッグしてないけど、昨日よりは理解できた。