Verify Apple ID token signature against JWKS

Fixes #25

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthew Knight 2026-02-17 16:06:41 -08:00
parent e3ef03ddcd
commit d780a3403a
No known key found for this signature in database
1 changed files with 59 additions and 5 deletions

View File

@ -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
}