タイトルと元ネタはこちらの記事からパクらせていただきました。
自分の場合、Goで署名を検証する必要がありました。この時点でOIDCについてはほとんど知識はありませんが、とりあえず枯れたモジュールを使ったほうがいい、くらいには思っていました。
GoでJWTを取り扱うモジュールはいくつかありますが、このZennの記事で非常に簡単に実現できると紹介されている、lestrrat-go/jwx
を試してみることにしました。
microsoftのopenid-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-configurationのjwks_uri
)で提供されてるKey Setには、alg
パラメータが存在しないのが原因とのこと。
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.WithKeySet
にjws.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) }