Перейти к содержимому
FidmosДокументация

Публичный клиент (SPA / мобильное)

Приложение без своего бэкенда — SPA в браузере, мобильное или нативное — входит через Fidmos напрямую. Код авторизации связывается с приложением через PKCE, а client_secret не нужен: у публичного клиента его нет.

  • Публичный (эта страница) — client_secret негде хранить: код целиком живёт в браузере, мобильном или нативном приложении. В консоли это тип «Публичное»: Client ID вы получаете, Client Secret не выдаётся, вход держит PKCE.
  • Серверный — есть бэкенд, где client_secret лежит в безопасности. Тогда вам сюда: пример бэкенда или редиректный вход.
  1. Сгенерируйте code_verifier, code_challenge (S256 от него), а также state и nonce. code_verifier и nonce держите в приложении до шага 3.

  2. Уведите пользователя на /oauth/authorize с code_challenge, state и nonce. Он входит на нашей странице и возвращается на ваш redirect_uri с code и state.

  3. Обменяйте code на токен на /oauth/token, передав code_verifier — без client_secret.

GEThttps://id.fidmos.com/oauth/authorize
?response_type=code
&client_id=ВАШ_CLIENT_ID
&redirect_uri=https://app.example/callback
&scope=openid profile email
&state=СЛУЧАЙНЫЙ_STATE
&nonce=СЛУЧАЙНЫЙ_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=ПОЛУЧЕННЫЙ_CODE&
redirect_uri=https://app.example/callback&
client_id=ВАШ_CLIENT_ID&
code_verifier=CODE_VERIFIER_С_ШАГА_1

Ответ содержит access_token, refresh_token и id_token. Пользователя опознайте по sub. Проще всего — через UserInfo с access_token: токен проверит наш сервер, разбирать JWT вручную не нужно.

Всё делает браузер — библиотека не нужна, PKCE считается через Web Crypto.

const ISSUER = 'https://id.fidmos.com';
const CLIENT_ID = 'ВАШ_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)) };
}

// Шаг 1-2. Уводим пользователя на страницу входа.
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); // вернётся на /callback с ?code=...&state=...
}

// Шаг 3. На странице /callback: меняем код на токен — без секрета.
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'),
    }),
  });

  // Разовые значения потока отработали — уберите их.
  sessionStorage.removeItem('pkce_verifier');
  sessionStorage.removeItem('oauth_state');

  // { access_token, refresh_token, id_token }. Опознайте пользователя через UserInfo
  // (см. ниже) или проверьте id_token, в т.ч. nonce из sessionStorage['oauth_nonce'].
  return res.json();
}