Paspo — стандартный провайдер OpenID Connect, так что SDK не обязателен. Ручная настройка подходит, если:
Цена: полностраничный редирект вместо попапа и без окна 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──
Перенаправьте браузер на 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 минут).
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");
import base64, hashlib, secrets
code_verifier = secrets.token_urlsafe(32)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()
state = secrets.token_urlsafe(16)
nonce = secrets.token_urlsafe(16)
$b64url = fn($b) => rtrim(strtr(base64_encode($b), '+/', '-_'), '=');
$codeVerifier = $b64url(random_bytes(32));
$codeChallenge = $b64url(hash('sha256', $codeVerifier, true));
$state = $b64url(random_bytes(16));
$nonce = $b64url(random_bytes(16));
const b64url = (bytes: Uint8Array) =>
btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const codeVerifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
const codeChallenge = b64url(new Uint8Array(digest));
Paspo перенаправляет на redirect_uri с одним из вариантов:
?code=…&state=… — успех;?error=access_denied&state=… — пользователь отказался; остальные значения — стандартные ошибки OAuth, например invalid_request или invalid_scope.При успехе:
state с сохранённым значением; при несовпадении — отказ. Удалите сохранённые значения — одна попытка на вход.client_secret, тем же redirect_uri и сохранённым code_verifier.nonce с сохранённым.sub, откройте сессию и перенаправьте в приложение.openid-client берёт на себя discovery, PKCE, state, nonce и проверку id_token.
npm install openid-client express express-session
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");
}
});
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").
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:
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.
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 не передаётся в сторонние фреймы.client_secret во фронтенд и мобильные приложения.