Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch

Single sign-on

Let a trusted identity provider sign users in to your self-hosted Nodaro. Configure EXTERNAL_SSO_PROVIDERS, choose the account-linking rules and allow SSO only.

Single sign-on (SSO) lets a trusted external identity provider sign users in to your self-hosted Nodaro. You describe each provider in the EXTERNAL_SSO_PROVIDERS variable, and the login page shows an SSO button. The account a user ends up in is an ordinary user of your install: SSO is only a way to start a session. This page covers the operator's side; the protocol is in Single sign-on for developers.

Which kind of provider to use

KindUse it whenStatus
assertionYour issuer cannot speak OpenID Connect or SAML but can sign a short-lived JWT, for example an app that embeds Nodaro. Nodaro verifies the token and exchanges it for a normal session.Works end to end.
oidcYour identity provider speaks OpenID Connect and is configured in your install's Supabase Auth. The browser does a normal Supabase sign-in redirect, and Supabase verifies the result.The provider's id must be the name of the OAuth provider in your Supabase Auth, such as keycloak or azure.
saml—Entries pass validation, but the login page cannot send a SAML domain, so they do not sign anyone in. Use assertion or oidc.

SSO is off by default. With EXTERNAL_SSO_PROVIDERS unset, the login page shows no SSO button, GET /v1/sso/providers returns an empty list, and every provider route answers 404 unknown_provider.

Configure the providers

Set EXTERNAL_SSO_PROVIDERS to a JSON array, inline or as @/path/to/providers.json to read a file.

[
  {
    "id": "librechat",
    "label": "LibreChat",
    "kind": "assertion",
    "secret": "a-dedicated-32+char-hmac-key-not-the-idp-session-secret",
    "audience": "nodaro",
    "claimMap": { "email": "email", "emailVerified": "email_verified", "subject": "sub" },
    "initiateUrl": "https://chat.example.com/oauth/nodaro",
    "maxLifetimeSeconds": 300
  },
  {
    "id": "keycloak",
    "label": "Acme (Keycloak)",
    "kind": "oidc",
    "supabaseProvider": "keycloak"
  }
]

A malformed value stops the boot. A typo in an entry that holds a secret must never drop a provider silently, which would look like SSO failing with 404, or half-configure sign-in.

On the compose stack, add EXTERNAL_SSO_PROVIDERS and EXTERNAL_SSO_LINK_EXISTING under environment: of the nodaro service, because the compose file does not pass them from .env. To use a file, mount it into the container and give its path inside the container.

Provider fields

FieldApplies toWhat it does
idAllA URL-safe name: a lowercase letter or digit first, then lowercase letters, digits, _ or -, 1 to 63 characters in all, no dots. It appears in the route /v1/sso/<id> and is stored on the user. Must be unique.
labelAllThe provider's name. It appears on the login button only when two or more providers exist and the surface profile sets no auth.ssoLabel.
kindAllassertion, oidc or saml.
secretassertion, requiredThe HS256 key that verifies the assertion's signature, at least 16 characters. Use a dedicated key, never the identity provider's own session secret, so that a leak on Nodaro's side cannot forge the provider's sessions.
audienceassertion, requiredThe value the assertion's aud claim must equal.
claimMapassertionWhich claims carry the email, the verified flag and the subject. The default is { "email": "email", "emailVerified": "email_verified", "subject": "sub" }.
initiateUrlassertionAn absolute URL. When a user clicks the login button, Nodaro redirects there. The provider signs the user in and comes back with the assertion. Without it, a click answers 400 no_assertion.
initiateUrlByHostassertionA map from a bare lowercase hostname, without a port, to an absolute URL. A click that arrives on that host goes there instead of initiateUrl. For installs that answer on several hostnames. A wrong key stops the boot, and the map is never published.
maxLifetimeSecondsassertionThe longest lifetime an assertion may have, exp minus iat. The default is 300, 5 minutes, and the maximum is 3600.
supabaseProvideroidcValidated but not used: the login page passes the provider's id to Supabase.
domainoidc, samlValidated but not used. An oidc or saml entry needs domain or supabaseProvider.

Only id, label and kind are ever published, by GET /v1/sso/providers. The secret never leaves the server.

What Nodaro checks in an assertion

An assertion provider's identity server signs a JWT and sends the browser to GET /v1/sso/<id>?assertion=<jwt>. Nodaro accepts it only when all of these hold:

  • It is signed with HS256 and the provider's secret.
  • aud equals the provider's audience.
  • exp is present and not past, with 5 seconds of tolerance, and the lifetime is within maxLifetimeSeconds.
  • jti is present and has never been used. Each assertion works once.
  • The email claim is present. The verified claim must be true to create or link an account.

A failed check answers 401. The route allows 20 requests per 60 seconds from one IP address, then answers 429. The full contract, the error codes and the redirect flow are in Single sign-on for developers.

How accounts are linked

When an assertion passes, Nodaro finds or creates the matching user. The rules make sure that an assertion can never take over an existing account only because it has the same email address.

SituationResult
No account has the email, and the email is verifiedA new user is created and linked to the provider.
No account has the email, and it is not verifiedRefused: 403 email_unverified.
The account is already linked to this providerSigned in.
The account is linked to a different providerRefused: 403 account_linked_other_provider, even with EXTERNAL_SSO_LINK_EXISTING=true.
A local account without SSO exists, EXTERNAL_SSO_LINK_EXISTING=true, and the email is verifiedThe account is linked to the provider and signed in.
A local account without SSO exists, and the flag is false or the email is not verifiedRefused: 403 account_exists.

EXTERNAL_SSO_LINK_EXISTING defaults to false, which is safe against takeovers. Only true or 1, in any letter case, enables it. Enable it only when you trust your providers' verified-email claims enough to attach them to existing accounts.

account_exists and email_unverified use the same wording for every account, so the login form cannot be used to probe which addresses exist. The server log records which account was targeted.

The login button

The login page shows an SSO button when at least one provider is configured.

  • One provider: the button reads Single sign-on, or the surface profile's auth.ssoLabel.
  • Several providers: each button shows its provider's label, unless the surface profile sets auth.ssoLabel.

After sign-in, the user lands on /projects. The provider can send a different page by adding &next=<path> when it redirects back. Nodaro honors it only when it is a relative path on the same origin, starting with / but not //. Anything else goes to /projects. On the hosts listed in CORS_ORIGIN, the landing redirect stays relative, on the host the user arrived on.

Allow sign-in through SSO only

On the Business edition, the surface profile can remove the other sign-in methods:

NODARO_SURFACE_PROFILE={"auth":{"methods":["sso"],"ssoLabel":"Sign in with Acme"}}

A list that names only sso also enables a rule on the server: every signed-in account must have been created or linked through SSO. Any other session is refused on its first API call with 403 sso_required, so an account registered directly on the sign-in service cannot use the install. Adding email to the list disables the rule. See Editions and surface profiles.

Security summary

  • A dedicated secret per provider, distinct from the provider's own session secret.
  • Single-use assertions, with a short, server-enforced lifetime.
  • Same-origin redirects only after sign-in.
  • No secret leaves the server. Assertions and one-time tokens are removed from request logs.
  • Rate limits on the exchange route, per IP address.
  • Ordinary sessions. After the exchange, the user is a normal user with no special credential.

Frequently asked questions

Last updated on

On this page