OAuth error codes
Source:
en/oauth/errors.md· Live: https://docs.1pass.dev/en/oauth/errors LLM-sanitized: internal links absolutized, VitePress containers → admonitions, line numbers in the Jump-to Index reference this rendered file (1-indexed).
📍 Jump-to Index
- L40-L59: ## Response signals at a glance
- L60-L215: ## Reference by code
- L65-L80: ### invalid_request
- L81-L98: ### invalid_client
- L99-L110: ### unauthorized_client
- L111-L131: ### invalid_grant
- L132-L145: ### invalid_scope
- L146-L156: ### unsupported_grant_type
- L157-L162: ### unsupported_response_type
- L163-L170: ### access_denied
- L171-L186: ### invalid_token
- L187-L196: ### authorization_pending / slow_down / expired_token
- L197-L215: ### rate_limited
- L216-L224: ## Error triage workflow
- L225-L237: ## Logging caveats
- L238-L244: ## Reference RFCs
OAuth error codes
Every error response follows the RFC 6749 §5.2 format.
{
"error": "<machine-readable code>",
"error_description": "<human-readable description>",
"error_uri": "https://docs.1pass.dev/oauth/errors#<error>",
"request_id": "8f0c1e7b-..."
}Response signals at a glance
The signals your RP server receives come over two channels: the response body and the response headers. They are designed to be connected to the console (web) by the same request_id, which makes it easy for an RP to cross-reference them in its own logs.
| Channel | Field | Purpose |
|---|---|---|
| body | error | Machine-readable code (RFC 6749 §5.2) |
| body | error_description | One-line human-readable description |
| body | error_uri | The anchor in this document for the code |
| body | request_id | The same key as the console request log |
| header | X-Logi-Request-Id | Lets you trace even when the body is empty (e.g. HEAD) |
| header | X-Logi-Console-Url | A deep link to the console request_logs, when the RP is authenticated in production |
| header | X-Logi-Scope-Drift | Included on token 200 responses — when drift was recorded within the last 7 days. Under the default block policy a drift request is itself rejected with invalid_scope; the header applies to apps explicitly set to log_only/alert, or as an echo of rejected/recorded history |
💡 Tip: If you're integrating for the first time Just record the response body's
error_descriptionin your RP server log at INFO level. Most integration mistakes are explained by one of two things —redirect_uri mismatchorPKCE verifier mismatch— and theerror_descriptioncarries the exact cause.
Reference by code
Each code anchor is where the error_uri of an OAuth response points. URL format: https://docs.1pass.dev/oauth/errors#<error>
invalid_request
Meaning: The request itself violates the RFC 6749 format.
Common causes:
- The
redirect_uridoes not exactly match the whitelist in the app registration (scheme/host/path/query, all of them) - The app is on the
sandboxtier but requested a real-domain redirect_uri other thanlocalhost/127.0.0.1— the sandbox tier only allows local hosts, so even if the redirect_uri is registered in the console it is rejected withredirect_uri not registered. To use a real domain you need production promotion code_challenge_methodis notS256(plainis not supported)- A required parameter is missing (
response_type,client_id,redirect_uri,code_challenge)
Quick diagnosis:
- Console → app detail → "Redirect URIs", and compare it character by character with the URL the RP sent
- Pay special attention to a trailing slash, a query string, or a fragment — any of these makes the two URIs differ and fail the exact match
- Confirm the PKCE library outputs
S256(Buffer.from(sha256(verifier)).toString('base64url')) - If
error_descriptionisredirect_uri not registeredbut the URI is clearly registered under the console's Redirect URIs → check whether the app tier issandbox. Sandbox only allowslocalhost/127.0.0.1, so a real domain can be used only after production promotion
invalid_client
Meaning: Client authentication failed — client_id / client_secret mismatch or a missing secret.
HTTP: 401
Common causes:
- A backend-less mobile/SPA/CLI app was registered as confidential (most common). If
error_descriptionisclient credentials missing, this is the likely case. Because the default client type isconfidential, apps that cannot safely store a secret fail every token exchange unless they are registered as public clients (PKCE). - The
client_secretenvironment variable is wired up incorrectly between dev and prod - A base64 encoding mistake in the HTTP Basic auth header (
client_id:client_secretform) - The RP server was not redeployed after rotating the secret → it's using the old secret
- A public client sent a
client_secretor HTTP Basic auth → rejected by downgrade defense (public clients must not present client_secret)
Quick diagnosis:
- Does
error_descriptionsayclient credentials missing? Check Client type in the console app detail. If there is no backend but the app isconfidential, re-register it as public (client_typecannot be changed after creation). See Public client guide and Public vs Confidential. - Console → app detail → "Rotate Secret" → update the RP env with the new secret
- A PKCE-only RP does not need a secret at all (PKCE guide)
unauthorized_client
Meaning: The client itself is authenticated, but it lacks permission for the current operation.
Common causes:
- The app is in
pendingstatus (awaiting admin approval — for domains other thanlocalhost) - The app is in
suspendedstatus (suspended by operations — check the console → audit log) - The
grant_typethe app used was not allowed at registration
Quick diagnosis:
- Console → app detail → check the Status pill. If it's
pending, proceed with the production promotion application. You must submit the application before the progress stepper appears on the app detail (the stepper is not shown before submission).
invalid_grant
Meaning: The authorization code or refresh token is not valid.
HTTP: 400
Common causes + the exact error_description:
code not found— the code was already exchanged once, or never existedcode already used— a second exchange attempt with the same code. An authorization code is single-usecode expired— more than 10 minutes elapsed after the code was issuedredirect_uri mismatch— the URI at/oauth/authorizediffers from the one at/oauth/tokenPKCE verifier mismatch— thecode_verifierdoes not match thecode_challengesubmitted at the startrefresh token not found— the RT was revoked or was issued to a different clientrefresh token reuse detected; chain revoked— reuse attack detected — the entire token chain is revoked. Force the user to log in againrefresh token expired— the RT passed its 30-day lifetime
Quick diagnosis:
- A lost PKCE verifier is the most common — when using
sessionStorage, check whether it evaporates on a tab switch or refresh - The
redirect_urimust be exactly identical at authorize and at token — even a single trailing slash difference fails - Console → app detail → error log → find the exact cause by
request_id
invalid_scope
Meaning: The requested scope is not registered on the app, or is empty.
Common causes:
- The default scope drift policy (
block) — at least one unregistered scope was in the request - A scope parameter was sent while
allowed_scopesis unset - A typo (e.g.
email↔email_address) - A missing namespace prefix on a custom scope (
<client_id>:reviewer_roleform)
Quick diagnosis:
- Console → app detail → check the exact names registered under "Allowed Scopes"
- If a Scope drift header (
X-Logi-Scope-Drift) appeared too, see Scope drift handling
unsupported_grant_type
Meaning: An unsupported grant_type was used.
Supported values:
authorization_coderefresh_tokenurn:ietf:params:oauth:grant-type:device_code(RFC 8628)
password, implicit, and client_credentials are not supported (intentionally omitted for security).
unsupported_response_type
Meaning: The response_type at /oauth/authorize is not code.
logi supports only the Authorization Code Flow. token (implicit) and a standalone id_token are not supported.
access_denied
Meaning: The user chose "Deny" on the consent screen, or it was denied in the device flow.
HTTP: 302 (authorize) or 400 (token)
Response: The RP should give the user a "Login cancelled" UX and surface a retry entry point.
invalid_token
Meaning: Bearer access_token verification failed at /oauth/userinfo.
HTTP: 401 + WWW-Authenticate: Bearer error="invalid_token"
Common causes:
- JWT signature verification failed (the RP used the wrong JWKS)
- The token expired (
expires_inelapsed) - The token was explicitly revoked
- The user account was soft-deleted
Quick diagnosis:
- Invalidate the JWKS cache and retry
- Compare the
expires_inat issuance with the current time
authorization_pending / slow_down / expired_token
Meaning: Polling responses for the Device Authorization Grant (RFC 8628).
| error | Meaning | Response |
|---|---|---|
authorization_pending | The user has not yet approved the device_code | Wait for interval, then poll again |
slow_down | Polling is too fast | Increase interval by 5 seconds and poll again |
expired_token | The device_code expired | Start over from the beginning |
rate_limited
Meaning: A rate-limit threshold was exceeded.
HTTP: 429
Limits:
| Endpoint | Limit | Key |
|---|---|---|
/session | 5/min (Cloudflare) · 10/3min (Rails) | IP |
/oauth/token | 20/min | client_id |
/oauth/device_authorization | 30/min | client_id |
/api/v1/me/merge/otp | 10/min | user_id |
Response: Respect the Retry-After header, use exponential backoff, and aggressively cache tokens for a single user.
Error triage workflow
The procedure to follow when a 4xx occurs in a new integration:
- Log the response body's
error_descriptionverbatim in your RP server log. — The cause of the most common mistakes is written there directly. - Record the
request_idorX-Logi-Request-Idheader alongside it. — It matches exactly in the console request_logs by the same key. - In production, click the URL in the
X-Logi-Console-Urlheader. — That request is shown in the console immediately. - Follow the
error_urianchor in this document. — The per-code sections above list common causes + quick diagnosis.
Logging caveats
Never log:
password,client_secret,code_verifier,refresh_token,access_token(including JWTs),logi_pak_*- logi itself never logs
password_digest,otp_secret_encrypted, JWTs, or PAK plaintext, not even once.
Safe to log:
error,error_description,error_urirequest_id,X-Logi-Request-Idclient_id,redirect_uri(not PII)