Authentication
JWT-based authentication for the ZopNight API. Login, signup, SAML SSO, token refresh, PATs, and request headers.
ZopNight uses JWT-based authentication. All API requests (except auth endpoints) require a valid access token.
Overview
The authentication flow works as follows:
- Authenticate via login or SSO to receive an access token and refresh token
- List your organizations with
GET /organisations, then callPOST /auth/refreshwith the chosen org to get an org-scoped access token - Include the access token in the
Authorizationheader for all API requests - When the access token expires, use the refresh token to obtain a new one
- The gateway validates the JWT, extracts your organization context, and routes the request
Authentication Providers
| Provider | How to sign in | Description |
|---|---|---|
| Email/Password | POST /auth/login with "provider": "email" | Standard email and password with OTP verification |
GET /auth/google/callback?code=<authorization code> | OAuth 2.0 with Google accounts | |
| GitHub | GET /auth/github/callback?code=<authorization code> | OAuth 2.0 with GitHub accounts |
| SAML | POST /auth/saml/initiate | Enterprise SSO via SAML 2.0 identity providers |
Login
/auth/login Authenticate and receive tokens.
curl -X POST https://api.zop.dev/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "user@company.com", "password": "your-password", "provider": "email" }'{ "data": { "access_token": "eyJhbGciOiJSUzI1NiIs...", "refresh_token": "dGhpcyBpcyBhIHJlZnJl..." }}The login response does not list your organizations. Fetch them with GET /organisations, then
select one by refreshing the token with its ID (see Token Refresh).
/auth/{provider}/callback Complete a Google or GitHub sign-in.
Google and GitHub sign-in do not use POST /auth/login. After the provider redirects back with an
authorization code, exchange it at the provider’s callback endpoint (google or github). The
response has the same access_token / refresh_token shape as email login.
curl "https://api.zop.dev/auth/google/callback?code=<authorization-code>"List Organizations
/organisations List the organizations you belong to.
curl https://api.zop.dev/organisations \ -H "Authorization: Bearer <access_token>"Signup
/auth/signup Create a new user account.
curl -X POST https://api.zop.dev/auth/signup \ -H "Content-Type: application/json" \ -d '{ "email": "newuser@company.com", "password": "secure-password", "name": "Jane Doe", "provider": "email" }'After signup, verify your email using the OTP sent to your inbox.
Email Verification
/auth/email/verify Request a verification OTP.
curl -X POST https://api.zop.dev/auth/email/verify \ -H "Content-Type: application/json" \ -d '{ "email": "user@company.com" }'/auth/otp/validate Validate the OTP code.
curl -X PATCH https://api.zop.dev/auth/otp/validate \ -H "Content-Type: application/json" \ -d '{ "email": "user@company.com", "otp": "123456" }'Token Refresh
Access tokens are short-lived. Use the refresh token to obtain a new access token before expiry.
Pass claims.tenant-id to mint a token scoped to a specific organization; this is also how you
switch organizations.
/auth/refresh Exchange a refresh token for a new access token.
curl -X POST https://api.zop.dev/auth/refresh \ -H "Content-Type: application/json" \ -d '{ "refresh_token": "dGhpcyBpcyBhIHJlZnJl...", "claims": { "tenant-id": "3f6c2a9e-8b1d-4c7e-9f2a-1d5e6b7c8a90" } }'{ "data": { "access_token": "eyJhbGciOiJSUzI1NiIs...(new token)", "refresh_token": "bmV3IHJlZnJlc2ggdG9r..." }}SAML SSO
Enterprise organizations can configure SAML 2.0 single sign-on with their identity provider.
/auth/saml/initiate Begin a SAML authentication flow.
curl -X POST https://api.zop.dev/auth/saml/initiate \ -H "Content-Type: application/json" \ -d '{ "domain": "enterprise.com", "callback_url": "https://api.zop.dev/auth/saml/callback", "redirect_url": "<your app callback URL>" }'callback_url must be the gateway’s SAML callback. Returns a redirect URL to your organization’s identity provider. After authentication, the IdP redirects back with a SAML assertion that is exchanged for JWT tokens.
Create Organization
/organisations Create a new organization for your account.
curl -X POST https://api.zop.dev/organisations \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "name": "My Company" }'Password Reset
/auth/password/forget Initiate a password reset.
curl -X POST https://api.zop.dev/auth/password/forget \ -H "Content-Type: application/json" \ -d '{ "email": "user@company.com" }'/auth/password/reset Complete the password reset.
curl -X PATCH https://api.zop.dev/auth/password/reset \ -H "Content-Type: application/json" \ -d '{ "email": "user@company.com", "otp": "123456", "password": "new-secure-password" }'Logout
/auth/logout Invalidate the current session.
curl -X POST https://api.zop.dev/auth/logout \ -H "Content-Type: application/json" \ -d '{ "refresh_token": "dGhpcyBpcyBhIHJlZnJl..." }'The session to invalidate is identified by the refresh token in the body.
Personal Access Tokens (PAT)
For CI pipelines, scripts, and other non-interactive clients, ZopNight supports Personal Access Tokens. PATs use the same Authorization: Bearer header as JWT access tokens but are validated against the auth service directly rather than through JWKS — they don’t expire on the same schedule as user JWTs.
| Property | Value |
|---|---|
| Prefix | zn_pat_ followed by an opaque secret |
| Where to use | Authorization: Bearer zn_pat_... |
| Created from | The ZopNight UI under Settings → API & MCP → API Tokens → + Create Token |
| Organizations | Required. The token is bound to the organizations you select; calls to any other org are refused with 403 |
| Permissions | Chosen per token and capped by your role in each org. Leave every permission unticked for a read-only token |
| Expiry | 30 days, 90 days, 1 year, a custom date, or no expiry |
curl https://api.zop.dev/resources \ -H "Authorization: Bearer zn_pat_xxxxxxxxxxxxxxxxxxxxxxxx"Organization Context
Most JWTs include the active organization in the tenant-id or org_id claim, and that value is used to scope every request. The X-Org-Id header is a fallback only: it is read when the token carries no org claim. It cannot override an org claim. To switch organizations with a session token, refresh it with claims.tenant-id.
| Header | Value | When to use |
|---|---|---|
X-Org-Id | The target organization’s ID (a UUID) | Only when the token carries no org claim. A token’s org claim always wins. |
Do not put the organization in the URL path. Paths such as /orgs/{orgId}/resources return 404; call /resources and the gateway adds the org for you.
Unauthenticated Endpoints
A small number of endpoints intentionally bypass JWT validation, including the ones below. They use other mechanisms (HMAC signatures, body-carried JWTs, or no auth at all):
| Endpoint | Authentication | Purpose |
|---|---|---|
POST /webhooks/github | HMAC-SHA256 signature in X-Hub-Signature-256 | GitHub webhooks for the deploy pipeline |
POST /build-callbacks/{revisionID} | JWT carried in the request body, signed with the build callback secret | Build runner reports back when an image is ready |
/auth/* (any method) | Varies (login, signup, refresh, OAuth callbacks) | Authentication flows themselves |
/oauth/* | OAuth 2.1 client authentication | Authorization server used by MCP clients |
Request Headers
| Header | Value | Required |
|---|---|---|
Authorization | Bearer <access_token | PAT> | Yes (all endpoints except those listed above) |
X-Org-Id | An organization ID | Optional. Used only when the token carries no org claim |
Content-Type | application/json | Yes (for POST/PUT/PATCH requests) |