API

Authentication

Browser sessions, OAuth discovery, and authentication handoffs.

The Auth API is how Openship signs users in and keeps their session. Almost everything here is served by Better Auth through a single catch-all — email/password sign-in and sign-up, OAuth, the organization/member/invitation endpoints, and the MCP OAuth authorization server. A small set of desktop-only endpoints sit in front of it to bootstrap a zero-auth local session and hand off cloud sign-in to the downloadable app.

This module is consumed almost entirely by the dashboard (and, in desktop mode, the Electron host) — you rarely call it directly. For programmatic API access, mint a personal access token instead (openship token create) and use it as a bearer header. See the API overview and Auth & sessions for the full model.

The Better Auth catch-all

The last route, GET|POST /api/auth/*, forwards the raw request to Better Auth's handler. Everything a normal sign-in flow needs lives here — there is no separate schema in this module because Better Auth owns the request and response shapes. The endpoints it serves (all under /api/auth) include:

  • Credentials — POST /sign-in/email, POST /sign-up/email, and POST /sign-out. Passwords are 8–128 characters. Email verification and password reset use codes through POST /email-otp/send-verification-otp, POST /email-otp/verify-email, and POST /email-otp/reset-password; delivery requires working instance mail or Cloud mail.
  • OAuth — POST /sign-in/social and GET /callback/:provider for GitHub and Google (each enabled only when its client ID/secret is set).
  • Passkeys — GET /passkey/generate-register-options, POST /passkey/verify-registration, GET /passkey/generate-authenticate-options, POST /passkey/verify-authentication, GET /passkey/list-user-passkeys, and POST /passkey/delete-passkey. Use the Better Auth browser client to perform the WebAuthn ceremony; raw credentials are never a substitute for it.
  • Two-factor authentication — POST /two-factor/enable returns an authenticator URI and recovery codes after checking the current password. POST /two-factor/verify-totp confirms enrollment or a pending login. POST /two-factor/verify-backup-code consumes a recovery code. Disabling protection (POST /two-factor/disable) and replacing codes (POST /two-factor/generate-backup-codes) require the current password. OAuth-only accounts can add their first password through POST /account-security/set-password with a recent authenticated session.
  • Organizations — /organization/*: create, set-active, list, invite-member, accept-invitation, update-member-role, remove-member, leave, and more. Member-listing reads are additionally shielded so restricted/member roles can't read admin-tier data.
  • MCP OAuth 2.1 — /mcp/authorize, /mcp/token, /mcp/register (dynamic client registration), /mcp/get-session, plus discovery at /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource (also re-served at the origin root). See the MCP API.

Roles and permissions live elsewhere

Better Auth's organization plugin only carries the role label (owner, admin, member, or restricted). The actual authorization for the rest of the API is resolved against resource grants — see Permissions.

For an account with 2FA enabled, a primary sign-in returns { "twoFactorRedirect": true } instead of a usable session. Preserve the signed challenge cookie and submit an authenticator or recovery code; only successful verification issues the session. Social callbacks redirect to the dashboard's /two-factor page and resume their validated destination after verification. Account security endpoints are available only where local accounts are hosted, including Openship Cloud's account service and self-hosted VPS installations. See Auth & sessions.

Get the current session

GET /api/auth/get-session

Returns the active session and user as JSON, or 401 when unauthenticated. In zero-auth desktop mode (self-hosted onboarding, no password), a miss triggers an inline bootstrap: Openship provisions the local admin user, mints a session, sets the cookie on the response, and returns it — so the dashboard's first navigation succeeds instead of looping to /login.

curl "$OPENSHIP_URL/api/auth/get-session" \
  -H "Cookie: openship.session_token=<session>"

Desktop session bootstrap

GET /api/auth/desktop-login mints a session for the zero-auth local admin and redirects to the dashboard. It is called once, right after self-hosted onboarding completes; if the instance isn't in zero-auth mode it redirects to /login instead. No request body.

Cloud sign-in handoff (desktop)

When the downloadable app signs in "with Cloud", the Electron host runs a PKCE flow and the API brokers it. The sequence is: register the nonce, open the system browser, poll, then claim.

POST /api/auth/desktop-auth-start

Guarded by the internal shared-token middleware — only the Electron host may register a nonce. Validated inline (there is no TypeBox schema module for auth); all three fields are required strings:

Prop

Type

The remaining steps take query parameters, not bodies:

  • GET /api/auth/cloud-callback?code=<code>&state=<state> — the cloud provider redirects here. With state it runs the desktop PKCE exchange and resolves the pending nonce; without state it runs the simpler browser exchange and sets the cookie directly. code is required.
  • GET /api/auth/desktop-auth-poll?nonce=<nonce> — Electron polls until the status is resolved, then opens the claim URL. nonce is required.
  • GET /api/auth/desktop-claim?code=<code> — exchanges the one-time claim code for a session cookie (set via the Set-Cookie header for cross-version Electron reliability) and redirects to the dashboard. code is required.

Errors

401 — Unauthorized

GET /api/auth/get-session returns this when there's no valid session and the instance is not in zero-auth mode. Sign in through the dashboard, or send a valid session cookie / bearer token.

400 — missing nonce, state, or code_verifier

POST /api/auth/desktop-auth-start rejects a body that's missing any of the three fields. The poll and claim routes return 400 similarly when their required query parameter is absent, and claim returns 400 Claim expired once the one-time code has been used or has timed out.

429 — too many requests

POST /api/auth/* shares a 10/min per-IP bucket. Sign-in/sign-up loops that hammer it will be throttled before the default API limit fires.

Operations

OperationREST API
Handle browser sessions and authentication through Better Auth.GET /api/auth/*
Handler authentication
Complete a browser-bound Cloud repository authorization; ordinary GitHub sign-in callbacks remain handled by Better Auth.GET /api/auth/callback/github
Handler authentication
Handle sign-in, account, and session mutations through Better Auth.POST /api/auth/*
Handler authentication
Exchange a cloud auth code for a local session cookie.GET /api/auth/cloud-callback
Handler authentication
Electron polls this by nonce until the cloud sign-in resolves.GET /api/auth/desktop-auth-poll
Handler authentication
Register a (nonce, state, PKCE verifier) tuple before the system browser opens.POST /api/auth/desktop-auth-start
Internal operator
Exchange a one-time claim code for a session cookie, then redirect.GET /api/auth/desktop-claim
Handler authentication
Mint a session for the zero-auth local admin and redirect to the dashboard.GET /api/auth/desktop-login
Handler authentication
Return the current session; in zero-auth desktop mode, bootstrap one on first hit.GET /api/auth/get-session
Handler authentication
Read a safe preview of a team invitation.GET /api/auth/invitation-preview/:id
Handler authentication
Read the public keys used to verify MCP OAuth tokens.GET /api/auth/mcp/jwks
Handler authentication
Read the identity associated with an MCP OAuth token.GET /api/auth/mcp/userinfo
Handler authentication
Accept an invitation through the browser session boundary.POST /api/auth/organization/accept-invitation
Handler authentication
Cancel a pending invitation through the browser session boundary.POST /api/auth/organization/cancel-invitation
Handler authentication
Invite a member through the browser session boundary.POST /api/auth/organization/invite-member
Handler authentication
Leave the active organization.POST /api/auth/organization/leave
Handler authentication
Reject a pending invitation through the browser session boundary.POST /api/auth/organization/reject-invitation
Handler authentication
Remove a member through the browser session boundary.POST /api/auth/organization/remove-member
Handler authentication
Update a member role through the browser session boundary.POST /api/auth/organization/update-member-role
Handler authentication
Handle sign-up with the installation's account-creation policy.POST /api/auth/sign-up/*
Handler authentication
Discover the OAuth authorization server.GET /.well-known/oauth-authorization-server
Public
Discover the OAuth authorization server.GET /.well-known/oauth-authorization-server/api/mcp
Public
Discover the OAuth authorization server.GET /.well-known/oauth-authorization-server/api/proxy/api/mcp
Public
Discover OAuth metadata for this protected resource.GET /.well-known/oauth-protected-resource
Public
Discover OAuth metadata for this protected resource.GET /.well-known/oauth-protected-resource/api/mcp
Public
Discover OAuth metadata for this protected resource.GET /.well-known/oauth-protected-resource/api/proxy/api/mcp
Public
Close a completed browser authentication handoff.GET /auth/callback/close
Browser callback
Complete the GitHub App browser handoff using installation state.GET /auth/callback/install
Browser callback

On this page