Authentication (OAuth 2.1)
The ResQueServe Dashboard is the only authorization server. Every API call needs a bearer token that you obtain from it:
Authorization: Bearer <access_token>
A token from the Dashboard (https://<dashboard-host>/oauth/token) is valid for the API of the Dashboard and of the Realm (https://<realm-host>/API/v1/...). See the overview for the hosts of the SIM Dispatcher.
Quick reference
| What | Value |
|---|---|
| Specifications | OAuth 2.1, RFC 7009 (revocation), RFC 7591 (dynamic client registration), RFC 8252 (native apps), RFC 8414 and RFC 9728 (metadata), RFC 9700 (security) |
| Token type | JWT bearer token |
| Access token lifetime | 15 minutes (expires_in: 900) |
| Grant types | client_credentials, authorization_code (PKCE required), refresh_token |
| PKCE method | S256 only |
| Client authentication | API clients: client_secret_post (secret in the form body). Authorization code and refresh requests are protected by PKCE and the one-time refresh token |
| Request format | application/x-www-form-urlencoded for /oauth/token and /oauth/revoke, JSON for /oauth/register |
| Scope | mcp:user |
Always use https. Token responses are sent with Cache-Control: no-store.
Choose a method
| Client credentials | Authorization code with PKCE | |
|---|---|---|
| Best for | Servers, scripts, automation | Applications that act for a signed-in user |
| User sign-in | No, the token has the rights of the account that owns the API client | Yes, in the browser |
| Credentials | API client from the account settings | Dynamic client registration |
| Effort | One HTTP request per token | Redirects, PKCE, callback |
Client credentials
Create an API client
Sign in to the Dashboard, open Account, and go to API Clients / Developer. Enter a description and click Create. Copy the client_id and the client_secret: the secret is shown only once. An account can have up to 10 API clients. Keep the secret out of source control.
Request a token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=<client-id>
&client_secret=<client-secret>
{
"access_token": "<JWT>",
"token_type": "Bearer",
"expires_in": 900
}
The token carries the roles of the account that owns the API client. There is no refresh token: request a new token with the client credentials when the old one expires. Renew a little early (for example 60 seconds before expires_in runs out), and request a new token once if an API call answers 401.
TOKEN=$(curl -sX POST https://<dashboard-host>/oauth/token \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=<client-id>" \
--data-urlencode "client_secret=<client-secret>" \
| jq -r '.access_token')
curl -H "Authorization: Bearer $TOKEN" https://<dashboard-host>/API/v1/...
The .NET SDK does all of this for you.
Authorization code with PKCE
Use this flow when your application acts on behalf of a signed-in ResQueServe user, for example a web or desktop application, a native app, or an MCP-compatible tool.
Client Dashboard User's browser
|-- POST /oauth/register ---->| |
|<-- client_id, secret -------| |
|-- redirect to /oauth/authorize --------------------------->|
| |<-- user signs in -----------|
|<-- callback with ?code=... ---------------------------------|
|-- POST /oauth/token (code + code_verifier) -->| |
|<-- access_token, refresh_token -| |
1. Register a client
POST /oauth/register
Content-Type: application/json
{
"redirect_uris": ["https://your-app.example.com/callback"],
"grant_types": ["authorization_code"],
"application_type": "web",
"client_name": "My Application"
}
| Field | Required | Description |
|---|---|---|
redirect_uris |
Yes | One or more absolute URIs without fragment, see redirect URIs |
grant_types |
No | Defaults to ["authorization_code"] |
application_type |
No | web (default) or native. Only native clients may register loopback redirects |
client_name |
No | Display name |
The 201 Created response contains client_id and client_secret. The secret is shown only once. Registered clients expire after a while and must then be registered again. If the server requires an initial access token, send it as Authorization: Bearer <token>. Registration is limited to 5 requests per minute per IP address.
Redirect URIs
Redirect URIs are compared as exact strings (RFC 9700). These forms are accepted:
| Form | Example | Notes |
|---|---|---|
Claimed https link |
https://app.example.com/callback |
Preferred for native apps (Universal Links, App Links) |
| Private-use scheme, reverse-domain form | com.example.app:/oauth |
The scheme must contain a dot (RFC 8252 ยง7.1) |
| Loopback | http://127.0.0.1/callback, http://localhost/callback, http://[::1]/callback |
native clients only. In the authorization request only the port may differ |
http on other hosts |
Development environments only |
A URI with a fragment or credentials is always rejected (invalid_redirect_uri), as are javascript:, data:, file: and ftp: URIs.
2. Create the PKCE values
Generate a random code_verifier (43 to 128 characters of A-Z a-z 0-9 - . _ ~) and derive the challenge:
code_challenge = BASE64URL(SHA-256(ASCII(code_verifier))) // no padding
CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '=+/' | cut -c1-64)
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')
3. Send the user to the authorization endpoint
GET /oauth/authorize
?response_type=code
&client_id=<client-id>
&redirect_uri=<redirect-uri>
&code_challenge=<code-challenge>
&code_challenge_method=S256
&scope=mcp:user
&state=<random-value>
| Parameter | Required | Description |
|---|---|---|
response_type |
Yes | code |
client_id |
Yes | From the registration |
redirect_uri |
Yes | Exactly one of the registered URIs |
code_challenge |
Yes | From step 2 |
code_challenge_method |
Yes | S256 |
scope |
No | Space-separated scopes, defaults to mcp:user |
state |
No | Random value against CSRF, returned unchanged in the callback. Verify it |
After the sign-in the user is redirected to your redirect_uri with code and state. A code is valid for 5 minutes and can be used once.
4. Exchange the code
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=<client-id>
&code=<code>
&code_verifier=<code-verifier>
&redirect_uri=<same-redirect-uri>
{
"access_token": "<JWT>",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "<opaque token>"
}
5. Refresh the token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&client_id=<client-id>
&refresh_token=<newest-refresh-token>
The response has the same format and contains a new refresh token. Every refresh token can be used once: always store the newest one. Using an old one again is treated as theft and ends the login, and the user has to sign in again. A login lasts 30 days from the sign-in; refreshing does not extend it. A suspended or deleted user gets no new token.
Sign out (revoke)
When the user disconnects your application, revoke the refresh token (RFC 7009):
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=<refresh-token>
&client_id=<client-id>
&access_token=<access-token>
The endpoint answers 200 OK, also for unknown tokens. Revoking a refresh token ends the whole login: all its refresh tokens and access tokens, including the ones issued before the last refresh, stop working. Other logins of the user are not affected. The optional access_token revokes that token as well. If token is an access token, only that token is revoked.
Discovery
Clients can configure themselves from the metadata documents:
| URL | Standard | Content |
|---|---|---|
GET /.well-known/oauth-authorization-server (Dashboard) |
RFC 8414 | Issuer, endpoints, grant types, PKCE methods, scopes |
GET /.well-known/oauth-protected-resource (Dashboard and Realm) |
RFC 9728 | The resource and its authorization server (always the Dashboard) |
{
"issuer": "https://<dashboard-host>",
"authorization_endpoint": "https://<dashboard-host>/oauth/authorize",
"token_endpoint": "https://<dashboard-host>/oauth/token",
"registration_endpoint": "https://<dashboard-host>/oauth/register",
"revocation_endpoint": "https://<dashboard-host>/oauth/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "none"]
}
Access token
The access token is a signed JWT. Treat it as opaque: use it only as a bearer credential, and do not rely on its claims in your client. The server decides about expiry and revocation, so a token can stop working before exp (for example after a revocation). Answer a 401 by requesting a new token.
Admin rights are never part of an OAuth access token, whatever the roles of the user are.
Limits
| Endpoint | Limit |
|---|---|
POST /oauth/token, POST /oauth/revoke |
10 requests per minute per IP address and client_id, and 60 per minute per IP address over all clients |
POST /oauth/register |
5 requests per minute per IP address |
A rejected request answers 429 with the error too_many_requests. Do not retry immediately; wait before the next attempt, and back off further if it fails again.
Errors
The OAuth endpoints answer with the RFC 6749 JSON format. This is the only part of the API that does not use RFC 7807 problem details.
{
"error": "invalid_grant",
"error_description": "Human-readable explanation."
}
| Endpoint | error |
HTTP | Cause |
|---|---|---|---|
/oauth/authorize |
unsupported_response_type |
400 | response_type is not code |
/oauth/authorize |
invalid_request |
400 | A required parameter is missing, or code_challenge_method is not S256 |
/oauth/authorize |
invalid_scope |
400 | A requested scope is not supported |
/oauth/authorize |
invalid_client |
400 | client_id is unknown or expired |
/oauth/authorize |
access_denied |
redirect | The user lacks the roles for the requested scope |
/oauth/token |
unsupported_grant_type |
400 | Unknown grant_type |
/oauth/token |
invalid_request |
400 | A required parameter is missing |
/oauth/token |
invalid_grant |
400 | Code or refresh token unknown, expired, revoked, already used or of another client; PKCE check failed; user suspended or deleted |
/oauth/token |
invalid_client |
401 | Client unknown or secret wrong |
/oauth/token |
unauthorized_client |
400 | The user of the API client is suspended |
/oauth/register |
invalid_client_metadata |
400 | redirect_uris missing, or application_type is neither web nor native |
/oauth/register |
invalid_redirect_uri |
400 | A redirect URI is not allowed, see redirect URIs |
/oauth/register |
access_denied |
401 | An initial access token is required and missing or wrong |
/oauth/revoke |
invalid_request |
400 | token or client_id missing |
| any rate-limited | too_many_requests |
429 | See limits |
invalid_client and unauthorized_client mean that a retry will not help: the client needs new credentials. Other errors can be handled in your application as usual.