Skip to content
FidmosDocumentation

Public client (SPA / mobile)

An app without its own backend — a browser SPA, mobile or native — signs in with Fidmos directly. The authorization code is bound to the app through PKCE, and no client_secret is needed: a public client does not have one.

  • Public (this page) — there is nowhere to keep a client_secret: the code lives entirely in the browser, mobile or native app. In the console this is the “Public” type: you get a Client ID, no Client Secret is issued, and PKCE holds sign-in together.
  • Server-side — there is a backend where client_secret sits safely. Then go to: backend example or redirect login.
  1. Generate code_verifier, code_challenge (S256 of it), and also state and nonce. Keep code_verifier and nonce in the app until step 3.

  2. Send the user to /oauth/authorize with code_challenge, state and nonce. They sign in on our page and return to your redirect_uri with code and state.

  3. Exchange code for a token at /oauth/token, passing code_verifier — without a client_secret.

GEThttps://id.fidmos.com/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.example/callback
&scope=openid profile email
&state=RANDOM_STATE
&nonce=RANDOM_NONCE
&code_challenge=BASE64URL(SHA256(code_verifier))
&code_challenge_method=S256
POSThttps://id.fidmos.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=RECEIVED_CODE&
redirect_uri=https://app.example/callback&
client_id=YOUR_CLIENT_ID&
code_verifier=CODE_VERIFIER_FROM_STEP_1

The response contains access_token, refresh_token and id_token. Identify the user by sub. The simplest way is through UserInfo with access_token: our server verifies the token, so you do not need to parse the JWT by hand.

The browser does it all — no library needed, PKCE is computed with Web Crypto.

const ISSUER = 'https://id.fidmos.com';
const CLIENT_ID = 'YOUR_CLIENT_ID';
const REDIRECT_URI = location.origin + '/callback';

const base64url = (bytes) =>
  btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

async function createPkce() {
  const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
  return { verifier, challenge: base64url(new Uint8Array(digest)) };
}

// Step 1-2. Send the user to the sign-in page.
export async function login() {
  const { verifier, challenge } = await createPkce();
  const state = base64url(crypto.getRandomValues(new Uint8Array(16)));
  const nonce = base64url(crypto.getRandomValues(new Uint8Array(16)));
  sessionStorage.setItem('pkce_verifier', verifier);
  sessionStorage.setItem('oauth_state', state);
  sessionStorage.setItem('oauth_nonce', nonce);

  const url = new URL(ISSUER + '/oauth/authorize');
  url.search = new URLSearchParams({
    response_type: 'code',
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    scope: 'openid profile email',
    state,
    nonce,
    code_challenge: challenge,
    code_challenge_method: 'S256',
  }).toString();
  location.assign(url); // comes back to /callback with ?code=...&state=...
}

// Step 3. On the /callback page: exchange the code for a token — without a secret.
export async function handleCallback() {
  const params = new URLSearchParams(location.search);

  const error = params.get('error');
  if (error) {
    throw new Error(params.get('error_description') ?? error);
  }
  if (params.get('state') !== sessionStorage.getItem('oauth_state')) {
    throw new Error('state mismatch');
  }
  const code = params.get('code');
  if (!code) {
    throw new Error('no code');
  }

  const res = await fetch(ISSUER + '/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      code_verifier: sessionStorage.getItem('pkce_verifier'),
    }),
  });

  // The one-time flow values have done their job — remove them.
  sessionStorage.removeItem('pkce_verifier');
  sessionStorage.removeItem('oauth_state');

  // { access_token, refresh_token, id_token }. Identify the user via UserInfo
  // (see below) or verify id_token, including the nonce from sessionStorage['oauth_nonce'].
  return res.json();
}