Files
upstream-ory-kratos/selfservice/strategy/oidc/provider_apple.go
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

176 lines
4.8 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"crypto/ecdsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"net/url"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/golang-jwt/jwt/v4"
"github.com/pkg/errors"
"golang.org/x/oauth2"
)
type ProviderApple struct {
*ProviderGenericOIDC
JWKSUrl string
}
var _ OAuth2Provider = (*ProviderApple)(nil)
func NewProviderApple(
config *Configuration,
reg Dependencies,
) Provider {
config.IssuerURL = "https://appleid.apple.com"
return &ProviderApple{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
JWKSUrl: "https://appleid.apple.com/auth/keys",
}
}
func (a *ProviderApple) newClientSecret() (string, error) {
// decode the pem format
block, _ := pem.Decode([]byte(a.config.PrivateKey))
if block == nil || block.Type != "PRIVATE KEY" {
return "", errors.New("failed to decode PEM block containing private key")
}
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return "", errors.Wrap(err, "Private key decoding failed")
}
privateKey, ok := parsedKey.(*ecdsa.PrivateKey)
if !ok {
return "", errors.New("Private key is not ecdsa key")
}
now := time.Now()
expirationTime := time.Now().Add(5 * time.Minute)
appleToken := jwt.NewWithClaims(jwt.SigningMethodES256,
jwt.RegisteredClaims{
Audience: []string{"https://appleid.apple.com"},
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(now),
Issuer: a.config.TeamId,
Subject: a.config.ClientID,
})
appleToken.Header["kid"] = a.config.PrivateKeyId
return appleToken.SignedString(privateKey)
}
func (a *ProviderApple) oauth2(ctx context.Context) (*oauth2.Config, error) {
// Apple requires a JWT token that acts as a client secret
secret, err := a.newClientSecret()
if err != nil {
return nil, err
}
a.config.ClientSecret = secret
endpoint := oauth2.Endpoint{
AuthURL: "https://appleid.apple.com/auth/authorize",
TokenURL: "https://appleid.apple.com/auth/token",
AuthStyle: oauth2.AuthStyleInParams,
}
return a.oauth2ConfigFromEndpoint(ctx, endpoint), nil
}
func (a *ProviderApple) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return a.oauth2(ctx)
}
func (a *ProviderApple) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
var options []oauth2.AuthCodeOption
if isForced(r) {
options = append(options, oauth2.SetAuthURLParam("prompt", "login"))
}
if len(a.config.RequestedClaims) != 0 {
options = append(options, oauth2.SetAuthURLParam("claims", string(a.config.RequestedClaims)))
}
// When requesting email or name, Apple requires the form_post response mode.
// This also means the return url will be called by Apple using POST method.
for _, scope := range a.config.Scope {
if scope == "email" || scope == "name" {
options = append(options, oauth2.SetAuthURLParam("response_mode", "form_post"))
break
}
}
return options
}
func (a *ProviderApple) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) {
claims, err := a.ProviderGenericOIDC.Claims(ctx, exchange, query)
if err != nil {
return claims, err
}
a.DecodeQuery(query, claims)
return claims, nil
}
// DecodeQuery decodes extra user info from Apple into the given `Claims`.
// The info is sent as an extra query parameter to the redirect URL.
// See https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3331292
// Note that there's no way to make sure the info hasn't been tampered with.
func (a *ProviderApple) DecodeQuery(query url.Values, claims *Claims) {
var user struct {
Name *struct {
FirstName *string `json:"firstName"`
LastName *string `json:"lastName"`
} `json:"name"`
}
if err := json.Unmarshal([]byte(query.Get("user")), &user); err == nil {
if name := user.Name; name != nil {
if firstName := name.FirstName; firstName != nil {
if claims.GivenName == "" {
claims.GivenName = *firstName
}
}
if lastName := name.LastName; lastName != nil {
if claims.LastName == "" {
claims.LastName = *lastName
}
if claims.FamilyName == "" {
claims.FamilyName = *lastName
}
}
}
}
}
var _ IDTokenVerifier = new(ProviderApple)
const issuerURLApple = "https://appleid.apple.com"
func (a *ProviderApple) Verify(ctx context.Context, rawIDToken string) (*Claims, error) {
keySet := oidc.NewRemoteKeySet(ctx, a.JWKSUrl)
ctx = oidc.ClientContext(ctx, a.reg.HTTPClient(ctx).HTTPClient)
return verifyToken(ctx, keySet, a.config, rawIDToken, issuerURLApple)
}
var _ NonceValidationSkipper = new(ProviderApple)
func (a *ProviderApple) CanSkipNonce(c *Claims) bool {
// Apple sets nonce_supported=true when the device requires nonce validation,
// so the nonce check may only be skipped when nonce_supported is false.
return !c.NonceSupported
}