Sessions and storage
Where the SDK keeps tokens, how restore and refresh work, staying signed in with offline_access, several tabs, signing out, and what happens when a person removes your app.
After sign-in the SDK holds a session: an access token, optionally a refresh token, and the profile. It keeps the access token fresh, shares the session between tabs when you ask, and ends it cleanly. This page explains the choices you have and what happens at each edge.
Where tokens live
The storage option decides where the SDK keeps the session and the one-time data of a sign-in in progress.
storage | Where | Survives | Notes |
|---|---|---|---|
"session" (default) | sessionStorage | the tab, until it is closed | One session per tab. A new tab starts signed out. The safest browser option. |
"local" | localStorage | browser restarts | Shared by all tabs. Readable by any script on your origin, so a cross-site scripting bug can steal it. Use it only with a strict content security policy. |
"memory" | the page's memory | nothing; lost on reload | Cannot complete a redirect sign-in: the page is left and the pending sign-in is lost. Useful for tests and tools. |
| your own object | anything | up to you | An object with get, set and remove (the KeyValueStore interface). |
If the browser blocks storage (some private modes), the SDK falls back to memory without throwing.
import { UnetId } from "@unet/id-web";
export const unet = new UnetId({
clientId: "unet_xxx",
redirectUri: `${location.origin}/auth/callback`,
storage: "local", // keep the person signed in across restarts
});Keys are namespaced by App ID (unet-id:<clientId>:...), so several apps on one origin do not collide. Tokens from a different issuer, for example after switching from the test server to production, are ignored.
Restoring a session
Call restore() once when your app starts. It loads the saved session, refreshes the token if needed, reads the profile from UNET, and returns the user, or null when nobody is signed in.
const user = await unet.restore();restore() makes a network request by default, so the profile is always current. If you want the app to start instantly from the profile saved last time, skip the request. The tokens still refresh on first use.
const user = await unet.restore({ revalidate: false });If the saved session is no longer valid (the refresh token was rejected), restore() clears it and returns null. Other failures, such as no network, are thrown as NetworkError and the session is kept.
Reading the session
| Call | Returns |
|---|---|
getUser() | The signed-in user from memory, or null. No request. |
isSignedIn() | true when tokens are saved. |
refreshUser() | Reloads the profile from UNET, for example after the person changed their name. |
getAccessToken() | A valid access token, refreshed first if it is about to expire. |
onSessionChange(cb) | Calls cb with the user on sign-in or profile change and with null on sign-out. Returns a function that removes the listener. |
const off = unet.onSessionChange((user) => {
document.querySelector("#who")!.textContent = user ? `@${user.username}` : "Signed out";
});
// off() stops listeningStaying signed in
An access token lasts about an hour. To keep the person signed in for longer, ask for offline_access: UNET then also issues a refresh token, and the SDK exchanges it for a new access token shortly before the old one expires (one minute early).
import { UnetId } from "@unet/id-web";
const unet = new UnetId({
clientId: "unet_xxx",
redirectUri: `${location.origin}/auth/callback`,
scopes: ["openid", "profile", "offline_access"],
});A refresh token is only issued when the person sees the permission screen, so the SDK adds prompt=consent to the sign-in whenever offline_access is requested and you set no prompt yourself. For an app with this permission, expect the consent screen at sign-in.
Without offline_access there is no refresh token. When the access token expires, getAccessToken() clears the session and throws SessionExpired, and the person has to sign in again.
How refreshing behaves
- Automatic. Every API call and
getAccessToken()refresh the token when it is within a minute of expiring. If an API call still gets a 401, the SDK forces one refresh and retries once. - One at a time. Concurrent calls share a single refresh request.
- Across tabs. Refresh tokens rotate: each exchange issues a new one and retires the old. In browsers with Web Locks, tabs take turns, and a tab that waited uses the tokens the other tab just obtained instead of spending the old refresh token again. With
storage: "local", tabs also mirror sign-in, sign-out and profile changes. - A failed refresh does not always sign out. Only the server saying the grant is gone (
invalid_grant) ends the session. A network error or a server error keeps the tokens, and the next call tries again.
Signing out
await unet.signOut();signOut() clears the session in this browser first, then asks UNET to revoke the refresh token (or the access token, if there is no refresh token). The revocation is best effort: if it fails, the local sign-out has already happened. It does not sign the person out of unet.social, only your app.
When the person removes your app
People can remove an app in their UNET settings under Connected apps. The effect is immediate: the next refresh fails, the SDK clears the session, and the call throws SessionExpired. An app should treat SessionExpired as "signed out", not as a bug.
import { SessionExpired } from "@unet/id-web";
try {
await api.feed();
} catch (e) {
if (e instanceof SessionExpired) {
showSignInButton(); // the session is gone: sign in again
} else {
throw e;
}
}
declare function showSignInButton(): void;If your server verifies tokens, it learns of the removal at its next check; see Verify on your server.
Common mistakes
- Expecting a new tab to be signed in with the default
"session"storage. Use"local"if that is what you want. - Not calling
restore()at startup.getUser()is empty until you do, or until sign-in. - Caching the access token yourself. Ask
getAccessToken()every time you need one; it returns the cached one when it is still good. - Treating
SessionExpiredas an error to log. It happens in normal use.