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/token grant (invalid_grant — a key is not a code, refresh token, or device code; device_code reports access_denied instead) 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-keys instead. See API Keys.

Introspection ownership: by default (INTROSPECTION_REQUIRE_OWNERSHIP=true) /oauth/introspect returns full metadata only for tokens your own client issued. Introspecting an active token that belongs to a different client yields just {"active": true} — no sub, 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 interval should 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 own client_id so 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_grant on refresh as terminal — trigger re-login, don’t retry
  • [ ] Treat access_denied as user-initiated — surface politely, don’t auto-retry
  • [ ] Retry server_error and network errors with exponential backoff
  • [ ] Honor Retry-After on 429
  • [ ] Log error_description server-side; never show it to end users
  • [ ] invalid_request / invalid_scope / unsupported_grant_type / unsupported_response_type / invalid_target are client bugs — fix, don’t retry
  • [ ] Monitor invalid_client spikes — someone is probing your credentials or a rotation/leak happened