Errors
Reference for OAuth error codes Signet returns, and how an integrator should handle each. All error responses follow RFC 6749 §5.2:
{
"error": "invalid_grant",
"error_description": "Human-readable description of what went wrong"
}
error_description is for logging and debugging — do not expose it to end users.
GitHub Login Issuer Validation
With OAUTH_ISS_VALIDATION_ENABLED=true (the default), Signet checks GitHub’s callback iss against https://github.com/login/oauth using exact string comparison. This issuer is built in; no additional environment variable or callback URL parameter is needed. A different value, including an added trailing slash, is rejected before token exchange. A missing iss remains allowed for backward compatibility. Setting the flag to false bypasses issuer validation for all third-party login providers.
Error Codes by Scenario
Authorization Endpoint Redirect Errors
When /oauth/authorize fails after the user has been redirected to your redirect_uri, the error is passed via query string:
https://yourapp.example/callback?error=access_denied&error_description=...&state=RANDOM_STATE&iss=https://your-signet
Error redirects (like success redirects) carry an iss parameter identifying Signet per RFC 9207. Before acting on the error, every client must require iss and exact-match it against the issuer recorded for this authorization request; reject missing or mismatched values, even when the client uses only one authorization server.
error |
Cause | Your action |
|---|---|---|
access_denied |
User declined consent, or admin revoked their access | Show “sign-in cancelled”; let user retry |
invalid_request |
Missing / malformed parameter, or PKCE is required for this client but code_challenge was absent |
Fix the request — this is a client bug |
invalid_scope |
Requested scope not permitted for this client | Drop the offending scope; check with admin |
unauthorized_client |
Client not permitted for Authorization Code Flow | Ask admin to enable Auth Code Flow for this client |
invalid_client |
Only with CIMD enabled: the URL client_id’s metadata document could not be fetched or failed validation (rendered as a local 400 page, never redirected — no redirect_uri is proven yet) |
Check the document is reachable, ≤ 64 KB, and that its client_id byte-exactly equals its URL |
unsupported_response_type |
response_type was not code |
Use response_type=code |
invalid_target |
A resource= parameter is not in this client’s allowed-resources allowlist — the server-wide CIMD_ALLOWED_RESOURCES list for CIMD clients — (deny-all by default), or failed RFC 8707 shape validation (non-http(s) scheme, fragment, empty host, > 10 entries, > 1024 chars) |
Fix the request — see Resource Indicator Errors below |
server_error |
Transient Signet failure | Retry with backoff |
Token Endpoint Errors (/oauth/token)
Returned as HTTP 400 JSON (except invalid_client, which is 401):
error |
HTTP | Common cause | Your action |
|---|---|---|---|
invalid_request |
400 | Missing required form field | Fix the request |
invalid_client |
401 | Wrong client_id / client_secret, or missing client auth |
Verify credentials; check HTTP Basic vs. body |
invalid_grant |
400 | Code / refresh token / device code is invalid, expired, used, or was revoked (incl. rotation reuse detection); or PKCE code_verifier did not match the original code_challenge |
Stop retrying. Restart the flow / re-authenticate the user |
invalid_scope |
400 | Scope exceeds what the client or original grant allows | Drop or narrow scopes |
unauthorized_client |
400 | Grant type not enabled for this client | Ask admin to enable the grant |
unsupported_grant_type |
400 | grant_type not recognized |
Use one of: authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:device_code, client_credentials |
invalid_target |
400 | resource= is malformed, not in this client’s allowed-resources allowlist (deny-all by default), or (on refresh_token / authorization_code / device_code grants) requests a resource that is not a subset of the original grant (RFC 8707 §2.2 narrowing rule) |
Fix the request — see Resource Indicator Errors below. Do not retry with the same value |
server_error |
500 | Signet internal error | Retry with backoff; escalate if persistent |
Device Flow Polling Errors
While polling /oauth/token with grant_type=urn:ietf:params:oauth:grant-type:device_code:
error |
Meaning | Your action |
|---|---|---|
authorization_pending |
User hasn’t approved yet | Keep polling at interval |
slow_down |
Polling too fast | Increase interval by ≥ 5 seconds |
access_denied |
User rejected | Stop. Tell user. |
expired_token |
device_code past expires_in |
Restart the flow from POST /oauth/device/code |
invalid_grant |
device_code unknown or already used |
Restart the flow |
See Device Flow for full details.
Token Introspection & Validation
| Endpoint | Failure mode | Response |
|---|---|---|
GET /oauth/tokeninfo |
Missing Bearer header | 401 {"error": "missing_token"} + WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource" |
GET /oauth/tokeninfo |
Invalid or expired token | 401 {"error": "invalid_token", ...} + WWW-Authenticate: Bearer error="invalid_token", resource_metadata="…/.well-known/oauth-protected-resource" |
GET /oauth/userinfo |
Missing/invalid Bearer | 401 + WWW-Authenticate: Bearer error="invalid_token", resource_metadata="…/.well-known/oauth-protected-resource" |
POST /oauth/introspect |
Missing/invalid client auth | 401 + WWW-Authenticate: Basic realm="signet" |
POST /oauth/introspect |
Token invalid / expired / revoked | 200 {"active": false} (per RFC 7662 — never a 4xx) |
POST /oauth/revoke |
Any outcome | 200 (per RFC 7009 — no error signal) |
GET /oauth/tokeninfo |
sgk_ key unknown, malformed, revoked, expired, bound to a disabled app, or feature off |
401 {"error": "invalid_token", ...} — all six are deliberately indistinguishable |
POST /oauth/introspect |
sgk_ key inactive for any of those reasons |
200 {"active": false} |
Personal API keys (
sgk_) are rejected by every/oauth/tokengrant (invalid_grant— a key is not a code, refresh token, or device code;device_codereportsaccess_deniedinstead) and by/oauth/userinfo(401 invalid_token— that surface takes JWT access tokens only). Because every key failure collapses into one 401, you cannot diagnose it from the response — check the key’s status at/account/api-keysinstead. See API Keys.
Introspection ownership: by default (
INTROSPECTION_REQUIRE_OWNERSHIP=true)/oauth/introspectreturns full metadata only for tokens your own client issued. Introspecting an active token that belongs to a different client yields just{"active": true}— nosub,scope,username,aud, etc. This is a stripped success, not an error. See Tokens & Revocation.
Rate Limit Errors — HTTP 429
If you exceed a rate limit, you’ll get 429 Too Many Requests. Most limits are per IP; /oauth/introspect is limited per client_id (with a per-IP ceiling behind it), so spreading one client across IPs does not buy it more budget.
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1735689600
Content-Type: application/json
{"error": "rate_limit_exceeded", "error_description": "..."}
How to handle:
- Wait until
X-RateLimit-Reset(Unix epoch seconds) before retrying.
- Back off exponentially with jitter on repeated 429s.
- If you’re polling (Device Flow), your
intervalshould already keep you well below the limit. If you see 429, you’re polling wrong — fix the client, don’t retry faster.
- For shared environments (multiple services behind one egress IP), ask your admin to raise the per-IP limit; for
introspect, each service should use its ownclient_idso it gets its own budget.
See Tokens & Revocation §Rate Limits for the defaults.
Special Case: Refresh Token Reuse → Family Revocation
In rotation mode, using a previously-rotated refresh token returns invalid_grant, and every refresh token in the same family is revoked server-side. This is a terminal state — do not retry.
{
"error": "invalid_grant",
"error_description": "Refresh token is invalid or expired"
}
What caused it:
- Two tabs/processes refreshed concurrently using the same stored token
- A retry after a partial failure where you didn’t persist the new token
- A stolen token was used by someone else first
Response: force the user to log in again. See Tokens & Revocation §Rotation Mode for prevention patterns.
Resource Indicator Errors (RFC 8707)
invalid_target is returned whenever a resource= parameter is rejected. Three categories:
Allowlist gate (applies to every grant that accepts resource — client_credentials, authorization_code, device_code, refresh_token):
Each client has an operator-managed allowed-resources allowlist. A client-supplied resource= is honored only when it is an exact-string match of an allowlist entry. The allowlist is deny-all by default — if it is empty, any resource= value is rejected, even a perfectly well-formed one. Sending no resource at all is always fine (the token’s aud falls back to the deployment-wide JWT_AUDIENCE).
| Cause | Fix |
|---|---|
Client has no allowlist configured but sent a resource= |
Ask your admin to add the resource to the client’s allowlist |
resource= value is not an exact match of an allowlist entry |
Use one of the exact URIs the admin registered, or ask to add yours |
The error_description names your offending value (e.g. requested resource "https://x" is not in this client's allowed resources). Breaking change: clients that previously passed resource freely now get invalid_target until an admin populates the allowlist.
Shape validation (applies to every endpoint that accepts resource):
| Cause | Fix |
|---|---|
Not an absolute URI (e.g. resource=/api) |
Pass the full https://api.example.com form |
Scheme is not http or https (e.g. javascript:, urn:, data:) |
RFC 8707 requires a network-locator URI |
Contains a fragment (#...) |
Strip the fragment — aud cannot carry fragments |
| Empty host | Provide a real host name |
More than 10 resource= values, or a single value > 1024 characters |
Reduce the list / shorten the URI |
Subset rule (RFC 8707 §2.2 — applies on the token endpoint for grants that carry a prior resource binding, after the allowlist gate above):
| Grant | Rule |
|---|---|
authorization_code |
resource= at /oauth/token must be a subset of what was sent on /oauth/authorize |
urn:ietf:params:oauth:grant-type:device_code |
resource= at /oauth/token must be a subset of what was sent on /oauth/device/code |
refresh_token |
resource= must be a subset of the original grant — widening is rejected, narrowing is allowed |
client_credentials |
No prior grant to subset against — only the allowlist gate and shape validation apply |
For both device_code and authorization_code, the requested resource is validated before the code is consumed, so an invalid_target does not burn the code — the client may retry the token request with a corrected resource set. (A successful exchange still consumes the one-time authorization_code per RFC 6749.)
See each flow’s guide for example requests: Authorization Code Flow, Device Authorization Flow, Client Credentials Flow.
Error Handling Checklist
- [ ] Treat
invalid_granton refresh as terminal — trigger re-login, don’t retry
- [ ] Treat
access_deniedas user-initiated — surface politely, don’t auto-retry
- [ ] Retry
server_errorand network errors with exponential backoff
- [ ] Honor
Retry-Afteron 429
- [ ] Log
error_descriptionserver-side; never show it to end users
- [ ]
invalid_request/invalid_scope/unsupported_grant_type/unsupported_response_type/invalid_targetare client bugs — fix, don’t retry
- [ ] Monitor
invalid_clientspikes — someone is probing your credentials or a rotation/leak happened