Skip to content

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.

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

💡 Tip: 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)
  • The app is on the sandbox tier but requested a real-domain redirect_uri other than localhost/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 with redirect_uri not registered. To use a real domain you need production promotion
  • 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'))
  4. If error_description is redirect_uri not registered but the URI is clearly registered under the console's Redirect URIs → check whether the app tier is sandbox. Sandbox only allows localhost/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_description is client credentials missing, this is the likely case. Because the default client type is confidential, apps that cannot safely store a secret fail every token exchange unless they are registered as public clients (PKCE).
  • 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
  • A public client sent a client_secret or HTTP Basic auth → rejected by downgrade defense (public clients must not present client_secret)

Quick diagnosis:

  1. Does error_description say client credentials missing? Check Client type in the console app detail. If there is no backend but the app is confidential, re-register it as public (client_type cannot be changed after creation). See Public client guide and Public vs Confidential.
  2. Console → app detail → "Rotate Secret" → update the RP env with the new secret
  3. 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:

  • 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 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가 제품의 신뢰를 만듭니다.