電波ビーチ

☆(ゝω・)v

GoでMicrosoft Entra IDのIDトークンを検証する

タイトルと元ネタはこちらの記事からパクらせていただきました。

qiita.com

自分の場合、Goで署名を検証する必要がありました。この時点でOIDCについてはほとんど知識はありませんが、とりあえず枯れたモジュールを使ったほうがいい、くらいには思っていました。

GoでJWTを取り扱うモジュールはいくつかありますが、このZennの記事で非常に簡単に実現できると紹介されている、lestrrat-go/jwxを試してみることにしました。

zenn.dev

github.com

microsoftopenid-configurationのURLに差し替えればそのまま動くかもと思ったら、エラーが出ます。

   tok, err := jwt.ParseString(id_token, jwt.WithKeySet(keySet), jwt.WithValidate(true), jwt.WithAudience(client_id), jwt.WithContext(ctx))

    if err != nil {
        log.Fatal(err) // could not verify message using any of the signatures or keys
    }

調べると、どうやらmicrosoftのJWKS endpoint(https://login.microsoftonline.com/common/v2.0/.well-known/openid-configurationjwks_uri)で提供されてるKey Setには、algパラメータが存在しないのが原因とのこと。

github.com

  tok, err := jwt.Parse(
    serialized,
    // Tell the parser that you want to use this keyset
    jwt.WithKeySet(keyset),

    // Replace the above option with the following option if you know your key
    // does not have an "alg"/ field (which is apparently the case for Azure tokens)
    // jwt.WithKeySet(keyset, jws.WithInferAlgorithmFromKey(true)),
  )

ということで、jwt.WithKeySetjws.WithInferAlgorithmFromKeyオプションを渡してあげてると、うまくいきました!

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/joho/godotenv"
    "github.com/lestrrat-go/jwx/v2/jwk"
    "github.com/lestrrat-go/jwx/v2/jws"
    "github.com/lestrrat-go/jwx/v2/jwt"
)

func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal(err)
    }
    // IDトークンの検証
    client_id := os.Getenv("CLIENT_ID")
    id_token := os.Getenv("ID_TOKEN")
    ctx := context.Background()

    // http client
    client := &http.Client{
        Timeout:   10 * time.Second,
        Transport: http.DefaultTransport,
    }

    // microsoftのopenid構成情報へのリクエスト
    req, err := http.NewRequestWithContext(ctx, "GET", "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", nil)
    if err != nil {
        log.Fatal(err)
    }
    resp1, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp1.Body.Close()
    if resp1.StatusCode != http.StatusOK {
        fmt.Printf("failed to request openid-configuration")
        log.Fatalf("%#v", resp1)
    }

    // openid-configurationのレスポンスからJWKsetのURIを取得
    var generic map[string]interface{}
    if err = json.NewDecoder(resp1.Body).Decode(&generic); err != nil {
        log.Fatal(err)
    }
    jwks_uri, ok := generic["jwks_uri"].(string)
    if !ok {
        log.Fatal("conversion failed")
    }
    // fmt.Printf("jwks_uri: %s\n", jwks_uri)

    // JWKset
    keySet, err := jwk.Fetch(ctx, jwks_uri)
    if err != nil {
        fmt.Println("failed to fetching JWK sets")
        log.Fatal(err)
    }
    // 署名確認とか検証
    tok, err := jwt.ParseString(id_token, jwt.WithKeySet(keySet, jws.WithInferAlgorithmFromKey(true)), jwt.WithValidate(true), jwt.WithAudience(client_id), jwt.WithContext(ctx))

    if err != nil {
        fmt.Println("無効なIDトークンです")
        log.Fatal(err)
    }

    fmt.Println("有効なIDトークンです")
    fmt.Printf("iss: %s\n", tok.Issuer())
    fmt.Printf("aud: %v\n", tok.Audience())
    fmt.Printf("exp: %s\n", tok.Expiration())
    fmt.Printf("sub: %s\n", tok.Subject())
    fmt.Printf("jti: %s\n", tok.JwtID())

    // ペイロード部の情報がほしい(microsoftアカウントなど)
    parts := strings.Split(id_token, ".")
    payload, err := base64.RawURLEncoding.DecodeString(parts[1])
    if err != nil {
        log.Fatal(err)
    }
    var p map[string]interface{}
    err = json.Unmarshal(payload, &p)
    if err != nil {
        log.Fatal(err)
    }

    name := p["name"].(string)
    username := p["preferred_username"]
    fmt.Printf("name: %s\n", name)
    fmt.Printf("user name: %s\n", username)
}