OpenID Connect
Quota implements the OpenID Connect discovery spec, publishes a JWKS, and signs id_tokens with RS256. Off-the-shelf clients like NextAuth and Auth.js bootstrap from a single discovery URL — no per-endpoint configuration required.
01 Discovery document
The discovery endpoint advertises every URL, supported algorithm, and capability flag a client needs to talk to Quota. Cached for one hour (Cache-Control: public, max-age=3600) — clients can safely re-fetch on startup without rate-limit concerns.
curl https://api.usequota.ai/.well-known/openid-configurationResponse
{
"issuer": "https://api.usequota.ai",
"authorization_endpoint": "https://api.usequota.ai/oauth/authorize",
"token_endpoint": "https://api.usequota.ai/oauth/token",
"userinfo_endpoint": "https://api.usequota.ai/oauth/userinfo",
"jwks_uri": "https://api.usequota.ai/.well-known/jwks.json",
"revocation_endpoint": "https://api.usequota.ai/oauth/revoke",
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"none"
],
"grant_types_supported": ["authorization_code", "refresh_token"],
"scopes_supported": [
"openid", "profile", "email",
"credits.read", "credits.spend",
"account.read", "account.write",
"apps.read", "apps.write"
],
"claims_supported": [
"sub", "iss", "aud", "exp", "iat", "auth_time",
"nonce", "email", "email_verified",
"name", "picture"
],
"code_challenge_methods_supported": ["S256", "plain"]
}Notable fields
| issuerstring | The canonical origin. id_tokens carry this as their iss claim — verify it on every token. |
| token_endpoint_auth_methods_supportedstring[] | client_secret_post for confidential clients; none for public PKCE clients (mobile, SPA) that cannot store a secret. |
| id_token_signing_alg_values_supportedstring[] | RS256 only. Verify with the public key from jwks_uri. |
| code_challenge_methods_supportedstring[] | S256 (recommended) and plain. See the PKCE page. |
| scopes_supportedstring[] | Closed vocabulary — see the scopes reference. |
02 JWKS
Public verification keys for id_tokens. The set includes every non-evicted signing key so verifiers can validate in-flight tokens across rotations. Cached for one hour; clients should re-fetch when an unknown kid appears in a token header.
curl https://api.usequota.ai/.well-known/jwks.jsonResponse
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "abc123def4567890",
"n": "0vx7agoebGcQSuuPiLJXZpt...",
"e": "AQAB"
}
]
}03 id_token
When the authorization request includes openid in its scope, the token response carries a signed id_token alongside the access and refresh tokens. The token is a standard RS256 JWT — verify with any OIDC-compatible library.
Token response
{
"access_token": "quota_token_...",
"token_type": "Bearer",
"expires_in": 604800,
"refresh_token": "quota_refresh_...",
"scope": "openid profile email credits.spend",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE4..."
}Decoded claims
{
"iss": "https://api.usequota.ai",
"sub": "bc9aac4f-1e3a-4c2f-b8d7-f6e7c2c4d521",
"aud": "quota_client_my_app",
"exp": 1762912800,
"iat": 1762909200,
"auth_time": 1762908900,
"nonce": "n-0S6_WzA2Mj",
"email": "user@example.com",
"email_verified": true,
"name": "Ada Lovelace",
"picture": "https://cdn.usequota.ai/avatars/bc9aac4f.png"
}Claim reference
| issstring | Issuer URL — must equal the issuer in discovery. |
| substring | Quota user ID. Stable for the lifetime of the user. |
| audstring | Your client_id. Reject tokens whose aud does not match yours. |
| expnumber | Expiration as Unix seconds. id_tokens live for 1 hour. |
| iatnumber | Issued-at timestamp as Unix seconds. |
| auth_timenumber | When the user actually authenticated (the moment the authorization code was issued). Useful for max_age enforcement. |
| noncestring | Echoed verbatim from the nonce parameter on /oauth/authorize. Compare against the value you generated to detect replay. |
| emailstring | Present only when scope includes email. |
| email_verifiedboolean | Reflects the database state — true only after the user has clicked the verification email link. |
| namestring | Display name. Present only when scope includes profile and the user has set one. |
| picturestring | Avatar URL. Present only when scope includes profile and the user has uploaded one. |
04 nonce
Pass a nonce on /oauth/authorize and Quota echoes it into the id_token. Compare what you receive against what you sent to defeat replay attacks. The nonce is opaque to Quota — any unguessable string works; OIDC libraries generate one for you.
https://api.usequota.ai/oauth/authorize
?response_type=code
&client_id=quota_client_my_app
&redirect_uri=https://myapp.com/callback
&state=xyz
&nonce=n-0S6_WzA2Mj
&scope=openid+profile+email05 prompt=login
Add prompt=login to the authorize URL to force a fresh login even if the user already has a Quota session cookie. Use this for re-authentication flows (e.g. before a sensitive operation) or to give the user a clear opportunity to switch accounts.
https://api.usequota.ai/oauth/authorize
?response_type=code
&client_id=quota_client_my_app
&redirect_uri=https://myapp.com/callback
&state=xyz
&prompt=login
&scope=openid06 NextAuth.js / Auth.js
OIDC discovery means a NextAuth provider is one block of config — no per-endpoint URL list, no scope hardcoding.
import NextAuth from "next-auth";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
{
id: "quota",
name: "Quota",
type: "oidc",
issuer: "https://api.usequota.ai",
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
authorization: {
params: { scope: "openid profile email credits.read" },
},
},
],
});07 openid-client (Node.js)
Lower-level. Useful when you want full control over the request flow — server-side scripts, custom backends, integration tests.
import { Issuer, generators } from "openid-client";
const quota = await Issuer.discover("https://api.usequota.ai");
const client = new quota.Client({
client_id: process.env.QUOTA_CLIENT_ID!,
client_secret: process.env.QUOTA_CLIENT_SECRET!,
redirect_uris: ["https://myapp.com/callback"],
response_types: ["code"],
});
// 1. Generate authorize URL with nonce + state.
const state = generators.state();
const nonce = generators.nonce();
const authUrl = client.authorizationUrl({
scope: "openid profile email credits.read",
state,
nonce,
});
// 2. Redirect the user to authUrl. On callback:
const params = client.callbackParams(req);
const tokenSet = await client.callback(
"https://myapp.com/callback",
params,
{ state, nonce },
);
// tokenSet.id_token is verified against JWKS automatically.
const claims = tokenSet.claims();
console.log(claims.sub, claims.email, claims.email_verified);08 Verify by hand (jose)
For the rare case where you need to verify a token outside an OIDC client — e.g. a backend service that receives an id_token from a trusted frontend.
import { jwtVerify, createRemoteJWKSet } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://api.usequota.ai/.well-known/jwks.json"),
);
const { payload } = await jwtVerify(idToken, JWKS, {
issuer: "https://api.usequota.ai",
audience: process.env.QUOTA_CLIENT_ID!,
});
// payload.sub, payload.email, payload.email_verified, payload.nonce, ...