GET /docs/single-page-apps · accept: text/markdown200 text/markdown; charset=utf-8 · vary: Accept
# Switch from Google to signo: single-page apps (signo.js) A single-page app runs in the browser, often with no server of its own, so it can't keep a client secret. signo.js signs people in to it with the authorization code flow and PKCE, which is what OAuth 2.0 for Browser-Based Apps recommends, and needs no secret. It is one file, served by signo. signo's browser tests sign in to a demo single-page app with it. The examples use the public signo at `https://usesigno.co.uk`. On your own copy of signo, use its address instead. ## 1. Add your site as a single-page app Sign in at https://usesigno.co.uk/account/sites, add your site, and tick "It's a single-page app with no server of its own". As the callback URL, use the exact address of the page that loads signo.js, such as `https://example.com/`. To let people sign out of signo from your app as well, add the same address as a sign-out return URL. The site gets a client ID and no secret. On your own signo, register it with `--public` instead: ``` docker compose exec signo signo clients add --name "Example" --redirect-uri https://example.com/ --public ``` ## 2. Add signo.js to the page ```html <button id="sign-in">Sign in</button> <script type="module"> import { createSigno } from 'https://usesigno.co.uk/signo.js'; // remember: true keeps people signed in after the tab closes, for up to 14 days. const signo = createSigno({ issuer: 'https://usesigno.co.uk', clientId: '<client id>', remember: true }); document.querySelector('#sign-in').onclick = () => signo.signIn(); // null, or { sub, email, email_verified, name }. A cancelled or failed sign-in throws a SignoError. const user = await signo.handleRedirect().catch((error) => { alert(error.message); return null; }); </script> ``` - `signIn()` sends the person to signo. - `handleRedirect()` belongs on every page load. It finishes a sign-in when signo sends the person back, putting back the address the page had when `signIn()` was called, without signo's code. Otherwise it returns whoever is signed in, checking with signo first once their ID token has expired, or `null`. It throws a `SignoError` only when a sign-in that signo just sent back failed or was cancelled. Calls made while one is running share its result, so React's StrictMode can call it twice. - `user()` returns the signed-in person as last checked, or `null`. - `signOut()` forgets the person in your app and deletes the key their refresh token was bound to. Give `createSigno` a `postLogoutRedirectUri` to send them to signo's sign-out page as well; whatever they choose there, your app's tokens for them then end in all their browsers. If your page used Google Identity Services, remove its script and button; signo.js replaces both. If your page sends a Content-Security-Policy, allow `https://usesigno.co.uk` in `script-src` and `connect-src`. With a bundler such as Vite, which doesn't import from URLs, save signo.js into your project and import it from there. ## 3. What signo.js checks - The response belongs to the sign-in your page started (`state`) and comes from signo (`iss`, RFC 9207). - The code only works together with the PKCE verifier your page kept, so a code seen by anyone else is useless. - The ID token is for your client ID and this sign-in (`nonce`), and hasn't expired. It comes straight from signo's token endpoint over TLS, which OpenID Connect accepts in place of checking its signature. - Every request to signo's token endpoint carries a proof signed with this browser's key (DPoP), so tokens issued to the page only work from it. signo answers browser requests to its token endpoint only from the origins of your callback URLs. ## 4. How long people stay signed in Without `remember`, signo.js keeps the person and their ID token in `sessionStorage`, with no refresh token. They stay signed in while the tab is open, until the ID token expires an hour after sign-in. Signing in again then takes one click while their signo session lasts. With `remember: true`, signo.js keeps them in `localStorage` with a refresh token, so they stay signed in after the tab closes. Once the ID token has expired, `handleRedirect()` asks signo again and gets a new one. If signo refuses, the person is signed out of your app; rate limits, outages and network failures leave them signed in, to try again on the next page load. The refresh token: - is bound to a key the browser keeps in IndexedDB (DPoP, RFC 9449). No script can read the key's private half, so a copy of the token is useless anywhere else, and `signOut()` deletes the key. Being useless when copied, it isn't replaced when used; - lasts at most 14 days from sign-in, and ends sooner if the person signs out of signo in a browser where they used your app, which ends it in all their browsers. A sign-in in progress waits in `localStorage` for up to an hour, because email links usually open in a new tab and the sign-in finishes there. Any script on your page can use what signo.js keeps while the page is open, so keep third-party scripts off pages that use it, as you would for any sign-in. ## What signo.js doesn't do yet - **Tokens for your own API.** The access token works only at signo's userinfo endpoint. Tokens for your API are planned, with resource indicators (RFC 8707). ## Keep the people who signed up with Google If your app keeps users anywhere, match them by verified email, as in the [Auth.js guide](https://usesigno.co.uk/docs/authjs.md). signo's emails are always verified.