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
176 lines
4.8 KiB
Go
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
|
|
}
|