# 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](#13-gotowa-wtyczka-wordpress-tryb-a-b-w-jednym-pakiecie) — 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 → { 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)?** ```bash 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`](https://auth.awnex.pl/llms.txt) — identyczna treść co ta strona, czysty tekst. - **OpenAPI 3.1**: [`https://auth.awnex.pl/doc/openapi.json`](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`](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`). ```sql 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: ```json { "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= &code_challenge= &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= &redirect_uri=https://moj-serwis.pl/api/auth/callback (dokładnie ten sam co w kroku 1) &code_verifier= &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ź: ```json { "access_token": "", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "<64-char hex>", "id_token": "", "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 ``` ```json { "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= &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: ```json { "sub": "", "iss": "https://auth.awnex.pl", "aud": "", "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 ` — **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_` 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 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ź: ```json { "success": true, "identity": { "id": "uuid", "email": "...", "name": "...", "services": [...], "providers": [...] }, "token": "" } ``` 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 | 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_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`](/downloads/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).