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 ( import (
"context" "context"
"crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/base64"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"io" "io"
"math/big"
"net/http"
"os" "os"
"time" "time"
@ -81,11 +85,61 @@ func (p *AppleProvider) getUserInfo(ctx context.Context, token *oauth2.Token) (*
return nil, fmt.Errorf("missing id_token") return nil, fmt.Errorf("missing id_token")
} }
// Parse without verification since we already got the token from Apple // Fetch Apple's JWKS
parser := jwt.NewParser(jwt.WithoutClaimsValidation()) resp, err := http.Get("https://appleid.apple.com/auth/keys")
parsed, _, err := parser.ParseUnverified(idToken, jwt.MapClaims{})
if err != nil { 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) claims, ok := parsed.Claims.(jwt.MapClaims)
@ -99,7 +153,7 @@ func (p *AppleProvider) getUserInfo(ctx context.Context, token *oauth2.Token) (*
return &OAuthUserInfo{ return &OAuthUserInfo{
ProviderUserID: sub, ProviderUserID: sub,
Email: email, Email: email,
Name: email, // Apple may not provide name in id_token Name: email,
}, nil }, nil
} }