SenseCrypt Docs
Reference

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_id and redirect_uri it 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 a code_challenge at /authorize or /par.
  • unsupported code_challenge_method — send S256 (or omit the method with an S256 challenge; the implicit plain default 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_uri and select the key by kid — 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; detail names the reason. Mint a fresh ssct_ token in the console, or check your M2M token.
  • 403 — the token authenticated but lacks the scim:read/scim:write capability 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_HOPS doesn'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.

SymptomCause / fix
Startup aborts: no active signing-key KEK configured — refusing to bootNo 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 emptyThe admin session secret isn't set. Configure a strong random value and restart.
API returns 500s with relation "..." does not existDatabase migrations haven't been applied — run them and restart.
OIDC/SAML endpoints return NotFoundHost 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 photoThe 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 arriveIn 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 lateDisable proxy buffering for the SSE routes and keep the SSE heartbeat interval below your proxy's idle timeout.

On this page