Verify Apple ID token signature against JWKS
Fixes #25 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e3ef03ddcd
commit
d780a3403a
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue