SenseCrypt Docs
Guides

Security best practices

Harden your SenseCrypt integration — redirect-URI hygiene, PKCE, secure token storage, refresh-token rotation, session management, JWT validation, and app attestation.

SenseCrypt is built to fail closed, but a secure integration is a shared responsibility. This guide collects the practices that matter most when you build on SenseCrypt. Most are simply "use the protections the platform already enforces, correctly."

Redirect-URI hygiene

Redirect URIs are the most abused corner of OAuth. SenseCrypt validates them against an exact-match allow-list (a trailing slash is significant) — lean into that:

  • Register exact, fully-qualified callback URLs — scheme, host, port, and path. Avoid wildcards; register each concrete URL you actually use.
  • Use HTTPS for every web callback. Reserve custom-scheme or loopback redirects for native/mobile clients.
  • Keep the list minimal. Every registered URI is attack surface. Remove ones you no longer use.
  • Don't put secrets or PII in redirect URIs. They land in browser history and logs.
  • The post-logout redirect URI is a separate allow-list. Register your post-logout URLs explicitly; they are not drawn from the /authorize callback list. A URL not on the list is rejected — SenseCrypt never performs an open redirect.

At /authorize, an unknown client and a bad redirect_uri return the same branded HTML refusal page — HTTP 400, no redirect, no JSON error (anti-enumeration) — so a surprising 400 page instead of a callback is often just an unregistered redirect URI. Check the registration first.

Always use PKCE

SenseCrypt supports S256 only and rejects plain. PKCE is mandatory for SPA (public) clients and strongly recommended for every client.

  • Generate a fresh, high-entropy code_verifier per authorization request (43–128 characters of the unreserved set), and send code_challenge = BASE64URL_NOPAD(SHA256(verifier)) with code_challenge_method=S256.
  • Never reuse a verifier, and never log it.
  • Don't send a verifier for a flow that had no challenge — SenseCrypt strictly rejects that (invalid_grant), which catches misconfigured clients.
  • Prefer PAR (Pushed Authorization Requests) when you can. Pushing the request server-side keeps the PKCE challenge and audience out of the browser, where the PAR-pushed values win over any tampered query parameters.

Validate tokens correctly

If your API consumes access tokens, validate them properly — don't trust an unverified JWT:

  1. Verify the signature against the tenant's jwks_uri, selecting the key by kid. Access tokens are per-tenant ES256; an ID token carries whichever algorithm its application registered (ES256 or RS256), so pin that one where you validate ID tokens.
  2. Check iss equals the expected tenant issuer, exp is in the future, and aud equals your API's audience.
  3. Authorize against permissions for per-action checks (see RBAC).
  4. Tolerate multiple JWKS keys during a rotation overlap, and refetch the JWKS on an unknown kid rather than caching forever. An immediate rotation (e.g. after a key compromise) drops the old key at once; a stale cache would keep trusting it.

Require token_use: "access" where relevant, and don't accept an ID token where an access token is expected (or vice-versa).

Store tokens safely

Where you keep tokens matters more than almost anything else:

  • Refresh tokens are opaque bearer secrets. Treat them like passwords. On mobile, use the platform keystore; on a server, use a secret store — never plaintext on disk or in a log.
  • Browser SPAs: avoid localStorage for tokens. It's readable by any script and a prime XSS target. Prefer in-memory access tokens with a short lifetime, and lean on refresh rotation.
  • Never put tokens in URLs. They leak via history, Referer headers, and access logs. (SenseCrypt itself follows this rule — email-link tokens ride in the URL fragment, and face tokens never travel through PAR.)
  • Scope down. Request only the scopes and the audience you need; don't request offline_access for a flow that doesn't need long-lived sessions.

Use refresh-token rotation

Rotation is on by default for SPA clients and available for others — use it, and handle it correctly:

  • Always store the newest refresh token and discard its predecessor.
  • Expect reuse detection. Replaying a rotated token outside a short overlap window revokes the entire family — the safe response to theft. Serialize refreshes in your client to avoid tripping it accidentally.
  • Respect the absolute lifetime. A token family cannot be refreshed past its issuance-fixed absolute expiry; design for periodic re-authentication.

See Refresh tokens and sessions for the mechanics.

Manage sessions and logout deliberately

There is a short-lived OP browser session (sc_op_session, 8 hours by default, configurable per tenant and disablable with 0); everything beyond it is token lifetimes, and logout ends both:

  • Keep access-token lifetimes short. Self-contained JWTs stay valid until exp unless revoked, so a short lifetime is what makes suspensions and permission changes take effect quickly.
  • Implement RP-Initiated Logout with an id_token_hint — not because the endpoint rejects a request without one, but because it is the only thing that proves your application is asking and so the only way you are redirected back instead of landing on SenseCrypt's signed-out page. Register your post-logout redirect URIs. If you want to be told when a session ends elsewhere, register a backchannel_logout_uri too.
  • Rely on eager revocation for lifecycle events, and offboard explicitly. Suspending or deleting a user immediately revokes every plane — device keys, passkeys, refresh-token families and browser sessions — so you don't have to script revocation for offboarding. Removing a user from a group is an authorization change, not offboarding: it eagerly cuts the refresh-token families for the applications they lose (and, for a user left in no app-attached group, their browser session too) but deliberately leaves the device key and passkey alone. To offboard, suspend or delete the user; don't rely on a group removal to do it.
  • For immediate cutoff at your resource servers, check the introspection endpoint (which honors the revocation denylist) rather than relying solely on local JWT verification — but cache introspection results to stay within rate limits.

Lean on app attestation (mobile)

If you operate the deployment, keep app attestation enabled in production (require_app_attestation). It's a deployment-wide floor with no per-client opt-out by design — a per-app exemption would be a weakest-link hole. It's what makes hardware key residency (Secure Enclave / StrongBox) a trustworthy guarantee: only the genuine, store-distributed Authenticator app can pass.

Attestation failures are deliberately opaque to clients (error: "forbidden", message: "attestation_failed"); the real reason goes to server logs. Don't try to branch client behavior on the subreason.

Don't defeat the anti-enumeration design

SenseCrypt goes to lengths to make failure paths indistinguishable — matching HTTP shape, bytes, and even latency. When you build on top:

  • Don't add a "helpful" error that reveals whether an account, email, or group exists. The opaque invalid_grant / unauthorized / {"active": false} responses are a feature.
  • Don't build a UX that leaks membership (for example, a different message for "not in group" vs "wrong password"). SenseCrypt won't tell you which it was — keep it that way in your own surfaces.

Protect your own admin/console surfaces

If you build management tooling against the Management API:

  • Use client_credentials M2M tokens for machine access — never embed long-lived admin credentials in a browser.
  • Grant least privilege. An M2M app's capabilities come from its bound roles; give it only the capabilities it needs.
  • Rotate client secrets using the grace-windowed rotation, so a rotation never causes an outage.

A quick checklist

  • Exact, HTTPS, minimal redirect URIs — and separate post-logout URIs registered.
  • PKCE S256 on every client; fresh verifier per request; PAR where possible.
  • JWTs validated: signature (by kid), iss, aud, exp; JWKS refetched on unknown kid.
  • Tokens stored in a keystore/secret store; never in localStorage or URLs.
  • Refresh rotation handled; newest token stored; refreshes serialized.
  • Short access-token lifetimes; logout with id_token_hint.
  • App attestation on in production.
  • Least-privilege M2M apps; secrets rotated.

On this page