oauth2-proxy/providers/apple.go

170 lines
4.9 KiB
Go

package providers
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net/url"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"golang.org/x/oauth2"
)
const (
appleProviderName = "Apple"
appleDefaultScope = "openid email name"
appleAudience = "https://appleid.apple.com"
)
var (
appleDefaultLoginURL = &url.URL{
Scheme: "https",
Host: "appleid.apple.com",
Path: "/auth/authorize",
}
appleDefaultRedeemURL = &url.URL{
Scheme: "https",
Host: "appleid.apple.com",
Path: "/auth/token",
}
)
// AppleProvider represents the Apple Sign in with Apple OIDC provider
// This provider is only configurable via AlphaConfig.
// See: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api
type AppleProvider struct {
*OIDCProvider
TeamID string
KeyID string
PrivateKey *ecdsa.PrivateKey
}
var _ Provider = (*AppleProvider)(nil)
// NewAppleProvider creates a new AppleProvider
func NewAppleProvider(p *ProviderData, appleOpts options.AppleOptions, oidcOpts options.OIDCOptions) (*AppleProvider, error) {
p.setProviderDefaults(providerDefaults{
name: appleProviderName,
loginURL: appleDefaultLoginURL,
redeemURL: appleDefaultRedeemURL,
profileURL: nil,
validateURL: nil,
scope: appleDefaultScope,
})
// Apple returns the authorization code via form POST rather than query
// parameters, so default the response mode accordingly
if p.AuthRequestResponseMode == "" {
p.AuthRequestResponseMode = "form_post"
}
oidcProvider := NewOIDCProvider(p, oidcOpts)
// Apple requires the client credentials in the POST body
oidcProvider.AuthStyle = oauth2.AuthStyleInParams
provider := &AppleProvider{
OIDCProvider: oidcProvider,
TeamID: appleOpts.TeamID,
KeyID: appleOpts.KeyID,
}
if err := provider.initialize(appleOpts); err != nil {
return nil, fmt.Errorf("could not initialize Apple provider: %v", err)
}
// Set up dynamic client secret generation
// Apple requires client_secret to be a JWT signed with ES256
p.ClientSecretFunc = provider.generateClientSecret
return provider, nil
}
// initialize validates and configures the Apple provider with the private key
func (p *AppleProvider) initialize(opts options.AppleOptions) error {
if opts.TeamID == "" {
return errors.New("apple provider requires teamID")
}
if opts.KeyID == "" {
return errors.New("apple provider requires keyID")
}
// Private key can be supplied via config or file, but not both
switch {
case opts.PrivateKey != "" && opts.PrivateKeyFile != "":
return errors.New("cannot set both privateKey and privateKeyFile options")
case opts.PrivateKey == "" && opts.PrivateKeyFile == "":
return errors.New("apple provider requires a private key for signing JWTs")
case opts.PrivateKey != "":
key, err := parseECPrivateKey([]byte(opts.PrivateKey))
if err != nil {
return fmt.Errorf("could not parse EC private key: %v", err)
}
p.PrivateKey = key
case opts.PrivateKeyFile != "":
keyData, err := os.ReadFile(opts.PrivateKeyFile)
if err != nil {
return fmt.Errorf("could not read private key file %s: %v", opts.PrivateKeyFile, err)
}
key, err := parseECPrivateKey(keyData)
if err != nil {
return fmt.Errorf("could not parse private key from file %s: %v", opts.PrivateKeyFile, err)
}
p.PrivateKey = key
}
return nil
}
// parseECPrivateKey parses a PEM-encoded EC private key (Apple .p8 format)
// See: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
func parseECPrivateKey(keyData []byte) (*ecdsa.PrivateKey, error) {
// Apple .p8 files contain a PEM-encoded PKCS#8 private key
block, _ := pem.Decode(keyData)
if block == nil {
return nil, errors.New("failed to decode PEM block")
}
// Try PKCS#8 first (Apple's format)
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err == nil {
if ecKey, ok := key.(*ecdsa.PrivateKey); ok {
return ecKey, nil
}
return nil, errors.New("key is not an EC private key")
}
// Fall back to EC private key format
ecKey, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse EC private key: %v", err)
}
return ecKey, nil
}
// generateClientSecret creates a JWT client_secret for Apple token requests
// Apple requires the client_secret to be a JWT signed with ES256
// See: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
func (p *AppleProvider) generateClientSecret() (string, error) {
now := time.Now()
claims := &jwt.RegisteredClaims{
Issuer: p.TeamID,
Subject: p.ClientID,
Audience: jwt.ClaimStrings{appleAudience},
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(5 * time.Minute)), // Short-lived for security
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
token.Header["kid"] = p.KeyID
return token.SignedString(p.PrivateKey)
}