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, andPOST /sign-out. Passwords are 8–128 characters. Email verification and password reset use codes throughPOST /email-otp/send-verification-otp,POST /email-otp/verify-email, andPOST /email-otp/reset-password; delivery requires working instance mail or Cloud mail. - OAuth —
POST /sign-in/socialandGET /callback/:providerfor 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, andPOST /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/enablereturns an authenticator URI and recovery codes after checking the current password.POST /two-factor/verify-totpconfirms enrollment or a pending login.POST /two-factor/verify-backup-codeconsumes 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 throughPOST /account-security/set-passwordwith 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-serverand/.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-sessionReturns 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-startGuarded 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. Withstateit runs the desktop PKCE exchange and resolves the pending nonce; withoutstateit runs the simpler browser exchange and sets the cookie directly.codeis required.GET /api/auth/desktop-auth-poll?nonce=<nonce>— Electron polls until the status isresolved, then opens the claim URL.nonceis required.GET /api/auth/desktop-claim?code=<code>— exchanges the one-time claim code for a session cookie (set via theSet-Cookieheader for cross-version Electron reliability) and redirects to the dashboard.codeis 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
| Operation | REST 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/githubHandler 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-callbackHandler authentication |
Electron polls this by nonce until the cloud sign-in resolves. | GET /api/auth/desktop-auth-pollHandler authentication |
Register a (nonce, state, PKCE verifier) tuple before the system browser opens. | POST /api/auth/desktop-auth-startInternal operator |
Exchange a one-time claim code for a session cookie, then redirect. | GET /api/auth/desktop-claimHandler authentication |
| Mint a session for the zero-auth local admin and redirect to the dashboard. | GET /api/auth/desktop-loginHandler authentication |
| Return the current session; in zero-auth desktop mode, bootstrap one on first hit. | GET /api/auth/get-sessionHandler authentication |
| Read a safe preview of a team invitation. | GET /api/auth/invitation-preview/:idHandler authentication |
| Read the public keys used to verify MCP OAuth tokens. | GET /api/auth/mcp/jwksHandler authentication |
| Read the identity associated with an MCP OAuth token. | GET /api/auth/mcp/userinfoHandler authentication |
| Accept an invitation through the browser session boundary. | POST /api/auth/organization/accept-invitationHandler authentication |
| Cancel a pending invitation through the browser session boundary. | POST /api/auth/organization/cancel-invitationHandler authentication |
| Invite a member through the browser session boundary. | POST /api/auth/organization/invite-memberHandler authentication |
| Leave the active organization. | POST /api/auth/organization/leaveHandler authentication |
| Reject a pending invitation through the browser session boundary. | POST /api/auth/organization/reject-invitationHandler authentication |
| Remove a member through the browser session boundary. | POST /api/auth/organization/remove-memberHandler authentication |
| Update a member role through the browser session boundary. | POST /api/auth/organization/update-member-roleHandler 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-serverPublic |
| Discover the OAuth authorization server. | GET /.well-known/oauth-authorization-server/api/mcpPublic |
| Discover the OAuth authorization server. | GET /.well-known/oauth-authorization-server/api/proxy/api/mcpPublic |
| Discover OAuth metadata for this protected resource. | GET /.well-known/oauth-protected-resourcePublic |
| Discover OAuth metadata for this protected resource. | GET /.well-known/oauth-protected-resource/api/mcpPublic |
| Discover OAuth metadata for this protected resource. | GET /.well-known/oauth-protected-resource/api/proxy/api/mcpPublic |
| Close a completed browser authentication handoff. | GET /auth/callback/closeBrowser callback |
| Complete the GitHub App browser handoff using installation state. | GET /auth/callback/installBrowser callback |