Security checklist
Go through this list with your integration open next to it. Most items are one line of code; each one closes a way that sign-in integrations are attacked.
Secrets
- The client secret lives only on your server, in a secret store or environment variable. It is not in front-end code, a mobile app, a repository or a log.
- You know how to rotate it: add a second secret in the console, deploy it, then retire the old one. Both work in between.
- The webhook signing secret (
whsec_...) and your cookie-signing secret are stored the same way.
The sign-in request
- Every request has a fresh random
state, a freshnonceand a fresh PKCE verifier, made with a cryptographic random source. - They are kept in an
HttpOnly,Secure,SameSite=Laxcookie scoped to your auth path, signed or encrypted, that lives about 10 minutes, or in your server-side session. - The redirect URI is
httpsand registered exactly. No wildcards.localhostonly on a development-mode app. - The button links to your own start route. Nothing builds an
/authorizeURL in the browser.
The callback
stateis compared with the stored value before anything else, and used once. A missing or different state ends the attempt.- The code is exchanged on your server with your client credentials and the stored verifier. The same
redirect_uriis sent as in the request. - The ID token is validated:
algisES256, the signature checks against a key fromhttps://id.avonao.com/.well-known/jwks.json,issishttps://id.avonao.com,audis your client ID,expandiatare sane,noncematches. - The keys are cached for at most 10 minutes and fetched again once on an unknown
kid. They are not pinned and not copied into your code. - Your own session is created only after all of that succeeded, with a new session id. A failed sign-in leaves no session behind.
- Both routes are rate limited.
- Tokens and codes are not logged and not put in URLs that you pass on. You rarely need to keep the ID token or access token: take what you need from the claims and drop them.
Accounts
- Users are keyed by
sub(a unique column), never by email. See Pairwise ids. - An existing local account is attached by email only when you verified that address on the local account too, and never when it already has a different
sub. - Your callback handles
access_deniedand every other error with a fixed message and a retry button, never in a loop, and never printingerror_descriptionas it came. See Errors.
Webhooks
- The signature is checked on the raw body, compared in constant time, and a timestamp older than 5 minutes is refused.
- The handler is idempotent and de-duplicates on
avonao-delivery. It answers a2xxwithin 5 seconds and ignores event types it does not know. member.deletederases that member's data, andgrant.revokedends their Avonao ID sessions. See Webhooks.
Going live
- Your domain is verified, every redirect URI and the sign-in start URL are
httpson it, and the app has a Privacy Policy URL. - You tested with a teammate in development mode, and with a member from outside your business after you went live.
- Your own sign-out ends your session, and "Manage your Avonao ID" links to
https://id.avonao.com/dashboard. - If you load the button's stylesheet or icon from
id.avonao.com, your Content Security Policy allows it instyle-srcandimg-src; or you host both yourself. See the Button kit.

