Docs / Switch from Google
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
<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 whensignIn()was called, without signo's code. Otherwise it returns whoever is signed in, checking with signo first once their ID token has expired, ornull. It throws aSignoErroronly 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, ornull.signOut()forgets the person in your app and deletes the key their refresh token was bound to. GivecreateSignoapostLogoutRedirectUrito 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. signo's emails are always verified.