Security requirements
Everything on this page is a requirement on your integration. None of it is enforced for you.
Protect the client secret
The secret is equivalent to your application's identity. Anyone holding it can redeem authorization codes issued to you and refresh tokens on your behalf.
- Keep it on a server. Never ship it in a browser bundle, a mobile binary, a public repository, or a client-side configuration file.
- Load it from a secret manager or environment variable, not from source.
- Rotate it from the Developer page if it may have been exposed. Deploy the new
value first — regeneration takes effect immediately and any process still
holding the old one starts failing with
invalid_client.
If your application cannot keep a secret, use PKCE and accept the limits in Client types.
Always validate state
Generate a random state per authorization attempt, bind it to the user's
session, and reject any callback whose state does not match.
Without this, an attacker can complete an authorization flow with their own Nanako account and hand the resulting callback URL to your user, linking the victim's session to the attacker's identity.
Keep the value URL-safe — hex or unpadded base64url. It is echoed into the
callback URL without escaping, so &, #, = and spaces corrupt the redirect.
Use PKCE
PKCE costs one hash and closes the window where a leaked authorization code —
from a proxy log, browser history, or a Referer header — can be redeemed by
someone else.
Send code_challenge_method=S256. The exact string; s256 silently falls back
to plain, which provides no protection.
Confidential clients benefit too. If you use both PKCE and a client secret, note that a code issued with a challenge is verified by the challenge, so keep the verifier as carefully as you keep the secret.
Register exact redirect URIs
Redirect URIs are matched byte for byte, which is the behaviour you want. Do not try to work around it:
- Register
https://for anything users reach.http://is accepted at registration for local development — never register a plaintext URI that is reachable from the internet. - One registered URI per real callback. Do not register a broad URI and route internally.
- Carry per-attempt data in
state, not in the redirect URI. A redirect URI containing a query string breaks the callback entirely.
Handle tokens as credentials
- Never log them. Access and refresh tokens are bearer credentials; anything holding one can act as the user.
- Store them encrypted at rest, scoped to the user they belong to.
- Do not put them in URLs. Access tokens go in the
Authorizationheader. - Do not parse them. They are opaque strings with no structure to read.
Access tokens live two hours. Refresh tokens live 30 days from the first authorization and do not extend on use — plan for the whole chain to end and the user to re-authorize.
Serialize refreshes
Refresh tokens rotate on every use, and presenting an already-rotated one is treated as a replay: every token for that user and application is revoked and the user is signed out of your app.
Two rules avoid it:
- Persist the new refresh token before doing anything else with the response. A crash between receiving and storing costs the user their session.
- One refresh at a time per user. Take a lock, or refresh proactively from a single scheduled worker rather than reactively from every request handler.
Verify the granted scope
The scope in the token response is what the user actually approved. Read it
and enforce your own behaviour against it. Do not assume the request was granted
as sent.
Honour revocation
A user can withdraw your application from their Nanako account at any time.
There is no callback for this — you find out when your next refresh returns
invalid_grant.
Treat that as consent withdrawn: delete the stored tokens and any cached profile, and present sign-in again. Do not retry, and do not keep serving the user from cached data.
When your own application signs a user out, revoke the tokens explicitly rather than just dropping them. See Step 6.
Key records on sub, not on email
sub is the stable identifier. Email addresses and phone numbers can be
changed, released and reused; display names can be changed at will. An account
that matches on email is an account takeover waiting to happen.
Store sub as your foreign key on first sign-in and match on it thereafter.
Browser-based applications
A third-party web origin cannot call /oauth/token or /oauth/userinfo
directly. Nanako's CORS allowlist is operator-configured with no wildcard, so
the browser blocks the response.
Worse, the block is not clean. The token request has a CORS-safelisted content type, so it is sent and processed — the authorization code is consumed server-side — before the browser discards the response your script cannot read. Each attempt burns a single-use code, so it fails and cannot be retried.
Do the token exchange on your server. If a browser-only architecture is genuinely required, get in touch before you build it.
What Nanako does
For your threat model, on the provider side:
- Access and refresh tokens are stored as SHA-256 digests, never in a form that can be replayed out of the database.
- Client secrets are stored as bcrypt hashes and are unrecoverable after creation.
- Refresh tokens rotate on every use, and reuse of a rotated token revokes the entire chain for that user and application.
- Authorization codes are bound to the client, the redirect URI and the user, expire in 10 minutes, and are single-use.
- Authorize, token and userinfo events are recorded with timestamp, IP address and device type, and are visible to the user in their account settings.
- Every response carries
X-Frame-Options: DENYand a frame-blocking Content-Security-Policy, so the flow cannot be framed.