Signing in with Google, GitHub and others

Instead of choosing a password, people using your app can sign in with an account they already have. JSONPad handles the OAuth flow and creates an identity for each person, so your app adds a button and a return page.

Six providers are supported: Google, GitHub, Apple, Microsoft, Discord and Facebook. Each identity group has its own provider settings, so a game and an admin tool can use different apps, or none at all.

You'll need an OAuth app of your own at each provider, and its client ID and secret. That part can't be avoided: the provider needs to know whose app is asking. The Google, GitHub, Apple, Microsoft, Discord and Facebook guides walk through it, and the dashboard shows you exactly what to paste where. (Apple needs a paid developer account; the others are free.)

How it works

  1. Someone clicks "Sign in with Google" in your app. Your app starts a sign-in and gets a URL at Google to send them to.
  2. They sign in at Google and agree to share their details.
  3. Google sends them back to your app's return page, with a code and a state in the URL (either directly, or through JSONPad: see callback modes).
  4. Your return page completes the sign-in. JSONPad swaps the code with Google for the person's details.
  5. If a Google account is already linked to an identity in the group, that identity is logged in. Otherwise a new identity is created and linked to it. Either way, your app gets an identity and an identity token, exactly as it would from logging in.

New identities are named after the provider and the account, e.g. google-104829..., with the provider's name as the display name. They have no password until they set one, and they can't be logged in to with a password. The created flag in the response tells your app whether someone is new, so it can show a welcome page or ask for a nickname.

Setting it up

  1. Open the identity group in the dashboard and choose a provider under Sign-in providers.
  2. Follow the five steps: create the OAuth app, register the callback URL, paste the client ID and secret, run a test sign-in, and enable it.
  3. Add the pages in your app that sign-ins return to, as the group's redirect URLs. A sign-in can only return to a URL in that list, so a stolen API token can't be used to send people's sign-ins somewhere else. They must use https, except for localhost.

The client secret is encrypted before it's stored, and is never returned by the API or shown in the dashboard again. The dashboard checks it with the provider as soon as you save it, so a mistyped secret is caught straight away rather than when someone tries to sign in.

Your app's API token needs the authenticate permission for the group, and register as well if signing in should be able to create identities. Nothing else changes: tokens that already allow people to register and log in need no new permissions.

In your app

Ask which providers are enabled, so a provider you turn on later needs no code change, then start a sign-in when someone clicks the button.

JS/TS (SDK)
1234567891011121314151617import JSONPad from '@basementuniverse/jsonpad-sdk'; const jsonpad = new JSONPad('<YOUR TOKEN>', 'players'); // Show a button for each provider you've set up const providers = await jsonpad.fetchIdentityOAuthProviders(); for (const { provider, name } of providers) { const button = document.createElement('button'); button.textContent = `Sign in with ${name}`; button.onclick = () => jsonpad.startIdentityOAuth(provider, { redirectUrl: 'https://myapp.com/auth/callback', }); document.querySelector('#sign-in')!.append(button); }

On the return page, complete the sign-in. The SDK reads the parameters from the page's URL, sends them to JSONPad with the verifier it kept when the sign-in started, and removes them from the address bar afterwards.

JS/TS (SDK)
123456789101112131415161718192021import JSONPad, { JSONPadError } from '@basementuniverse/jsonpad-sdk'; const jsonpad = new JSONPad('<YOUR TOKEN>', 'players'); try { // Reads the parameters from the page's URL, then tidies them away const { identity, token, created } = await jsonpad.completeIdentityOAuth(); // The SDK is now logged in as this identity; keep the token if you want to // stay logged in after a reload localStorage.setItem('identity-token', token!); location.href = created ? '/welcome' : '/'; } catch (error) { if (error instanceof JSONPadError && error.code === 20018) { // The person changed their mind at the provider location.href = '/sign-in'; } else { showMessage("Sorry, we couldn't sign you in. Please try again."); } }

A sign-in can only be completed once, from the same browser and with the same API token that started it, and within 10 minutes. If someone shares the URL of their return page (or it ends up in a log), it's no use to anyone else.

Callback modes

Every provider has a list of URLs it's allowed to send people back to, and only your app's own pages or JSONPad can be in it. Each provider you set up uses one of two modes. Apple only works with the second, because it posts its result as a form rather than redirecting:

  • Straight back to your app. The provider sends people to your return page. You register each of your app's return pages with the provider, including the ones you use in development. Google can then show your own domain on its consent screen, which is why it's the default for Google.
  • Through JSONPad. The provider sends people to JSONPad's callback URL, which passes the code straight on to whichever of the group's redirect URLs started the sign-in. You register one URL with the provider and never touch it again, which suits GitHub (an OAuth app there has only one callback URL), and is the only way for providers that post the result back instead of redirecting.

JSONPad's callback page doesn't do anything else: it isn't authenticated, it doesn't use up the sign-in, and it sends no referrer. Your app's code is the same in both modes.

Linking accounts

An identity can sign in with a password, with several provider accounts, or with any mix of them. A logged-in identity links another account with the same start-and-return flow.

JS/TS (SDK)
1234567891011121314// On the account settings page: "Connect your Google account" await jsonpad.linkSelfIdentityProvider('google', { redirectUrl: 'https://myapp.com/settings/accounts', }); // The return page finishes it the same way as signing in; `token` is // undefined, because the identity is already logged in const { identity } = await jsonpad.completeIdentityOAuth(); // Which accounts can this identity sign in with? const accounts = await jsonpad.fetchSelfIdentityProviders(); // "Disconnect". An identity's last way of signing in can't be removed await jsonpad.unlinkSelfIdentityProvider('google');

Accounts are never linked automatically, not even when the email addresses match: an email address alone isn't proof that the same person owns both. If someone signs in with a provider account whose verified email address already belongs to another identity in the group, the sign-in fails with IDENTITY_EMAIL_IN_USE (20010). Tell them to log in the way they did before and connect the account from their settings page.

Unlinking an identity's last way of signing in is refused with IDENTITY_LAST_LOGIN_METHOD (20022), so nobody can lock themselves out. Offer to set a password first. (You can still unlink anything from the dashboard, as the account owner.)

Email addresses

A new identity only gets an email address if the provider says it has been verified: Google's email_verified, GitHub's primary verified address, Apple's (including its private relay addresses) or a Discord account that has confirmed its address. Microsoft and Facebook addresses are never trusted: Microsoft lets an administrator set any address on an account in their own tenant, and Facebook doesn't say whether it has checked one, so an identity created with either has no email address until it adds one. An unverified address is still shown to you on the identity's linked account in the dashboard, but it isn't the identity's own email address, so it can't be used to log in or to reset a password.

If the identity group requires an email address and the provider doesn't give a verified one, the sign-in fails with IDENTITY_EMAIL_REQUIRED (20023). Ask for an address in your app and set it on the identity instead.

Local development

Add your development URL, e.g. http://localhost:5173/auth/callback, to the group's redirect URLs. http is allowed for localhost only. In "through JSONPad" mode that's all you need; in "straight back to your app" mode, register the same URL with the provider as well. Apple never accepts localhost at its end, but its sign-ins come back through JSONPad, so a localhost return page still works.

Consider separate identity groups (and separate OAuth apps) for development and production, so a development client secret is never enough to sign in to your live app.

Going to production

  • Add your production return page to the group's redirect URLs.
  • Register it with the provider too, if you're using "straight back to your app" mode.
  • Publish your Google app, so people outside your list of test users can sign in. See the Google guide. For Apple, register your sending domain if you'll email its private relay addresses: see the Apple guide.
  • Run a test sign-in from the dashboard after any change to the OAuth app: it shows you exactly what the provider sent back, without creating or signing in to an identity.

When it goes wrong

  • 20015 IDENTITY_OAUTH_PROVIDER_NOT_CONFIGUREDThe provider isn't set up, or isn't enabled, for this identity group. Check the group's page in the dashboard, and that your app is asking for the right group.
  • 20016 IDENTITY_OAUTH_REDIRECT_NOT_ALLOWEDThe redirectUrl isn't one of the group's redirect URLs. They're compared exactly, so a missing slash or a different port counts as a different URL. The error lists the URLs that are allowed.
  • 20017 IDENTITY_OAUTH_STATE_INVALIDThe sign-in has expired (they take at most 10 minutes), has already been completed, or is being completed by a different browser or API token. Start again.
  • 20018 IDENTITY_OAUTH_CANCELLEDThe person decided not to sign in. Show your sign-in page again, not an error.
  • 20019 IDENTITY_OAUTH_PROVIDER_ERRORThe provider refused. The message says what to do: a rejected client ID or secret, a callback URL that isn't registered with the provider, or an expired code.
  • 20020 IDENTITY_OAUTH_NOT_REGISTEREDNobody is linked to that provider account, and your API token can't register identities. Add the register permission for the group, or tell them to sign up first.
  • 20021 IDENTITY_PROVIDER_ALREADY_LINKEDThat provider account is already linked to another identity, or this identity already has an account with this provider.

Every code is listed on the errors page, and the provider guides have a table of the provider's own error messages.

2026-09-17