Skip to content

OAuth error codes

Every error response follows the RFC 6749 §5.2 format.

json
{
  "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.

ChannelFieldPurpose
bodyerrorMachine-readable code (RFC 6749 §5.2)
bodyerror_descriptionOne-line human-readable description
bodyerror_uriThe anchor in this document for the code
bodyrequest_idThe same key as the console request log
headerX-Logi-Request-IdLets you trace even when the body is empty (e.g. HEAD)
headerX-Logi-Console-UrlA deep link to the console request_logs, when the RP is authenticated in production
headerX-Logi-Scope-DriftIncluded 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

If you're integrating for the first time

Just record the response body's error_description in your RP server log at INFO level. Most integration mistakes are explained by one of two things — redirect_uri mismatch or PKCE verifier mismatch — and the error_description carries 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_uri does not exactly match the whitelist in the app registration (scheme/host/path/query, all of them)
  • code_challenge_method is not S256 (plain is not supported)
  • A required parameter is missing (response_type, client_id, redirect_uri, code_challenge)

Quick diagnosis:

  1. Console → app detail → "Redirect URIs", and compare it character by character with the URL the RP sent
  2. 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
  3. Confirm the PKCE library outputs S256 (Buffer.from(sha256(verifier)).toString('base64url'))

invalid_client

Meaning: Client authentication failed — client_id / client_secret mismatch or a missing secret.

HTTP: 401

Common causes:

  • The client_secret environment variable is wired up incorrectly between dev and prod
  • A base64 encoding mistake in the HTTP Basic auth header (client_id:client_secret form)
  • The RP server was not redeployed after rotating the secret → it's using the old secret

Quick diagnosis:

  1. Console → app detail → "Rotate Secret" → update the RP env with the new secret
  2. 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 pending status (awaiting admin approval — for domains other than localhost)
  • The app is in suspended status (suspended by operations — check the console → audit log)
  • The grant_type the app used was not allowed at registration

Quick diagnosis:

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 existed
  • code already used — a second exchange attempt with the same code. An authorization code is single-use
  • code expired — more than 10 minutes elapsed after the code was issued
  • redirect_uri mismatch — the URI at /oauth/authorize differs from the one at /oauth/token
  • PKCE verifier mismatch — the code_verifier does not match the code_challenge submitted at the start
  • refresh token not found — the RT was revoked or was issued to a different client
  • refresh token reuse detected; chain revokedreuse attack detected — the entire token chain is revoked. Force the user to log in again
  • refresh token expired — the RT passed its 30-day lifetime

Quick diagnosis:

  1. A lost PKCE verifier is the most common — when using sessionStorage, check whether it evaporates on a tab switch or refresh
  2. The redirect_uri must be exactly identical at authorize and at token — even a single trailing slash difference fails
  3. 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_scopes is unset
  • A typo (e.g. emailemail_address)
  • A missing namespace prefix on a custom scope (<client_id>:reviewer_role form)

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_code
  • refresh_token
  • urn: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_in elapsed)
  • The token was explicitly revoked
  • The user account was soft-deleted

Quick diagnosis:

  • Invalidate the JWKS cache and retry
  • Compare the expires_in at issuance with the current time

authorization_pending / slow_down / expired_token

Meaning: Polling responses for the Device Authorization Grant (RFC 8628).

errorMeaningResponse
authorization_pendingThe user has not yet approved the device_codeWait for interval, then poll again
slow_downPolling is too fastIncrease interval by 5 seconds and poll again
expired_tokenThe device_code expiredStart over from the beginning

rate_limited

Meaning: A rate-limit threshold was exceeded.

HTTP: 429

Limits:

EndpointLimitKey
/session5/min (Cloudflare) · 10/3min (Rails)IP
/oauth/token20/minclient_id
/oauth/device_authorization30/minclient_id
/api/v1/me/merge/otp10/minuser_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:

  1. Log the response body's error_description verbatim in your RP server log. — The cause of the most common mistakes is written there directly.
  2. Record the request_id or X-Logi-Request-Id header alongside it. — It matches exactly in the console request_logs by the same key.
  3. In production, click the URL in the X-Logi-Console-Url header. — That request is shown in the console immediately.
  4. Follow the error_uri anchor 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_uri
  • request_id, X-Logi-Request-Id
  • client_id, redirect_uri (not PII)

Reference RFCs

Identity가 제품의 신뢰를 만듭니다.