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