Публичный клиент (SPA / мобильное)
Приложение без своего бэкенда — SPA в браузере, мобильное или нативное — входит через
Fidmos напрямую. Код авторизации связывается с приложением через PKCE, а client_secret
не нужен: у публичного клиента его нет.
Когда публичный, когда серверный
Заголовок раздела «Когда публичный, когда серверный»- Публичный (эта страница) —
client_secretнегде хранить: код целиком живёт в браузере, мобильном или нативном приложении. В консоли это тип «Публичное»: Client ID вы получаете, Client Secret не выдаётся, вход держит PKCE. - Серверный — есть бэкенд, где
client_secretлежит в безопасности. Тогда вам сюда: пример бэкенда или редиректный вход.
Как идёт вход
Заголовок раздела «Как идёт вход»-
Сгенерируйте
code_verifier,code_challenge(S256от него), а такжеstateиnonce.code_verifierиnonceдержите в приложении до шага 3. -
Уведите пользователя на
/oauth/authorizeсcode_challenge,stateиnonce. Он входит на нашей странице и возвращается на вашredirect_uriсcodeиstate. -
Обменяйте
codeна токен на/oauth/token, передавcode_verifier— безclient_secret.
Переход на авторизацию
Заголовок раздела «Переход на авторизацию»?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Обмен кода на токен
Заголовок раздела «Обмен кода на токен»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();
}Нативные приложения берут системный AppAuth (iOS / Android) или expo-auth-session
(React Native): библиотека сама генерирует PKCE-пару, открывает вход в системном браузере
и обменивает код без секрета. От Fidmos ей нужны только:
issuer: https://id.fidmos.com (discovery подхватит эндпоинты и ключи)
client_id: ВАШ_CLIENT_ID (без client_secret)
redirect_uri: myapp://callback (своя схема или App Link / Universal Link)
scope: openid profile emailЧто дальше
Заголовок раздела «Что дальше»- Профиль пользователя — имя, аватар, почта.
- Коды ошибок — что вернёт
/oauth/token. - Появился бэкенд? — серверный обмен кода с
client_secret.