test(e2e-playground): live-playground e2e suite for OIDC/SAML/SCIM/SSO/OAuth/MFA#729
Open
lakhansamani wants to merge 74 commits into
Open
test(e2e-playground): live-playground e2e suite for OIDC/SAML/SCIM/SSO/OAuth/MFA#729lakhansamani wants to merge 74 commits into
lakhansamani wants to merge 74 commits into
Conversation
mock-oauth (4000) simulates all 10 social-login providers path-routed per provider, plus issuer/JWKS/discovery for the OIDC-verified subset (google/apple/microsoft/twitch), matching the route convention Task 2 wired TestOAuth*BaseURL to. mock-saml-idp (4001) deviates from the plan's assumed samlify usage in two ways verified against the installed samlify build (2.13.1): - IdentityProvider() now requires singleSignOnService in its settings or construction throws ERR_IDP_METADATA_MISSING_SINGLE_SIGN_ON_SERVICE. - createLoginResponse's non-customTagReplacement path unconditionally renders an empty AttributeStatement, so loginResponseTemplate.attributes alone never appears in the response. Authorizer's real SP (crewjam/saml) reads email/given_name/family_name out of the AttributeStatement by attribute name, so /sso now parses the inbound AuthnRequest via parseLoginRequest to get the real ACS URL and SP entity ID (rather than trusting a made-up acs_url query param Authorizer never sends), and builds the response through a customTagReplacement callback that fills in the attribute tags samlify's own tvalue path drops. sms-sink (4100) is a plain in-memory store-and-retrieve server for Task 3's TestWebhookProvider. Test-only SAML cert/key generated by gen-certs.sh are checked in per the plan's Global Constraints; .gitignore's blanket certs/ rule gets narrow negations for the two e2e-playground cert paths so `git add` can actually pick them up.
…sconfig into mock Docker images
…d client Wires authorizer + mock-oauth/mock-saml-idp/sms-sink (task 5) + mailpit into one compose stack, and adds fixtures/adminClient.ts as the GraphQL admin seed client every later spec-file task consumes. Fixed against the real cmd/root.go and a live docker compose run: - --http-port (not --port), plus --jwt-type/--client-id/--client-secret, all required at startup but missing from the original draft. - --database-url moved off /data (nonexistent, non-root user can't create it) onto the already-owned /authorizer workdir. - admin auth header is x-authorizer-admin-secret, not Bearer. - GraphQL mutations need an Origin header or the CSRF middleware 403s. - playwright.config.ts testDir widened from ./tests (doesn't exist yet) to include fixtures/, with testIgnore fixed so it stops importing (and thereby executing) the mocks' own server.test.ts files. Smoke-tested end to end: build, up --wait, playwright test, down -v.
Adds tests/oidc-provider.spec.ts covering discovery doc, JWKS shape, signup+login, replayed-code rejection, and redirect_uri allowlist rejection against the real docker-compose stack. Two calibrations against real behavior vs the plan's draft: - graphql-request client needs an explicit Origin header, same as fixtures/adminClient.ts, since CSRFMiddleware (csrf.go) requires Origin/Referer on state-changing GraphQL mutations. - JWKS assertion checks array shape, not non-empty length: this stack runs --jwt-type=HS256, and jwks.go intentionally never publishes HMAC (symmetric) keys.
…test to OIDC provider spec
…y) spec Real HRD screen lives at /app (not /app/login), placeholder-only email input, "Continue" button text -- calibrated against a live stack with --enable-org-discovery=true. home-realm-discovery JIT-provisioning test is test.fixme: unconditionally blocked by validateSSOIssuerURL's https-only issuer_url check and SafeHTTPClient's private-IP rejection (mock-oauth's docker-network address is unavoidably private), neither of which has a test/dev bypass. Confirmed live: connection creation rejects http://, and a real browser-driven login attempt with an https:// issuer dead-ends at sso_upstream_error "could not reach the identity provider". Unrecognized-domain fallback test passes for real against a stack with the flag enabled.
… (in progress) Adds SafeHTTPClientAllowPrivate (internal/validators) gated behind a new --test-allow-private-sso-hosts flag, threaded through the 3 SSO-broker outbound-fetch call sites and validateSSOIssuerURL's https-only check, plus a second authorizer-sso docker-compose service (--enable-org-discovery=true) and matching Playwright project so Task 8's home-realm-discovery spec doesn't collide with Task 7's PKCE flow test. Task 8's JIT-provisioning test still fails: mock-oauth's docker-internal hostname isn't reachable from a real browser running on the host. Root cause and options recorded in .superpowers/sdd/progress.md.
Runs the Playwright test process itself as a docker-compose service (mcr.microsoft.com/playwright image) on the same network as authorizer/ authorizer-sso/mock-oauth/mock-saml-idp, instead of on the host. A real browser previously could not resolve mock-oauth's docker-internal hostname when Authorizer redirected it there mid-OAuth-flow (confirmed live: chrome-error://chromewebdata/) — every browser-driven mock-oauth redirect (SSO discovery, all 10 planned social-login specs) was broken by this. Running the browser inside the network fixes it: every service hostname now resolves identically for server-side fetches and browser navigation. `make e2e-playground` now brings up the core services, then runs the suite via `docker compose run --rm playwright`, then tears down. Getting this fully green also surfaced and required fixing: - CSRF: added the docker-internal origin (http://authorizer:8080 etc.) to --allowed-origins, since state-changing requests from inside the container present that Origin, not the host-published localhost one. - Added --url (the new trusted-canonical-base-URL flag from the security(auth) hardening merged upstream) pinned to each service's own docker-internal address, per that flag's own recommendation. - --app-cookie-secure defaults to true and --app-cookie-same-site defaults to "none" — SameSite=None cookies require Secure, and Secure cookies are silently dropped by the browser over plain HTTP except on the literal "localhost" origin (which Chromium treats as a trustworthy-origin exception). Neither authorizer/authorizer-sso's docker-internal hostname gets that exception, so every session cookie was being silently dropped. Set --app-cookie-secure=false / --app-cookie-same-site=lax for this plain-HTTP test stack.
…, not just 4 named providers
Task 8's SSO relying-party flow registers an OIDC connection against a
synthetic realm name (sso-org-<id>), not one of the 4 hardcoded
OIDC_PROVIDERS (google/apple/microsoft/twitch). The token endpoint only
signed an id_token for that fixed set, so Authorizer's SSO broker got a
token response with no id_token at all ("token response has no id_token").
Also: /authorize never captured the incoming nonce, so once id_token
issuance was fixed for arbitrary realms, verification still failed with
"id_token nonce mismatch" (Authorizer's OIDC broker requires nonce to
round-trip through the id_token per spec). Now captured per-code at
/authorize and echoed into the signed id_token at /token.
Harmless for the 6 REST-profile social providers (github/facebook/
linkedin/discord/twitter/roblox) — their code path in oauth_callback.go
never reads token.Extra("id_token"), so the extra field is ignored.
…ertion test.fixme -> test now that the browser-reachability, CSRF-origin, --url, cookie, and mock-oauth id_token/nonce fixes (Task 6b + prior commits) make the flow actually completable. The assertion itself had a trailing-slash bug (checked pathname === '/app', but Authorizer's real redirect lands on '/app/') and was too weak (only checked for absence of the word "error"); tightened to match either form and to assert the JIT-provisioned employee's email is actually visible on the landed page. Verified live end-to-end via the containerized Playwright runner: both tests in this file pass (home-realm discovery + JIT provisioning, and the unrecognized-domain fallback), and tests/oidc-provider.spec.ts's 5 tests remain green — no regression.
Covers SP-initiated login (redirect -> IdP -> POST-binding assertion -> JIT provisioning), a malformed-SAMLResponse abuse case at the ACS, and the SP metadata endpoint. mock-saml-idp now terminates TLS with its own signing cert: Authorizer's idp_sso_url validation is https-only with no test bypass (unlike OIDC's --test-allow-private-sso-hosts), so the mock has to speak https to be registerable at all. The spec ignores its self-signed cert via Playwright's ignoreHTTPSErrors, scoped to this file only.
…potent across re-runs
…ied checkpoint) Extends mock-saml-idp with a fake-SP role (samlify supports both IdP and SP from the same package) so tests can drive a real SP-initiated flow against Authorizer's actual IdP-side routes (internal/http_handlers/saml_idp.go), which expect a genuine crewjam/saml AuthnRequest, not a hand-rolled query param. Adds adminClient.ts helpers for the SAML IdP-side admin surface (signupUser, getUserIdByEmail, verifyUserEmail, addOrgMember, createSAMLServiceProvider) and the two tests (SP-initiated login with a real signed-assertion readback, unregistered-SP rejection). Recovered from an interrupted session before this work was verified against a live stack or reviewed — committing as a checkpoint before rebasing onto 2.4.0-rc.7. Live verification and review follow in subsequent commits.
…o-submit
Root.tsx's post-login resumption effect only handled the /authorize
resumption shape (state/response_type/response_mode/client_id/scope) and
silently dropped bounceSAMLIDPToLogin's redirect_uri (path
/saml/idp/{slug}/sso?saml_continue=...), so the browser landed on the
plain dashboard instead of resuming the pending SAML flow. Added a
narrow, separate check: same-origin + exact /saml/idp/{slug}/sso path +
saml_continue param, only then window.location.replace to that literal
URL. Anything else falls through unchanged - this is not a generic
redirect_uri navigator, since redirect_uri is attacker-controllable on
/app.
Fixing that exposed a second, previously-unreachable bug: the SSO
endpoint's auto-submit response (crewjam WriteResponse, a self-posting
HTML form to the SP's ACS URL) was blocked by the global CSP's
form-action 'self', since the ACS URL is always a different origin by
design. Scoped a relaxed CSP (form-action omitted, not inherited from
default-src) to just the two SAML IdP SSO routes - safe because the ACS
destination is never request-supplied, it's validated server-side
against the org's registered SP record before the response is built.
Verified via the containerized Playwright e2e suite: saml-idp.spec.ts
now 2/2 (was 1/2), no regression in oidc-provider.spec.ts,
oidc-sso-rp.spec.ts, or saml-sp.spec.ts.
Covers GET /api/v1/org-discovery directly via the request fixture: OIDC-only and SAML-only routing, SAML precedence over OIDC when both are configured, the privacy-preserving indistinguishable null response (unknown domain vs. verified-domain-with-no-connection), malformed email 400, and the domain-uniqueness rejection at verification time. Runs under the sso-discovery Playwright project (authorizer-sso, the only compose service with --enable-org-discovery=true; the flag defaults to false/opt-in on the plain authorizer service). Adds an optional baseUrl param to adminClient.createSAMLConnection, mirroring createOIDCConnection/createOrg/addVerifiedDomain, so SAML connections can be created against authorizer-sso instead of always the default :8080 client.
Root derives mfaRedirect from window.location.href on every render and checks it before token. skip/verify success set a real token via setAuthData but never touched the URL, so the still-present mfa_required=1 query params kept matching on the next render and the app got stuck on the MFA screen forever instead of falling through to the dashboard/resumption effect. Hits every login that lands on /app via a server redirect carrying these params: OAuth social login, magic-link, and email-verification click-through - found via a real Google OAuth e2e login.
Adds tests/social/helpers.ts: configureProviderProfile + runSocialLoginHappyPath, the pattern tasks 14-22 (the other 9 OAuth2 providers) will reuse - mock profile setup, real button-click-driven login, and a real session assertion (dashboard's "Signed in as <email>" render) instead of a "no error text" tautology. google.spec.ts covers the OIDC-verified-provider shape: first-time signup maps id_token claims onto the stored user (given_name/ family_name/signup_methods via adminClient's new getUserByEmail), and a consent-denied negative test hits /oauth_login + /oauth_callback directly to get a real server-issued state and assert the callback's actual error/replay behavior.
…hared social-login helper
Covers first-time signup with given_name/family_name split from GitHub's single `name` field, the empty-primary-email fallback to /user/emails, and the consent-denied negative path, reusing the shared social helpers from task 13.
Covers happy-path signup (localizedFirstName/localizedLastName -> given_name/family_name, email always via the separate emailAddress endpoint, unlike GitHub's fallback-only path) and the shared consent-denied negative path.
Apple's real flow has its hosted consent page POST a `user` form field (name only, first-login) alongside the code - not part of the id_token, and not something mock-oauth's authorize/token exchange builds. Without it, processAppleUserInfo's json.Unmarshal on an empty FormValue 400s on every Apple login, mock or real second-time. Mirror it in mock-oauth's /authorize redirect (as a query param, which Go's FormValue reads same as a POST body) so the generic runSocialLoginHappyPath flow reaches the same code path real Apple does on first signup.
… field Apple sends the `user` form field only on an account's first authorization; every later login omits it (a one-time grant, not re-sent). The handler unconditionally json.Unmarshal'd it, so an empty value failed and 400'd every returning Apple user's login. Only parse it when non-empty; a non-empty malformed value still errors. Email always comes from the verified id_token regardless of this field, so nothing else changes.
… field Add omit_user_field to mock-oauth's /apple/authorize so a test profile can suppress the `user` query param, mirroring real Apple's one-time-grant behavior. Extend apple.spec.ts with a third test: sign up, log out, log back in with `user` omitted, and confirm the session still establishes.
Every other social provider (Google, GitHub, Facebook, LinkedIn, Apple, Twitter, Microsoft, Twitch, Roblox) derives its is_<provider>_login_enabled flag in internal/service/meta.go from ClientID/ClientSecret being set - Discord's was simply never assigned, so it always returned false regardless of configuration. Any frontend gating a Discord login button on this flag (e.g. authorizer-react's AuthorizerSocialLogin, once it added Discord support in 2.2.0-rc.2) could never render one, even with Discord OAuth fully configured and working end to end on the backend. Bump web/app's authorizer-react to 2.2.0-rc.2 to pick up its Discord button. With both fixes in place, tests/social/discord.spec.ts (Task 19) goes from 1/3 to 3/3 passing.
…ials.get Task 26 review caught the describe-block comment claiming authorizer-js's "source tree" was grepped with zero matches for navigator.credentials.get. authorizer-js ships no source in node_modules (compiled lib/ only), and that compiled output does contain one call to the API - inside loginWithPasskey, for WebAuthn (publicKey mode), unrelated to WebOTP (otp mode) and to the SMS-OTP verify input this file exercises. Corrects the claim without changing any test behavior.
…erification MagicLinkLogin's isEmailVerificationEnabled check only looked at EnableEmailVerification, unlike signup.go's identical guard (which also requires IsEmailServiceEnabled). With email verification on but SMTP not fully configured (e.g. no sender email), the mutation still created a VerificationRequest row and fired an async SendEmail guaranteed to fail - the caller got a generic success message, but no email was ever deliverable and the failure was only visible at debug log level. Found live while building e2e coverage for magic-link login: the compose service needed --smtp-sender-email added because nothing had exercised a real send through this stack before, surfacing a "gomail: invalid address" failure that a caller would never see.
--admin-cookie-secure defaults to true (cmd/root.go), and was never set to false anywhere in docker-compose.yml, unlike --app-cookie-secure=false which already is on every service. Over plain HTTP, a Secure cookie is silently dropped by the browser entirely, so the admin login cookie never actually persisted, and any full-page navigation after logging into the dashboard lost the session. Found building the first e2e spec to drive the dashboard admin UI through a real browser (all prior admin interactions in this plan went through the GraphQL API directly via fixtures/adminClient.ts, never a real login form + page reload).
ListOrgDomainsRequest.pagination is PaginatedRequest, which itself
wraps a `pagination: PaginationRequest` field
(internal/graph/schema.graphqls) - fetchDomains sent the single-nested
`pagination: { limit: 100 }`, which the server rejected on every call
with GRAPHQL_VALIDATION_FAILED. The error was silently swallowed
(`res.data?._org_domains?.org_domains || []` never checked res.error),
so the verified-domains table always rendered "No verified domains
yet." even when domains existed - this feature (added in #707) has
never worked since it shipped.
Also adds the missing res.error handling fetchDomains lacked, matching
every other mutation handler in this file, and a regression test
asserting the exact query variable shape sent (the existing test
file's generic client mock never validated variable shape against a
real schema, which is why this went unnoticed by 6 passing unit
tests).
Found while building the first e2e spec to drive this UI through a
real browser and backend.
Covers the two flows OrgDomains.tsx supports: adding a domain without DNS verification (super-admin shortcut) then deleting it, and the full DNS challenge flow (request, see the TXT record to publish, verify - which fails against the test domain's real absence of DNS records, surfacing the retryable "not verified yet" hint). loginAsAdmin polls for the admin session cookie to actually land in the browser's cookie jar before navigating away: every other spec in this suite only ever does client-side routing after login, so this is the first to hit a real race between the login response resolving and Chromium's cookie-jar write completing, under headless/Docker Chrome.
IsValidWebhookEventName's allow-list was never updated when PR #705 added the SCIM/group webhook events (user.provisioned, user.deprovisioned, user.scim_updated, group.created, group.updated, group.deleted - internal/constants/webhook_event_scim.go). The SCIM service fires these events correctly, but an admin could never register a webhook for any of them - _add_webhook rejected every one as "invalid event name", leaving the whole feature with no working configuration path since it shipped. Rewritten as a map lookup rather than extending the existing 7-way != chain to 13, which was already at the edge of readability. Found while extending e2e SCIM coverage for #705's provisioning webhooks - the test could not even register a webhook to observe delivery against, let alone verify it fired.
BREAKING CHANGE: 6 List*Request input types (ListClients, ListTrustedIssuers,
ListSAMLServiceProviders, ListOrganizations, ListOrgDomains, ListOrgMembers)
declared their pagination field as PaginatedRequest, a wrapper type whose
only field is itself named `pagination: PaginationRequest` - forcing every
caller to double-nest (`pagination: { pagination: { limit, page } }`).
3 more operations (_verification_requests, _webhooks, _email_templates)
took PaginatedRequest as their entire top-level params type.
This wrapper never existed on the canonical proto/gRPC surface
(proto/authorizer/v1/pagination.proto and admin.proto - every List RPC
takes PaginationRequest directly, no wrapper), and 4 other GraphQL-only
request types (ListUsers, UserOrganizations, ListWebhookLog, ListAuditLog)
already embedded PaginationRequest directly. The double-nesting was a
GraphQL-only accident, not an intentional API design - and it was
actively broken: OrgDomains.tsx (this repo's own dashboard) sent the
single-nested shape and got GRAPHQL_VALIDATION_FAILED on every call
until a prior fix matched the wrapper, and authorizer-js's TypeScript
types for all 6 List*Request types were already (accidentally) typed as
single-nested PaginationRequest, meaning no strictly-typed SDK consumer
could construct the double-nested shape the schema required either.
Removes the PaginatedRequest type entirely; the 9 affected fields/params
now declare PaginationRequest directly, matching every other GraphQL
endpoint and the proto surface. internal/utils/pagination.go's shared
GetPagination helper now takes *model.PaginationRequest directly,
eliminating the wrap-then-call boilerplate every "already single-nested"
caller needed to reuse it. The gRPC handler layer's duplicate
modelPaginatedRequest/modelPaginationRequest converters collapse into one.
Every consumer in this repo (internal/service, internal/graphql,
internal/grpcsrv/handlers, web/dashboard) is updated to match.
Follows the schema fix (previous commit): Clients.tsx, TrustedIssuers.tsx,
Organizations.tsx, and OrganizationDetail.tsx's org-members fetch drop
their double-nested `pagination: { pagination: {...} } }` calls down to
single-level. OrgDomains.tsx reverts the double-nesting patch from an
earlier fix now that the schema itself no longer requires it. Webhooks.tsx
and EmailTemplates.tsx drop the `pagination:` wrapper key entirely, since
_webhooks/_email_templates now take PaginationRequest directly as their
whole params type instead of the removed PaginatedRequest wrapper.
…spec (#698) Covers verify_otp's 5-failed-attempt lockout (internal/service/verify_otp.go): distinct "too many failed attempts" error on the 6th attempt for both TOTP and SMS-OTP, correct code still refused while locked, and a successful verification resetting the counter (checked via a second login whose own 5 wrong attempts all get plain invalid-otp errors, not an immediate lock - proving the prior session's count didn't carry over).
Picks up the latest authorizer-js (3.3.0-rc.4, pagination + signup MFA-flag fixes) plus real WebOTP auto-fill via navigator.credentials.get(). Verified against the live playground stack: oidc-provider, oidc-sso-rp, magic-link, webauthn, totp, sms-otp, web-otp, and mfa-routing-matrix specs all pass (18/18, 1 pre-existing unrelated skip).
# Conflicts: # internal/http_handlers/oauth_callback.go # internal/http_handlers/oauth_callback_test.go # internal/http_handlers/oauth_discord_test.go # internal/http_handlers/oauth_roblox_test.go # internal/http_handlers/oauth_twitter_test.go
… fails up -d --wait and the playwright run were separate recipe lines; if the stack failed to start (image build error, healthcheck timeout), make aborted before reaching `down -v`, leaking the partial stack. Chain everything into one shell invocation so teardown always runs.
Security review of the test-only OAuth/SMS/SSRF hooks (internal/oauth, internal/sms, internal/validators/safe_http.go) confirmed all four capabilities are true no-ops when unset, but flagged two gaps in the regression coverage backing that claim: - internal/sms: no test asserted TestSMSWebhookURL unset falls through to the untouched Twilio branch (only the test-webhook happy path was covered). - internal/service: validateSSOIssuerURL (the plain-http SSRF escape hatch for e2e-playground's mock IdP) had zero test coverage at all. Both gated behaviors were already correct; this closes the coverage gap so the no-op guarantee is enforced going forward, not just true today.
…eal WebOTP test
- Root.tsx: wire hasSmsOtp on the mfa-redirect AuthorizerVerifyOtp so
authorizer-react rc.3's real navigator.credentials.get() auto-fill
actually activates for that flow (mirrors authorizer#727 on main).
- Root.tsx: the origin/main merge left two copies of the SAML IdP
resumption effect (main's separate useEffect duplicating logic HEAD's
combined effect already handled inline) - removed the redundant one,
keeping the single effect the original comment already explains is
intentional (avoids two effects racing on window.location.replace).
- web-otp.spec.ts: authorizer-react's WebOTP PR (in the rc.3 dependency
bump) added a real navigator.credentials.get({otp:...}) call, closing
the gap the skipped test's investigation had found. Rewrote it to stub
navigator.credentials.get via page.addInitScript, bridging into Node
(page.exposeFunction) to resolve with the real SMS-sink code the same
way a genuine WebOTP-capable browser would (polls for the actual
message, never a canned value) - exercises the real product code path
instead of test-authored glue. All 3 tests in the file pass against
the live stack.
Webhook registration (ValidateEndpointURL) and delivery (SafeHTTPClient) both reject private/loopback IPs unconditionally, so e2e-playground's docker-private webhook-sink is unreachable and SCIM lifecycle webhooks cannot be tested end to end. - add Config.TestAllowPrivateWebhookHosts + --test-allow-private-webhook-hosts (default false), kept independent of TestAllowPrivateSSOHosts so each escape hatch is scoped to one call site (least privilege) - thread the flag into ValidateEndpointURL and deliver()'s client selection; it relaxes ONLY the private-IP check — scheme allow-list and DNS-rebinding host pin stay enforced, true no-op when unset - reachable solely via the operator CLI flag, never a request/GraphQL/webhook field Mirrors the existing --test-allow-private-sso-hosts precedent.
- add webhook-sink mock: records POSTs keyed by user email + event name, captures raw body so a test can verify the HMAC over exact bytes - wire webhook-sink into docker-compose; set --test-allow-private-webhook-hosts=true only on the `authorizer` service - add addWebhook admin-client helper - un-skip the SCIM webhook test: register user.provisioned/scim_updated/ deprovisioned, drive a SCIM create/patch/delete, and assert each delivery's payload, event name, and X-Authorizer-Signature HMAC
… add README - fixtures/adminClient.ts's deleteSAMLConnectionByEntityID still sent the old double-nested pagination shape from before the pagination standardization fix - caused a real, reproducible GraphQL validation failure in saml-sp.spec.ts's SP-initiated login test. - Raised --rate-limit-rps/--rate-limit-burst on all 6 authorizer instances: the default (30rps/20burst) intended for production traffic occasionally tripped under a full sequential suite run's admin-API call volume, producing flaky 429s unrelated to product correctness. These are ephemeral, purpose-built test stacks - no real DoS concern. - Added README.md: how to run the full suite / a subset, how to view results (HTML report, traces) after a run. Full suite verified clean: 69/69 passed. One pre-existing, unrelated intermittent flake noted (not fixed here): social/twitter.spec.ts's "repeat login" test occasionally hits "invalid redirect uri" on its second browser context (~1 in 4 runs observed); reproduces standalone regardless of rate limits, so it's not the rate-limit issue above - tracked separately, not blocking.
Drives the live e2e-playground stack through the published authorizer-go SDK (v2.2.0-rc.4, real go get require) over the feature areas its own integration suite never covers: TOTP, SMS-OTP, MFA enforcement routing, OTP lockout, SCIM admin+webhook, SAML IdP admin, WebAuthn. - New standalone nested module e2e-playground/sdk-tests/go (out of the parent's build/CI); additive go-sdk-tests compose service + a make e2e-playground-sdk target with teardown-on-failure. - WebAuthn registration is completed as a FULL ceremony through the SDK via a pure-Go software authenticator (descope/virtualwebauthn), no browser. - Documents two confirmed SDK gaps (Login discards Set-Cookie; VerifyOTP/WebauthnLoginVerify take no headers) that block the cookie-based verify_otp path end to end — labelled raw-HTTP is used only for those calls, never as a silent workaround. - Social OAuth and SAML SP login are documented as inherently browser/redirect flows with no SDK surface, left to the Playwright suite. Live: 9 services healthy, 14 passed / 0 failed, teardown clean.
Drive the e2e-playground stack's MFA/enterprise features through the published authorizer-py SDK (0.3.0rc3), the Python analogue of sdk-tests/go — catching SDK<->server wire-shape drift the Playwright/raw-HTTP suites can't. - 28 tests, all live-verified: TOTP, SMS-OTP, OTP lockout, MFA routing, SCIM admin + webhook HMAC delivery, SAML admin (SP CRUD + IdP key rotation + metadata import), social OAuth (10 providers), and a FULL WebAuthn ceremony via a soft-webauthn software authenticator (UV-enabled). - Runs in-network via an additive python-sdk compose service (mirrors the playwright service; no existing service touched) so mock-oauth and the WebAuthn RP origin resolve. - Honest SDK/raw split documented per area: inbound SCIM REST and the social-OAuth/magic-link/SAML-SSO browser ceremonies are raw-HTTP by design, not SDK surface. - ruff + mypy-strict clean, config mirrored from authorizer-py.
…und-e2e # Conflicts: # e2e-playground/docker-compose.yml
13 e2e-playground-only root flags (10 OAuth mock-base-URL flags, 1 SMS
webhook flag, 2 SSRF private-host bypass flags) added test-only surface
to the production CLI. Collapse them into a single new constants.E2EEnv
("e2e") value on the existing --env flag instead.
Kept distinct from the pre-existing constants.TestEnv ("test"), which
the whole internal/integration_tests suite runs under, so this carries
zero blast radius into that suite (verified: full internal/... suite
green, including new isolation tests proving TestEnv triggers none of
this behavior).
Go unit tests for the 4 OAuth providers that spin up their own local
httptest.Server still need an arbitrary override URL; added a
test-only package var (config.TestOAuthMockBaseOverride) for that
instead of reintroducing per-provider flags.
docker-compose.yml's authorizer/authorizer-sso services now pass a
single --env=e2e in place of the 13 removed flags.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A comprehensive live-playground end-to-end test suite (
e2e-playground/) that drives a realauthorizerbinary + real browser (Playwright) — no unit-level mocking of Authorizer itself. Third-party services (10 social OAuth providers, SMS delivery, an external SAML IdP, webhook receivers) are stood in by small local mock servers, wired in via new test-only CLI overrides that are true no-ops when unset (see Part A below).69/69 tests pass. See
e2e-playground/README.mdfor how to run it and how to view results.Coverage
Part A — production-path test hooks (Go, ships in the real binary)
Test-only CLI overrides so the suite can redirect social OAuth / SMS / webhook delivery to local mocks:
--test-oauth-<provider>-base-url(×10),--test-sms-webhook-url,--test-allow-private-sso-hosts,--test-allow-private-webhook-hosts. Every one is a verified no-op when unset — independently traced by a dedicated code review and two separate security-engineer reviews (SSO escape hatch, then webhook escape hatch), both concluding safe to merge, no fixes required. Full findings in.superpowers/sdd/progress.md.Real bugs found and fixed along the way (separate PRs, already merged to main)
Magic-link email-service gate, Roblox/Twitter/Discord/Apple OAuth email bugs, SCIM webhook event allow-list (backend + dashboard dropdown), a GraphQL pagination schema inconsistency (fixed across the backend + all 3 first-party SDKs), a SAML IdP login-resumption bug, and a WebOTP wiring gap in
web/app— see individual PRs #713-727.Both originally-skipped tests are now real, passing tests
authorizer-react's WebOTP support (rc.3 dependency bump) wasn't being exercised — fixed a missinghasSmsOtpprop inweb/app, then rewrote the test to stubnavigator.credentials.get()and verify the real product code path end-to-end.Final verification (post-merge with latest main)
go build ./...,go vet ./...— cleanTEST_DBS=sqlite go test -p 1 ./internal/...— full suite, 0 failuresmake lint— golangci-lint 0 issues, both web apps prettier-cleanOne pre-existing, unrelated intermittent flake noted (not introduced by this branch, not fixed here):
social/twitter.spec.ts's "repeat login" test occasionally (~1 in 4 runs observed) hits "invalid redirect uri" on its second browser context; reproduces standalone regardless of load/rate-limits. Tracked separately.Test plan
go build/go vet/gofmtcleaninternal/...Go test suite (SQLite): 0 failuresmake lint: 0 issues