SSO#
Lens Agents authenticates users over OIDC. Out of the box it uses Lens ID; point it at your own identity provider and users sign in with their corporate credentials, with every action attributed to that identity in the audit trail.
OIDC is the only user authentication protocol. SAML and SCIM are not supported.
Point the install at your provider#
Two Helm values do it:
helm upgrade lens-agents oci://ghcr.io/lensapp/lens-agents --reset-then-reuse-values \
--set oidc.issuer="https://your-org.okta.com" \
--set oidc.jwksUri="https://your-org.okta.com/oauth2/v1/keys"
| Value | Default | Purpose |
|---|---|---|
oidc.issuer |
Lens Cloud realm | Issuer URL of your OIDC provider. |
oidc.jwksUri |
Lens Cloud realm | JWKS endpoint used to verify signed tokens. |
oidc.clientId |
(empty) | Leave empty for self-activation. Set only for a pre-provisioned deployment that should skip activation. |
Both default to the Lens Cloud realm, which is what makes Lens ID login work on a fresh install. Override them together — an issuer whose JWKS endpoint does not match cannot verify a token.
Issuer URLs by provider:
| Provider | Issuer |
|---|---|
| Okta | https://your-org.okta.com |
| Microsoft Entra ID | https://login.microsoftonline.com/<tenant-id>/v2.0 |
| JumpCloud | https://oauth.id.jumpcloud.com/ |
Any standards-compliant OIDC provider works. Those three are validated.
Register the application#
In your identity provider, register Lens Agents as a web application. The admin UI builds its redirect URI from the address it is served on:
<config.publicUrl>/auth/callback
So an install with config.publicUrl=https://agents.example.com uses https://agents.example.com/auth/callback, and the post-logout redirect is config.publicUrl itself.
Clients that sign in on a loopback redirect — http://localhost, 127.0.0.1, or [::1] on any port, as Claude Code and Codex do — come back through the platform rather than straight to the client. Where you set oidc.clientId yourself, allow this redirect URI on that client as well:
<config.publicUrl>/oauth/callback
Without it, those sign-ins fail with a redirect mismatch. The platform requires PKCE with S256 on them.
Changing config.publicUrl later changes both redirect URIs. Update the IdP registration at the same time, or logins will fail with a redirect mismatch.
Authentication flow#
- A user opens the platform.
- The platform redirects to your identity provider over OIDC.
- The user authenticates, including MFA if your IdP requires it.
- The IdP returns a signed JWT.
- The platform verifies the token against
oidc.jwksUriand creates or updates the session. - The user is signed in with their organization and project roles.
Desktop AI tools connecting to the MCP endpoint trigger the same flow on first connection, then hold a refresh token for later sessions. That is why an interactive sign-in needs no token to create or store.
What SSO does and does not decide#
SSO decides who a user is. It does not decide what they may do.
Permissions come from organization and project roles inside the platform, and from policy bindings. A user who authenticates successfully but holds no project role can sign in and reach no project.
Group claims from your IdP are not mapped to project roles automatically. Manage project members in the platform.
Who gets an account#
A corporate IdP authenticates every employee, so on a default install every employee can sign in and collect an account. Installation owners is the control for that: name your owners with config.ownerEmails and a signed-in person is admitted only if they belong to an organization, may create one, or hold a pending invitation. Everybody else is refused with 403 and no account is stored.
Set it as part of pointing the install at your provider rather than afterwards — it is the same decision.
The email claim must be in the access token, and verified
Owner matching reads the email from the access token, not the id_token. Microsoft Entra ID in particular omits email from access tokens until you configure it, and an install that names owners whose email never arrives refuses everyone who belongs to no organization — owners included. Confirm the claim before you name owners.
The claim must be verified by the IdP as well. Where a user can set their own email without proof, that user can claim an owner address.
Direct API access#
Signing in and using the API directly are separate decisions. When an application of yours calls Lens Agents on behalf of people and enforces its own limits on them, those people should not be able to take their own token to the REST API, /mcp, or the web UI and skip those limits. The direct-access gate closes that path with a realm role from your IdP.
| Value | Default | Purpose |
|---|---|---|
oidc.directAccessRole |
(empty) | The realm role a person needs to use the API directly — web UI, nexusctl, and MCP clients. Read from the access token's realm_access.roles. Empty turns the gate off. |
oidc.delegatedClients |
[] |
OAuth clients admitted without the role, because they call on behalf of people and gate them themselves. |
oidc.sandboxIngressClients |
{} |
Client-to-role map for bearer tokens that open a sandbox's private port, each with the role it needs instead of directAccessRole. |
oidc.directAccessReportOnly |
false |
Log every request the gate would refuse, and let it through. |
With the gate on and the optional client API installed, add its bff.tokenExchange.clientId to oidc.delegatedClients. The install fails otherwise.
With oidc.directAccessRole set, an OIDC token is accepted on REST, /mcp, and WebSockets only if it is an access token and either was issued to this installation's own client to a person who holds the role or is a named installation owner with a verified email, or was issued to a client in oidc.delegatedClients. A person without the role gets 403, whatever organizations or invitations they have. An ID token, or a token from any other client, gets 401. API tokens are not OIDC tokens and are not affected.
The browser login to a sandbox's private port signs in through oidc.clientId, so with the gate on it needs the role too. A person without it reaches a private port only with a bearer token from a client in oidc.sandboxIngressClients. An ingress session already open is not re-checked, so removing the role does not end it.
The gate is off when the role is empty, so an install behaves as before until you set it. The install refuses to start on a combination that looks gated but is not, such as delegated or ingress clients, or report-only mode, without a role.
Confirm the role is in the access token
Decode a real access token issued to oidc.clientId and check realm_access.roles before you enforce. An IdP client that does not map realm roles into its tokens leaves the role out, and every direct user is refused.
Roll it out in report-only mode#
- Grant the role, through a group, to everyone who uses the web UI,
nexusctl, or an MCP client. - Deploy with
oidc.directAccessReportOnly: true. Each refusal the gate would make is logged with the person's subject and client, at most once an hour for each person, client, and reason, and the install warns hourly that the gate is not enforcing. - Grant the role to anyone in the log who should keep direct access. They sign out and back in.
- Set
oidc.directAccessReportOnly: false.
The token endpoint relays sign-in grants only#
The install's /oauth/token endpoint, which browsers and MCP clients use to complete sign-in, relays only the authorization_code, refresh_token, and device-code (urn:ietf:params:oauth:grant-type:device_code) grants. Any other grant type, client_credentials included, is refused with 400 unsupported_grant_type, so the endpoint cannot be used to mint a token for the installation's own client.
Non-interactive access#
Machines cannot complete a browser flow. CI jobs, scripts, and external agents use API tokens, which are separate principals with their own bindings and their own audit attribution.
Related#
- Activation — the OAuth client a fresh install registers
- Organizations and projects — where permissions actually come from
- Identity — users, API tokens, and sandboxes as principals
- API tokens — authenticating without a browser