Token Endpoints

POST /token/exchange

Exchange a one-time authorization code (delivered to a web/https redirect_uri by GET /login) for a token pair. Web apps use this instead of reading tokens from the callback URL.

Request:

{
  "code": "one-time-code-from-callback",
  "app_id": "uuid",
  "code_verifier": "original-pkce-verifier"
}

code_verifier is required only if you sent a code_challenge on the /login request.

Success Response (200):

{
  "access_token": "eyJhbG...",
  "refresh_token": "a1b2c3...",
  "token_type": "Bearer",
  "expires_in": 900
}

The code is single-use and expires 60 seconds after issuance.

Errors: | Status | Detail | Meaning | |--------|--------|---------| | 400 | Invalid or expired code | Code not found, already used, or older than 60s | | 400 | code_verifier required / mismatch | PKCE challenge was set but verifier missing or wrong | | 400 | Code does not belong to this app | app_id mismatch |


POST /token/refresh

Exchange a valid refresh token for a new access + refresh token pair. The old refresh token is revoked (rotation).

Request:

{
  "refresh_token": "a1b2c3d4...",
  "app_id": "uuid"
}

Success Response (200):

{
  "access_token": "eyJhbG...",
  "refresh_token": "new-token...",
  "token_type": "Bearer",
  "expires_in": 900
}

Errors: | Status | Detail | Meaning | |--------|--------|---------| | 401 | Invalid or expired refresh token | Token not found, expired, or already used | | 401 | Token does not belong to this app | app_id mismatch | | 403 | Account suspended | User suspended for this app |

Refresh tokens rotate on every use — the returned refresh token replaces the one you sent. Store it securely server-side.

Replay Detection: Reuse of a revoked refresh token is treated as compromise and revokes the entire token family (ALL refresh tokens for that user+app).


POST /token/revoke

Revoke a refresh token (logout).

Request:

{
  "refresh_token": "a1b2c3d4..."
}

Response (200):

{ "status": "ok" }

Always returns 200, even if the token doesn't exist (prevents information leakage).


POST /token/verify

Verify an access token and return its claims. For apps that can't do local JWT verification.

Request:

{
  "token": "eyJhbG...",
  "audience": "your-app-id"
}

audience is required — pass your app_id. It is checked against the token's aud claim. This endpoint validates user access tokens only; service tokens (token_type: "service") presented as user tokens are rejected with valid: false.

Valid Response:

{
  "valid": true,
  "claims": {
    "sub": "user-uuid",
    "email": "user@example.com",
    "roles": ["user", "admin"],
    "aud": "app-uuid",
    "iss": "https://keymaster.cloud-monitor.com",
    "exp": 1710548100,
    "token_type": "access"
  }
}

Invalid Response:

{
  "valid": false,
  "error": "Token has expired"
}

Note: Prefer local JWKS verification for production. This endpoint is for debugging and apps that can't handle JWTs locally.


POST /auth/token

OAuth2 token endpoint. Currently supports client_credentials grant.

Request (form-encoded):

grant_type=client_credentials
&client_id=app-uuid
&client_secret=your-secret
&scope=push:send

Requests may also be sent as JSON with the same fields. scope is space-separated.

Success Response (200):

{
  "access_token": "eyJhbG...",
  "token_type": "bearer",
  "expires_in": 900,
  "scope": "push:send"
}

The returned JWT carries token_type: "service" and a scope claim, and has no aud claim. Service tokens are not accepted as user tokens.

The app is only granted scopes it is entitled to: each requested scope must be listed in the app's config["service_scopes"] in Keymaster, otherwise the request returns 400 invalid_scope.

Errors: | Status | Detail | |--------|--------| | 400 | unsupported_grant_type | | 400 | invalid_scope: xyz (unknown scope, or one the app is not entitled to) | | 401 | invalid_client | | 429 | too_many_requests (10 attempts/15min per client_id) |


GET /.well-known/jwks.json

Public keys for JWT verification. Cache these and refresh hourly or when kid doesn't match.

Response:

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "a1b2c3d4e5f6",
      "n": "base64url-encoded-modulus...",
      "e": "AQAB"
    }
  ]
}

GET /.well-known/openid-configuration

OIDC discovery document.

Response:

{
  "issuer": "https://keymaster.cloud-monitor.com",
  "authorization_endpoint": "https://keymaster.cloud-monitor.com/login",
  "token_endpoint": "https://keymaster.cloud-monitor.com/auth/token",
  "userinfo_endpoint": "https://keymaster.cloud-monitor.com/userinfo",
  "jwks_uri": "https://keymaster.cloud-monitor.com/.well-known/jwks.json",
  "revocation_endpoint": "https://keymaster.cloud-monitor.com/token/revoke",
  "response_types_supported": ["code"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "scopes_supported": ["openid", "profile", "email", "roles", "push:send"],
  "token_endpoint_auth_methods_supported": ["client_secret_post"],
  "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"]
}