Troubleshooting
Common problems integrating with or operating SenseCrypt, and how to fix them — sign-in failures, token and PKCE errors, redirect-URI issues, SCIM problems, refresh-token behavior, and self-hosting first-run failures.
Common problems and their fixes, grouped by where they show up. For the exact error taxonomy see Error codes.
Sign-in and authorization
Users can't sign in — everything returns an opaque error
The access gate is default-closed: an application with zero attached groups admits nobody, and every rejection is deliberately opaque (unknown app, unknown user, suspended user, and missing group all look identical).
Fix: attach at least one group to the application and add the user to it. Confirm the user is active (not suspended or deleted) and, for SCIM-provisioned users, that they have completed biometric enrollment on the app (a pending shell can't sign in). The gate re-runs on every token and refresh, so a removed or suspended user fails on their next call.
invalid_grant at the token endpoint
On the authorization_code grant this is intentionally uniform — it covers an invalid/expired/already-used code, a cross-tenant client, PKCE failures, and access-gate failures. Work through:
- Code already used or expired — authorization codes are single-use and short-lived. Exchange immediately; never retry an exchange with the same code.
- PKCE mismatch — see PKCE errors below.
- Wrong client or redirect_uri — the code is bound to the
client_idandredirect_uriit was issued for; the token request must present the same ones. - Access gate — the user must still pass the group gate at exchange time.
PKCE errors
SenseCrypt requires S256 and rejects plain.
code_challenge is required for this client— SPA (public) clients must send acode_challengeat/authorizeor/par.unsupported code_challenge_method— sendS256(or omit the method with anS256challenge; the implicitplaindefault is not honored).code_verifier does not match code_challenge— your verifier must be the exact preimage:BASE64URL_NOPAD(SHA256(verifier)) == challenge. Check you aren't re-encoding or truncating.code_verifier supplied but session was not registered with PKCE— you sent a verifier for a flow that had no challenge. Send the challenge at authorization, or drop the verifier.
redirect_uri rejected (or an opaque 401 at /authorize)
Redirect URIs are matched against an exact-match allow-list (with trailing-slash tolerance). At /authorize, an unknown client and a bad redirect_uri return the same opaque 401 unauthorized to prevent enumeration — so a "mysterious" 401 is often just an unregistered redirect URI.
Fix: register the exact callback URL on the application, including scheme, host, port, and path. Note that the post-logout redirect URI has a separate allow-list from the /authorize callback list.
invalid_target when requesting an API audience
The client requested a resource/audience it isn't allowed to mint for. Unknown API and ungranted API are indistinguishable (both fail closed).
Fix: create the resource server and grant the application access to that audience. For M2M, the audience must be exactly the tenant's Management API identifier ("{issuer}/v1/admin/").
No permissions claim in the access token
permissions is emitted only when you target a resource server that has enforce_rbac turned on. Without an audience, or against a resource server that doesn't enforce RBAC, there's no permissions claim by design.
Fix: request the API's audience and enable enforce_rbac on that resource server. Also confirm the user actually has a role (directly or via a group) granting permissions on that API — the claim may legitimately be an empty array.
Tokens and sessions
Refresh suddenly fails with invalid_grant
- Reuse detected — reusing an already-rotated refresh token outside the short overlap window is treated as a breach and revokes the entire token family. Store only the latest token returned and never reuse a superseded one (avoid concurrent refreshes racing on the same token).
- The user lost access — the access gate re-runs on every refresh, so a suspended/deleted user, or one removed from the app's groups, fails closed.
- Absolute expiry — the family has an absolute lifetime fixed at issuance that rotation can't extend.
I revoked a token but the access token still works
Access tokens are self-contained JWTs and remain valid until exp unless explicitly revoked (their jti denylisted, or their refresh family revoked). Keep access-token lifetimes short and refresh as needed. UserInfo and introspection do honor the denylist immediately.
"Logout" doesn't sign the user out
There is no server-side SSO cookie — every sign-in is a fresh on-device face ceremony. RP-Initiated Logout revokes the subject's refresh tokens and requires an id_token_hint (a missing hint is invalid_request, not a silent success). Outstanding access tokens still live until exp unless revoked.
Signature verification fails at my API
- Verify against the tenant's
jwks_uriand select the key bykid— tokens are signed per tenant with ES256. - Tolerate multiple keys in the JWKS during a rotation overlap.
- If you cache the JWKS, a rotation (especially an immediate one for a compromised key) means your cache may still trust an old key until it expires. Cache with a sane TTL and refetch on an unknown
kid.
SCIM provisioning
Filters return 400 invalidFilter
An unknown attribute path or a malformed/over-long filter is a 400 invalidFilter — never a silent full-directory listing. Use supported paths and operators (eq ne co sw ew gt ge lt le pr, and/or/not, grouping, emails[...]). Multi-valued complex attributes support presence (pr) only.
Group member removal isn't working
A remove on members without a target is rejected as 400 invalidValue — it is never treated as clear-all. Send the specific member (members[value eq "<id>"], or the id in the body).
409 uniqueness on create
A userName/email collision (users) or displayName collision (groups) within the tenant. Reconcile the existing record instead of creating a duplicate.
SCIM auth fails (401/403)
- 401 — the bearer token is missing or invalid;
detailnames the reason. Mint a freshssct_token in the console, or check your M2M token. - 403 — the token authenticated but lacks the
scim:read/scim:writecapability for the method. Grant the SCIM capabilities to the M2M app's role.
A SCIM-managed group can't be edited in the console
That's intentional — SCIM-managed groups are read-only in the admin console so your external IdP stays the source of truth. Edit them through your IdP.
Device / mobile
426 upgrade_required
The client's protocol version is below the supported floor. The user must update the Authenticator app.
A device sign-in returns attestation_failed
App attestation is enforced deployment-wide and the failure is opaque (error: "forbidden", message: "attestation_failed"). Causes include a non-genuine/sideloaded app, a failed device-integrity verdict, or a self-hosting misconfiguration.
If you self-host: confirm your attestation prerequisites are configured — the platform integrity credentials, and the package name, signing-cert fingerprints, and iOS team/bundle IDs. A missing credential fails the verifier closed. Server logs carry the specific subreason; clients never do. See Self-hosting for the exact configuration.
clock_skew errors from the device
The request timestamp is outside the allowed window. This is recoverable — the device should sync its clock (NTP) and retry.
Rate limiting
Unexpected 429 responses
You've exceeded a rate limit. Read the Retry-After header and back off. Two common surprises:
- Introspecting on every request. Verify the JWT locally against the JWKS and reserve introspection for revocation checks.
- All clients appearing as one IP (self-hosted). If
TRUSTED_PROXY_HOPSdoesn't match your proxy topology, every client can be attributed to a single IP and trip the IP-scoped limits. Set it to the actual number of proxy hops.
Self-hosting first-run failures
For the exact configuration behind each fix below, see Self-hosting.
| Symptom | Cause / fix |
|---|---|
Startup aborts: no active signing-key KEK configured — refusing to boot | No at-rest key-encryption key resolved. The service fails closed rather than store signing keys as plaintext — configure a signing-key encryption key (locally or via KMS). |
| Admin login fails complaining the admin session secret is empty | The admin session secret isn't set. Configure a strong random value and restart. |
API returns 500s with relation "..." does not exist | Database migrations haven't been applied — run them and restart. |
OIDC/SAML endpoints return NotFound | Host resolution is disabled, or the request host doesn't resolve to a tenant slug. Protocol surfaces live on per-tenant subdomain hosts — configure your issuer base domain. |
A 502 minter_unavailable when creating a user from a photo | The sealing step for admin photo provisioning is unreachable. Only admin photo-provisioning depends on it — sign-in, mobile self-signup, and token rotation are unaffected. Check that provisioning is configured. |
| PINs / verification links never arrive | In a development configuration, mail is only logged, not sent — grab PINs and links from the application log. In production, enable email delivery with a DKIM-verified sender. |
| SSE streams (live activity feed, QR page) stall or arrive late | Disable proxy buffering for the SSE routes and keep the SSE heartbeat interval below your proxy's idle timeout. |
Related
- Error codes — every error and its meaning.
- Rate limits — the throttling policy.
- Self-hosting — deployment, configuration, and operations.
- Security best practices — hardening your integration.
Glossary
Definitions of the terms used across the SenseCrypt documentation — protocol, authorization, key-custody, and device concepts, in the precise sense SenseCrypt uses them.
API Reference
The full SenseCrypt REST API — OIDC protocol endpoints, the SAML IdP, SCIM 2.0, and the admin/Management API — generated from the OpenAPI spec, with the auth model and base URL for each surface.