SSO & Sesje
Co naprawdę dzieje się między klikiem login a renderem dashboardu.
SSO & Sesje
Przestań traktować OIDC jak black box. Ta strona przechodzi przez cały taniec loginu — co jest na kablu, co w cookies, jak jeden login w WolfieAuth daje user-owi jednoczesny dostęp do każdej apki którą zarejestrowałeś.
OIDC handshake (PKCE flow)
┌─────────────┐ ┌──────────────────────┐
│ User │ │ WolfieAuth │
│ Browser │ │ auth.wolfieguard.com │
└──────┬──────┘ └───────────┬──────────┘
│ │
│ 1. GET /auth/login (twoja apka) │
│ ───────────────────▶ │
│ │
│ 2. 302 → /authorize?response_type=code │
│ &client_id=<twojaorg>-<myapp> │
│ &redirect_uri=<twoj-callback> │
│ &scope=openid profile email org │
│ &code_challenge=<sha256-of-verifier> │
│ &state=<csrf-token> │
│ ◀────────────────────────────────────── │
│ │
│ 3. GET /authorize?... (WolfieAuth) │
│ ──────────────────────────────────────▶ │
│ │
│ 4. Renderuje brand-owany login page │
│ ◀────────────────────────────────────── │
│ │
│ 5. POST email + password │
│ ──────────────────────────────────────▶ │
│ │
│ 6. 302 → <twoj-callback>?code=ABC&state=<csrf> │
│ ◀────────────────────────────────────── │
│ │
│ 7. GET <twoj-callback>?code=ABC │
│ ───────────────────▶ │
│ │
│ (twoja apka POSTuje do WolfieAuth /token) │
│ grant_type=authorization_code │
│ code=ABC │
│ code_verifier=<oryginalny-PKCE-verifier> │
│ │
│ ◀── { access_token, id_token, refresh_token } ── │
│ │
│ (twoja apka waliduje id_token signature │
│ przeciwko /.well-known/jwks.json) │
│ │
│ (twoja apka GETuje /userinfo z access_token │
│ żeby pobrać claims do session cookie) │
│ │
│ 8. 302 → /dashboard + Set-Cookie: session=<sealed> │
│ ◀─────────────────── │
│ │
│ 9. Kolejne requesty niosą session cookie │
│ apka czyta cookie, parsuje claims, renderuje │
└─────────────────────────────────────────────────────────┘
Kluczowe punkty:
- PKCE chroni przed code interception.
code_verifiergenerowany client-side, nigdy nie wysyłany aż do/tokenexchange. Atakujący kradnący URL z?code=ABCnie wymieni go na tokeny bez verifier-a. - State to Twoja CSRF defence. Generuj losowy nonce na
/auth/login, zachowaj w sealed cookie, porównaj na callback. Każdy SDK to robi za Ciebie. - Tokeny nigdy nie idą do przeglądarki. Tylko sealed session cookie dociera do przeglądarki user-a.
access_token/id_token/refresh_tokenzostają server-side w session store SDK-a. - Brak hit-u DB per request.
id_tokento signed JWT. Validate przeciwko JWKS endpoint przy boocie (cache in-process), potem trust contents na każdym kolejnym request.
Czynniki uwierzytelnienia podczas handshake
Krok 5 w diagramie powyżej (“POST email + password”) to skrót. Login screen WolfieAuth oferuje cztery czynniki które user może mieszać i łączyć — Twoja apka nie widzi różnicy. Cokolwiek user wybierze, Twoja apka dostaje ten sam id_token na końcu:
- Hasło. Klasyk. Bcrypt-hashed at rest, rate-limited per IP i per email. Fallback gdy nic innego nie skonfigurowane.
- Passkeys / WebAuthn. Hardware-backed, phishing-resistant. Jeśli user zarejestrował passkey na urządzeniu, login screen prosi o niego zamiast (albo dodatkowo do) hasła. End-to-end passwordless.
- Magic links. Podpisany URL emailowany do user-a. One-shot, szybko wygasa. Pełni rolę MFA — udowodnienie kontroli nad emailem liczy się jako drugi czynnik.
- TOTP. Sześciocyfrowe kody z dowolnego authenticator app (1Password, Bitwarden, Aegis, Authy). Wymagane gdy user (albo plan flag
requireTwoFactorTwojej apki) je włączy. Recovery codes wystawiane przy enrolment-ie do lockout-recovery.
User może zarejestrować wiele czynników i login screen wybiera najlepszy dostępny. Recovery codes są single-use i regenerowalne z /account/security.
Co widzi Twoja apka: id_token niesie amr (Authentication Methods References) array wskazujący które czynniki zadziałały — np. ["pwd", "mfa"] albo ["webauthn"]. Jeśli chcesz wymusić fresh strong-factor login dla wrażliwej akcji (re-auth przed zmianą metody płatności), redirectuj do /authorize?prompt=login&max_age=0 i user dostaje password / passkey prompt znowu.
Co jest w Twoim session cookie
SDK seal-uje (encrypt + HMAC) mały JSON object do jednego cookie:
{
"sub": "clxxx...",
"email": "[email protected]",
"name": "Pawel Wolfie",
"role": "USER",
"wolfieauth_org_id": "org_abc",
"wolfieauth_org_slug": "acme",
"wolfieauth_membership_kind": "MEMBER",
"wolfieauth_membership_approval": "APPROVED",
"wolfieauth_role_slug": "editor",
"wolfieauth_permissions": ["invoices.read", "invoices.write"],
"wolfieauth_plans": [...],
"wolfieauth_features": ["wolfie-wolfiecrm:ksef_enabled", ...],
"exp": 1768432200
}
Cookie jest httpOnly + secure + sameSite=lax. Nie da się go przeczytać z JavaScriptu (brak XSS exfiltration). Nowy login refresh-uje go. Logout czyści.
Org scope w tokenach — multi-tenancy w warstwie auth
Claim wolfieauth_org_id to najważniejsze pole dla każdej multi-tenant apki i to które developerzy najczęściej przeoczają. Każdy token jest scopowany do jednej orgi — nawet gdy user należy do kilku. Robione celowo: pozwala Twoim DB queries używać WHERE org_id = $claim prosto z tokenu bez zastanawiania się “ale na którą org user akurat patrzy?”
Jak wybierana jest org:
homeOrgIduser-a to default — org do której się zarejestrował albo był zaproszony jako primary.- User może zmienić aktywną orgę z menu konta (
/account/orgs); kolejny mint OIDC tokenu odzwierciedla nową aktywną orgę. - SDK-i akceptują
?orgId=…na URL startu SSO żeby wymusić konkretną orgę dla tego logowania. Przydatne gdy Twoja apka już wie którego tenanta user dotyczy (sub-domain routing, bookmark z org slug w URL).
Co płynie z aktywnej orgi:
wolfieauth_org_id,wolfieauth_org_slug,wolfieauth_org_parentId(gdy org siedzi pod resellerem).wolfieauth_role_slugiwolfieauth_permissions[]— rola user-a w tej orgi. Ten sam user może byćadminw org A iviewerw org B.wolfieauth_plansiwolfieauth_features— plan który ta org subskrybuje dla Twojej apki. Dwie orgi używające Twojej apki obok siebie mogą mieć różne feature setty.wolfieauth_membership_kind(MEMBER/GUEST/SPECIAL_ADMIN) iwolfieauth_membership_approval.
Jeśli user należy do wielu org i Twoja apka wspiera “org switcher”, wywołaj GET /api/auth/me?orgId=<inna-org-id> żeby wybić fresh session przeciwko tej orgi bez forsowania pełnego OIDC re-handshake. Helpery SDK-ów eksponują to jako switchOrg(orgId) (React, SvelteKit) albo Auth::switchOrg($id) (Laravel).
Cross-app SSO
Cały sens posiadania JEDNEGO WolfieAuth z N zarejestrowanymi apkami: user loguje się raz, jego przeglądarka trzyma cookie na auth.wolfieguard.com, każda inna apka na innym domenie może zakończyć swoje OIDC handshake silently (bez UI).
1. User loguje się w app1.com → cookie na auth.wolfieguard.com
2. User klika "Sign in" w app2.com (inny domen)
3. app2.com redirectuje na auth.wolfieguard.com/authorize
4. WolfieAuth widzi wciąż-ważne SSO cookie, skipuje password prompt
5. Redirectuje z powrotem na app2.com/callback z fresh code
6. app2.com wymienia code na tokeny, ustawia SWOJE session cookie
User postrzega to jako “dwa kliki i jestem w środku” zamiast “wpisz hasło ponownie”. Claim linked_accounts[] mówi Twojej apce do których innych apek ten user się uwierzytelnił:
"linked_accounts": {
"wolfie-perfex-main": { "external_user_id": "1", "external_email": "...", "role": "admin" },
"wolfie-vendor-wolfieguard": { "external_user_id": "42", "external_email": "...", "role": "administrator" }
}
Po co to istnieje: gdy user loguje się do Twojej apki przez WolfieAuth, Twoja apka pewnie ma własne wewnętrzne user ID (row w wp_users, tblstaff, users, cokolwiek). To mapowanie — “WolfieAuth user clxxx… to wewnętrzny user 42 w tej instalce Perfex” — jest zapisywane raz po stronie WolfieAuth jako row LinkedAccount, i ląduje w każdym kolejnym tokenie jako linked_accounts[twoj-client-id].external_user_id. Twoja apka czyta to z JWT claim, znajduje lokalny row po integer ID i pomija email-matching dance przy każdym logowaniu.
Jeśli potrzebujesz pobrać to samo mapowanie dla innych zarejestrowanych apek (np. CRM chce znać WordPress user ID dla tej samej osoby), czytasz z tego samego linked_accounts map — keyed po client_id. Tak workflow cross-app typu “stwórz fakturę w Perfex z zamówienia WooCommerce” stitchuje tożsamości bez ekstra lookup service-u.
Logout — RP-initiated end-session
Gdy user klika Logout w Twojej apce, MUSISZ przekierować go przez WolfieAuth /end_session endpoint, nie tylko wyczyścić lokalny cookie. Inaczej WolfieAuth cookie na auth.wolfieguard.com zostaje żywe i następny login skipuje password prompt — myli i jest niebezpieczne na shared devices.
// Każdy SDK to robi w swoim /auth/logout route
GET /auth/logout
→ wyczyść lokalny session cookie
→ 302 do https://auth.wolfieguard.com/session/end?id_token_hint=<id_token>&post_logout_redirect_uri=https://twoja-apka.com/
User ląduje z powrotem na homepage, w pełni wylogowany z OBYDWOCH (Twojej apki I WolfieAuth).
Token refresh
Jeśli wymagałeś offline_access scope, dostałeś refresh_token razem z innymi. Użyj go żeby wybić fresh access/id tokeny bez forsowania user-a do re-auth:
POST https://auth.wolfieguard.com/token
grant_type=refresh_token
refresh_token=<the-token>
Większość SDK robi to automatycznie — wykrywa near-expired session i silently refreshes w tle. Refresh tokeny same się rotują (każde użycie daje nowego), więc skradziony refresh token ma wąskie okno przed unieważnieniem.
Custom claimy przez per-app JWT templates
Czasami standardowe nazwy claimów nie matchują tego co downstream service oczekuje — Hasura chce https://hasura.io/jwt/claims, PostgREST chce role, edge function expects tenant, etc. Zamiast forsować każdą apkę do re-shape claims przy konsumpcji, możesz skonfigurować JWT template na swoim OidcClient który injectuje custom claimy przy mint-time tokenu.
W panelu admin otwórz /admin/clients/<twoja-apka>/jwt-templates. Drop in JSON template z Mustache-style interpolacją:
{
"https://hasura.io/jwt/claims": {
"x-hasura-user-id": "{{sub}}",
"x-hasura-org-id": "{{wolfieauth_org_id}}",
"x-hasura-default-role": "{{wolfieauth_role_slug}}",
"x-hasura-allowed-roles": ["admin", "editor", "viewer"]
}
}
Każdy claim dostępny dla user-a (każde pole wolfieauth_* plus standard OIDC) może być podstawiony. Obok edytora jest Preview button który renderuje template przeciwko Twoim własnym aktualnym claimom — wklej, klik preview, zobacz dokładnie co Twoja apka dostanie przed zapisaniem. Templaty są walidowane na balans {{…}} i rozmiar przy write-time żeby literówka nie mintowała silently broken tokenów.
To też jest odpowiedź na “chcę żeby moja apka dostawała groups: [...] zamiast wolfieauth_role_slug” — zrób one-line template który aliasuje pole. Downstream tool myśli wtedy że gada z generic OIDC providerem bez wymogu wiedzy WolfieAuth-specific.
Approval gate
Jeśli org do której logujesz się ma requireMembershipApproval: true, pierwszy login ląduje claims.wolfieauth_membership_approval = "PENDING". Każdy template SDK middleware automatycznie odbija tych userów na ekran “waiting for approval” z buttonem logout. Dla hand-rolled flow sprawdź isApproved(claims):
import { isApproved } from '@wolfieauth/sdk-core/oidc/claims';
if (!isApproved(claims)) return redirect('/auth/pending');
Poza standardowy flow
Kilka endpointów istnieje których większość SDK-ów nie eksponuje, ale warto wiedzieć dla nietypowych scenariuszy:
- Token introspection (
POST /token/introspection, RFC 7662) — Twój backend może zapytać WolfieAuth “czy ten access token jest jeszcze ważny i co w nim siedzi?” Przydatne gdy upstream proxy widzi token ale nie ma JWKS scache-owanego. - Token revocation (
POST /token/revocation, RFC 7009) — explicite unieważnij access lub refresh token przed wygaśnięciem (np. user kliknął “log out everywhere”). Cookie sesji naauth.wolfieguard.comjest revoke-owane osobno przez/session/end. - Userinfo endpoint (
GET /mezAuthorization: Bearer <access_token>) — zwraca ten sam claim set coid_token, scope-gated. Większość SDK-ów dotyka tego raz przy logowaniu żeby zapełnić session cookie; rzadko musisz wołać bezpośrednio. - Public branding endpoint (
GET /api/public/clients/<twoj-client-id>/branding) — zwraca theme tokens (kolory, logo, copy strings) skonfigurowane dla Twojej apki. Konsumenci SDK używają tego do renderowania login button stylowanego pod issuer-a. Bez auth, safe wołać z przeglądarki. - Discovery (
GET /.well-known/openid-configuration) i JWKS (GET /jwks) — dwa URL-e które każda OIDC client library auto-discoveruje. Jeśli integrujesz starszą bibliotekę która tego nie robi, te są URL-ami do wskazania.
Jeśli third-party tool który próbujesz integrować pyta o którykolwiek z URL-i powyżej, wszystkie siedzą pod https://auth.wolfieguard.com/.
Co jeśli WolfieAuth jest down?
Wtedy nikt nie może się zalogować. To realna konsekwencja używania hostowanego auth providera, i powinieneś to akceptować tak samo jak akceptujesz że Twoja apka idzie down gdy AWS pada. Mitygacje:
- Cache JWKS aggressywnie — apka boot-uje nawet gdy WolfieAuth nieosiągalny, dopóki ma cached JWKS.
- Refresh tokeny mają długie TTL (default 30d) — istniejący zalogowani userzy działają dalej.
- Używaj claim
wolfieauth_plans, nie roundtripów — jeśli gating czyta z JWT, Twoja apka nie martwi się czy WolfieAuth jest osiągalny per request.
Czytaj dalej
- Plany & Billing — modele cenowe
- Admin — UI w którym spędzisz większość czasu
Ostatnia aktualizacja: