package auth import ( "context" "encoding/json" "errors" "fmt" "github.com/golang-jwt/jwt/v5" "github.com/pocketbase/pocketbase/tools/auth/internal/jwk" "github.com/pocketbase/pocketbase/tools/types" "github.com/spf13/cast" "golang.org/x/oauth2" ) func init() { Providers[NameAppleNative] = func() Provider { return NewAppleProvider(NameAppleNative) } Providers[NameAppleWeb] = func() Provider { return NewAppleProvider(NameAppleWeb) } EquivalentProviders[NameAppleNative] = []string{NameAppleWeb} EquivalentProviders[NameAppleWeb] = []string{NameAppleNative} // Keep old "apple" provider for backward compatibility Providers["apple"] = func() Provider { return NewAppleProvider(NameAppleWeb) } } var _ Provider = (*Apple)(nil) // NameAppleNative is the unique name of the Apple provider. const NameAppleNative string = "apple_native" const NameAppleWeb string = "apple_web" // Apple allows authentication via Apple OAuth2. // // OIDC differences: https://bitbucket.org/openid/connect/src/master/How-Sign-in-with-Apple-differs-from-OpenID-Connect.md. type Apple struct { BaseProvider jwksURL string } // NewAppleProvider creates a new Apple provider instance with some defaults. func NewAppleProvider(providerName string) *Apple { var displayName string if providerName == NameAppleNative { displayName = "Apple (Native)" } else { displayName = "Apple (Web/Android)" } return &Apple{ BaseProvider: BaseProvider{ ctx: context.Background(), displayName: displayName, pkce: true, scopes: []string{"name", "email"}, authURL: "https://appleid.apple.com/auth/authorize", tokenURL: "https://appleid.apple.com/auth/token", }, jwksURL: "https://appleid.apple.com/auth/keys", } } // FetchAuthUser returns an AuthUser instance based on the provided token. // // API reference: https://developer.apple.com/documentation/signinwithapple/authenticating-users-with-sign-in-with-apple#Retrieve-the-users-information-from-Apple-ID-servers. func (p *Apple) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } rawUser := map[string]any{} if err := json.Unmarshal(data, &rawUser); err != nil { return nil, err } extracted := struct { EmailVerified any `json:"email_verified"` // could be string or bool Email string `json:"email"` Id string `json:"sub"` // not returned at the time of writing and it is usually // manually populated in apis.recordAuthWithOAuth2 Name string `json:"name"` }{} if err := json.Unmarshal(data, &extracted); err != nil { return nil, err } user := &AuthUser{ Id: extracted.Id, Name: extracted.Name, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, } user.Expiry, _ = types.ParseDateTime(token.Expiry) if cast.ToBool(extracted.EmailVerified) { user.Email = extracted.Email } return user, nil } // FetchRawUserInfo implements Provider.FetchRawUserInfo interface. // // Note that Apple doesn't have a UserInfo endpoint and claims about // the users are included in the id_token (without the name - see #7090). func (p *Apple) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) { idToken, _ := token.Extra("id_token").(string) claims, err := p.parseAndVerifyIdToken(idToken) if err != nil { return nil, err } return json.Marshal(claims) } func (p *Apple) parseAndVerifyIdToken(idToken string) (jwt.MapClaims, error) { if idToken == "" { return nil, errors.New("empty id_token") } // extract the token claims // --- claims := jwt.MapClaims{} _, _, err := jwt.NewParser().ParseUnverified(idToken, claims) if err != nil { return nil, err } // validate common claims per https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user#3383769 // --- jwtValidator := jwt.NewValidator( jwt.WithExpirationRequired(), jwt.WithIssuedAt(), jwt.WithLeeway(idTokenLeeway), jwt.WithIssuer("https://appleid.apple.com"), jwt.WithAudience(p.clientId), ) err = jwtValidator.Validate(claims) if err != nil { return nil, err } // validate id_token signature // // note: this step could be technically considered optional because we trust // the token which is a result of direct TLS communication with the provider // (see also https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation) // --- err = jwk.ValidateTokenSignature(p.ctx, idToken, p.jwksURL) if err != nil { return nil, fmt.Errorf("id_token validation failed: %w", err) } return claims, nil }