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 |
| Microsoft | GET /auth/azure/callback?code=<authorization code> | OAuth 2.0 with Microsoft (Entra ID) accounts |
| SAML | POST /auth/saml/initiate | Enterprise SSO via SAML 2.0 identity providers |
Login
/auth/loginAuthenticate 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}/callbackComplete a Google, GitHub or Microsoft sign-in.
Google, GitHub and Microsoft 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, github or
azure for Microsoft). 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
/organisationsList the organizations you belong to.
curl https://api.zop.dev/organisations \
-H "Authorization: Bearer <access_token>"Signup
/auth/signupCreate 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/verifyRequest 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/validateValidate 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/refreshExchange 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/initiateBegin 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
/organisationsCreate 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/forgetInitiate 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/resetComplete 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/logoutInvalidate 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) |