Before you start
You need a client_id, a client_secret, and a registered redirect URI. Getting Started shows how to create them. Every value on this page is an example: replace hosts, credentials, and tokens with your own.
Systems involved
(Web) App
The user's browser or client app. Here: http://localhost:5173.
Your backend
Your server, the OAuth client. It holds the secret and the tokens. Here: http://localhost:3000.
Yoursafe
The OpenID provider at accounts.yoursafe.com.
Arrows show the direction of each HTTP request. Redirects are marked as such: your backend answers and the browser makes the next request.
The flow at a glance
- The app sends the user to
/auth/loginon your backend. - Your backend redirects the browser to Yoursafe with
stateandnonce. - The user signs in at Yoursafe and is redirected to your callback with a
code. - Your backend checks
state, exchanges the code for tokens, and validates the ID token. - Your backend starts its own session and redirects the user back to the app.
- The app reads the signed-in user from your backend, and can sign out there.
Step 1Start sign-in
Your backend loads the discovery document, creates state and nonce, and redirects the browser to Yoursafe.
Login route
User action
User clicks “Sign in with Yoursafe ID”.
Request
(Web) App→Your backend
GET http://localhost:3000/auth/loginOpen this route with a full-page navigation (a link or window.location), not fetch, so the browser follows the redirects that come next.
Discovery
Your backend reads the endpoint URLs from the discovery document. Fetch it once and cache it, rather than on every sign-in.
Request
Your backend→Yoursafe
GET https://accounts.yoursafe.com/.well-known/openid-configurationResponse
Yoursafe→Your backend
Abbreviated. The live document is the source of truth; ... stands for keys such as introspection and revocation.
{
"issuer": "https://accounts.yoursafe.com",
"authorization_endpoint": "https://accounts.yoursafe.com/oauth2/authorize",
"token_endpoint": "https://accounts.yoursafe.com/oauth2/token",
"userinfo_endpoint": "https://accounts.yoursafe.com/userinfo",
"end_session_endpoint": "https://accounts.yoursafe.com/connect/logout",
"jwks_uri": "https://accounts.yoursafe.com/oauth2/jwks",
"response_types_supported": ["code"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "default", "profile", "platform"],
...
}State and nonce
Your backend(no request)
Generate two fresh random values for every sign-in, with a cryptographically secure generator, and store both in the user's server-side session.
state=FfMX8LwzcisrQWH190RkCg
nonce=2RSzKHpxWleZuA3JOIcrgQ- state
- Yoursafe returns it on the callback. You compare it with the stored value, which proves the callback belongs to a sign-in your backend started (CSRF protection).
- nonce
- Yoursafe puts it in the ID token. You compare it with the stored value, which ties the token to this sign-in and stops a token from another sign-in being replayed.
Authorize redirect
Your backend answers GET /auth/login with a redirect to the authorization_endpoint. All six parameters are required.
Response
Your backend→(Web) App(redirect)
HTTP/1.1 302 Found
Location: https://accounts.yoursafe.com/oauth2/authorize
?response_type=code
&client_id=cljxc37g6f27s
&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback
&scope=openid%20default
&state=FfMX8LwzcisrQWH190RkCg
&nonce=2RSzKHpxWleZuA3JOIcrgQLine breaks are for reading only. The real Location is one URL.
- response_type
- Always
code. - client_id
- Your Yoursafe ID client.
- redirect_uri
- Your callback, exactly as registered, URL-encoded. Send the same value again in the token exchange.
- scope
- Space-separated (
%20). Always includeopenid, plus the Yoursafe scopes your client is registered for:default,platform, orprofile. Scopes decide which claims you get; see the Claims Glossary. - state
- The value you just stored.
- nonce
- The value you just stored.
Step 2Handle the callback
Yoursafe sends the browser back to your redirect URI. Your backend checks state, exchanges the code for tokens, validates the ID token, and starts its own session.
Callback
User action
User signs in at Yoursafe and approves the requested scopes.
Request
(Web) App→Your backend(redirect from Yoursafe)
The query string carries either a code or an OAuth error, always with state.
Success: an authorization code.
GET http://localhost:3000/auth/callback?code=xDE-FXbh7l1KWE...&state=FfMX8LwzcisrQWH190RkCgError: no code, so there is nothing to exchange.
GET http://localhost:3000/auth/callback?error=access_denied&error_description=...&state=FfMX8LwzcisrQWH190RkCgCheck state, then errors
Your backend(no request)
- Compare
statewith the value in the session. If it is missing or different, stop and respond with an error. Do not exchange the code. - Remove
statefrom the session, so it works only once. - If the query has an
error, show the user a message and a way to try again.access_deniedmeans the user did not approve;invalid_scopemeans you asked for a scope your client is not registered for.
Token exchange
Send the code to the token_endpoint together with the same redirect_uri you used in Step 1.
Request
Your backend→Yoursafe
PUT https://accounts.yoursafe.com/oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=xDE-FXbh7l1KWE...
&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback
&client_id=cljxc37g6f27s
&client_secret=***The body is one URL-encoded string, broken here for reading. Yoursafe uses PUT here, not the POST most OAuth libraries send. If your library cannot change the method, make this one request yourself. The code works once and expires quickly: exchange it straight away, and start a new sign-in if the exchange fails.
Response
Yoursafe→Your backend
{
"access_token": "eyJ...abc",
"id_token": "eyJ...xyz",
"token_type": "Bearer",
"expires_in": 1800,
"scope": "openid default"
}Depending on your client, the response can also include a refresh_token. Keep all tokens on the server.
Validate the ID token
The id_token is a signed JWT. Validate it before you trust any claim in it. A maintained OpenID Connect or JOSE library does these checks for you.
- Signature
RS256, with the key fromjwks_uriwhosekidmatches the token header. Cache the key set, and fetch it again when a token has an unknownkid.- iss
- Exactly
https://accounts.yoursafe.com. - aud
- Contains your
client_id. - exp
- In the future, allowing a little clock skew.
- nonce
- Equal to the value in the session. Then remove it from the session.
A decoded ID token payload for openid default:
{
"iss": "https://accounts.yoursafe.com",
"sub": "G0Y999.yoursafe.id",
"aud": "cljxc37g6f27s",
"exp": 1793697600,
"iat": 1793695800,
"nonce": "2RSzKHpxWleZuA3JOIcrgQ",
"aliastoken": "G0Y999.yoursafe.id",
"accountstatus": "active",
"countrycode": "NL"
}Start your session and redirect
Store the tokens and claims in your server-side session. The same callback request then ends with a redirect to the app, carrying only a session cookie.
Response
Your backend→(Web) App(redirect)
HTTP/1.1 302 Found
Location: http://localhost:5173/login/success
Set-Cookie: sid=8f3c...; Path=/; HttpOnly; SameSite=LaxHttpOnly keeps the cookie away from JavaScript. Add Secure once you serve over HTTPS. From here on, your own session authorizes the app's requests; the browser never calls Yoursafe.
User action
User lands on the app, signed in.
Step 3Read the user's claims
The app asks your backend who is signed in. Your backend answers from the validated ID token and, when it needs more, from the UserInfo endpoint.
Your profile route
Request
(Web) App→Your backend
GET http://localhost:3000/api/me
Cookie: sid=8f3c...From the app, send the cookie with fetch(url, { credentials: "include" }).
Response
Your backend→(Web) App
The shape is up to you. Return only what the app needs, and 401 when there is no session.
{
"authenticated": true,
"user": {
"sub": "G0Y999.yoursafe.id",
"aliastoken": "G0Y999.yoursafe.id",
"accountstatus": "active",
"countrycode": "NL"
}
}UserInfo (optional)
To fetch the claims fresh, call the userinfo_endpoint with the access token. Which claims come back depends on the granted scopes; the Claims Glossary lists them.
Request
Your backend→Yoursafe
GET https://accounts.yoursafe.com/userinfo
Authorization: Bearer eyJ...abcResponse
Yoursafe→Your backend
{
"sub": "G0Y999.yoursafe.id",
...
}The access token is valid for expires_in seconds from the token response.
Step 4Sign out
End your own session. To sign the user out at Yoursafe as well, send the browser on to the end_session_endpoint.
Logout route
Request
(Web) App→Your backend
POST http://localhost:3000/auth/logout
Cookie: sid=8f3c...Submit this as a form, a full-page navigation, so the browser can follow the redirect in the response.
Response
Your backend→(Web) App(redirect)
Delete the session, clear the cookie, and redirect to Yoursafe with the ID token you stored.
HTTP/1.1 303 See Other
Location: https://accounts.yoursafe.com/connect/logout
?id_token_hint=eyJ...xyz
&post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A5173%2F
&client_id=cljxc37g6f27s
Set-Cookie: sid=; Path=/; Max-Age=0; HttpOnly; SameSite=Laxpost_logout_redirect_uri must be registered for your client. To sign out of your app only, redirect to the app instead; the user then stays signed in at Yoursafe.
Implementation checklist
- Register the redirect URI exactly, and send the identical value to authorize and to the token exchange.
- Create a fresh
stateandnoncefor every sign-in, check both, and use each once. - Validate the ID token's signature,
iss,aud,exp, andnoncebefore you use a claim. - Keep
client_secretand all tokens on the server. The browser only gets anHttpOnlysession cookie. - Call the token endpoint with
PUT. - Cache the discovery document and the key set. Set timeouts on every call to Yoursafe.
- Serve every redirect URI over HTTPS in production.
Every endpoint, parameter, and response is in the Yoursafe ID API reference.