Ручная настройка

Подключите вход через Paspo без SDK — стандартный OpenID Connect authorization code flow с PKCE.

Paspo — стандартный провайдер OpenID Connect, так что SDK не обязателен. Ручная настройка подходит, если:

  • в вашем стеке уже есть OIDC-библиотека (Auth.js, Passport, allauth, Spring Security, ASP.NET…);
  • страницы рендерятся на сервере и нужен обычный редирект без JavaScript на фронтенде;
  • вы не можете добавлять npm-зависимости.

Цена: полностраничный редирект вместо попапа и без окна FedCM. Нужен попап — используйте SDK: протокол окна не является публичным API.

Поток

Browser ──GET /auth/paspo/login──▶ Your server
                                    • state, nonce, code_verifier → session
Browser ◀──302 paspo.id/oauth/authorize?…──
Browser ──user confirms in Paspo──▶ paspo.id
Browser ◀──302 <redirect_uri>?code=…&state=…──
Browser ──GET <redirect_uri>?code&state──▶ Your server
                                    • check state
                                    • POST /api/w/v1/oauth/token
                                    • verify id_token, check nonce
                                    • find user by sub, start session
Browser ◀──302 /app──

1. Запрос авторизации

Перенаправьте браузер на https://paspo.id/oauth/authorize с параметрами:

ПараметрОбязателен
client_idдаВаш client ID
redirect_uriдаРовно один из зарегистрированных URI
response_typeдаcode — единственное поддерживаемое значение
scopeдаЧерез пробел: openid и любые из profile, email, phone
stateдаСлучайное значение, ≥ 128 бит. Защита от CSRF
code_challengeдаBASE64URL(SHA256(code_verifier)) без паддинга
code_challenge_methodдаS256
nonceрекомендуетсяСлучайное значение. Вернётся в id_token
https://paspo.id/oauth/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fpaspo%2Fcallback
  &response_type=code
  &scope=openid%20email
  &state=Q2s9…
  &nonce=bX4r…
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256

Храните state, nonce и code_verifier на сервере (в сессии или в зашифрованной cookie HttpOnly; Secure; SameSite=Lax со сроком жизни 10 минут).

Генерация PKCE

import { createHash, randomBytes } from "node:crypto";

const codeVerifier = randomBytes(32).toString("base64url");
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
const state = randomBytes(16).toString("base64url");
const nonce = randomBytes(16).toString("base64url");

2. Callback

Paspo перенаправляет на redirect_uri с одним из вариантов:

  • ?code=…&state=… — успех;
  • ?error=access_denied&state=… — пользователь отказался; остальные значения — стандартные ошибки OAuth, например invalid_request или invalid_scope.

При успехе:

  1. Сравните state с сохранённым значением; при несовпадении — отказ. Удалите сохранённые значения — одна попытка на вход.
  2. Обменяйте код с client_secret, тем же redirect_uri и сохранённым code_verifier.
  3. Проверьте id_token и сравните его nonce с сохранённым.
  4. Найдите или создайте пользователя по sub, откройте сессию и перенаправьте в приложение.

Node.js с openid-client

openid-client берёт на себя discovery, PKCE, state, nonce и проверку id_token.

npm install openid-client express express-session
server.ts
import express from "express";
import session from "express-session";
import * as oidc from "openid-client";

const REDIRECT_URI = process.env.PASPO_REDIRECT_URI!; // https://app.example.com/auth/paspo/callback
const config = await oidc.discovery(
  new URL("https://paspo.id"),
  process.env.PASPO_CLIENT_ID!,
  process.env.PASPO_CLIENT_SECRET!,
);

const app = express();
app.use(session({ secret: process.env.SESSION_SECRET!, resave: false, saveUninitialized: false,
  cookie: { httpOnly: true, secure: true, sameSite: "lax" } }));

app.get("/auth/paspo/login", async (req, res) => {
  const codeVerifier = oidc.randomPKCECodeVerifier();
  const state = oidc.randomState();
  const nonce = oidc.randomNonce();
  req.session.paspo = { codeVerifier, state, nonce };

  const url = oidc.buildAuthorizationUrl(config, {
    redirect_uri: REDIRECT_URI,
    scope: "openid email",
    code_challenge: await oidc.calculatePKCECodeChallenge(codeVerifier),
    code_challenge_method: "S256",
    state,
    nonce,
  });
  res.redirect(url.href);
});

app.get("/auth/paspo/callback", async (req, res) => {
  const pending = req.session.paspo;
  delete req.session.paspo;
  if (!pending) return res.redirect("/login?error=paspo");

  try {
    const tokens = await oidc.authorizationCodeGrant(
      config,
      new URL(req.originalUrl, REDIRECT_URI),
      { pkceCodeVerifier: pending.codeVerifier, expectedState: pending.state, expectedNonce: pending.nonce },
    );
    const claims = tokens.claims()!;
    const user = await upsertUserByPaspoSub(claims.sub, claims); // your DB
    req.session.regenerate(() => {
      req.session.userId = user.id;
      res.redirect("/");
    });
  } catch {
    res.redirect("/login?error=paspo");
  }
});

Auth.js (NextAuth)

auth.ts
import NextAuth from "next-auth";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    {
      id: "paspo",
      name: "Paspo",
      type: "oidc",
      issuer: "https://paspo.id",
      clientId: process.env.AUTH_PASPO_ID,
      clientSecret: process.env.AUTH_PASPO_SECRET,
      authorization: { params: { scope: "openid email" } },
      checks: ["pkce", "state", "nonce"],
    },
  ],
});

Зарегистрируйте redirect URI https://app.example.com/api/auth/callback/paspo. Вход запускается через signIn("paspo").

Django (django-allauth)

settings.py
INSTALLED_APPS += ["allauth.socialaccount.providers.openid_connect"]

SOCIALACCOUNT_PROVIDERS = {
    "openid_connect": {
        "OAUTH_PKCE_ENABLED": True,
        "APPS": [
            {
                "provider_id": "paspo",
                "name": "Paspo",
                "client_id": os.environ["PASPO_CLIENT_ID"],
                "secret": os.environ["PASPO_CLIENT_SECRET"],
                "settings": {"server_url": "https://paspo.id", "scope": ["openid", "email"]},
            }
        ],
    }
}

Redirect URI: https://app.example.com/accounts/oidc/paspo/login/callback/.

Spring Boot (Spring Security)

application.yml
spring:
  security:
    oauth2:
      client:
        registration:
          paspo:
            client-id: ${PASPO_CLIENT_ID}
            client-secret: ${PASPO_CLIENT_SECRET}
            authorization-grant-type: authorization_code
            scope: openid,email
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
        provider:
          paspo:
            issuer-uri: https://paspo.id

Spring добавляет PKCE автоматически только для публичных клиентов. Для конфиденциального клиента включите его через OAuth2AuthorizationRequestCustomizers.withPkce() в резолвере запроса авторизации. Redirect URI: https://app.example.com/login/oauth2/code/paspo.

ASP.NET Core

Program.cs
builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = "paspo";
    })
    .AddCookie()
    .AddOpenIdConnect("paspo", options =>
    {
        options.Authority = "https://paspo.id";
        options.ClientId = builder.Configuration["Paspo:ClientId"];
        options.ClientSecret = builder.Configuration["Paspo:ClientSecret"];
        options.ResponseType = "code";
        options.UsePkce = true;
        options.CallbackPath = "/auth/paspo/callback";
        options.Scope.Clear();
        options.Scope.Add("openid");
        options.Scope.Add("email");
        options.MapInboundClaims = false;
    });

Другие стеки

Подойдёт любой сертифицированный OpenID Connect-клиент: укажите issuer https://paspo.id, включите PKCE S256 и используйте client_secret_post или client_secret_basic. Не уверены, как именно? Воспользуйтесь промптом для ручной настройки.

Чего не делать

  • Не используйте response_type=token (implicit flow) — он не поддерживается.
  • Не встраивайте страницу авторизации в iframe — cookie сессии Paspo не передаётся в сторонние фреймы.
  • Не используйте маски и префиксы в redirect URI — они должны совпадать точно.
  • Не кладите client_secret во фронтенд и мобильные приложения.