Quota
/ docs
Dashboard
Docs/Authentication/OpenID Connect

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.

The discovery URL
Point any OIDC client at https://api.usequota.ai/.well-known/openid-configuration. The client reads every endpoint, signing algorithm, scope, and claim from that one document.

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.

GEThttps://api.usequota.ai/.well-known/openid-configuration
curl https://api.usequota.ai/.well-known/openid-configuration

Response

{
  "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

issuerstringThe 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.

GEThttps://api.usequota.ai/.well-known/jwks.json
curl https://api.usequota.ai/.well-known/jwks.json

Response

{
  "keys": [
    {
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "kid": "abc123def4567890",
      "n": "0vx7agoebGcQSuuPiLJXZpt...",
      "e": "AQAB"
    }
  ]
}
Key rotation
JWKS advertises every active signing key, so verifiers keep validating tokens that were minted with a now-rotated key. Pick the key by matching the token's kid header to the JWKS entry.

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

issstringIssuer URL — must equal the issuer in discovery.
substringQuota user ID. Stable for the lifetime of the user.
audstringYour client_id. Reject tokens whose aud does not match yours.
expnumberExpiration as Unix seconds. id_tokens live for 1 hour.
iatnumberIssued-at timestamp as Unix seconds.
auth_timenumberWhen the user actually authenticated (the moment the authorization code was issued). Useful for max_age enforcement.
noncestringEchoed verbatim from the nonce parameter on /oauth/authorize. Compare against the value you generated to detect replay.
emailstringPresent only when scope includes email.
email_verifiedbooleanReflects the database state — true only after the user has clicked the verification email link.
namestringDisplay name. Present only when scope includes profile and the user has set one.
picturestringAvatar URL. Present only when scope includes profile and the user has uploaded one.
Always verify the signature
Do not trust the body of an id_token until you have verified its signature against a key from jwks_uri, confirmed iss matches the discovery issuer, aud matches your client_id, and exp is in the future. OIDC libraries do this for you — do not roll it by hand.

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+email

05 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=openid

06 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" },
      },
    },
  ],
});
Public clients (no secret)
For mobile or SPA clients that cannot safely store QUOTA_CLIENT_SECRET, omit it and use PKCE instead. Discovery advertises token_endpoint_auth_methods_supported: ["none"] for exactly this case.

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, ...