Self-service AWS CodePipeline platform — developers ship compliant CI/CD pipelines in minutes via dashboard, CLI, CDK, or AI prompt, while platform teams enforce policy-as-code guardrails, governance, and per-team isolation.
Pipeline Builder supports three ways to sign in, side by side:
tokenVersion invalidation; see Roles & Permissions).sso entitlement.The first two are global: one app registration per provider, shared by every organization on the deployment. The third is per-organization: each org registers its own IdP and can force its users through it.
Social login is enabled per provider by setting that provider’s client
credentials as environment variables on the platform service. A provider is
enabled if and only if its OAUTH_<P>_CLIENT_ID is set — the behavior is
fail-soft: an unconfigured provider is simply hidden, never an error. The
login page fetches the enabled set (GET /api/auth/oauth/providers) and renders
its buttons data-driven, so a “Sign in with GitLab” button appears the
moment GitLab credentials are present and disappears when they’re removed.
Credentials are global / platform-wide — one app registration per provider covers the whole deployment. There is no per-org social-login registration.
| Provider | Env vars | Register an app at |
|---|---|---|
OAUTH_GOOGLE_CLIENT_ID, OAUTH_GOOGLE_CLIENT_SECRET |
Google Cloud Console → OAuth 2.0 Client ID | |
| GitHub | OAUTH_GITHUB_CLIENT_ID, OAUTH_GITHUB_CLIENT_SECRET |
GitHub → Settings → Developer settings → OAuth Apps |
OAUTH_FACEBOOK_CLIENT_ID, OAUTH_FACEBOOK_CLIENT_SECRET |
Meta for Developers → Facebook Login | |
| Microsoft | OAUTH_MICROSOFT_CLIENT_ID, OAUTH_MICROSOFT_CLIENT_SECRET, OAUTH_MICROSOFT_TENANT |
Microsoft Entra admin center → App registrations |
| GitLab | OAUTH_GITLAB_CLIENT_ID, OAUTH_GITLAB_CLIENT_SECRET, OAUTH_GITLAB_BASE_URL |
GitLab → User Settings → Applications (or your self-hosted instance) |
OAUTH_LINKEDIN_CLIENT_ID, OAUTH_LINKEDIN_CLIENT_SECRET |
LinkedIn Developers → “Sign in with LinkedIn using OpenID Connect” |
Shared across all providers:
| Variable | Default | Description |
|---|---|---|
OAUTH_CALLBACK_BASE_URL |
${PLATFORM_FRONTEND_URL} |
Origin the provider redirects back to. Each handler appends /auth/callback/<provider> (e.g. /auth/callback/microsoft). Register this exact callback URL in the provider’s console. |
OAUTH_STATE_TTL_MS |
600000 |
CSRF state token TTL (10 min). |
OAUTH_CLEANUP_INTERVAL_MS |
60000 |
Stale-state cleanup interval. |
Provider-specific notes:
OAUTH_MICROSOFT_TENANT scopes the
authority and defaults to common (any organizational or personal account);
set it to a specific tenant id or domain to restrict to one directory. The
tenant is interpolated into the authorize/token URLs; userinfo is the
tenant-agnostic Microsoft Graph endpoint.OAUTH_GITLAB_BASE_URL defaults to https://gitlab.com;
point it at a self-hosted GitLab instance to authenticate against that. The
email must be verified (email_verified === true).email claim.Whatever OAUTH_CALLBACK_BASE_URL resolves to, the redirect URI you register in
each provider’s developer console is:
<OAUTH_CALLBACK_BASE_URL>/auth/callback/<provider>
for example https://ci.acme.com/auth/callback/google. A mismatch here is the
most common cause of a failed social login.
Each walkthrough registers one platform-wide app in the provider’s console,
sets the redirect URI to <OAUTH_CALLBACK_BASE_URL>/auth/callback/<provider>,
and copies the resulting client ID/secret into the env vars below. The scopes
listed are the ones the platform requests automatically — you generally don’t
declare them in the console (Google, Microsoft, GitLab, and LinkedIn surface a
consent screen; GitHub and Facebook request scopes at authorize time). Replace
<base> with whatever OAUTH_CALLBACK_BASE_URL resolves to.
openid, email, profile. While the screen is in Testing only listed test users can sign in — Publish it to allow anyone.<base>/auth/callback/google.OAUTH_GOOGLE_CLIENT_ID, OAUTH_GOOGLE_CLIENT_SECRET.Requested scopes: openid email profile. The account’s email_verified must be true.
OAUTH_GOOGLE_CLIENT_ID=your-client-id
OAUTH_GOOGLE_CLIENT_SECRET=your-client-secret
<base>/auth/callback/github.OAUTH_GITHUB_CLIENT_ID, OAUTH_GITHUB_CLIENT_SECRET.Requested scopes: read:user user:email (OAuth Apps don’t pre-declare scopes). The platform reads the primary verified email via /user/emails, so a user with no verified email can’t sign in.
OAUTH_GITHUB_CLIENT_ID=your-client-id
OAUTH_GITHUB_CLIENT_SECRET=your-client-secret
<base>/auth/callback/facebook.OAUTH_FACEBOOK_CLIENT_ID, App Secret → OAUTH_FACEBOOK_CLIENT_SECRET.email permission is granted by default for login, but going live for a broad audience may require Meta App Review / Advanced Access for email.Requested scopes: email,public_profile. Facebook is OAuth2 (not OIDC); if the user declines email the login fails because no account email is returned.
OAUTH_FACEBOOK_CLIENT_ID=your-app-id
OAUTH_FACEBOOK_CLIENT_SECRET=your-app-secret
OAUTH_MICROSOFT_TENANT: any org + personal → common; single directory → that tenant’s ID/domain.<base>/auth/callback/microsoft.OAUTH_MICROSOFT_CLIENT_SECRET. Overview → Application (client) ID → OAUTH_MICROSOFT_CLIENT_ID. Set OAUTH_MICROSOFT_TENANT (common unless you scoped to one directory).Requested scopes: openid email profile.
OAUTH_MICROSOFT_CLIENT_ID=your-application-client-id
OAUTH_MICROSOFT_CLIENT_SECRET=your-client-secret-value
OAUTH_MICROSOFT_TENANT=common
<your-instance>/-/profile/applications) → Add new application.<base>/auth/callback/gitlab; check scopes openid, email, profile; keep Confidential enabled.OAUTH_GITLAB_CLIENT_ID, Secret → OAUTH_GITLAB_CLIENT_SECRET. For a self-hosted instance also set OAUTH_GITLAB_BASE_URL to its origin.Requested scopes: openid email profile. The GitLab email must be verified.
OAUTH_GITLAB_CLIENT_ID=your-application-id
OAUTH_GITLAB_CLIENT_SECRET=your-secret
# Self-hosted GitLab only (defaults to https://gitlab.com):
OAUTH_GITLAB_BASE_URL=https://gitlab.example.com
<base>/auth/callback/linkedin. Copy the Client ID/secret → OAUTH_LINKEDIN_CLIENT_ID, OAUTH_LINKEDIN_CLIENT_SECRET.Requested scopes: openid email profile.
OAUTH_LINKEDIN_CLIENT_ID=your-client-id
OAUTH_LINKEDIN_CLIENT_SECRET=your-client-secret
OAUTH_<P>_CLIENT_ID / _CLIENT_SECRET (and any provider-specific var) on the platform service, plus OAUTH_CALLBACK_BASE_URL if it isn’t already your public frontend origin.curl <PLATFORM_BASE_URL>/api/auth/oauth/providers lists it, and a matching “Sign in with …” button appears on the login page.<base>/auth/callback/<provider> exactly (scheme, host, and path).Social login and social sign-up are the same flow — there is no separate
OAuth registration endpoint. The callback verifies the provider identity, then
resolves it in three cases (authService.findOrCreateOAuthUser):
isEmailVerified). Linking into an unverified account is refused with a 409 (ACCOUNT_EMAIL_UNVERIFIED) — this closes the vector where someone plants an unverified password account on a victim’s address and gets the victim signed into it on their first social login. The user verifies (or resets the password on) that account first, then links.User is created (marked email-verified, since the provider verified it) and flagged needsOnboarding, and in a single transaction the platform also creates:
[a-z0-9_-], length-capped, and made unique);First-run onboarding. Because a social signup collected no org name or plan,
a brand-new user is flagged needsOnboarding and the app’s auth guard routes
them to a one-time “Name your organization” screen (/dashboard/onboarding)
before the dashboard. There they rename the auto-created org and — when billing
is enabled — pick a plan; POST /auth/onboarding/complete renames the org
(reusing the identity-rename path), provisions the plan (fire-and-forget,
mirroring register), and clears the flag. “Skip for now” keeps the derived name
and clears the flag. Email/password registrants and SSO-provisioned users are
not flagged (they already supplied an org name or belong to an enforced org).
A few consequences worth knowing:
planId is inert.email scope; if they decline, sign-in fails with OAUTH_NO_EMAIL and no
account or org is created.sso-entitled org IdP, the social grant is rejected with
SSO_REQUIRED (see enforcement) rather than
creating a personal org — the user is routed through their org’s IdP instead.Apple, X (Twitter), Amazon, Discord, and Slack are not named social-login
buttons today. Where they are OIDC-compliant they can be wired up as a per-org
enterprise SSO provider through generic OIDC (below). A native
Sign in with Apple button is a planned future addition — it needs an ES256
signed-JWT client secret and a form_post callback, so it lands as a dedicated
effort rather than a standard OAuth handler.
An organization can register its own identity provider so its users sign in
through corporate SSO instead of a password. This is configured per
organization inside the app — never through environment variables — and is
stored as an OrgIdpConfig (one config per org).
clientId /
clientSecret. The client secret is encrypted at rest (per-org HKDF-derived
key + AES-256-GCM) and is never returned in plaintext by the config API.id_token. The login flow reads the IdP’s
OIDC discovery document (/.well-known/openid-configuration), exchanges the
authorization code, and validates the returned id_token signature against
the IdP’s published JWKS before trusting any identity claim.allowedEmailDomains pins an org to one or
more email domains. When set, users in those domains are turned away from
password login and routed through SSO — and only IdP users whose email
matches an allowed domain may sign in to that org (so an over-broad corporate
IdP can’t let evil-contractor.com in through your acme.com config).sso entitlement. SSO is a tier/bundle feature. It enforces only when the
org’s config is enabled and the org is sso-entitled (Team / Enterprise
tier, or the sso add-on bundle). Entitlements pool at the account root, so a
team reads its root’s entitlement. A disabled or unentitled config is a no-op:
password login keeps working and the SSO routes refuse — a half-configured or
downgraded org never locks its users out.The login-page endpoint POST /auth/sso/discover tells the client whether a
given email is forced through SSO — it returns only { sso: boolean } and
deliberately does not leak the internal orgId or provider (it is
unauthenticated, so returning those would make it a tenant-enumeration oracle);
the org handle needed to initiate is delivered through the authenticated
SSO_REQUIRED login rejection instead. GET /auth/sso/:orgId/authorize returns
the IdP redirect URL; POST /auth/sso/:orgId/callback exchanges the code and
validates the id_token.
Social login also honors SSO enforcement. A user in an SSO-enforced domain
cannot bypass their org’s IdP by using “Sign in with Google/GitHub/…” — the
OAuth callback runs the same enforcement check as password login and rejects
with SSO_REQUIRED.
Multi-replica: the OAuth/SSO CSRF
state+ OIDCnonceare held in the shared Redis, so theauthorizeandcallbackrequests can land on different replicas. Without Redis they fall back to per-pod memory (single-replica only).
SSO providers are the OIDC-capable set (deliberately narrower than the social-login
list — a standards-OIDC id_token flow is required):
generic-oidc — any OIDC issuer with a discovery URL. Covers Okta,
Microsoft Entra ID, Auth0, Ping, OneLogin, Keycloak, and AWS IAM Identity
Center, plus any other compliant issuer (this is also the path for
Apple / X / Amazon / Discord / Slack where they are OIDC-compliant).cognito — AWS Cognito as a named provider. The admin supplies the
region + userPoolId and the discovery URL is derived
(https://cognito-idp.<region>.amazonaws.com/<userPoolId>/.well-known/openid-configuration) —
no hand-entered URL. (A Cognito user-pool id is not an AWS account id and
is safe to store.)Each walkthrough registers one OIDC application per org in the identity
provider, whitelists the org’s callback URL, and copies the resulting values into
the OrgIdpConfig fields (provider, clientId,
clientSecret, and either discoveryUrl or Cognito’s region + userPoolId).
The engine requests scopes openid email profile and validates the returned
id_token against the IdP’s JWKS.
The redirect / callback URL to whitelist — this is per-org, so it embeds the org id:
<OAUTH_CALLBACK_BASE_URL>/auth/sso/<orgId>/callback
for example https://ci.acme.com/auth/sso/2f9c…/callback. Find <orgId> on the
IdP/SSO config page (superadmin /admin/org-idp, or the org’s own settings). A
mismatch here is the most common cause of a failed SSO login.
<OAUTH_CALLBACK_BASE_URL>/auth/sso/<orgId>/callback. Grant type: Authorization Code.https://<your-okta-domain>/.well-known/openid-configuration.provider: generic-oidc, clientId, clientSecret, discoveryUrl.<OAUTH_CALLBACK_BASE_URL>/auth/sso/<orgId>/callback.https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration.provider: generic-oidc, clientId, clientSecret, discoveryUrl.<OAUTH_CALLBACK_BASE_URL>/auth/sso/<orgId>/callback.provider: google, clientId, clientSecret — the discovery URL is well-known (https://accounts.google.com/.well-known/openid-configuration), so you don’t enter one.<OAUTH_CALLBACK_BASE_URL>/auth/sso/<orgId>/callback.https://<your-tenant>.<region>.auth0.com/.well-known/openid-configuration.provider: generic-oidc, clientId, clientSecret, discoveryUrl.<OAUTH_CALLBACK_BASE_URL>/auth/sso/<orgId>/callback.https://<keycloak-host>/realms/<realm>/.well-known/openid-configuration.provider: generic-oidc, clientId, clientSecret, discoveryUrl.<OAUTH_CALLBACK_BASE_URL>/auth/sso/<orgId>/callback; enable the Authorization code grant and openid email profile scopes.provider: cognito, clientId, clientSecret, region, userPoolId — do not set discoveryUrl; it’s derived as https://cognito-idp.<region>.amazonaws.com/<userPoolId>/.well-known/openid-configuration.After saving, set
allowedEmailDomainsto force those domains through SSO, flipenabled: true, and confirm the org issso-entitled. Secret-bearing writes require step-up re-authentication on both config surfaces.
The same OrgIdpConfig is manageable from two places, which stay in lockstep
(shared service, quota reservation, and audit actions):
| Surface | Who | Where |
|---|---|---|
| Superadmin / fleet | Platform operators (Super Admin) | /admin/org-idp — register or edit SSO for any org on their behalf (the “IdP / SSO” dashboard page). |
| Org-admin self-service | An org’s own admin | Managed under the org’s settings, gated on the org:idp capability — the customer’s admin configures their own org’s SSO without an operator (GET/PUT/PATCH/DELETE /organization/:id/idp). |
Both surfaces gate the secret-bearing writes behind step-up re-authentication,
and the self-service surface additionally requires the org to be sso-entitled
and only lets an admin touch their own org (or a team they manage). Every
create / update / delete is recorded in the audit trail
(admin.org-idp.upsert / admin.org-idp.delete).
Separate from SSO, an org can let people with a verified company email domain discover and join it — so coworkers land in one org instead of many one-person orgs. Requires the Team or Enterprise tier. Full design + threat model: docs/plans/domain-based-org-join.md.
acme.com). Public/freemail domains
(gmail.com, …) are rejected._pipeline-builder-verify.acme.com TXT "pb-verify=<token>"
The platform resolves the record (bounded timeout) and, on match, marks the domain verified. First org to verify a domain owns it.
On first sign-in, the onboarding screen shows a “Join your team” section listing discoverable orgs for their verified email domain. Auto joins in one click; Request files a request an admin reviews. Discovery is verified-email-gated and only ever shows the user’s own domain’s orgs.
auto domain — the join falls back
to a request for an admin to approve.org.domain.*, org.join.*).OAUTH_* variable.org:idp/org:kms capabilities, sessions, and tokenVersion invalidation.sso add-on bundle and feature entitlements.