diff --git a/pkg/identity/oauth/apple/apple.go b/pkg/identity/oauth/apple/apple.go index da1373d63..051c9d224 100644 --- a/pkg/identity/oauth/apple/apple.go +++ b/pkg/identity/oauth/apple/apple.go @@ -12,10 +12,12 @@ import ( "net/url" "strings" + go_oidc "github.com/coreos/go-oidc/v3/oidc" "github.com/go-jose/go-jose/v3/jwt" "golang.org/x/oauth2" "github.com/pomerium/pomerium/internal/httputil" + "github.com/pomerium/pomerium/internal/jwtutil" "github.com/pomerium/pomerium/internal/urlutil" "github.com/pomerium/pomerium/internal/version" "github.com/pomerium/pomerium/pkg/identity/identity" @@ -28,9 +30,10 @@ const Name = "apple" const ( defaultProviderURL = "https://appleid.apple.com" - tokenURL = "/auth/token" //nolint: gosec - authURL = "/auth/authorize" - revocationURL = "/auth/revoke" + tokenURLPath = "/auth/token" //nolint: gosec + authURLPath = "/auth/authorize" + revocationURLPath = "/auth/revoke" + keysURLPath = "/auth/keys" ) var ( @@ -44,6 +47,7 @@ var ( type Provider struct { oauth *oauth2.Config authCodeOptions map[string]string + issuerURL string } // New instantiates an OpenID Connect (OIDC) provider for Apple. @@ -61,6 +65,7 @@ func New(_ context.Context, o *oauth.Options) (*Provider, error) { maps.Copy(p.authCodeOptions, defaultAuthCodeOptions) maps.Copy(p.authCodeOptions, options.AuthCodeOptions) + p.issuerURL = options.ProviderURL // Apple expects the AuthStyle to use Params instead of Headers // So we have to do our own oauth2 config p.oauth = &oauth2.Config{ @@ -69,8 +74,8 @@ func New(_ context.Context, o *oauth.Options) (*Provider, error) { Scopes: options.Scopes, RedirectURL: options.RedirectURL.String(), Endpoint: oauth2.Endpoint{ - AuthURL: urlutil.Join(options.ProviderURL, authURL), - TokenURL: urlutil.Join(options.ProviderURL, tokenURL), + AuthURL: urlutil.Join(p.issuerURL, authURLPath), + TokenURL: urlutil.Join(p.issuerURL, tokenURLPath), AuthStyle: oauth2.AuthStyleInParams, }, } @@ -134,7 +139,7 @@ func (p *Provider) Revoke(ctx context.Context, t *oauth2.Token) error { params.Add("client_id", p.oauth.ClientID) params.Add("client_secret", p.oauth.ClientSecret) - err := httputil.Do(ctx, http.MethodPost, revocationURL, version.UserAgent(), nil, params, nil) + err := httputil.Do(ctx, http.MethodPost, revocationURLPath, version.UserAgent(), nil, params, nil) if err != nil && errors.Is(err, httputil.ErrTokenRevoked) { return fmt.Errorf("identity/apple: unexpected revoke error: %w", err) } @@ -185,10 +190,27 @@ func (p *Provider) SignOut(_ http.ResponseWriter, _ *http.Request, _, _, _ strin // VerifyAccessToken verifies an access token. func (p *Provider) VerifyAccessToken(_ context.Context, _ string) (claims map[string]any, err error) { + // apple does not appear to have any way of verifying access tokens return nil, identity.ErrVerifyAccessTokenNotSupported } // VerifyIdentityToken verifies an identity token. -func (p *Provider) VerifyIdentityToken(_ context.Context, _ string) (claims map[string]any, err error) { - return nil, identity.ErrVerifyIdentityTokenNotSupported +func (p *Provider) VerifyIdentityToken(ctx context.Context, rawIdentityToken string) (claims map[string]any, err error) { + keySet := go_oidc.NewRemoteKeySet(ctx, urlutil.Join(p.issuerURL, keysURLPath)) + verifier := go_oidc.NewVerifier(p.issuerURL, keySet, &go_oidc.Config{ + ClientID: p.oauth.ClientID, + }) + + identityToken, err := verifier.Verify(ctx, rawIdentityToken) + if err != nil { + return nil, fmt.Errorf("error verifying identity token: %w", err) + } + + claims = jwtutil.Claims(map[string]any{}) + err = identityToken.Claims(&claims) + if err != nil { + return nil, fmt.Errorf("error unmarshaling identity token claims: %w", err) + } + + return claims, nil } diff --git a/pkg/identity/oauth/apple/apple_test.go b/pkg/identity/oauth/apple/apple_test.go new file mode 100644 index 000000000..1dfe9cf14 --- /dev/null +++ b/pkg/identity/oauth/apple/apple_test.go @@ -0,0 +1,72 @@ +package apple_test + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v3/jwt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pomerium/pomerium/internal/testutil" + "github.com/pomerium/pomerium/internal/urlutil" + "github.com/pomerium/pomerium/pkg/identity/oauth" + "github.com/pomerium/pomerium/pkg/identity/oauth/apple" +) + +func TestVerifyIdentityToken(t *testing.T) { + t.Parallel() + + ctx := testutil.GetContext(t, time.Minute) + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + jwtSigner, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: privateKey}, nil) + require.NoError(t, err) + iat := time.Now().Unix() + exp := iat + 3600 + + m := http.NewServeMux() + m.HandleFunc("GET /auth/keys", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + {Key: privateKey.Public(), Use: "sig", Algorithm: "RS256"}, + }, + }) + }) + srv := httptest.NewServer(m) + + rawIdentityToken1, err := jwt.Signed(jwtSigner).Claims(map[string]any{ + "iss": srv.URL, + "aud": "CLIENT_ID", + "sub": "subject", + "exp": exp, + "iat": iat, + }).CompactSerialize() + require.NoError(t, err) + + p, err := apple.New(ctx, &oauth.Options{ + ProviderURL: srv.URL, + ClientID: "CLIENT_ID", + ClientSecret: "CLIENT_SECRET", + RedirectURL: urlutil.MustParseAndValidateURL("https://www.example.com"), + }) + require.NoError(t, err) + + claims, err := p.VerifyIdentityToken(ctx, rawIdentityToken1) + require.NoError(t, err) + delete(claims, "iat") + delete(claims, "exp") + assert.Equal(t, map[string]any{ + "aud": "CLIENT_ID", + "iss": srv.URL, + "sub": "subject", + }, claims) +}