Login Flows
Login Flows
Openlane supports credential, OAuth, passkey, and organization-enforced single sign-on (SSO) login, backed by a WebFinger discovery endpoint and SSO enforcement and provisioning logic. The flows below trace how each login request moves through the handlers, which tells you where to look when debugging a failed login.
Credential Login
The credential handler is implemented in internal/httpserve/handlers/login.go:
LoginHandlerparses aLoginRequestand callsgetUserByEmailto fetch the account.- If the account does not exist,
RegisterHandlercreates the user andsendVerificationEmaildispatches the confirmation email. ssoOrgForUserdetermines if the organization enforces SSO for the email and redirects non-owner users toSSOLoginHandlerwhen required.- Unverified users can request a new token via
ResendEmail, which invokesstoreAndSendEmailVerificationToken. - When the password is validated with
passwd.VerifyDerivedKey,AuthManager.GenerateUserAuthSessionissues the access and refresh tokens andupdateUserLastSeenrecords the login.
OAuth / Social Login
The OAuth handlers are defined in internal/httpserve/handlers/oauth_login.go and
oauth_register.go:
GetGoogleLoginHandlersandGetGitHubLoginHandlersredirect users to the provider.- After the provider callback,
issueGoogleSessionorissueGitHubSessioncallCheckAndCreateUserto create or update the user record. ssoOrgForUserchecks the email's organization and redirects toSSOLoginHandlerwhen SSO is enforced for non-owner members.- New accounts are verified by
sendVerificationEmailinsidestoreAndSendEmailVerificationToken. - A session is created via
AuthManager.GenerateOauthAuthSessionand the last login is stored withupdateUserLastSeen.
Passkey Login
Passkey operations live in internal/httpserve/handlers/webauthn.go:
BeginWebauthnLoginissues a challenge withWebAuthn.BeginDiscoverableLoginand stores it in the session.FinishWebauthnLoginparses the response usingprotocol.ParseCredentialRequestResponseBodyand validates it withWebAuthn.ValidateDiscoverableLogin.- The user is loaded with
getUserByID, thenAuthManager.GenerateUserAuthSessioncreates the token pair. updateUserLastSeenrecords that the login was via a passkey.
Organization-Enforced SSO
SSO logic is implemented in internal/httpserve/handlers/sso.go:
SSOLoginHandlerchecks the organization policy viafetchSSOStatusand redirects to the configured IdP.- On return,
SSOCallbackHandlerexchanges the authorization code withrp.CodeExchangeand retrieves the user data. - The account is ensured using
CheckAndCreateUser; new users receive a verification email viasendVerificationEmail. AuthManager.GenerateOauthAuthSessionissues the session and the login time is recorded withupdateUserLastSeen.
WebFinger Endpoint
WebFinger is an HTTP discovery protocol defined by RFC 7033. It allows a client to look up information about a resource, typically identified by a URI such as an email address.
Openlane exposes a WebFinger endpoint at /.well-known/webfinger. The login page queries this endpoint to determine whether an organization requires SSO authentication and which identity provider should be used.
Querying the Endpoint
Send a GET request to /.well-known/webfinger with a resource parameter.
/.well-known/webfinger?resource=acct:[email protected]
/.well-known/webfinger?resource=org:01HAC1M7J3A2YHC4ZK6B2QWJNT
The endpoint returns an SSOStatusReply structure:
{
"success": true,
"enforced": true,
"provider": "OKTA",
"discovery_url": "https://id.example.com/.well-known/openid-configuration",
"organization_id": "01HAC1M7J3A2YHC4ZK6B2QWJNT"
}
Why WebFinger?
Using WebFinger provides a lightweight way for the UI to discover if SSO is enforced before the user authenticates. When the email entered on the login form maps to an organization that enforces SSO, the browser can immediately redirect to the configured identity provider without any additional API calls or user interaction.
OIDC Handler Flows
SSO Enforcement & Exemptions
Whether a member must use SSO is decided by Evaluate in pkg/ssoutils/enforcement.go, fed by LoadEnforcement in pkg/ssoutils/load.go. LoadEnforcement reads the organization setting (identity_provider_login_enforced, multifactor_auth_enforced, sso_exempt_domains) and, when a user id is supplied, the member's role and sso_exempt flag.
Evaluate resolves exemption with first-match precedence, then computes MustSSO = SSOEnforced && !Exempt:
- Two-factor authentication (TFA) is independent:
TFARequiredmirrorsmultifactor_auth_enforcedregardless of SSO exemption. - The same decision is consulted at every entry point:
login.goandoauth_login.gocallorgEnforcementsForUser→fetchSSOStatus;switch.gocallsfetchSSOStatusfor the target org;webfinger.goexposes it for the UI.fetchSSOStatus(insso.go) only applies the per-user exemptions when a user id is provided, so anorg:-scoped webfinger lookup reports raw enforcement while anacct:lookup reports the specific user's requirement.
JIT Provisioning
SSOCallbackHandler (in sso.go) provisions membership after a successful IdP exchange, before the session is issued, via jitProvisionMembership (in ent.go):
- Runs only when the org has
identity_provider_login_enforcedandidentity_provider_jit_provisioningset. - When
jit_allowed_email_domainsis non-empty, the authenticated email's domain must be in the list; otherwise provisioning is skipped. - Existing members are left untouched; new memberships are created with the Member role under an org-scoped caller so the membership hooks resolve the organization.
The callback also marks the IdP-verified email confirmed (via setEmailConfirmed) before session generation. That confirmation triggers the email-confirmation hook, which drives domain auto-join for non-enforced organizations (see below); the enforced target org is joined by just-in-time (JIT) provisioning, not auto-join.
Domain-Based Membership: Auto-Join vs JIT
autoJoinOrganizationsForUser (in internal/ent/hooks/usersettings.go) runs from the email-confirmation hook and now filters on organizationsetting.IdentityProviderLoginEnforced(false) in addition to AllowMatchingDomainsAutojoin(true) and the email domain being contained in allowed_email_domains. So:
- SSO not enforced → membership by
allowed_email_domains+ auto-join toggle, on email confirmation. - SSO enforced → auto-join is skipped; membership comes from JIT (
jit_allowed_email_domains) or an invitation.
The two domain lists are independent fields and never apply simultaneously to the same organization.
Shareable Per-Org SSO Initiation
SSOInitiateHandler (in sso.go, route GET /v1/orgs/:slug_name/sso, unauthenticated) resolves an organization by slug_name and mirrors SSOLoginHandler: it sets the state/nonce/organization_id cookies and returns the IdP redirect URL as JSON, rather than redirecting from the API directly. The console proxies it so those cookies are re-set on the UI origin, after which the existing /login/sso callback page completes the exchange unchanged.
slug_name is derived from the organization name in the create hook (internal/ent/hooks/organization.go, strcase.KebabCase). The org-setting read inside oidcConfig uses an allow context so a non-member can start the flow; the client secret is only used server-side for the code exchange and is never returned.