AWNEXID · DokumentacjaOpenID Configuration

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.ts w tamtym repo, issuer https://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:

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

TrybKiedy używaćJak działa
A. OAuth2/OIDC Authorization Code + PKCENowa aplikacja (Next.js, SPA, mobile, Jellyfin itp.), użytkownik loguje się przez hostowany ekran auth.awnex.pl/loginStandardowy redirect flow, patrz §3
B. Service-to-service identity syncIstniejący system z własnym logowaniem (np. WordPress/WooCommerce), który chce mieć konto w AWNEX ID bez przepisywania UI logowaniaBackend-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_hash to sha256(client_secret) w hex, nie przechowuj plaintextu.
  • redirect_uris musi zawierać dokładny URI używany przy /api/oauth/authorize i /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-configuration

Zwraca:

{
  "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=S256
  • redirect_uri musi string-equal pasować do wpisu w oauth_clients.redirect_uris, inaczej invalid_redirect_uri.
  • PKCE (code_challenge/code_verifier) jest opcjonalne w kodzie, ale mocno zalecane — szczególnie dla SPA/mobile bez bezpiecznego backendu do trzymania client_secret. code_verifier wygeneruj 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 kod authorize/route.ts), endpoint wystawia auth code od razu bez pokazywania ekranu logowania. W normalnym flow (przeglądarka bez tego nagłówka) leci redirect do auth.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-SEKRET

Autoryzacja 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 przez verifyAccessToken(). - Identity token (lib/identity.ts, issueIdentityToken) — iss = "awnex-auth" (string, nie URL), inny kształt payloadu (services, providers wprost w tokenie), zwracany przez /api/auth/login, /api/auth/register, /api/identity/sync (tryb B, §7). Weryfikowany przez verifyIdentityToken().

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 pojedynczy refresh_token. Wymaga uwierzytelnienia klienta (client_id+client_secret, form body lub Basic). Zawsze zwraca 200, niezależnie czy token istniał (żeby nie dało się tym sondować ważności tokenów).
  • POST /api/auth/logout z Authorization: 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żej

Pułapka: nigdy nie buduj redirect_uri (ani żadnego innego absolutnego URL wysyłanego do auth.awnex.pl) z request.url/request.nextUrl w 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.ts i inne) — ten serwis sam na to uważa i zawsze buduje URL-e z AWNEX_AUTH_PUBLIC_URL, rób tak samo po swojej stronie.

10. Kody błędów

EndpointerrorZnaczenie
/authorizeunsupported_response_typeresponse_type inny niż code
/authorize, /complete, /tokeninvalid_clientnieznany/nieaktywny client_id, albo zła para client_id+secret
/authorize, /completeinvalid_redirect_uriredirect_uri nie ma dokładnego dopasowania w oauth_clients.redirect_uris
/tokeninvalid_code / code_already_used / code_expiredauth code nieprawidłowy / już wymieniony / wygasł (>10 min)
/tokenclient_mismatch / redirect_uri_mismatchcode wystawiony dla innego client_id/redirect_uri niż w tym wywołaniu
/tokencode_verifier_required / invalid_code_verifierPKCE: brak code_verifier albo nie pasuje do code_challenge
/tokeninvalid_refresh_tokenrefresh token nieznany, unieważniony albo wygasły (>30 dni)
/tokentoo_many_attempts (429)20 nieudanych prób client_secret / 5 min / IP+client_id
/userinfo, /logoutunauthenticated (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/syncunauthorized_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_uri zwraca {"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 to GET /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

  1. Zdecyduj tryb: A (redirect/OAuth) dla nowej appki z własnym hostingiem, B (sync) dla istniejącego systemu logowania.
  2. (Tryb A) Poproś o dopisanie klienta do oauth_clients (§2) — potrzebujesz client_id, wygenerowanego client_secret, dokładnego redirect_uri.
    (Tryb B) Poproś o service key (env var albo wiersz w service_keys, §7.1).
  3. (Tryb A) Zaimplementuj: redirect do /api/oauth/authorize → callback wymieniający code na tokeny przez /api/oauth/token/api/oauth/userinfo po dane usera. Użyj PKCE jeśli nie masz bezpiecznego backendu.
    (Tryb B) Wołaj /api/identity/sync przy rejestracji/logowaniu w swoim systemie.
  4. Buduj wszystkie URL-e po swojej stronie z jawnego PUBLIC_URL, nie z request.url (§9).
  5. 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/startauth.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).