AWNEX ID — integracja z auth.awnex.pl
Dokument opisuje kontrakt API auth.awnex.pl (centralny provider tożsamości ekosystemu Awnex) na podstawie aktualnego kodu (app/api/**, lib/oauth.ts, lib/identity.ts, db/schema.ts). Cel: żeby nowy serwis dało się podłączyć bez czytania źródeł.
Nie mylić z legacy providerem hostowanym na samym
awnex.pl(lib/awnex-oauth.tsw tamtym repo, issuerhttps://awnex.pl) — to inny, starszy system, do stopniowego wygaszenia. Ten dokument dotyczy wyłącznie auth.awnex.pl / AWNEX ID.
Szybki start
Masz gotowy system logowania (WordPress/WooCommerce)? Zainstaluj wtyczkę AWNEX ID Connect — konfiguracja bez pisania kodu, patrz §13.
Piszesz własną integrację (Next.js, SPA, mobile, backend)? Trzy kroki (pełny opis w §3):
1. GET /api/oauth/authorize?client_id=...&redirect_uri=...&response_type=code&scope=openid+profile+email&state=...&code_challenge=...&code_challenge_method=S256
→ przekierowanie przeglądarki, użytkownik loguje się na hostowanym ekranie, wraca na Twój redirect_uri?code=...
2. POST /api/oauth/token (server-to-server)
grant_type=authorization_code&code=...&redirect_uri=...&code_verifier=...&client_id=...&client_secret=...
→ { access_token, refresh_token, id_token, expires_in }
3. GET /api/oauth/userinfo
Authorization: Bearer <access_token>
→ { id, email, name, avatar, services, providers, ... }Masz istniejący system i chcesz tylko zarejestrować/dopasować konto bez ekranu logowania (Tryb B, w pełni curlowalne, bez przeglądarki)?
curl -X POST https://auth.awnex.pl/api/identity/sync \
-H "Authorization: Bearer $SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{"service":"moj-serwis","localUserId":"42","email":"user@example.com","name":"Jan Kowalski"}'Zanim zaczniesz, potrzebujesz client_id+client_secret (Tryb A) albo service key (Tryb B) — patrz §2 / §7.1, rejestracja jest ręczna po stronie administratora auth.awnex.pl.
Dla botów / agentów AI
Ten dokument jest dostępny też w formatach, które nie wymagają parsowania HTML:
- Surowy Markdown:
https://auth.awnex.pl/llms.txt— identyczna treść co ta strona, czysty tekst. - OpenAPI 3.1:
https://auth.awnex.pl/doc/openapi.json— pełna maszynowo czytelna specyfikacja wszystkich endpointów (parametry, kształty odpowiedzi, kody błędów), gotowa do wczytania przez narzędzia generujące kod klienta albo function-calling. - OIDC discovery:
https://auth.awnex.pl/.well-known/openid-configuration— dla bibliotek OIDC z auto-konfiguracją.
Jeśli generujesz kod integracji automatycznie: użyj openapi.json jako źródła prawdy o kształtach requestów/odpowiedzi, a tego dokumentu (Markdown) jako źródła kontekstu/uzasadnień (dlaczego coś działa tak, a nie inaczej — patrz zwłaszcza pułapki w §5 i §9).
1. Dwa tryby integracji
| Tryb | Kiedy używać | Jak działa |
|---|---|---|
| A. OAuth2/OIDC Authorization Code + PKCE | Nowa aplikacja (Next.js, SPA, mobile, Jellyfin itp.), użytkownik loguje się przez hostowany ekran auth.awnex.pl/login | Standardowy redirect flow, patrz §3 |
| B. Service-to-service identity sync | Istniejący system z własnym logowaniem (np. WordPress/WooCommerce), który chce mieć konto w AWNEX ID bez przepisywania UI logowania | Backend-do-backend, sekretny service key, patrz §7 |
Oba tryby operują na tych samych kontach (accounts) — użytkownik zalogowany w jednym serwisie przez tryb A i zsynchronizowany przez inny serwis w trybie B to wciąż jedno konto, dopasowywane po e-mailu.
2. Rejestracja nowego klienta OAuth
Nie ma samoobsługowego endpointu ani panelu admina do rejestracji klienta — trzeba dopisać wiersz bezpośrednio w tabeli oauth_clients bazy awnex_auth (Postgres, kontener awnex_auth_db).
INSERT INTO oauth_clients (id, name, secret_hash, redirect_uris, scopes, active)
VALUES (
'moj-serwis', -- client_id, dowolny slug, publiczny
'Mój Serwis', -- nazwa czytelna dla ludzi
encode(sha256('SUPER-TAJNY-SEKRET'::bytea), 'hex'), -- sha256(client_secret), hex
'["https://moj-serwis.pl/api/auth/callback"]'::jsonb, -- dokładne redirect_uri, string match
'["openid", "profile", "email"]'::jsonb,
true
);id(client_id) jest jawny/publiczny — jest wpisany na stałe w kodzie klienta.secret_hashtosha256(client_secret)w hex, nie przechowuj plaintextu.redirect_urismusi zawierać dokładny URI używany przy/api/oauth/authorizei/api/oauth/token— porównanie jest string-equal, żadnych wildcardów.- Po dodaniu klienta zapisz sobie w pamięci projektowej (memory) fakt rejestracji, żeby audyty ekosystemu (
oauth_clients) zostały aktualne — patrz [[project-awnex-ecosystem-auth]].
3. Tryb A — Authorization Code + PKCE (rekomendowany)
3.1 Discovery
GET https://auth.awnex.pl/.well-known/openid-configurationZwraca:
{
"issuer": "https://auth.awnex.pl",
"authorization_endpoint": "https://auth.awnex.pl/api/oauth/authorize",
"token_endpoint": "https://auth.awnex.pl/api/oauth/token",
"userinfo_endpoint": "https://auth.awnex.pl/api/oauth/userinfo",
"revocation_endpoint": "https://auth.awnex.pl/api/oauth/revoke",
"jwks_uri": "https://auth.awnex.pl/.well-known/jwks.json",
"scopes_supported": ["openid", "profile", "email", "identity"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"id_token_signing_alg_values_supported": ["HS256"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
"code_challenge_methods_supported": ["S256", "plain"]
}Jeśli używasz gotowej biblioteki OIDC (np. openid-client, Duende IdentityModel.OidcClient), discovery wystarczy do auto-konfiguracji. jwks_uri istnieje wyłącznie żeby spec-compliant klienci nie odrzucali discovery — zwraca zawsze {"keys":[]}, bo tokeny są HS256 (współdzielony sekret), nie RS256. Jeśli Twoja biblioteka próbuje zweryfikować podpis JWT lokalnie przez JWKS, to się nie uda — zweryfikuj token przez /api/oauth/userinfo zamiast tego (patrz §5).
3.2 Krok 1 — redirect do login
Przekieruj użytkownika (zwykły window.location, nie fetch) na:
GET https://auth.awnex.pl/api/oauth/authorize
?client_id=moj-serwis
&redirect_uri=https://moj-serwis.pl/api/auth/callback
&response_type=code
&scope=openid profile email
&state=<losowy string, anty-CSRF, wygenerowany i zapamiętany przez Ciebie>
&code_challenge=<base64url(sha256(code_verifier))>
&code_challenge_method=S256redirect_urimusi string-equal pasować do wpisu woauth_clients.redirect_uris, inaczejinvalid_redirect_uri.- PKCE (
code_challenge/code_verifier) jest opcjonalne w kodzie, ale mocno zalecane — szczególnie dla SPA/mobile bez bezpiecznego backendu do trzymaniaclient_secret.code_verifierwygeneruj losowo (43–128 znaków), zachowaj po stronie klienta (sesja/localStorage) do kroku 3. - Jeśli w przeglądarce jest już ważny identity token w nagłówku
Authorization: Bearer(rzadki przypadek — patrz kodauthorize/route.ts), endpoint wystawia auth code od razu bez pokazywania ekranu logowania. W normalnym flow (przeglądarka bez tego nagłówka) leci redirect doauth.awnex.pl/login?...z tymi samymi parametrami w query.
Ekran logowania (/login) obsługuje: e-mail+hasło, rejestrację, Google, Facebook, logowanie telefonem (SMS OTP), passkeys (WebAuthn) — wszystko to jest już ogarnięte po stronie auth.awnex.pl, integrujący serwis nic z tym nie musi robić. Po udanym logowaniu użytkownik wraca na Twój redirect_uri z ?code=...&state=....
3.3 Krok 2 — wymiana code na tokeny
Server-side (nigdy z przeglądarki — tu potrzebny client_secret):
POST https://auth.awnex.pl/api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<code z redirectu>
&redirect_uri=https://moj-serwis.pl/api/auth/callback (dokładnie ten sam co w kroku 1)
&code_verifier=<jeśli używałeś PKCE>
&client_id=moj-serwis
&client_secret=SUPER-TAJNY-SEKRETAutoryzacja klienta akceptuje też Authorization: Basic base64(client_id:client_secret) zamiast pól w body.
Odpowiedź:
{
"access_token": "<JWT HS256>",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "<64-char hex>",
"id_token": "<JWT HS256, tylko jeśli scope zawierał 'openid'>",
"scope": "openid profile email"
}Auth code jest jednorazowy (usedAt), ważny 10 minut, powiązany 1:1 z client_id + redirect_uri z kroku 1 — mismatch którejkolwiek wartości = redirect_uri_mismatch / client_mismatch.
3.4 Krok 3 — dane użytkownika
GET https://auth.awnex.pl/api/oauth/userinfo
Authorization: Bearer <access_token>{
"sub": "uuid",
"email_verified": true,
"picture": "https://...",
"id": "uuid",
"email": "user@example.com",
"name": "Jan Kowalski",
"avatar": "https://...",
"services": [{ "service": "plus-awnex", "localUserId": "123", "role": "member", "linkedAt": "..." }],
"providers": [{ "provider": "google", "email": "...", "linkedAt": "..." }],
"createdAt": "..."
}To jest jedyny wiarygodny sposób odczytania danych użytkownika po stronie klienta — nie dekoduj id_token/access_token ręcznie licząc na to, że zawiera aktualne dane; oba są tylko podpisanymi tokenami dostępu, userinfo zawsze czyta świeży stan z bazy.
4. Odświeżanie tokenu
POST /api/oauth/token
grant_type=refresh_token
&refresh_token=<z kroku 3.3>
&client_id=moj-serwis
&client_secret=...Zwraca nowy access_token (1h TTL) z tym samym scope. Refresh token sam się nie rotuje (ten sam ważny 30 dni, aż do wylogowania/rewokacji) — nie oczekuj nowego refresh_token w odpowiedzi.
5. Weryfikacja access tokenu bez roundtripu do userinfo
Access token to JWT-podobny format (header.payload.sig, HS256), ale podpisany współdzielonym sekretem `AWNEX_AUTH_TOKEN_SECRET` — możesz go zweryfikować lokalnie tylko jeśli Twój serwis ma ten sekret (typowo tylko usługi Awnex Group, nie zewnętrzni integratorzy). Payload:
{ "sub": "<accountId>", "iss": "https://auth.awnex.pl", "aud": "<client_id>", "scope": "openid profile email", "iat": ..., "exp": ... }Pułapka historyczna (naprawiona, ale ważna do zrozumienia architektury): w auth.awnex.pl istnieją dwa różne formaty tokenu HS256, oba podpisane tym samym sekretem, ale z innym payloadem i innym
iss: - Access token (lib/oauth.ts,issueAccessToken) —iss= URL issuera (https://auth.awnex.pl), używany w standardowym flow OAuth (§3), weryfikowany przezverifyAccessToken(). - Identity token (lib/identity.ts,issueIdentityToken) —iss="awnex-auth"(string, nie URL), inny kształt payloadu (services,providerswprost w tokenie), zwracany przez/api/auth/login,/api/auth/register,/api/identity/sync(tryb B, §7). Weryfikowany przezverifyIdentityToken().Te dwie funkcje weryfikujące nie są zamienne — podanie identity tokenu tam gdzie oczekiwany jest access token (albo odwrotnie) zawsze kończy się
invalid_token, mimo że token "wygląda" poprawnie. Jeśli integrujesz się przez §3 (OAuth), zawsze dostajesz i używasz access tokenu. Jeśli integrujesz się przez §7 (service sync), dostajesz identity token.
6. Wylogowanie / rewokacja
POST /api/oauth/revoke(RFC 7009) — unieważnia pojedynczyrefresh_token. Wymaga uwierzytelnienia klienta (client_id+client_secret, form body lub Basic). Zawsze zwraca200, niezależnie czy token istniał (żeby nie dało się tym sondować ważności tokenów).POST /api/auth/logoutzAuthorization: Bearer <access_token>— globalne wylogowanie: unieważnia wszystkie refresh tokeny konta, na wszystkich klientach naraz. Nie ma tu sesji/cookie do skasowania po stronie auth.awnex.pl — każdy klient sam musi skasować lokalnie trzymany access token po stronie przeglądarki/urządzenia.
7. Tryb B — Service-to-service identity sync
Dla systemów z własnym mechanizmem logowania (typowo WordPress/WooCommerce), które chcą mieć konto AWNEX ID bez przechodzenia przez hostowany ekran logowania — np. istniejący sklep WooCommerce synchronizuje konto przy rejestracji/checkout.
7.1 Klucz serwisu
Każdy serwis w tym trybie potrzebuje service key — albo zmienna środowiskowa w auth.awnex.pl (AWNEX_SERVICE_KEY_<SERVICE_NAZWA_WIELKIMI> albo globalny AWNEX_SERVICE_KEY), albo wiersz w tabeli service_keys (service, key_hash = sha256(key), active). Ten sekret nigdy nie trafia do przeglądarki — żyje tylko po stronie backendu integrującego się serwisu.
7.2 Wywołanie
POST https://auth.awnex.pl/api/identity/sync
Authorization: Bearer <service key>
Content-Type: application/json
{
"service": "moj-sklep",
"localUserId": "42", // ID użytkownika w Twoim systemie
"email": "user@example.com",
"name": "Jan Kowalski", // opcjonalne
"password": "haslo-jednorazowe", // opcjonalne — patrz niżej
"role": "customer", // opcjonalne, dowolny string
"forcePasswordChange": true, // opcjonalne
"sendWelcomeEmail": true // opcjonalne
}Zachowanie: - Szuka konta po email. Jeśli nie istnieje — tworzy nowe. - Zawsze upsertuje service_links (accountId + service unikalne) — tak samo działa przy kolejnych wywołaniach dla tego samego użytkownika (aktualizuje localUserId/role/lastSeenAt). - Jeśli podasz password i konto jest nowe, ustawia hasło na to konto (np. hasło jednorazowe wygenerowane przez Twój system) — użytkownik będzie mógł się nim zalogować bezpośrednio na auth.awnex.pl/login albo w innych serwisach AWNEX ID. - forcePasswordChange: true ustawia mustChangePassword — przy najbliższym logowaniu przez tryb A użytkownik zostanie zmuszony do ustawienia własnego hasła zanim dokończy OAuth (flow w /api/auth/complete, patrz kod). - sendWelcomeEmail: true (tylko gdy konto nowe i podano password) wysyła e-mail z loginem i hasłem jednorazowym przez skonfigurowany SMTP. - provider/providerUserId (opcjonalne) — jeśli użytkownik przyszedł np. z Google po stronie Twojego systemu, możesz od razu zapisać to jako provider_links.
Odpowiedź:
{ "success": true, "identity": { "id": "uuid", "email": "...", "name": "...", "services": [...], "providers": [...] }, "token": "<identity token, iss=awnex-auth>" }Zwrócony token to identity token (§5) — jeśli chcesz od razu zalogować użytkownika sesją AWNEX ID (np. do SSO między Twoim systemem a innym serwisem Awnex bez ponownego logowania), przekaż go dalej zgodnie z tym co dany kontekst oczekuje. Do zwykłego "czy to konto istnieje/zsynchronizowane" wystarczy sam success: true.
8. Reset hasła (dla kont z hasłem)
POST /api/auth/forgot-password { "email": "..." } → zawsze { success: true }, wysyła mail jeśli konto istnieje i ma hasło
POST /api/auth/reset-password { "token": "...", "password": "...", client_id?, redirect_uri?, scope?, state?, code_challenge?, code_challenge_method? }Jeśli w reset-password przekażesz kontekst OAuth (client_id+redirect_uri zarejestrowane dla tego klienta), odpowiedź zawiera redirectTo z gotowym auth code — przydatne przy wymuszonej zmianie hasła w środku logowania (§3.2), żeby użytkownik nie musiał logować się drugi raz.
9. Zmienne środowiskowe (po stronie klienta integrującego się)
Typowy wzorzec używany w innych serwisach Next.js ekosystemu (plus.awnex.pl, panel.awnex.pl, money.awnex.pl):
AWNEX_AUTH_URL=https://auth.awnex.pl
AWNEX_AUTH_CLIENT_ID=moj-serwis
AWNEX_AUTH_CLIENT_SECRET=...
PUBLIC_URL=https://moj-serwis.pl # do budowania własnego redirect_uri — patrz pułapka niżejPułapka: nigdy nie buduj
redirect_uri(ani żadnego innego absolutnego URL wysyłanego do auth.awnex.pl) zrequest.url/request.nextUrlw Next.js za reverse proxy (nginx) — potrafi rozwiązać się do wewnętrznego adresu kontenera zamiast publicznej domeny, co widać dosłownie w komentarzach kodu auth.awnex.pl samego (app/api/auth/complete/route.tsi inne) — ten serwis sam na to uważa i zawsze buduje URL-e zAWNEX_AUTH_PUBLIC_URL, rób tak samo po swojej stronie.
10. Kody błędów
| Endpoint | error | Znaczenie |
|---|---|---|
/authorize | unsupported_response_type | response_type inny niż code |
/authorize, /complete, /token | invalid_client | nieznany/nieaktywny client_id, albo zła para client_id+secret |
/authorize, /complete | invalid_redirect_uri | redirect_uri nie ma dokładnego dopasowania w oauth_clients.redirect_uris |
/token | invalid_code / code_already_used / code_expired | auth code nieprawidłowy / już wymieniony / wygasł (>10 min) |
/token | client_mismatch / redirect_uri_mismatch | code wystawiony dla innego client_id/redirect_uri niż w tym wywołaniu |
/token | code_verifier_required / invalid_code_verifier | PKCE: brak code_verifier albo nie pasuje do code_challenge |
/token | invalid_refresh_token | refresh token nieznany, unieważniony albo wygasły (>30 dni) |
/token | too_many_attempts (429) | 20 nieudanych prób client_secret / 5 min / IP+client_id |
/userinfo, /logout | unauthenticated (401, brak nagłówka) / invalid_token (401) | brak/zły/wygasły access token |
/login (form) | too_many_attempts (429) | 10 prób hasła / 5 min / IP+email (login), 5 SMS / 10 min / IP+telefon (start), 10 prób kodu / 10 min / IP+telefon (verify) |
/identity/sync | unauthorized_service (401) | zły/brak service key dla podanego service |
11. Bezpieczeństwo — co warto wiedzieć zanim zintegrujesz
- HS256, sekret współdzielony, brak prawdziwego JWKS.
jwks_urizwraca{"keys":[]}tylko żeby spec-compliant biblioteki OIDC nie odrzucały discovery. Nie da się zweryfikować tokenu przez klucz publiczny — jedyna wiarygodna weryfikacja dla zewnętrznego integratora toGET /api/oauth/userinfo. - Rate limiting jest in-memory, per-instance. Jeśli auth.awnex.pl kiedyś będzie skalowany do >1 kontenera, limity przestaną być globalnie spójne (nie wpływa to na integrujące się serwisy, ale warto wiedzieć że to nie jest twardy limit rozproszony).
- `client_secret` i `service key` nigdy nie mogą trafić do przeglądarki — wymiana code→token, sync, revoke to zawsze wywołania serwer-serwer.
- PKCE jest technicznie opcjonalne (endpoint akceptuje request bez
code_challenge), ale dla każdej aplikacji, która nie może bezpiecznie trzymaćclient_secret(SPA, mobile, WebView) — wymagane w praktyce, inaczej auth code przechwycony w locie (np. przez malicious redirect) da pełny dostęp bez znajomości sekretu.
12. Skrót — checklist integracji nowego serwisu
- Zdecyduj tryb: A (redirect/OAuth) dla nowej appki z własnym hostingiem, B (sync) dla istniejącego systemu logowania.
- (Tryb A) Poproś o dopisanie klienta do
oauth_clients(§2) — potrzebujeszclient_id, wygenerowanegoclient_secret, dokładnegoredirect_uri.
(Tryb B) Poproś oservice key(env var albo wiersz wservice_keys, §7.1). - (Tryb A) Zaimplementuj: redirect do
/api/oauth/authorize→ callback wymieniającycodena tokeny przez/api/oauth/token→/api/oauth/userinfopo dane usera. Użyj PKCE jeśli nie masz bezpiecznego backendu.
(Tryb B) Wołaj/api/identity/syncprzy rejestracji/logowaniu w swoim systemie. - Buduj wszystkie URL-e po swojej stronie z jawnego
PUBLIC_URL, nie zrequest.url(§9). - Zarejestruj fakt integracji w audycie ekosystemu, jeśli masz do niego dostęp.
13. Gotowa wtyczka WordPress (Tryb A + B w jednym pakiecie)
Dla WordPressa/WooCommerce nie trzeba pisać integracji od zera — AWNEX ID Connect to gotowa, konfigurowalna wtyczka implementująca pełny Tryb A (redirect + PKCE) i opcjonalnie Tryb B (identity sync) w jednym pakiecie.
Pobierz: awnex-id-connect.zip
Co robi: - Dodaje przycisk logowania na standardowym ekranie wp-login.php oraz (jeśli WooCommerce aktywne) w formularzach logowania i rejestracji WooCommerce — bez edycji motywu, przez standardowe hooki (login_form, woocommerce_login_form_start, woocommerce_register_form_start), więc przetrwa redesign motywu. - Pełny flow PKCE: /wp-json/awnexid/v1/start → auth.awnex.pl/api/oauth/authorize → /wp-json/awnexid/v1/callback → wymiana code na tokeny → /api/oauth/userinfo. - Dopasowuje/loguje użytkownika WP po e-mailu; opcjonalnie tworzy nowe konto WP automatycznie (rola customer jeśli WooCommerce aktywne, inaczej subscriber). - Opcjonalnie woła /api/identity/sync po każdym logowaniu (jeśli skonfigurowano service key), żeby ta strona była widoczna w service_links innych integracji ekosystemu. - Ekran ustawień (Ustawienia → AWNEX ID) pokazuje gotowy redirect_uri do przekazania administratorowi auth.awnex.pl oraz sprawdza na żywo połączenie z discovery document. - Shortcode [awnexid_login_button] do ręcznego wstawienia przycisku gdziekolwiek.
Instalacja: 1. Rozpakuj awnex-id-connect.zip do wp-content/plugins/ i aktywuj wtyczkę. 2. Ustawienia → AWNEX ID → skopiuj wyświetlony Redirect URI. 3. Poproś administratora auth.awnex.pl o zarejestrowanie klienta OAuth (§2) z tym redirect_uri — dostaniesz client_id i client_secret. 4. Wklej client_id/client_secret w ustawieniach wtyczki, zapisz. Gotowe — przycisk logowania pojawi się automatycznie.
Kod źródłowy wtyczki (do audytu lub forka pod inny CMS): wordpress-plugin/awnex-id-connect/ w tym repozytorium (auth.awnex.pl), plik awnex-id-connect.php — pojedynczy plik, bez zależności poza rdzeniem WordPressa, jawnie komentowany kontrakt API (patrz nagłówek pliku).
