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 (with trailing-slash tolerance) — 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 opaque 401 (anti-enumeration), so a surprising 401 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. Tokens are per-tenant ES256.
  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's no server-side SSO cookie — sessions are token lifetimes, and logout is revocation:

  • 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 (it's required — there's no cookie to clear) and register your post-logout redirect URIs.
  • Rely on eager revocation for lifecycle events. Suspending or deleting a user, or removing them from a group, immediately revokes their device keys and refresh-token families — you don't have to script revocation for offboarding.
  • 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