FerrisKeyDocs

Compass: authentication flows

Compass records each authentication attempt as a flow: a sequence of named steps, each with its own outcome, duration, and error details. It is how you find out where a sign-in actually went wrong.

Why

From outside, authentication is one request and one token. Inside, it is redirect validation, credential hashing, an MFA challenge, maybe an external IdP round trip, session creation, and token signing. When it fails, 401 tells you nothing about which of those broke.

With Compass you get this instead:

Flow 01914b3c-... for client my-frontend, grant authorization_code: authorize (success) → credential_validation (success, 85 ms) → mfa_challenge (failure, invalid_otp_code)

The flow is still pending at that point: the user is on the login page and can enter another code.

FerrisKey Compass dashboard listing authentication executions with grant type, outcome, step count and duration FerrisKey Compass dashboard listing authentication executions with grant type, outcome, step count and duration

Each row is one authentication execution: its grant type, its outcome, how many steps it went through and how long it took. A pending row is a flow nobody has closed yet: a sign-in in progress, one the user abandoned, or one that failed a check and may still be retried. Twelve hours after it started, a pending flow becomes expired.

What gets recorded

With Compass on, an authentication opens a flow and each stage writes a step. The entry points that are traced are:

Entry pointgrant_typeSteps
Browser sign-in with the authorization code flowauthorization_codeauthorize, credential_validation, mfa_challenge, finalize, token_exchange
Password grant on the token endpointpasswordtoken_exchange
Client credentials grant, failures onlycredentialstoken_exchange
RFC 8693 token exchangeurn:ietf:params:oauth:grant-type:token-exchangesubject_token_exchange
SAML sign-insaml_ssosaml_authn_request, the sign-in steps, saml_assertion
Sign-in through an external identity providerbroker_<alias>, with the provider aliasidp_redirect, idp_callback, then token_exchange when the client redeems its code

The refresh token grant is not traced, and neither is a client credentials grant that succeeds: both fire on every renewal and would bury the interesting flows.

graph TD
    A[authorize] --> B[credential_validation]
    B --> C{OTP credential?}
    C -->|Yes| D[mfa_challenge]
    C -->|No| E[mfa_challenge skipped]
    D --> F[finalize]
    E --> F
    F --> G[token_exchange]
    G --> H[Flow closed]

Flow lifecycle

The flow opens

When an authentication starts, Compass creates a CompassFlow with the realm, client, grant type, IP address and user agent. Its status is pending.

Steps are recorded

Each stage of the sign-in writes a step with its outcome, duration and error details. Steps are saved in the background: the writer collects them for up to 100 ms and saves them together, so a step appears in the API a moment after it happened.

The flow closes

When the sign-in ends, the flow gets its final status, completed_at, duration_ms and the user. For a browser sign-in this happens at the token step. A wrong password or a wrong code on the login page records a failed step but does not close the flow, because the user can try again on the same page.

Old flows are cleaned up

An hourly sweep expires flows that stayed pending for 12 hours and deletes flows older than 30 days, with their steps. See Architecture.

Flow structure

A CompassFlow holds the whole attempt:

FieldTypeDescription
idUUIDv7Flow identifier, time-ordered
realm_idUUIDRealm context
client_idstring, nullableThe OAuth client_id that started the authentication
user_idUUID, nullableThe authenticated user. Set when the flow closes, so it is null while the flow is pending, and it stays null on password grant flows
grant_typestringSee the table above
statusstringpending, success, failure or expired
ip_addressstring, nullableSource IP address
user_agentstring, nullableClient user agent
started_atdate-timeWhen the flow began
completed_atdate-time, nullableWhen the flow closed. Null while it is pending
duration_msinteger, nullableDuration of the closing step, in milliseconds
stepsarrayThe step records, in the order they started

duration_ms measures the closing step, which for a browser sign-in is the exchange of the authorization code for tokens. It does not include the time the user spent on the login page. For wall-clock time, subtract started_at from completed_at.

Flow status values

StatusMeaning
pendingNot closed. A sign-in in progress, abandoned, or retryable after a failed credential or code check
successClosed by a successful token issue or SAML assertion
failureClosed by a failure at the token step, or by a failed SAML, identity provider or token exchange attempt
expiredWas pending for more than 12 hours when the hourly sweep ran

Flow steps

Each step records:

FieldTypeDescription
idUUIDv7Step identifier
flow_idUUIDParent flow
step_namestringOne of the ten step types below
statusstringsuccess, failure or skipped
duration_msinteger, nullableStep duration in milliseconds. Some steps record none
error_codestring, nullableMachine-readable error, on failure
error_messagestring, nullableHuman-readable detail, on some failures
detailsJSON, nullableExtra data. Only idp_callback sets it
started_atdate-timeWhen the step began

Step types

StepRecorded when
authorizeThe authorization request was accepted: client, redirect URI and PKCE checks passed and the login session was created
credential_validationThe username and password were checked. On failure error_code is the name of the internal error, for example InvalidPassword, UserDisabled or AccountLocked
mfa_challengeThe one-time code was checked, with invalid_otp_code as the error on failure. The step is skipped when the account has no authenticator
finalizeThe login completed and an authorization code was issued
token_exchangeThe authorization code, or the password of a password grant, was exchanged for tokens. duration_ms is the time the exchange took
idp_redirectThe sign-in came back from an external identity provider, so the user had been redirected to it. No duration
idp_callbackThe code returned by the provider was exchanged at its token endpoint. On success details.provider holds the provider alias. On failure error_message carries the provider’s error and error_description
saml_authn_requestThe service provider’s AuthnRequest was parsed and checked. A failure carries an error code and a message
saml_assertionThe assertion was built and signed for the service provider
subject_token_exchangeAn RFC 8693 exchange. On failure error_code is the OAuth error

Step status values

StatusMeaning
successThe step completed
failureThe step failed, see error_code and error_message
skippedThe step did not apply, for example mfa_challenge for an account with no authenticator

Configuration

SettingDefaultDescription
compass_enabledtrueRecord flows for this realm

Turn it off and authentication behaves the same, minus the recording: no flows, no steps, no database writes. That is worth doing on a realm where you do not need step-level observability.

With compass_enabled set to false, the recorder returns before it builds a flow. Nothing is queued and nothing is written.