From d780a3403a5dfa8f483c441dd4b59a81d5465ed5 Mon Sep 17 00:00:00 2001 From: Matthew Knight Date: Tue, 17 Feb 2026 16:06:41 -0800 Subject: [PATCH] Verify Apple ID token signature against JWKS Fixes #25 Co-Authored-By: Claude Opus 4.6 --- internal/auth/apple.go | 64 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/internal/auth/apple.go b/internal/auth/apple.go index cbf8e1d..6138ad8 100644 --- a/internal/auth/apple.go +++ b/internal/auth/apple.go @@ -2,11 +2,15 @@ package auth import ( "context" + "crypto/rsa" "crypto/x509" + "encoding/base64" "encoding/json" "encoding/pem" "fmt" "io" + "math/big" + "net/http" "os" "time" @@ -81,11 +85,61 @@ func (p *AppleProvider) getUserInfo(ctx context.Context, token *oauth2.Token) (* return nil, fmt.Errorf("missing id_token") } - // Parse without verification since we already got the token from Apple - parser := jwt.NewParser(jwt.WithoutClaimsValidation()) - parsed, _, err := parser.ParseUnverified(idToken, jwt.MapClaims{}) + // Fetch Apple's JWKS + resp, err := http.Get("https://appleid.apple.com/auth/keys") if err != nil { - return nil, fmt.Errorf("parse id_token: %w", err) + return nil, fmt.Errorf("fetch apple JWKS: %w", err) + } + defer resp.Body.Close() + + var jwks struct { + Keys []struct { + Kty string `json:"kty"` + Kid string `json:"kid"` + Use string `json:"use"` + Alg string `json:"alg"` + N string `json:"n"` + E string `json:"e"` + } `json:"keys"` + } + if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil { + return nil, fmt.Errorf("decode apple JWKS: %w", err) + } + + // Parse and verify the token + parsed, err := jwt.Parse(idToken, func(t *jwt.Token) (interface{}, error) { + kid, ok := t.Header["kid"].(string) + if !ok { + return nil, fmt.Errorf("missing kid header") + } + + for _, key := range jwks.Keys { + if key.Kid == kid { + // Decode RSA public key from JWK + nBytes, err := base64.RawURLEncoding.DecodeString(key.N) + if err != nil { + return nil, fmt.Errorf("decode key N: %w", err) + } + eBytes, err := base64.RawURLEncoding.DecodeString(key.E) + if err != nil { + return nil, fmt.Errorf("decode key E: %w", err) + } + + e := 0 + for _, b := range eBytes { + e = e*256 + int(b) + } + + return &rsa.PublicKey{ + N: new(big.Int).SetBytes(nBytes), + E: e, + }, nil + } + } + return nil, fmt.Errorf("key %s not found in JWKS", kid) + }) + if err != nil { + return nil, fmt.Errorf("verify id_token: %w", err) } claims, ok := parsed.Claims.(jwt.MapClaims) @@ -99,7 +153,7 @@ func (p *AppleProvider) getUserInfo(ctx context.Context, token *oauth2.Token) (* return &OAuthUserInfo{ ProviderUserID: sub, Email: email, - Name: email, // Apple may not provide name in id_token + Name: email, }, nil }