Quickstart
Create an App, wire up the browser SDK, and sign in for the first time — entirely in sandbox.
By the end of this page a browser will sign in against your own App and hand your code an access token. Everything happens in sandbox, where the users are yours to throw away.
1. Create an App
Sign in to the console. An account with no Apps is met by a three-step wizard whose second step is the create form; an account that already has one creates the next from Apps → Create App.
The name you give it becomes a slug, and the slug becomes two permanent addresses:
| Environment | Address |
|---|---|
| Production | https://acme.kleora.eu |
| Sandbox | https://acme.sandbox.kleora.eu |
Those are the App's issuers. The slug cannot be changed afterwards: it is
what the iss claim of every token says, and what every service that has
verified one of your tokens has cached. Pick it as carefully as a domain name.
Finishing takes you to the App's sandbox overview, which shows the three values the rest of this page needs — the issuer, the default client id, and the discovery URL.
2. Register your redirect URIs
Go to Integration → Clients and open the client named Default. It is a
spa client, created with your App, and it has no redirect URIs at all.
Until you add one, the authorization endpoint has nowhere to send a browser
back to.
Add the two your development server will use:
| Field | Value |
|---|---|
| Redirect URI | http://localhost:5173/callback |
| Post-logout redirect URI | http://localhost:5173/ |
Both are matched exactly. A trailing slash, a different port or an extra query string is a different URI as far as the authorization endpoint is concerned.
3. Install the browser SDK
npm install @kleora-io/browser
It has no runtime dependencies and ships as ES modules only. Installing an SDK covers the other four packages.
4. Sign in
Three values go into the client: the issuer and the client id from the overview page, and the redirect URI you just registered.
import { createKleora } from '@kleora-io/browser'
const kleora = createKleora({
issuer: 'https://acme.sandbox.kleora.eu',
clientId: '<client_id>',
redirectUri: location.origin + '/callback',
})
if (location.pathname === '/callback') {
await kleora.handleRedirectCallback()
} else if (!(await kleora.checkSession())) {
await kleora.loginWithRedirect()
}
const user = kleora.getUser()
const token = await kleora.getAccessToken()
Run it. The browser leaves for your App's hosted sign-in page, where you can
create the first user — self-registration is on by default in a new
environment — and comes back to /callback with an authorization code.
handleRedirectCallback() redeems it, validates the ID token, stores the
tokens and strips the code out of the address bar.
Signing out is one call, and returnTo has to be one of the client's
registered post-logout URIs:
await kleora.logout({ returnTo: location.origin + '/' })
5. Verify the token on your own API
The access token is a signed JWT. Your API verifies it against your issuer's published key set — no call to us, no shared secret.
import { authenticate } from '@kleora-io/node'
const claims = await authenticate(request, {
issuer: 'https://acme.sandbox.kleora.eu',
audience: '<client_id>',
})
console.log(claims.sub, claims.tenant, claims.permissions)
authenticate takes anything with a headers.authorization property, so it
works with any Node framework; @kleora-io/node also ships Express and
Fastify middleware. claims.roles and claims.permissions are already in the
token, so authorising a request costs you nothing but a list lookup.
What the SDK is doing for you
| Call | What it handles |
|---|---|
loginWithRedirect() | Mints state, nonce and a PKCE verifier, then navigates to the authorization endpoint. |
handleRedirectCallback() | Validates state and iss, redeems the code, validates the ID token, stores the tokens, cleans the URL. |
getAccessToken() | Returns the cached token until it is within 30 seconds of expiry, then refreshes. Refresh tokens rotate on every use. |
checkSession() | Recovers a session on page load, silently, without showing the sign-in page again. |
logout({ returnTo }) | Clears local state and ends the session at the issuer. |
onAuthChange(cb) | Fires on sign-in, sign-out and refresh. Returns an unsubscribe function. |
Where the tokens live
By default both tokens are held in memory, in a closure. Nothing survives a
reload, which is why a page load calls checkSession(). Passing
storage: 'session' additionally mirrors the refresh token into
sessionStorage, which is scoped to the tab and cleared when the tab closes.
localStorage is not an option and will not become one. It persists
indefinitely, every tab on the origin shares it, and every script that ever runs
there can read it — including one that arrives tomorrow inside a dependency you
shipped today.
Next
- Installing an SDK — Nuxt, Node, Python and Django.
- Turn on TOTP, invite people into workspaces or brand the sign-in page from the console; none of it changes the code above.