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.
When public, when server-side
Section titled “When public, when server-side”- 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_secretsits safely. Then go to: backend example or redirect login.
How sign-in goes
Section titled “How sign-in goes”-
Generate
code_verifier,code_challenge(S256of it), and alsostateandnonce. Keepcode_verifierandnoncein the app until step 3. -
Send the user to
/oauth/authorizewithcode_challenge,stateandnonce. They sign in on our page and return to yourredirect_uriwithcodeandstate. -
Exchange
codefor a token at/oauth/token, passingcode_verifier— without aclient_secret.
Redirect to authorization
Section titled “Redirect to authorization”?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=S256Exchange the code for a token
Section titled “Exchange the code for a 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_1The 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.
Example
Section titled “Example”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();
}Native apps use the system AppAuth (iOS / Android) or expo-auth-session (React Native): the
library generates the PKCE pair itself, opens sign-in in the system browser and exchanges the code
without a secret. From Fidmos it needs only:
issuer: https://id.fidmos.com (discovery picks up the endpoints and keys)
client_id: YOUR_CLIENT_ID (no client_secret)
redirect_uri: myapp://callback (a custom scheme or App Link / Universal Link)
scope: openid profile emailWhat’s next
Section titled “What’s next”- User profile — name, avatar, email.
- Error codes — what
/oauth/tokenreturns. - Got a backend? — server-side code exchange with
client_secret.