Authentication
Zipline supports optional authentication to secure access to the platform. When enabled, users authenticate through the frontend (via OAuth or SSO), and backend services validate requests using signed JWTs.
Overview
Authentication flows through three components:
- Frontend — identity provider with OAuth, SSO (OIDC/SAML), JWT issuance, and device authorization for the CLI
- CLI (
zipline auth) — authenticates via the device authorization flow (RFC 8628), sends JWTs to backend services - Backend services (Orchestration, Eval) — validate JWTs statelessly via JWKS
Enabling Authentication
Frontend
Set the following environment variables on the frontend service:
| Variable | Required | Description |
|---|---|---|
AUTH_ENABLED | Yes | Set to true to enable authentication |
AUTH_URL | Yes | Base URL of the frontend (e.g., https://zipline.example.com). Used for OAuth redirect URIs and as the JWT issuer / JWKS origin |
AUTH_EXTERNAL_URL | No | Public URL when the frontend is behind a reverse proxy (CloudFront, API Gateway, ingress) and AUTH_URL is an internal origin. Used for user-facing URLs — the SAML post-login redirect and the CLI device-login verification URL — so they resolve to the host users actually reach. AUTH_URL (and the JWT issuer / JWKS) stay internal. Leave unset when AUTH_URL is already the public URL |
AUTH_SECRET | Yes | Random secret (32+ characters) used for signing keys. Generate with openssl rand -base64 32 |
AUTH_ALLOWED_HOSTS | No | Comma-separated list of additional allowed hosts for incoming requests (e.g., internal service names like orchestration-ui-service). AUTH_URL and host.docker.internal:* are always allowed |
AUTH_ALLOWED_PRINCIPALS | No | Comma-separated allowlist of who may sign in. Each entry is either a domain (yourcompany.com) or an exact email (alice@partner.com). When unset/empty, anyone who can authenticate with a configured provider is allowed. See Restricting Who Can Sign In |
You must also configure at least one authentication provider (see below).
Behind a reverse proxy: If users reach Zipline through an external domain (CloudFront, API Gateway, an ingress) that forwards to a different internal origin, set
AUTH_URLto the internal origin — so the JWT issuer and JWKS endpoint stay reachable by the backend services — andAUTH_EXTERNAL_URLto the public domain. User-facing URLs (the SAML post-login redirect and thezipline auth logindevice URL) then use the public domain while tokens keep the internal issuer. If you don't need the issuer to stay internal, you can instead setAUTH_URLto the public URL and omitAUTH_EXTERNAL_URL.
Backend Services (Orchestration & Eval)
| Variable | Required | Description |
|---|---|---|
AUTH_ENABLED | Yes | Set to true to enable JWT validation |
AUTH_JWKS_URL | Yes | URL of the JWKS endpoint (e.g., https://zipline.example.com/api/auth/jwks) |
When AUTH_ENABLED=true but the JWKS endpoint is unavailable (e.g., frontend is down), protected routes return 503 Authentication service unavailable rather than allowing unauthenticated access.
Authentication Providers
Configure one or more of the following providers. Each provider is enabled by setting its respective environment variables.
Google OAuth
| Variable | Description |
|---|---|
GOOGLE_OAUTH_CLIENT_ID | OAuth 2.0 client ID from Google Cloud Console |
GOOGLE_OAUTH_CLIENT_SECRET | OAuth 2.0 client secret |
To create credentials: Google Cloud Console -> APIs & Services -> Credentials -> Create OAuth client ID (Web application). Add {AUTH_URL}/api/auth/callback/google as an authorized redirect URI.
GitHub OAuth
| Variable | Description |
|---|---|
GITHUB_OAUTH_CLIENT_ID | OAuth App client ID from GitHub |
GITHUB_OAUTH_CLIENT_SECRET | OAuth App client secret |
To create credentials: GitHub Settings -> Developer settings -> OAuth Apps -> New OAuth App. Set the authorization callback URL to {AUTH_URL}/api/auth/callback/github.
Note: A GitHub OAuth App can be authorized by any GitHub user — restricting the OAuth App to your org does not block outside logins. To limit access, set
AUTH_ALLOWED_PRINCIPALS. Keep in mind a GitHub account's primary email is often a personal address, so prefer listing the exact emails you trust (or, for org-wide control, use SSO).
Microsoft Entra ID
| Variable | Description |
|---|---|
MICROSOFT_ENTRA_OAUTH_CLIENT_ID | Application (client) ID |
MICROSOFT_ENTRA_OAUTH_CLIENT_SECRET | Client secret |
MICROSOFT_ENTRA_TENANT_ID | Directory (tenant) ID. Defaults to common (multi-tenant) |
To create credentials: Azure Portal -> App registrations -> New registration. Add {AUTH_URL}/api/auth/callback/microsoft-entra-id as a redirect URI (Web platform).
SSO (OIDC)
For enterprise single sign-on using any OIDC-compliant provider (Okta, Auth0, Keycloak, Azure AD, etc.):
| Variable | Required | Description |
|---|---|---|
SSO_PROVIDER_ID | No | Identifier for the provider (e.g., okta). Defaults to default-sso |
SSO_DOMAIN | Yes | Email domain to match users to this provider (e.g., yourcompany.com) |
SSO_ISSUER | Yes | OIDC issuer URL. Endpoints are auto-discovered via {SSO_ISSUER}/.well-known/openid-configuration |
SSO_CLIENT_ID | Yes | OIDC client ID |
SSO_CLIENT_SECRET | Yes | OIDC client secret |
Okta example:
- In Okta Admin -> Applications -> Create App Integration
- Select OIDC -> OpenID Connect -> Web Application -> Next
- Set the sign-in redirect URI to
{AUTH_URL}/api/auth/sso/callback/{SSO_PROVIDER_ID} - Copy the Client ID and Client Secret
SSO_PROVIDER_ID="okta"
SSO_DOMAIN="yourcompany.com"
SSO_ISSUER="https://your-org.okta.com"
SSO_CLIENT_ID="<Client ID>"
SSO_CLIENT_SECRET="<Client Secret>"SSO (SAML)
For enterprise SSO via SAML 2.0:
| Variable | Required | Description |
|---|---|---|
SSO_PROVIDER_ID | No | Identifier for the provider (e.g., okta). Defaults to default-sso |
SSO_DOMAIN | Yes | Email domain to match users to this provider |
SSO_SAML_ENTRY_POINT | Yes | SAML IdP SSO URL |
SSO_SAML_ISSUER | Yes | The IdP's issuer / entity ID — must match your IdP's SAML issuer (e.g. Okta's SAML Issuer ID, http://www.okta.com/{externalId}) |
SSO_SAML_CERT | Yes | IdP signing certificate (base64, without BEGIN/END wrapper) |
SSO_SAML_CALLBACK_URL | No | SAML ACS callback URL. Auto-generated if not set |
Note: Only one SSO mode can be active at a time. If
SSO_ISSUERis set, OIDC takes precedence over SAML.
Okta example:
- In Okta Admin -> Applications -> Create App Integration
- Select SAML 2.0 -> Next
- Set Single sign-on URL to
{AUTH_URL}/api/auth/sso/saml2/sp/acs/{SSO_PROVIDER_ID} - Set Audience URI (SP Entity ID) to
{AUTH_URL} - Set Name ID format to
EmailAddress
SSO_PROVIDER_ID="okta"
SSO_DOMAIN="yourcompany.com"
SSO_SAML_ENTRY_POINT="https://your-org.okta.com/app/xxxxx/sso/saml"
SSO_SAML_ISSUER="http://www.okta.com/{externalId}"
SSO_SAML_CERT="MIIDp..."
SSO_SAML_CALLBACK_URL="{AUTH_URL}/api/auth/sso/saml2/sp/acs/{SSO_PROVIDER_ID}"The user's display name is built from the SAML firstName + lastName attributes (Okta's default profile attribute names). A custom SAML app sends no attributes by default, so add Attribute Statements for them (Name firstName → Value user.firstName, and likewise lastName). For an IdP that uses different attribute names, override with IDP_FIRSTNAME_CLAIM / IDP_LASTNAME_CLAIM (e.g. givenName / surname). If absent, the name falls back to the email. The name (and role) re-sync from the IdP on each login.
Restricting Who Can Sign In
By default, anyone who can authenticate with a configured provider is allowed in — for open providers like GitHub or Google that means anyone with a GitHub/Google account. Set AUTH_ALLOWED_PRINCIPALS to restrict provisioning to a known set of people:
# Everyone @yourcompany.com, plus two named external collaborators
AUTH_ALLOWED_PRINCIPALS="yourcompany.com,alice@partner.com,bob@gmail.com"| Entry form | Matches |
|---|---|
yourcompany.com | any email at that domain (*@yourcompany.com) |
@yourcompany.com | same as above (a leading @ is tolerated) |
alice@partner.com | only that exact address |
How it behaves:
- Unset or empty → allow all (unchanged default behavior).
- Matching is case-insensitive and runs against the provider-asserted email. It applies to all providers (Google, GitHub, Microsoft, SSO/OIDC/SAML, and SCIM) — make sure any domain you provision via SCIM/SSO is also listed.
- Subdomains are not implied — list
corp.yourcompany.comseparately if needed. - It is checked both when an account is first created and on every login, so an existing user who is no longer on the list is blocked the next time they sign in. Sessions that were already active when you tighten the list keep working until they expire — to cut someone off immediately, remove/ban them from Admin -> Users.
- A blocked attempt is rejected (the user lands back on the sign-in page with an "account isn't permitted" message) and recorded as a
user_create_denied(new account) orauthn_login_denied(existing account) audit event.
GitHub specifics: a GitHub account's primary email is frequently a personal address, so a company-domain rule may not match your teammates. List the exact emails you trust, or — if everyone you'd allow is in your GitHub org — prefer mapping access to org membership (today that means using SSO for lifecycle control rather than the GitHub OAuth provider).
Roles & Permissions
Zipline uses role-based access control (RBAC) with three roles:
| Role | Description |
|---|---|
admin | Full access — can manage users, roles, SCIM, plus all operator permissions |
operator | Can cancel/start workflows, deploy schedules, upload/sync configs |
viewer | Read-only access to all data |
The first user to sign up is automatically promoted to admin. All subsequent users default to viewer. Admins can change roles from the Admin -> Users page.
Permission Matrix
| Action | Admin | Operator | Viewer |
|---|---|---|---|
| View workflows, configs, schedules, metrics | ✅ | ✅ | ✅ |
| Cancel / start workflows | ✅ | ✅ | ❌ |
| Upload / sync configs | ✅ | ✅ | ❌ |
| Deploy / delete schedules | ✅ | ✅ | ❌ |
| Manage user roles | ✅ | ❌ | ❌ |
| Manage SCIM provider connections | ✅ | ❌ | ❌ |
IdP Role Mapping
When using SSO, roles can be assigned automatically from the IdP:
| Variable | Required | Description |
|---|---|---|
IDP_ROLE_MAPPING | No | Comma-separated mapping of IdP group/claim values to Zipline roles (strict allowlist). Omit for passthrough. |
IDP_ROLE_CLAIM | No | IdP attribute/claim name to read the role from. Defaults to groups. |
IDP_GROUP_CLAIM | No | Legacy alias of IDP_ROLE_CLAIM (kept for back-compat; IDP_ROLE_CLAIM takes precedence). |
Mapping mode — IDP_ROLE_MAPPING format idp-group:zipline-role,idp-group2:zipline-role2:
IDP_ROLE_MAPPING="zipline-admins:admin,zipline-operators:operator"
IDP_ROLE_CLAIM="groups"Passthrough mode — omit IDP_ROLE_MAPPING and point IDP_ROLE_CLAIM at a claim that already carries the role; the value is used directly (no identity mapping needed):
IDP_ROLE_CLAIM="role"If a user belongs to multiple mapped groups, the highest-privilege role wins (admin > operator > viewer). Values that don't resolve to a role are ignored, and users with no matching role keep their existing one.
Configuring group claims in Okta:
- OIDC: App -> Sign On -> OpenID Connect ID Token -> Add
groupsclaim with filter - SAML: App -> Sign On -> Group attribute statements -> Add Name
groupswith filterMatches regex .*
IDP_ROLE_CLAIM (alias IDP_GROUP_CLAIM) is just the SAML/OIDC attribute name to read — it doesn't have to be a group list. Instead of sending every group, you can send a single value via a SAML Attribute Statement whose value is an Okta Expression:
- Profile attribute: Name
role, Valueuser.profile.ziplineRole(a custom attribute added in Directory -> Profile Editor), withIDP_ROLE_CLAIM="role"and noIDP_ROLE_MAPPING(passthrough). - Computed from groups: Name
groups, ValueisMemberOfGroupName("Zipline Admins") ? "admin" : "viewer".
Okta Identity Engine references profile attributes as
user.profile.<var>(the older Classic Engine useduser.<var>), and the expression must emit one value per role — a comma-joined string is treated as a single literal.
SCIM (User Provisioning)
Zipline supports SCIM 2.0 for automated user provisioning and deprovisioning from identity providers like Okta, Azure AD, and OneLogin.
SCIM allows an IdP to automatically:
- Create users when they're assigned to the app in the IdP
- Update user attributes (name, email) when they change
- Deactivate/delete users when they're removed from the app
Setup
- Generate a SCIM token from the Zipline admin UI at
/admin/scim. The token is shown once and cannot be retrieved again - Configure your IdP to use the SCIM base URL:
{AUTH_URL}/api/auth/scim/v2 - Provide the generated token as the bearer token in your IdP's SCIM configuration
Okta SCIM Setup
- In Okta Admin -> Applications -> select your app
- Go to Provisioning -> Configure API Integration
- Check Enable API Integration and enter:
- SCIM 2.0 Base URL:
{AUTH_URL}/api/auth/scim/v2 - OAuth Bearer Token: The token from the admin UI
- SCIM 2.0 Base URL:
- Click Test API Credentials to verify
- Under Provisioning -> To App, enable Create Users, Update User Attributes, and Deactivate Users
Azure AD / Entra ID SCIM Setup
- In Azure Portal -> Enterprise Applications -> select your app -> Provisioning
- Set Provisioning Mode to Automatic
- Under Admin Credentials:
- Tenant URL:
{AUTH_URL}/api/auth/scim/v2 - Secret Token: The token from the admin UI
- Tenant URL:
- Click Test Connection to verify
- Configure attribute mappings and set Provisioning Status to On
CLI Authentication
The zipline auth commands authenticate the CLI via the device authorization flow:
| Command | Description |
|---|---|
zipline auth login --url <URL> | Authenticate via device flow (prompts for URL if omitted) |
zipline auth logout | Clear stored credentials |
zipline auth status | Show current auth state and validate token |
zipline auth get-access-token | Print a short-lived JWT to stdout (for scripts and curl) |
Usage
# Log in (opens browser for approval)
zipline auth login --url https://zipline.example.com
# Use the JWT in scripts
curl -H "Authorization: Bearer $(zipline auth get-access-token)"
https://zipline.example.com/workflow/v2/listCredentials are stored in ~/.zipline/auth.json.
Auth Priority
When making requests to the backend, the CLI tries the following in order:
ZIPLINE_TOKENenvironment variable — session token or JWT (see Token Environment Variable below)- Hub auth — session token from
zipline auth login, exchanged for a short-lived JWT - GCP IAM — service account via Application Default Credentials
- Azure CLI — DefaultAzureCredential
ID_TOKENenvironment variable — fallback for custom setups
Service Principals
For CI pipelines, automation, and non-interactive environments, admins can create service principals — long-lived credentials that don't require browser-based login.
- Navigate to Admin -> Service Principals in the Zipline UI
- Create a new service principal with a name (e.g.,
ci-pipeline) and a role (viewer,operator, oradmin) - Copy the generated token — it is shown only once and cannot be retrieved again
The token can be rotated or the service principal deleted from the same admin page.
Token Environment Variable
The ZIPLINE_TOKEN environment variable provides a cloud-agnostic way to authenticate CLI commands without interactive login. It accepts two token types, auto-detected by format:
| Token type | Source | ZIPLINE_AUTH_URL required? | Lifetime |
|---|---|---|---|
| Session token | Service principal (admin UI) | Yes | Until rotated/deleted |
| JWT | zipline auth get-access-token | No | ~15 minutes |
Examples:
# CI / automation — service principal token (long-lived, auto-exchanges for JWTs)
export ZIPLINE_TOKEN="<token from admin UI>"
export ZIPLINE_AUTH_URL="https://zipline.example.com"
zipline hub backfill compiled/joins/team/my_join --start-ds=2024-01-01 --end-ds=2024-01-01
# Quick testing — JWT used directly (short-lived, no ZIPLINE_AUTH_URL needed)
ZIPLINE_TOKEN=$(zipline auth get-access-token) zipline hub backfill ...| Variable | Description |
|---|---|
ZIPLINE_TOKEN | A service principal session token or a JWT from get-access-token |
ZIPLINE_AUTH_URL | Frontend URL for token exchange (e.g., https://zipline.example.com). Required when ZIPLINE_TOKEN is a session token |
Docker Networking
When running backend services in Docker with the frontend running locally (via --skip-frontend):
AUTH_JWKS_URLdefaults tohttp://host.docker.internal:5173/api/auth/jwks- Docker compose files include
extra_hosts: ["host.docker.internal:host-gateway"]for Linux compatibility
When running the full stack in Docker:
AUTH_JWKS_URLdefaults tohttp://frontend:3000/api/auth/jwks