Integration questions
The things people ask once they have sign-in working.
Tokens
How long does an access token last? One hour. Refresh tokens last 30 days and rotate on every use — the old one stops working the moment you exchange it.
Do I get a refresh token?
Only if you ask for offline_access in your scopes. Without it you get an access
token and an ID token and nothing else, which is the right answer for a site that
just needs to know who signed in.
What happens if I use a refresh token twice? Every token descended from that sign-in is revoked immediately and the user has to sign in again. A refresh token can only be used once, so a second use means two parties are holding it — and there is no way to tell which one is asking. We would rather end both sessions than keep a stolen one alive.
If you see this happening in normal operation, something is racing: two requests refreshing at once, or a retry replaying an earlier attempt. Serialise refreshes per user.
What is in the ID token, and what is in /oauth/userinfo?
The ID token has the standard claims — sub, given_name, family_name,
email, email_verified, phone_number, gender, picture, birthdate.
/oauth/userinfo returns those same claims plus every custom field you
configured, under https://0account.com/claims/fields.
If you only need a name and an email, decode the ID token and skip the request.
Identity
Is sub stable?
Yes, for your app. It never changes for that user signing into that app.
It is also different for every app. The same person signing into two sites
gets two unrelated subjects, so the two cannot compare records and work out they
are the same person. If you need to match a user across your own apps, do it on a
verified email, not on sub.
Is the email verified?
Yes. email_verified is true whenever an email is present, because an
unverified address cannot be approved.
Can two users share an email? No. An email belongs to one profile.
Fields
I added a field and it is not coming back. Three things to check, in order:
- Is it configured on this app? Fields are per-app.
- Are you reading
/oauth/userinfo, not just the ID token? Custom fields are only in userinfo. - Are you looking under
https://0account.com/claims/fields? Custom fields are never at the top level.
Adding a field to the authorize URL does nothing — the approval screen is built from the app's configuration.
Why is the claim name a URL? So it cannot collide with a standard claim, or with another provider's. It is a key, not an address; nothing fetches it.
Can I ask for a field only sometimes? Not per-request. The field list belongs to the app. If two flows need different data, that is two apps.
Sessions
The user signed out of my app. Are they signed out of 0account?
Not unless you say so. POST https://v1.0account.com/oauth/logout with
id_token_hint set to the ID token you were issued, and we end the session on
our side too. Your server can call it directly — no redirect, and the user does
not need to be present.
They signed out of 0account. Do I find out?
Yes, if you registered a backchannel logout URI. We POST a signed logout
token naming the sid that ended. Without one you find out when their access
token stops working.
Should I store sid?
Yes, next to your own session record. It is what a logout token names, and
without it you cannot tell which session to end — sub alone would sign the user
out on every device at once.
Approval
How long does the user have to approve? Ten minutes by default. You can set anything from 1 to 30 minutes per app. If you ask for more data, allow more time.
What happens if they decline, or the code expires?
Your redirect_uri is called with an error parameter rather than a code.
Handle it the same way you would any failed sign-in; most libraries surface it as
an error already.
Setting up
Do I need allowed origins? Only if a browser calls our API directly. A server-side integration — Auth.js, Passport, Allauth, Spring Security — needs none. Redirect URIs are always required.
Can I run this without a backend? Not securely. A client secret in a browser is not a secret. Use your framework's server side, which is where every guide here puts it.
I lost the client secret. Generate a new one in the dashboard. It replaces the old one at once, so there is a gap between generating it and deploying it during which sign-in fails — keep it short, and do it when nobody is signing in if you can.
My secret is rejected and I am sure it is right.
Check the prefix. Secrets are issued as 0account_sec_… and must be sent
complete — a copy that lost the prefix is rejected as malformed.