Skip to content

Troubleshooting

We've organized the errors you'll commonly run into as symptom → cause → fix. The higher up, the more frequent.

Widget (Widget SDK)

The QR code's finder pattern is cut off and won't scan

Symptom: the widget mounts fine, but the QR code's right finder pattern (top-right) or the bottom alignment pattern inside the iframe looks cut off. The camera can't recognize it, or recognizes it but reads the wrong payload.

Cause: the RQRCode library emits the SVG at native pixel size (module_size × modules + offset). The longer the payload (URL + session UUID, etc.), the more modules there are, so the SVG grows larger than the 200px container. Without overflow:hidden on the container, the SVG spills outside the visible area; with it, the SVG is clipped.

Fix (already applied; regression-prevention note): embed_qr_controller.js#renderQR forces width="100%" height="100%" preserveAspectRatio="xMidYMid meet" on the SVG element. Use the same pattern whenever you add new SVG-based QR render code:

js
// ✅ Scale while preserving the viewBox ratio to fit the container
svgEl.setAttribute("width",  "100%")
svgEl.setAttribute("height", "100%")
svgEl.setAttribute("preserveAspectRatio", "xMidYMid meet")

// ❌ Wrong — using RQRCode's native size as-is → container overflow
container.replaceChildren(svgEl)

Why native size is dangerous: when the payload length changes (e.g. a nonce rotation, an added scope), the number of QR modules grows → the SVG width increases → at some point it exceeds 200px → the finder is clipped. Defense-in-depth: always fit the SVG to the container ratio, and control the QR size by the container's own size.

Widget shows "unknown client_id" inside iframe

Symptom: the logi QR page comes up inside the iframe, but it only shows "unknown client_id / one moment… / close" text and the QR itself isn't drawn. The Render log shows POST /oauth/qr/start ... Parameters: {"qr_login" => {}} + Completed 400 Bad Request.

Cause: the iframe page attaches two Stimulus controllers (embed-qr + qr-login) at the same time, and both auto-run POST /oauth/qr/start in connect() — a race. If the view fills in the data-*-value of only one namespace, the other controller POSTs with an empty body → 400.

Fix (server side): attach only embed-qr to the data-controller attribute of app/views/embed/qr/show.html.erb. The single embed-qr controller owns the lifecycle (start → poll → approved → ?embed=1 complete → postMessage), and the qr-login DOM target attributes (data-qr-login-target=...) remain only for use as plain selectors.

erb
<%# ❌ Wrong — qr-login also auto-starts, causing a race + 400 %>
<section data-controller="embed-qr qr-login" ...>

<%# ✅ Correct — embed-qr alone; move all of qr-login's namespace values
    into the embed-qr namespace too %>
<section data-controller="embed-qr"
         data-embed-qr-oauth-params-value='<%= ... %>'
         ...>

Regression guard: add an inline comment above the controller declaration in app/views/embed/qr/show.html.erb stating "why it's a single attach." If a PR review revives attaching both controllers at once, reject it.

The widget only mounts and shows an empty iframe

Symptom: only a gray box appears where <div data-logi-qr> is, and no QR shows. The console shows a CSP or X-Frame-Options error.

Candidate causes:

  1. The RP's origin is not registered in OauthApplication.widget_origins
  2. widget_enabled = false
  3. The RP page's CSP frame-ancestors blocks embed.1pass.dev

Fix:

ruby
# In the Rails console or the [start.1pass.dev/developer](https://start.1pass.dev/developer) console
app = OauthApplication.find_by!(name: "your-rp")
app.update!(
  widget_enabled: true,
  widget_origins: [
    "https://your-app.com",      # production
    "http://localhost:3000"       # dev — exact scheme + port
  ]
)

CSP check:

# Recommended (RP page headers)
Content-Security-Policy: frame-src https://embed.1pass.dev; child-src https://embed.1pass.dev;

The widget doesn't work even after calling mountWidget

Symptom: after loading <script src="…/widget.js"> and dynamically adding a <div>, the widget doesn't mount.

Cause: widget.js's auto-init runs only once, on DOMContentLoaded. Mount nodes added after that are not discovered automatically.

Fix: call it directly

js
const mount = document.getElementById("logi-mount");
// after setting all the data-* attributes
if (window.LogiWidget) window.LogiWidget.mountWidget(mount);

Or, for React/Vue/Svelte, see the Widget SDK framework examples.

postMessage doesn't reach the parent

Symptom: the user finishes scanning the QR + approving in the app, but data-on-success isn't called.

Cause: the widget verifies event.origin, but it mismatches the parent page's origin. Or the function data-on-success="…" points to isn't on window.

Fix:

  • Confirm the callback function is registered on windowwindow.handleLogin = function() {…}
  • The parent must also verify event.origin === "https://embed.1pass.dev" — messages from any other origin are ignored (security)

Standard OAuth Flow

401 invalid_client

Symptom: the /oauth/token POST returns 401 + {"error": "invalid_client"}.

Candidate causes:

  1. The client_secret isn't picked up from the environment variable (Render: build-time vs runtime separation)
  2. The client_id / client_secret are values from a different environment (production / staging)
  3. The client_secret was rotated and the old value is still in use
  4. (Mobile RP) the build script forgot to inject the client_id → a placeholder string is baked into the app binary. The Flutter String.fromEnvironment defaultValue trap is the classic example. → see the Flutter integration guide

Fix:

bash
# On the RP backend
echo $LOGI_CLIENT_ID    # logi_xxxxxxxxxxxx format
echo $LOGI_CLIENT_SECRET | head -c 8  # show only the first 8 chars

# Compare that client_id matches on the RP settings page in the 1pass console
# If it doesn't match, suspect a rotation — Console → app detail → "Reveal current secret"

Render environment variable trap

Referencing $LOGI_CLIENT_SECRET in the Build Command yields an empty string — Render's secret environment variables are injected only at runtime. They're usable only in the Start Command, like bundle exec rails s.

302 redirect_uri_mismatch

Symptom: /oauth/authorize does a 302 to an error page.

Cause: the redirect_uri parameter does not match the RP's registered redirect_uris character for character. Common traps:

  • trailing slash (/cb vs /cb/)
  • http vs https
  • subdomain (app.example.com vs www.app.example.com)
  • whether a query string is included (/cb?env=prod vs /cb) — logi does not allow a query string in a registered redirect_uri

Fix: copy the URI registered in the console and use it verbatim.

invalid_request: redirect_uri not registered

Symptom: /oauth/authorize returns a JSON error straight away:

json
{ "error": "invalid_request", "error_description": "redirect_uri not registered" }

Cause: it looks like redirect_uri_mismatch but it's different — mismatch means "there's a similar candidate but the characters differ," while not registered means there's no candidate in the whitelist at all. The most common paths:

  • A mobile RP (e.g. redirect_uris=["app://oauth/1pass/callback"]) starts also exposing a web surface under the same client_id → the web callback (https://app.example.dev/auth/1pass/callback) isn't in the whitelist
  • A new staging / preview domain was created and the RP update was missed
  • An attempt to use a branch preview URL (e.g. Vercel *-git-feature-x.vercel.app) → violates the static whitelist
  • An RP-side callback path rename (e.g. /auth/1pass/callback/auth/logi/callback) applied only in the RP code, missing the synchronized IdP whitelist update — the most common regression pattern. It's discovered on the first user click right after deploy

Fix:

bash
# 1. Check the RP's current redirect_uris
logi app show $CLIENT_ID
# 2. Add the missing URI (keep existing entries, append)
logi app update $CLIENT_ID --add-redirect-uri "https://app.example.dev/auth/1pass/callback"
# 3. Verify right after adding
logi apps verify $CLIENT_ID -r "https://app.example.dev/auth/1pass/callback"

SSH workaround (when the CLI isn't installed and you have direct access to logi-server):

bash
ssh <LOGI_WEB_SERVICE_ID>@<RENDER_SSH_HOST> \
  'cd server && bundle exec rails runner "app = OauthApplication.find_by(client_id: %q[logi_xxxxxxxxxxxxxxxxxxxx]); nu = %q[https://app.example.dev/auth/1pass/callback]; app.update!(redirect_uris: (app.redirect_uris + [nu]).uniq) unless app.redirect_uris.include?(nu); puts app.reload.redirect_uris.inspect"'

Security

For a public + PKCE RP, sharing the same client_id between mobile and web is safe. For a confidential RP (using a client_secret), separating surfaces is safer — we recommend issuing a separate client_id.

Regression prevention (callback path rename)

A PR that changes the callback URL on the RP side (e.g. /auth/old/callback/auth/new/callback for a brand integration) must also handle the IdP whitelist update in the same PR. Recommended procedure:

  1. Keep the old path + add the new path to expand the whitelist first (before deploy)
  2. Deploy the RP → verify with real traffic
  3. After a while (once in-flight sessions have expired, typically 7+ days), remove the old path

Verify immediately before deploy:

bash
# Check that the new redirect_uri is in the whitelist (302 response = OK, JSON error = missing)
curl -sI "https://api.1pass.dev/oauth/authorize?client_id=$CLIENT_ID&redirect_uri=$NEW_URI_ENCODED&response_type=code&scope=openid&state=preflight&code_challenge=x&code_challenge_method=S256" | head -1

Putting the call above in CI catches the omission before it reaches production traffic.

state mismatch / lost session

Symptom: the callback arrives but state verification fails.

Candidate causes:

  1. The user started login, then started again in another tab (overwriting state)
  2. The session cookie wasn't sent after the first redirect because it's Lax/Strict (cross-site)
  3. The RP is stateless (load balancer) without sticky sessions

Fix:

  • Keep the state → verifier mapping as a dict (not a single value) — the session[:onepass_pkce] pattern in recommended-architecture
  • Use a session store that can be shared across instances (Redis, DB) — cookie_store is fine
  • iframe integration may require SameSite=None; Secure

The code has expired

Symptom: invalid_grant at token exchange.

Cause: the code 1pass issues expires after 10 minutes (RFC 6749 §4.1.2 best practice — OauthAccessGrant::EXPIRY = 10.minutes). Or reuse of a code that was already used.

Fix:

  • Exchange immediately upon receiving the callback (don't wait for user input)
  • On retry, re-issue starting from state

403 anonymous_not_allowed

Symptom: a mobile-app user approved on the consent screen, but logi returns 403 + {"error": "anonymous_not_allowed"}, and the app shows a message like "Only accounts that have completed email sign-up…".

Cause: the user attempted to consent to an allow_anonymous_grants=false RP while being a logi anonymous account (not linked to Apple/Google, no email registered). This is not a logi problem — it's that RP's policy. External RPs accept only identified subjects by default.

The RP developer's options:

  1. Accept anonymous too — if your service is suitable for guest mode, toggle allow_anonymous_grants=true in the RP settings. (How to configure)
  2. Inform the user — use the application_name + remediation fields in the 403 response to offer a CTA like "This app requires an identified account. Please link Apple/Google or register an email in your account settings." Response shape: Anonymous Grants — block response

The user's fix: logi app → Settings → Account → link Apple / Google, or register an email. After promotion, return to the RP and retry.

A smoother experience (recommended): the 403 response also comes with a promotion object. Using this object, you don't need to throw the user outside the OAuth dance — you can collect Apple/Google/email registration via an inline promotion sheet and continue the same flow with a resume_token. For the full flow and response shape: JIT Promotion.

422 resume_token_expired / resume_token_already_used / promotion_incomplete

Symptom: you finished registration via JIT promotion and called POST /api/v1/oauth/authorize/resume, but got a 422.

ErrorMeaningFix
resume_token_expiredThe 5-minute TTL exceededStart over (retry /oauth/authorize)
resume_token_already_usedThe same token was redeemed twiceGet a new token (retry)
promotion_incompleteThe token is valid but the user is still anonymousThe registration step didn't finish. Re-check the /api/v1/me/connected_identities or /api/v1/me/emails response
resume_user_mismatch (403)The PAK diverged to a different user after the token was issuedStart over

Device Flow (RFC 8628)

Repeated slow_down error

Symptom: the /oauth/device/poll response keeps returning slow_down.

Cause: the poll interval is too fast (RFC recommendation: 5 seconds).

Fix: follow the Retry-After header in the response or the interval value in the body. It's safe to increase the interval by +5 seconds on each slow_down.

user_code expires too quickly

Symptom: it expires before the user scans the QR.

Cause: 1pass's device code TTL is 10 minutes.

Fix: auto-refresh — issue a new device code as it nears 10 minutes. The user_code entry screen on the logi app side works the same way.

id_token verification

iss mismatch

Symptom: the id_token's iss is https://api.1pass.dev but the RP code compares against https://1pass.dev → failure.

Cause: the production issuer is https://api.1pass.dev. (start.1pass.dev is the console; 1pass.dev is the docs.)

Fix: use the issuer field of .well-known/openid-configuration verbatim.

js
// Pattern for fetching it dynamically
const config = await fetch("https://api.1pass.dev/.well-known/openid-configuration").then(r => r.json());
expect(idToken.iss).toEqual(config.issuer);

Verification fails after JWKS key rotation

Symptom: it worked until yesterday, but suddenly id_token signature verification fails.

Cause: the JWKS key was rotated and the RP is caching the old key.

Fix: keep the JWKS fetch cache to 1 hour or less, and refresh immediately if the kid isn't in the cache. Libraries (jose, jwt) usually handle this for you.

General debugging tools

1. Check the discovery document

bash
curl -s https://api.1pass.dev/.well-known/openid-configuration | jq .

Confirm that issuer, authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri, and embed_endpoint all return a healthy 200.

2. Check the JWKS

bash
curl -s https://api.1pass.dev/.well-known/jwks.json | jq '.keys[].kid'

3. Decode the id_token (no signature verification — debugging only)

bash
echo "$ID_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

4. Hand-build the authorization start URL

bash
echo "https://api.1pass.dev/oauth/authorize?$(cat <<EOF | tr -d ' \n'
response_type=code&
client_id=logi_xxx&
redirect_uri=https%3A%2F%2Fexample.com%2Fcb&
scope=openid+profile%3Abasic+email&
state=test123&
code_challenge=$CHALLENGE&
code_challenge_method=S256
EOF
)"

5. The widget demo page

A working production sample: https://api.1pass.dev/widget-demo.html. It mounts against the actual 1pass-demo RP, so it's a reference you can use to confirm the widget works correctly.

If it still doesn't work

  • Developer console → app detail → "Recent error logs"
  • Information that helps debugging: the client_id (first 12 chars), the time, the error + error_description, and the SDK version in use

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