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
/authorizecallback 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_verifierper authorization request (43–128 characters of the unreserved set), and sendcode_challenge = BASE64URL_NOPAD(SHA256(verifier))withcode_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:
- Verify the signature against the tenant's
jwks_uri, selecting the key bykid. Tokens are per-tenant ES256. - Check
issequals the expected tenant issuer,expis in the future, andaudequals your API's audience. - Authorize against
permissionsfor per-action checks (see RBAC). - Tolerate multiple JWKS keys during a rotation overlap, and refetch the JWKS on an unknown
kidrather 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
localStoragefor 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,
Refererheaders, 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_accessfor 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
expunless 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_credentialsM2M 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
S256on every client; fresh verifier per request; PAR where possible. - JWTs validated: signature (by
kid),iss,aud,exp; JWKS refetched on unknownkid. - Tokens stored in a keystore/secret store; never in
localStorageor 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.
Related
- Security — the platform's own security model.
- Refresh tokens and sessions — rotation, revocation, logout.
- RBAC: roles and permissions — validating and enforcing
permissions. - Rate limits — staying within the throttling policy.
Testing your integration
Test your app against SenseCrypt in a dev environment — read email verification PINs from the logs when SES is off, run with app attestation disabled, and drive the real OIDC ceremony against a throwaway tenant.
SDKs & apps
The SenseCrypt Authenticator mobile app for end users, and how to integrate your own web application as a relying party.