Skip to content

Auth

Auth decides who can sign in and what they can do. You must choose a backend. If MARIMOHUB_AUTH_BACKEND is unset, marimohub refuses to start instead of falling back to local auth.

Selector: MARIMOHUB_AUTH_BACKEND. Full variables: Configuration -> Auth.

Project roles decide who can edit a notebook. The editor sandbox-sharing policy controls whether those editors share one live sandbox or use exclusive ownership.

Choose a backend

BackendSelectorUse for
OIDCoidcProduction with Google, Okta, Auth0
Trusted proxy headersproxy-headeroauth2-proxy, Google IAP, Tailscale Serve
Cloudflare Accesscloudflare-accessWorkers deployments behind Access
Dev bypassdevLocal development only

Configure it

OIDC (production)

App-native OpenID Connect is the production backend. marimohub discovers the provider endpoints from /.well-known/openid-configuration. You supply the issuer, client credentials, and redirect URI.

bash
MARIMOHUB_AUTH_BACKEND=oidc
MARIMOHUB_AUTH_OIDC_ISSUER=https://accounts.example.com
MARIMOHUB_AUTH_OIDC_CLIENT_ID=
MARIMOHUB_AUTH_OIDC_CLIENT_SECRET=
MARIMOHUB_AUTH_OIDC_REDIRECT_URI=https://hub.example.com/api/auth/callback
MARIMOHUB_AUTH_SESSION_SECRET=            # signs the session cookie (HS256, ≥32 bytes)
MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS=example.com  # REQUIRED allowlist (verified email); `*` allows all
# MARIMOHUB_AUTH_OIDC_AUDIENCE=…           # deprecated and ignored; aud must contain the client ID
# MARIMOHUB_AUTH_OIDC_PROMPT=consent       # optional: override the default (select_account) OAuth prompt
# MARIMOHUB_AUTH_OIDC_SCOPES="openid email profile groups" # add only provider-required scopes

The redirect URI is always https://<your-host>/api/auth/callback. Register this exact value with your provider. A different value causes a redirect_uri_mismatch error. ALLOWED_EMAIL_DOMAINS is required. Set one or more domains, or set * to allow all.

If the provider publishes UserInfo, marimohub uses it for profile claims. UserInfo must have the same sub as the validated ID token. Email verification is required by default. If a trusted issuer omits email_verified, use MARIMOHUB_AUTH_OIDC_EMAIL_VERIFICATION=trusted-issuer. This mode also permits an omitted claim when a domain allowlist is active. If the claim is present, its value must be boolean true.

The signed session JWT has a 3,800-byte limit. If necessary, marimohub omits the profile picture first and the display name second. Required identity and authorization claims are never omitted. Login fails if they exceed the limit.

The issuer, callback, discovered authorization, and discovered logout endpoints must use HTTPS and cannot contain embedded credentials.

Groups and roles

Group authorization is optional and uses exact provider group IDs. Set a JSON Pointer to the provider array. Then set at least one group policy:

bash
MARIMOHUB_AUTH_OIDC_GROUPS_CLAIM=/groups
MARIMOHUB_AUTH_OIDC_ALLOWED_GROUPS=hub-users
MARIMOHUB_AUTH_OIDC_SUPER_ADMIN_GROUPS=hub-platform-admins
MARIMOHUB_AUTH_OIDC_PROJECT_CREATION_GROUPS=hub-project-creators
MARIMOHUB_AUTH_OIDC_DEFAULT_VIEWER_GROUPS=hub-viewers
MARIMOHUB_AUTH_OIDC_DEFAULT_EDITOR_GROUPS=hub-editors
MARIMOHUB_AUTH_OIDC_DEFAULT_MANAGER_GROUPS=hub-project-managers

Nested claims use JSON Pointer syntax, such as /realm_access/roles. ALLOWED_GROUPS controls login. The other lists map groups to internal entitlements. The session cookie stores mapped entitlements, not raw groups.

PROJECT_CREATION_GROUPS controls who can create projects:

  • If the variable is not set, all authenticated users can create projects.
  • If the value is empty, only super admins can create projects.
  • If the value contains group IDs, super admins and matching users can create projects.

If no super admin is configured, an empty value prevents every user from creating projects. Setting the variable implies MARIMOHUB_PROJECT_CREATION=restricted, which also works without group mapping; combining it with MARIMOHUB_PROJECT_CREATION=open is rejected at startup.

An empty value does not require GROUPS_CLAIM. A non-empty value requires the claim and creates a group-derived session entitlement.

Group sessions last at most one hour by default. This limit bounds the delay after an IdP removes a user from a group. Kernels inherit the session JWT expiry as a fixed authorization deadline. Active editors cannot extend it. Session reuse keeps the earliest caller credential deadline. At expiry, the lifecycle destroys the kernel and the proxy closes WebSockets. This teardown skips the final capture so that the kernel stops promptly. Periodic snapshots limit potential data loss.

Missing, malformed, or oversized group data cannot satisfy the login policy. marimohub accepts at most 200 group IDs. It does not resolve group-overage references from the provider. Configure the IdP to emit only the groups that marimohub needs. Group-derived roles and project-creation access apply only to the browser session. They do not transfer to personal access tokens.

After you enable project-creation groups, matching users must sign in again. Existing sessions do not contain the new entitlement.

For a strict rollout, first deploy the new version without the variable. Then set the variable after all replicas run the new version.

The user ID is the OIDC sub within the configured issuer. The same sub from another issuer can identify a different person. Therefore, an issuer URL change is an identity migration. Reconcile stored owners and members before the change.

Generate a session secret with openssl rand -base64 32.

Login-policy module

When a group mapping cannot express your access rule — for example, an approved department AND a minimum level AND a set of required attribute values — load a trusted login-policy module instead:

bash
MARIMOHUB_AUTH_OIDC_LOGIN_POLICY_BACKEND=library
MARIMOHUB_AUTH_OIDC_LOGIN_POLICY_LIBRARY=/etc/marimohub/oidc-login-policy.mjs
# MARIMOHUB_AUTH_OIDC_LOGIN_POLICY_TIMEOUT_SECONDS=5        # 1–30; a timeout denies login
# MARIMOHUB_AUTH_OIDC_LOGIN_POLICY_SESSION_TTL_SECONDS=3600 # 300–3600

The built-in adapter still completes all OIDC protocol work: discovery, PKCE, state and nonce, ID-token verification, UserInfo subject binding, email verification, and the email-domain allowlist. The module runs after that validation and before session signing. It receives the validated ID-token and UserInfo claims as separate read-only objects and returns one bounded result: an allow or deny decision, plus the built-in entitlements (super-admin, project-creator, default-role:viewer, default-role:editor, default-role:manager). project-creator is only meaningful when MARIMOHUB_PROJECT_CREATION=restricted; without it every authenticated user can create projects. MARIMOHUB_AUTH_OIDC_LOGIN_POLICY_BACKEND=none (or unset) disables the module.

Login-policy configuration is mutually exclusive with the group variables above. A module can reproduce any group rule in code. The module applies to browser sessions only; personal access tokens never receive login-policy entitlements.

The module is trusted code and runs in-process with server privileges. Bundle it (with its dependencies) into one .mjs file, pin its version, and mount the same artifact on every replica. A module that fails to load stops the server at startup. During login, a policy denial shows the user a generic access-policy message; a policy error, timeout, or malformed result fails closed with the generic sign-in error and a bounded operator log event — the host never persists, logs, or writes raw claims into the session cookie. That guarantee covers the host only: the module sees every claim and runs with server privileges, so your policy code must not log or store claim values, and reviews should verify that it doesn't.

Policy sessions last at most one hour, like group sessions, which bounds the delay after an attribute or policy change. A module change requires a server restart and takes effect on the next login.

This feature maps identity to login eligibility and coarse roles. It is not resource-level access control: it cannot see projects or notebooks, and an entitlement never bypasses project-role checks. See Security for the boundary.

See examples/external-adapter/oidc-login-policy.mjs for a complete example.

Google

  1. In the Google Cloud Console, open APIs & Services → Credentials.
  2. Create Credentials → OAuth client ID, application type Web application.
  3. Under Authorized redirect URIs, add https://hub.example.com/api/auth/callback.
  4. Copy the Client ID and Client secret.
bash
MARIMOHUB_AUTH_OIDC_ISSUER=https://accounts.google.com
MARIMOHUB_AUTH_OIDC_CLIENT_ID=…apps.googleusercontent.com
MARIMOHUB_AUTH_OIDC_CLIENT_SECRET=
MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS=example.com   # a single domain is also sent to Google as the `hd` hint

The default OAuth prompt is select_account, which displays the Google account chooser. Set MARIMOHUB_AUTH_OIDC_PROMPT=consent to display the consent screen again.

See Google's OpenID Connect docs.

Microsoft Entra ID

  1. In the Entra admin center (or Azure Portal), go to App registrations → New registration.
  2. Set a Web redirect URI of https://hub.example.com/api/auth/callback.
  3. From Overview, copy the Application (client) ID and Directory (tenant) ID; under Certificates & secrets, create a client secret.
bash
# tenant-scoped issuer (use `organizations` or `common` for multi-tenant)
MARIMOHUB_AUTH_OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0
MARIMOHUB_AUTH_OIDC_CLIENT_ID=<application-client-id>
MARIMOHUB_AUTH_OIDC_CLIENT_SECRET=

See Microsoft's OIDC docs.

Okta

  1. In the Okta Admin Console, open Applications → Create App Integration.
  2. Choose OIDC - OpenID Connect and Web Application.
  3. Add https://hub.example.com/api/auth/callback as a Sign-in redirect URI.
  4. Copy the Client ID and Client secret from the app's General tab.
bash
MARIMOHUB_AUTH_OIDC_ISSUER=https://<your-org>.okta.com
MARIMOHUB_AUTH_OIDC_CLIENT_ID=
MARIMOHUB_AUTH_OIDC_CLIENT_SECRET=

If you use an Okta authorization server, the issuer is https://<your-org>.okta.com/oauth2/<server-id>. See Okta's OIDC docs.

Auth0

  1. In the Auth0 Dashboard, open Applications → Create Application and pick Regular Web Application.
  2. Under Settings → Allowed Callback URLs, add https://hub.example.com/api/auth/callback.
  3. Copy the Domain, Client ID, and Client Secret from Settings.
bash
# note the trailing slash on the issuer
MARIMOHUB_AUTH_OIDC_ISSUER=https://<tenant>.auth0.com/
MARIMOHUB_AUTH_OIDC_CLIENT_ID=
MARIMOHUB_AUTH_OIDC_CLIENT_SECRET=

See Auth0's OIDC docs.

Trusted proxy headers

Use this backend behind oauth2-proxy, Tailscale Serve, Google IAP, or another SSO proxy.

CAUTION: In header mode, block direct access to marimohub. The proxy must remove client-supplied identity headers.

Both modes require MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS. Set * only to allow all authenticated domains.

oauth2-proxy can supply the default marimohub headers:

bash
MARIMOHUB_AUTH_BACKEND=proxy-header
MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS=example.com

oauth2-proxy sends these headers when --pass-user-headers=true. Current releases enable this option by default.

If you disable this option, marimohub receives no identity and returns 401.

For Nginx auth_request, enable --set-xauthrequest. Copy both response headers upstream. Then set their names in marimohub:

bash
MARIMOHUB_AUTH_PROXY_HEADER=X-Auth-Request-Email,X-Auth-Request-User

For a custom pair, set two comma-separated names:

bash
MARIMOHUB_AUTH_PROXY_HEADER=X-Auth-Email,X-Auth-Subject

Tailscale Serve supplies both values in one header:

bash
MARIMOHUB_AUTH_BACKEND=proxy-header
MARIMOHUB_AUTH_PROXY_HEADER=Tailscale-User-Login
MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS=example.com

For Google IAP, set the signed-header JWT audience:

bash
MARIMOHUB_AUTH_BACKEND=proxy-header
MARIMOHUB_AUTH_PROXY_JWT_AUDIENCE=/projects/123456789/global/backendServices/987654321
MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS=example.com

The audience selects JWT mode. The adapter uses the IAP header, issuer, and JWKS URL by default.

You can override these values for an IAP-compatible deployment:

bash
MARIMOHUB_AUTH_PROXY_HEADER=X-Verified-Assertion
MARIMOHUB_AUTH_PROXY_JWT_ISSUER=https://issuer.example.com
MARIMOHUB_AUTH_PROXY_JWKS_URL=https://issuer.example.com/.well-known/jwks.json

JWT mode accepts ES256 assertions only. It verifies the signature, issuer, audience, lifetime, subject, and email.

See the provider guides for oauth2-proxy headers, Tailscale Serve headers, and Google IAP signed headers.

Cloudflare Access

Cloudflare Access is used by the Workers entrypoint. It reads unprefixed runtime variables (AUTH_MODE, ACCESS_TEAM, ACCESS_AUD) from the Worker environment. See Deploying on Cloudflare.

Dev bypass

A fixed, unauthenticated identity for local development only — never use it in production (it lets anyone in as the same user).

bash
MARIMOHUB_AUTH_BACKEND=dev
# all optional — these are the defaults:
MARIMOHUB_AUTH_DEV_USER_ID=user
MARIMOHUB_AUTH_DEV_EMAIL=user@localhost
MARIMOHUB_AUTH_DEV_NAME='Local Dev'

Verify it

After deployment:

  1. Start the server without an authentication configuration error.
  2. Sign in through the configured provider.
  3. Create a project.
  4. Add a second user with a lower role.
  5. Verify that the second user has only the permitted access.

Production cautions

  • Do not use dev auth for any deployment that serves real users.
  • Set MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS for OIDC and proxy-header. Use * only to allow all domains.
  • In proxy-header mode, block proxy bypasses and remove client-supplied identity headers.
  • Review MARIMOHUB_DEFAULT_ROLE before launch. The default is permissive for a trusted single-tenant deployment.
  • Treat auth errors as fail-closed until configuration proves otherwise.

Authorization roles

Authentication decides who you are. Authorization decides what you may do on a project. Each project has an owner, who is implicitly admin, and a member list. Roles are ordered viewer < editor < manager < admin; each role includes the capabilities below it. Manager is the highest role that can be assigned to a member. Admin is reserved for project owners, deployment super admins, and legacy member rows. One deployment-wide exception sits above this per-project model: a super admin is treated as admin on every project.

RoleDescription
viewerRead projects, notebooks, code, and version history. Cannot change state.
editorViewer access, plus create, update, and delete notebooks, restore versions, and run kernel sessions.
managerEditor access, plus update or delete the project and manage members.
adminReserved authority with all Manager capabilities.
Capabilityviewereditormanageradmin
See projects and notebooks, read versions and codexxxx
Create, update, and delete notebooks; save and restore versionsxxx
Start and stop kernel sessionsxxx
Start, open, and use notebook apps*xxx
Stop or restart the shared notebook appxxx
Update or delete projects; manage membersxx

* Viewers get app access only when the deployment sets MARIMOHUB_VIEWER_MODE=applications (or ephemeral-sandbox) — see What viewers see and Notebook apps.

Enforcement is server-side. A write with an insufficient role returns 403 FORBIDDEN. By default any authenticated user can create a project; the creator becomes the project owner. Set MARIMOHUB_PROJECT_CREATION=restricted to limit creation to super admins and holders of the project-creator entitlement, granted by an OIDC group mapping or login-policy module. Projects and notebooks can also carry security labels, which only remove access on top of the role.

Members: user ids and email invites

A member is identified by user id (canonical) or by email. Managers can add a member either way: a known email — someone who has signed in before — is resolved to their user id, while an unknown email is stored as a pending invite. At request time the caller matches a membership by their user id or, case-insensitively, by their login email, so an invite grants access the first time that person signs in, with no extra step. One person can never hold both an invite row and an id row — adding a member is rejected (409) when any of their known identifiers is already on the roster, so removing a member always revokes their access.

After an invitee signs in, the next membership write or maintenance sweep replaces their email invite with a user-id row while preserving their role. A legacy roster that contains both forms is collapsed to one user-id row with the higher role. Email matching remains active until that claim occurs, so access is continuous.

The login email grants access, so OIDC requires email_verified: true by default. trusted-issuer permits an enterprise issuer to omit the claim, including when a domain allowlist is active. If the claim is present, its value must be boolean true.

Invite emails are PII of people who never signed in: the members list and project detail show them only to project managers (and to the invitee themself). The add-member picker searches the user directory (GET /api/v1/users/search — email, name, or id substring; everyone who has signed in at least once). Under MARIMOHUB_DEFAULT_ROLE=none the caller must own or belong to at least one project to search; with a default role set — or as a super admin — any authenticated user may.

Rollout note: code older than this feature cannot parse a project.json containing an email invite row. Finish rolling out a release with this feature before creating email invites, and treat a rollback across it as requiring those invites to be removed first.

What viewers see: MARIMOHUB_VIEWER_MODE

What a viewer gets depends on MARIMOHUB_VIEWER_MODE. The modes are ordered: each tier includes everything the previous one grants.

  • static (default): opening a notebook shows the last captured HTML snapshot. No compute, no code execution. Apps stay editor-only.
  • applications: additionally, viewers can use notebook apps — start one, open it, and keep it alive while they have it open. The app is the same shared, per-notebook session editors use (viewers cannot stop or restart it). Note that the app kernel runs notebook code with the project's integration secrets and federated credentials, so enable this only for audiences you trust with what the app can reach. Opening a notebook (rather than its app) still shows the static snapshot.
  • ephemeral-sandbox: additionally, opening a notebook provisions a real kernel in a temporary, private session. The viewer can run and edit code, but nothing is written back — no version, snapshot, or workspace changes. Edits are discarded when the session ends.

Ephemeral sessions are per-user: each viewer gets their own sandbox, isolated from every other user's, and only its owner can reach it. Refreshing or re-opening the notebook reconnects to the same live session, so in-session state survives a reload; the session ends on explicit Stop or after the idle timeout, and the next visit starts fresh from the notebook's saved version.

Default access for non-members

A logged-in user who is not the owner or a member falls back to MARIMOHUB_DEFAULT_ROLE:

  • editor (default): every logged-in user can edit notebooks and run sessions in any project, but cannot update or delete projects.
  • manager: every logged-in user can manage every project. Use only in a fully trusted deployment.
  • viewer: every logged-in user can read any project.
  • none: non-members cannot see projects they do not own or belong to.

Super admins: MARIMOHUB_SUPER_ADMINS

MARIMOHUB_SUPER_ADMINS is a comma-separated list of operators who are treated as admin on every project, regardless of membership or MARIMOHUB_DEFAULT_ROLE. A super admin can see and list all projects (even under MARIMOHUB_DEFAULT_ROLE=none), read and write every notebook, secret, and integration, control any session, and read the audit trail. It is the one grant that overrides the per-project role model. Only super admins can manage organization-wide integrations. Project roles never grant this access.

The web application gives super admins access to the users, settings, audit-log, and debug pages. They can suspend or reactivate any other user from the users page. The audit page uses GET /api/v1/events, which returns at most 30 UTC days per query. The debug page runs the sandbox startup diagnostic. Project managers retain access to each project's daily audit log.

Existing non-owner Admin memberships remain valid and can be demoted or removed, but the API does not allow new Admin assignments. A deployment introducing Manager must stop all old replicas before the first Manager row is stored; older versions cannot parse that role. Rolling back requires converting Manager rows first.

An entry containing @ matches the caller's login email, case-insensitively; any other entry matches the user id (the IdP sub) exactly. The two namespaces do not overlap — an email entry never elevates a caller whose id happens to equal that string, and vice versa. Email matching trusts the IdP-asserted login email, the same trust model as email invites.

Two bounds still hold for a super admin: a project owner cannot be demoted or removed, and a soft-deleted project stays unreachable (404) like it is for everyone else. Session and app rate caps are not bypassed. A personal access token minted by a super admin carries the same power, so scope those tokens accordingly. Unset (the default) means no super admins.

Deprovisioning and user suspension

Super admins can suspend a known user from Admin -> Users, or with PUT /api/v1/admin/users/{id}/suspension; DELETE on the same path reactivates the user. Suspension blocks both browser-session authentication and personal access tokens. Requests authenticated with a browser session receive 403 USER_SUSPENDED; PAT authentication fails as an invalid credential. Suspension and reactivation write user.suspended and user.unsuspended audit events with the operator and target user ids.

Enforcement uses a bounded per-user cache in each server process. An active result is fresh for 10 seconds, then served stale while it refreshes until a hard limit of 30 seconds. Past that limit the request waits for storage; if the status cannot be verified, the API fails closed with 503 SERVICE_UNAVAILABLE. A suspended result is cached for five minutes and remains denied while a stale entry refreshes. This asymmetry bounds unauthorized access without making a storage outage reactivate anyone. Pair suspension with session revocation at the identity provider when access must end immediately.

Profile and suspension updates use ETag compare-and-swap. An authenticated profile refresh therefore cannot overwrite a concurrent suspension change.

Suspension does not terminate an already-running notebook sandbox. Its normal lifetime and idle policies still apply. This lifecycle flag is also the intended target for future SCIM deprovisioning: a SCIM active: false update can suspend the same identity without changing the authentication-time enforcement path.

Troubleshooting

See Troubleshooting -> Login fails.

Provider-agnostic. Deploy anywhere.