Skip to main content

Enterprise Authentication Surfaces

AxonFlow Enterprise uses more than one authentication model because the platform exposes more than one kind of surface.

The most common source of confusion is assuming that all enterprise APIs use the same header or credential pattern. They do not.

The Main Authentication Surfaces

1. Customer portal session authentication

This is the standard login flow for the customer portal and many portal-backed enterprise APIs.

Core endpoints:

  • POST /api/v1/auth/login
  • POST /api/v1/auth/logout
  • GET /api/v1/auth/session
  • GET /api/v1/auth/sso/availability

Successful login creates the axonflow_session cookie. Portal-backed APIs then use that session context. Current portal sessions use SameSite=Lax.

The login endpoint is rate-limited to 5 attempts per minute per IP address.

Password reset and change endpoints:

  • POST /api/v1/auth/forgot-password
  • POST /api/v1/auth/reset-password
  • POST /api/v1/auth/change-password

This is also the model used by the current SDK portal-login helpers.

2. SSO portal login (SAML and OIDC)

Enterprise portal login (plane a of the three identity planes) is tenant-aware and exposes provider-aware routes for both protocols.

SAML routes:

  • GET /auth/saml/{tenantID}/metadata
  • GET /auth/saml/{tenantID}/login
  • POST /auth/saml/{tenantID}/callback

OIDC routes (v9.16.0):

  • GET /auth/oidc/{tenantID}/login
  • GET /auth/oidc/{tenantID}/callback

The pre-login availability check (GET /api/v1/auth/sso/availability) returns the correct login URL for whichever protocol the tenant configured, so a client does not need to know the protocol in advance.

The customer portal also exposes SSO configuration endpoints once you are authenticated. Both SAML and OIDC portal-login providers are configured through the same /api/v1/sso/config surface. The SAML metadata-fetch and provider-default helpers are SAML-specific; OIDC providers are configured from their issuer, audience, JWKS URI, and client credentials. This is portal login only; per-user fleet OIDC tokens (X-User-Token) are a separate plane, covered in Per-Developer Identity.

3. Portal API keys

Portal API keys are managed through authenticated portal APIs and are useful for automation around the customer portal surface.

Key-management endpoints:

  • GET /api/v1/keys
  • POST /api/v1/keys
  • GET /api/v1/keys/{keyID}
  • DELETE /api/v1/keys/{keyID}

These are not the same thing as SCIM tokens or the public runtime's client credentials.

4. SCIM bearer-token authentication

SCIM provisioning is its own integration surface:

  • base path: /scim/v2
  • guarded by SCIM-specific bearer-token auth

SCIM discovery endpoints exist within the same surface:

  • /scim/v2/ServiceProviderConfig
  • /scim/v2/Schemas
  • /scim/v2/ResourceTypes

And the core provisioning resources are:

  • /scim/v2/Users
  • /scim/v2/Groups

Treat SCIM as a dedicated provisioning API, not as a generic extension of portal session auth.

Portal Login Flow

For standard customer-portal access:

  1. the user submits org_id and password to POST /api/v1/auth/login
  2. the portal validates the organization and password
  3. the backend creates a session record
  4. the response sets the axonflow_session cookie
  5. authenticated portal APIs use the session context on subsequent requests

The portal also supports a session-check route:

GET /api/v1/auth/session

That is the easiest way to verify whether the portal session is still active.

Password Recovery

The portal login page has a "Forgot password?" link. What happens next depends on how the deployment is configured.

Self-service reset by email

When the deployment has an email backend configured, submitting an organization ID on the forgot-password page emails a reset link to the organization's contact email address:

  1. the user submits their org_id on /forgot-password (POST /api/v1/auth/forgot-password)
  2. the portal always answers with the same generic confirmation - it never reveals whether the organization exists
  3. if the organization exists and has a contact email, a reset link is sent to that address; the link expires after 1 hour (one active token per organization)
  4. the link opens /reset-password, where the user chooses a new password (minimum 8 characters); the token is single-use
  5. the user signs in with the new password

The forgot-password endpoint is rate-limited to 5 requests per minute per IP address, matching the login endpoint.

Behind more than one proxy layer, set AXONFLOW_TRUST_PROXY_HOPS

The login and password-reset rate limits are counted per client address, which the portal reads from the X-Forwarded-For header your ingress sets. By default it trusts one proxy layer (an ALB, or a single nginx), which is correct for most deployments.

If requests reach the portal through two or more trusted proxies - for example Cloudflare in front of your own nginx - set AXONFLOW_TRUST_PROXY_HOPS to the number of proxy layers. Otherwise every user is counted as the same client, and five failed sign-ins from anyone in the organization locks out everyone for a minute.

Set it to the real number of trusted proxies and no higher: a value larger than the number of proxies makes the limit count on a value the caller can choose, which defeats it. The resolved setting is printed in the portal's startup log so you can confirm it.

Independently of the per-IP limits, each organization has a re-issue cooldown: while an unused reset link issued in the last 5 minutes is still valid, a repeat request sends no second email and does not replace the outstanding link. This keeps a reset link that has already reached someone's inbox working, and stops a caller who knows an organization ID from using the endpoint to mail that organization repeatedly. Requesting again after the link is used, expires, or the window passes issues a fresh one and invalidates the previous.

Password recovery works for SSO-enforced organizations too

The organization password is the documented always-works fallback when SSO is misconfigured or the IdP is unavailable, so the reset flow is deliberately not gated on enforce_sso.

Deployment configuration

Email delivery is configured on the customer-portal service. The portal supports two transports and activates exactly one.

TransportConfigure withSuits
SMTP relayAXONFLOW_SMTP_HOST + AXONFLOW_SMTP_PORTSelf-hosted and in-VPC deployments. No third-party vendor, no sending-domain verification, and reset mail never leaves your own infrastructure.
ResendRESEND_API_KEYDeployments already using Resend, with a sending domain verified there (DKIM + SPF).

If you run AxonFlow yourself, use SMTP. You almost certainly already operate a relay (Postfix, Exchange, an SES SMTP endpoint, your provider's submission host), and routing reset mail through it keeps the message inside your own network, which is usually the only answer a regulated deployment can give. Resend is an HTTPS service: using it means signing up with a third party and verifying a sending domain there.

Both transports need the same two shared settings, AXONFLOW_FROM_EMAIL and AXONFLOW_PORTAL_BASE_URL, described below.

Precedence: if both transports are configured, Resend wins

When RESEND_API_KEY is set, Resend is selected and the AXONFLOW_SMTP_* settings are ignored entirely. The portal logs this explicitly at startup. Unset RESEND_API_KEY to use the relay. On an AWS CloudFormation stack you cannot even create the ambiguity: setting both ResendAPIKeySecretArn and SMTPHost fails the stack operation. The selected transport is named in the startup log, so you can confirm which one is live.

SMTP relay setup

# Required together. There is no default port: 587 and 465 need different
# transport security, so the portal will not guess one for you.
AXONFLOW_SMTP_HOST=smtp.internal.acme.example # bare hostname, no port
AXONFLOW_SMTP_PORT=587

# Required, shared with the Resend transport (see below).
AXONFLOW_FROM_EMAIL="Acme Portal <[email protected]>"
AXONFLOW_PORTAL_BASE_URL=https://portal.acme.example

# Optional. Set BOTH or NEITHER. Many internal relays need neither.
AXONFLOW_SMTP_USERNAME=portal
AXONFLOW_SMTP_PASSWORD=...

# Optional. starttls (default) | tls | none
AXONFLOW_SMTP_TLS=starttls

# Optional. PEM bundle for a relay whose certificate comes from your own CA.
AXONFLOW_SMTP_CA_FILE=/etc/axonflow/relay-ca.pem

Transport security.

  • starttls is the default when AXONFLOW_SMTP_TLS is unset: the connection opens in plaintext and is upgraded before AUTH or any message bytes. Typical port 587. If the relay does not advertise STARTTLS the send fails; it never continues unencrypted.
  • tls is implicit TLS from the first byte. Typical port 465.
  • none sends the session in the clear. It exists for an internal relay on a trusted network, it has to be typed (you cannot reach it by leaving the variable unset), and the portal refuses to start if you combine it with AXONFLOW_SMTP_USERNAME, because SMTP AUTH PLAIN is base64 encoding, not encryption. Choosing it logs a warning naming the relay.

There is deliberately no setting that disables certificate verification. Certificates are verified against the hostname you configured in AXONFLOW_SMTP_HOST.

A relay with a private CA needs the compose path

If your relay presents a certificate from an internal CA, point AXONFLOW_SMTP_CA_FILE at a PEM bundle for that CA. The value is a path inside the container, so it needs a mounted file: that is available on the Docker Compose and self-hosted install paths, and there is no CloudFormation parameter equivalent. On a CloudFormation stack, use a relay whose certificate chains to a public root.

Resend setup

RESEND_API_KEY=re_...
AXONFLOW_FROM_EMAIL="Acme Portal <[email protected]>"
AXONFLOW_PORTAL_BASE_URL=https://portal.acme.example

The sending domain must be verified in your Resend account.

The two shared settings

VariablePurpose
AXONFLOW_FROM_EMAILThe from-address, for example Acme Ops <[email protected]>. With SMTP, your relay must be willing to send as this address. AXONFLOW_RESET_FROM_EMAIL overrides it for reset mail specifically; either one satisfies the startup check.
AXONFLOW_PORTAL_BASE_URLThe portal's external base URL, for example https://portal.acme.example. Every emailed reset link is built from it. Required in every deployment mode, including SaaS.

Fail-closed behavior

Transport selection has exactly three outcomes, and "silently does nothing" is not one of them:

ConfigurationResult
No transport configuredPOST /api/v1/auth/forgot-password returns 501, exactly as it did before this feature existed. Use the administrator fallback below.
A transport fully configuredMail is sent. Startup logs which transport is live.
A transport partly configuredThe portal refuses to build the sender, names the specific variable in the startup log, and the endpoint stays on 501.

The third row is the one that matters. Setting AXONFLOW_SMTP_PORT and forgetting AXONFLOW_SMTP_HOST, typing AXONFLOW_SMTP_TLS=tsl, giving a username with no password, or writing the port into the host as smtp.example.com:587 does not quietly fall back to "email disabled". Each is a named error in the startup log. A silent 501 reads as "the feature was never built"; a log line reads as "you set two of three variables".

The same applies to the two shared settings. A transport configured without AXONFLOW_FROM_EMAIL, or without AXONFLOW_PORTAL_BASE_URL, is refused in every deployment mode. Without a base URL, every emailed link would carry a live reset token to the AxonFlow SaaS portal, a host you do not control. A value that is only separators (/, //) or whitespace is not a usable base URL and counts as unset.

The portal never claims a link was sent without sending one, and never mails a link pointing at a host the operator does not control.

SSO_BASE_URL is now AXONFLOW_PORTAL_BASE_URL

The portal's external base URL builds the emailed reset link and the SAML ACS / OIDC redirect URLs. Naming it SSO_BASE_URL led deployments that use password login and no SSO to skip it, after which reset mail either refused to start or pointed at the wrong host.

AXONFLOW_PORTAL_BASE_URL is the name to use. SSO_BASE_URL keeps working as a deprecated alias, read only when the new name is unset or empty, and logged once at startup so you know to rename it. If both are set, the new name wins.

Nothing breaks on upgrade: a deployment that sets only SSO_BASE_URL behaves exactly as before, and the CloudFormation template renders both names from the single PortalExternalBaseURL parameter. An unusable value under one name never shadows a working value under the other: the two names are normalized before precedence is decided, so AXONFLOW_PORTAL_BASE_URL=/ alongside a real SSO_BASE_URL resolves to the real one.

CloudFormation parameters

On AWS deployments the CloudFormation template exposes these parameters for the customer-portal task. All default to empty, so a stack that sets none of them renders exactly the task definition it rendered before.

ParameterMaps to
SMTPHost, SMTPPortAXONFLOW_SMTP_HOST, AXONFLOW_SMTP_PORT
SMTPUsername, SMTPPasswordSecretArnAXONFLOW_SMTP_USERNAME, AXONFLOW_SMTP_PASSWORD (the password by Secrets Manager ARN)
SMTPTLSModeAXONFLOW_SMTP_TLS
ResendAPIKeySecretArnRESEND_API_KEY (by Secrets Manager ARN)
FromEmailAddressAXONFLOW_FROM_EMAIL
PortalExternalBaseURLAXONFLOW_PORTAL_BASE_URL (and the deprecated SSO_BASE_URL, from the same parameter)
TrustedProxyHopsAXONFLOW_TRUST_PROXY_HOPS

The template enforces the same completeness rules the portal does, at stack time rather than at boot: SMTPPort is required with SMTPHost, SMTPPasswordSecretArn is required with SMTPUsername, FromEmailAddress and PortalExternalBaseURL are required with either transport, and setting both transports is rejected outright.

A reset token is never returned over HTTP on a deployed stack

On a local development machine with no email backend, the endpoint returns the reset token in the response body as a testing convenience. That branch is limited to ENVIRONMENT unset, development, dev or local. Every deployed environment - including staging - answers 501 instead, because echoing a live password-changing token to an unauthenticated caller would be a disclosure channel.

Deployments without an email backend

Self-hosted (in-VPC) deployments that have not configured email keep the previous behavior: POST /api/v1/auth/forgot-password returns 501 Not Implemented, and the forgot-password page shows an honest "password reset by email is not configured for this deployment" notice instead of a fake confirmation.

Administrator fallback

Operators can always reset an organization's portal password directly with POST /api/v1/admin/organizations/{org_id}/password (authenticated with X-Admin-API-Key), regardless of email configuration. Self-hosted install bundles also ship ./reset-portal-credential.sh for the deployment org.

SSO configuration (SAML and OIDC)

Enterprise customers can configure portal SSO per tenant, using SAML or OIDC.

Useful portal-authenticated SSO configuration endpoints include:

  • GET /api/v1/sso/config
  • POST /api/v1/sso/config
  • PUT /api/v1/sso/config
  • PATCH /api/v1/sso/config
  • DELETE /api/v1/sso/config
  • POST /api/v1/sso/config/test
  • GET /api/v1/sso/audit
  • GET /api/v1/sso/providers
  • GET /api/v1/sso/providers/defaults
  • GET /api/v1/sso/sp-metadata
  • POST /api/v1/sso/fetch-metadata

The pre-login availability check is:

GET /api/v1/auth/sso/availability?org_id=YOUR_ORG_ID

This lets a portal client decide whether to present password auth, SSO, or both.

SCIM Token Management

SCIM token management is handled from the authenticated portal side, while the SCIM provisioning requests themselves use SCIM bearer-token auth.

Token-management endpoints:

  • GET /api/v1/scim/tokens
  • POST /api/v1/scim/tokens
  • DELETE /api/v1/scim/tokens/{tokenID}

This separation is worth understanding operationally:

  • portal admins create and manage SCIM tokens through the authenticated portal
  • the identity provider uses those tokens against /scim/v2/...

Admin Organization APIs

The organization-admin APIs are a separate admin surface under:

/api/v1/admin

These routes are guarded by admin-specific middleware rather than the ordinary customer-portal session model.

Examples include:

  • GET/POST /api/v1/admin/organizations
  • GET/PATCH/DELETE /api/v1/admin/organizations/{org_id}
  • POST /api/v1/admin/organizations/{org_id}/password
  • GET/POST/DELETE /api/v1/admin/organizations/{org_id}/license

This is one of the clearest examples of why "all enterprise APIs use the same auth" is the wrong mental model.

SDK Portal Authentication

The current SDKs support portal login helpers that target the standard login endpoint and store the session cookie client-side.

The underlying behavior is aligned across SDKs:

  • login via POST /api/v1/auth/login
  • store the session cookie
  • use that cookie for portal-backed requests
  • clear it on logout

That means SDK-driven portal operations are using the same real authentication model as browser portal usage, not a separate fake or simplified API.

When to Use Which Authentication Method

Use caseRecommended auth model
human user using the portalsession auth
browser-based enterprise UI workflowssession auth
organization portal SSO rolloutSAML or OIDC portal login plus session auth
per-user identity on a tool's governed API callsX-User-Token OIDC token (fleet identity)
automated portal administrationportal API keys where appropriate
identity provisioning from Okta, Microsoft Entra ID, OneLogin, or another SCIM IdPSCIM bearer tokens
admin org lifecycle operationsadmin-gated /api/v1/admin APIs

Common Mistakes

  • treating SCIM as if it used the same auth model as the portal
  • assuming all enterprise endpoints accept the same org headers or bearer token
  • forgetting that portal login establishes a cookie-backed session
  • debugging SSO failures as if they were password-login failures
  • using portal API keys where a SCIM token is actually required