The sign-in flow
How signIn and handleRedirectCallback work in practice. Options, the return page, cancelling, returnTo, and what the person sees on unet.social.
Signing in is a round trip: your page sends the person to unet.social, they approve, and UNET sends them back to your return page. The quickstart shows the shortest version; this page covers the details you meet in a real app. For how the pieces fit together, see How sign-in works.
Starting the sign-in
signIn() does three things and then leaves the page: it reads UNET's discovery document, saves a one-time state and a PKCE verifier in the SDK's storage, and sends the browser to unet.social. Nothing is sent to your server.
await unet.signIn({ returnTo: "/dashboard" });| Option | What it does |
|---|---|
returnTo | A path or address of your own that handleRedirectCallback() hands back to you. Default "/". The SDK keeps it locally; it is not sent to UNET. |
prompt | The OIDC prompt parameter: "login", "consent", "none" or "select_account". See below. |
loginHint | Pre-fills the account on the sign-in screen, for example a username you already know. |
extraParams | Extra parameters for the authorization request. The protocol ones (client_id, redirect_uri, response_type, scope, state, code_challenge, code_challenge_method) are ignored if you pass them. |
Two prompt values matter in practice:
"consent"shows the permission screen again. Use it after you add scopes, so the person can grant the new ones. See Permissions."login"asks the person to sign in again even if they are already signed in on unet.social.
If your scopes include offline_access and you pass no prompt, the SDK sends prompt=consent for you: a refresh token is only issued that way. See Sessions and storage.
The return page
The page at your return URL finishes the sign-in with handleRedirectCallback(). It checks state, exchanges the code for tokens (PKCE, no secret), loads the profile, and resolves with the user and the returnTo you passed to signIn().
import { AccessDenied, StateMismatch, UnetIdError } from "@unet/id-web";
try {
const { user, returnTo } = await unet.handleRedirectCallback();
console.log(`Signed in as ${user.username}`);
location.replace(returnTo);
} catch (e) {
if (e instanceof AccessDenied) {
location.replace("/"); // the person pressed Deny
} else if (e instanceof StateMismatch) {
location.replace("/sign-in?retry=1"); // expired, reused, or started somewhere else
} else if (e instanceof UnetIdError) {
console.error(e.code, e.message); // e.g. "invalid_scope"
} else {
throw e;
}
}What can go wrong on the return page
| Error | code | Meaning |
|---|---|---|
AccessDenied | access_denied | The person pressed Deny. Not a failure; offer to try again. |
StateMismatch | state_mismatch | There is no pending sign-in, the state differs, the sign-in is older than 15 minutes, or it was already used. |
UnetIdError | issuer_mismatch | The response says it came from a different issuer than yours. |
UnetIdError | invalid_scope and other OIDC errors | The server refused the request. message carries its explanation. A scope you did not register on the app causes invalid_scope. |
UnetIdError | invalid_client | The App ID is wrong, the app is turned off, or it is on the other server (test or production). |
UnetIdError | invalid_response | The return URL has no code. |
NetworkError | network_error | UNET could not be reached while exchanging the code. |
The pending sign-in is one-shot: it is removed as soon as handleRedirectCallback() reads it, even when the call fails. After any error the only way forward is to call signIn() again.
One page for the callback and the app
isRedirectCallback() tells you whether the current address is an authorization response for your app: it has a state and a code or an error. Use it when the return URL is the same page as the rest of your app.
if (unet.isRedirectCallback()) {
const { returnTo } = await unet.handleRedirectCallback();
history.replaceState(null, "", returnTo); // drops ?code=... from the address bar
}Call handleRedirectCallback() once per page load. In frameworks that run effects twice in development, guard it, as the React recipe does.
returnTo and deep links
A person who clicks a link to /projects/42 while signed out should land on /projects/42 after signing in, not on the home page. Pass the current location:
await unet.signIn({ returnTo: location.pathname + location.search });returnTo is stored in the browser by the SDK and returned to you unchanged. It does not travel through UNET, so nobody else can change it. If you ever build it from something the person controls, such as a ?next= parameter, accept only paths on your own site before you navigate to it:
function safeReturn(value: string | null): string {
return value && value.startsWith("/") && !value.startsWith("//") ? value : "/";
}What the person sees on unet.social
On unet.social the person signs in with whatever their account uses: password, an authenticator code, or a security key or passkey. Your app never sees which. Then they see a permission screen that lists what your app asks for, in plain words, and choose Allow or Deny. An app whose domain is not verified is marked as unverified. If the person is already signed in on unet.social and has allowed your app before, they may return to your page without seeing anything.
Common mistakes
- The return URL does not match. The address in the console and
redirectUrimust be identical, including scheme, port, path and trailing slash. - Starting in one tab, finishing in another. The pending sign-in lives in the storage of the tab or browser that started it. Open links from the confirmation screen in the same tab.
- Using
storage: "memory". The page is left during sign-in, so a memory store is empty on return and the sign-in always ends inStateMismatch. See Sessions and storage. - Calling
handleRedirectCallback()twice. The second call finds no pending sign-in and throwsStateMismatch. - Trusting
returnTofrom the address bar. Check it first, as above.