sitmpcz / oidc
Integration oidc client to Nette
Installs: 47
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/sitmpcz/oidc
Requires
- php: >=8.1
- contributte/psr7-http-message: ^0.10.0
- facile-it/php-openid-client: ^0.3.5
- nette/di: ^3.1
- nette/http: ^3.1
- web-token/jwt-framework: ^3.4
This package is not auto-updated.
Last update: 2026-02-20 09:11:34 UTC
README
OpenID Connect (OIDC) extension pro Nette Framework s podporou Keycloak a dalších OIDC providerů.
Knihovna integruje facile-it/php-openid-client do Nette aplikací a poskytuje jednoduché API pro autentizaci přes OpenID Connect, včetně podpory pro backchannel logout (Single Sign-Out).
Požadavky
- PHP 8.1 nebo vyšší
- Nette Framework 3.1+
- OpenID Connect provider (např. Keycloak)
Instalace
composer require sitmpcz/oidc
Konfigurace
Zaregistrujte extension v config.neon:
extensions: openid: Sitmpcz\oidc\DI\OpenIDExtension openid: issuerUrl: %env.ISSUER_URL% # URL OIDC providera clientId: %env.CLIENT_ID% # Client ID z OIDC providera clientSecret: %env.CLIENT_SECRET% # Client Secret z OIDC providera redirectUri: "/sign/callback" # volitelné postLogoutRedirectUri: "/" # volitelné backchannelLogoutUri: "/sign/out-slo" # volitelné scopes: [openid, profile, email] # volitelné
Parametry konfigurace
| Parametr | Povinný | Popis |
|---|---|---|
issuerUrl |
Ano | URL vašeho OIDC providera (např. https://keycloak.example.com/realms/myrealm) |
clientId |
Ano | Client ID z konfigurace OIDC providera |
clientSecret |
Ano | Client Secret z konfigurace OIDC providera |
redirectUri |
Ne | URI pro callback po přihlášení. Pokud neuvedete, použije se aktuální URL z requestu |
postLogoutRedirectUri |
Ne | URI pro přesměrování po odhlášení. Výchozí: / |
backchannelLogoutUri |
Ne | URI endpoint pro backchannel logout (Single Sign-Out) |
scopes |
Ne | OIDC scopes. Výchozí: [openid, profile, email] |
Relativní vs. Absolutní URL:
Všechny URI parametry podporují relativní cesty (např. /sign/callback). Knihovna automaticky doplní schéma, doménu a port z aktuálního HTTP requestu. Můžete také používat absolutní URL.
Podpora Reverse Proxy
Pokud běžíte za reverse proxy (nginx, Apache) nebo v Kubernetes Ingress, knihovna automaticky detekuje:
X-Forwarded-Proto- pro detekci HTTPSX-Forwarded-Host- pro správný hostnameX-Forwarded-Port- pro správný port
Ujistěte se, že vaše proxy tyto hlavičky správně nastavuje.
Příklad pro nginx:
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port;
Příklad pro Kubernetes Ingress: Většina Ingress controllers (nginx-ingress, Traefik) nastavuje tyto hlavičky automaticky.
Použití v presenteru
<?php declare(strict_types=1); namespace App\Presenters; use Nette\Application\UI\Presenter; use Sitmpcz\oidc\Security\OpenIDClientService; final class SignPresenter extends Presenter { public function __construct( private OpenIDClientService $oidc ) {} public function actionLogin(): void { $this->redirectUrl($this->oidc->getAuthorizationUrl()); } public function actionCallback(): void { $userinfo = $this->oidc->handleCallback(); $this->getUser()->login($userinfo['preferred_username']); $this->redirect('Homepage:'); } public function actionLogout(): void { // Získat ID token pro správné odhlášení z OIDC providera $idToken = $this->oidc->getIdToken(); // Vyčistit lokální session $this->oidc->logout(); $this->getUser()->logout(); // Přesměrovat na OIDC provider pro globální odhlášení $this->redirectUrl($this->oidc->getLogoutUrl($idToken)); } /** * Endpoint pro backchannel logout - volá ho Keycloak při odhlášení z jiné aplikace * URL: /sign/out-slo */ public function actionOutSlo(): void { $logoutToken = $this->getHttpRequest()->getPost('logout_token'); if (!$logoutToken) { $this->error('Missing logout_token', 400); } try { $success = $this->oidc->handleBackchannelLogout($logoutToken); if ($success) { // Odhlásit uživatele z Nette $this->getUser()->logout(true); } // OIDC specifikace vyžaduje HTTP 200 bez obsahu $this->sendResponse(new \Nette\Application\Responses\TextResponse('')); } catch (\RuntimeException $e) { $this->error($e->getMessage(), 400); } } }
Použití s Redis sessions (contributte/redis)
Pokud používáte Redis pro ukládání sessions, backchannel logout vyžaduje speciální přístup, protože Keycloak nemá přímý přístup k vaší aktivní session - musíte vyhledat session v Redis podle sid (session ID) z logout tokenu.
Konfigurace Redis
config/redis.neon:
extensions: redis: Contributte\Redis\DI\RedisExtension redis: debug: %debugMode% connection: default: uri: tcp://redis:6379 sessions: false storage: true options: ['parameters': ['database': 0]] session: uri: tcp://redis:6379 sessions: true # Redis jako session handler storage: false options: ['parameters': ['database': 1]] # Oddělená databáze pro sessions
config/common.neon:
extensions: openid: Sitmpcz\oidc\DI\OpenIDExtension openid: issuerUrl: %env.ISSUER_URL% clientId: %env.CLIENT_ID% clientSecret: %env.CLIENT_SECRET% redirectUri: "/sign/callback" postLogoutRedirectUri: "/sign/in" backchannelLogoutUri: "/sign/out-slo" scopes: [openid, profile, email] services: # SignPresenter s explicitním Redis klientem pro backchannel logout - App\Presenters\SignPresenter( redisSession: @redis.connection.session.client )
Presenter s Redis backchannel logout
<?php declare(strict_types=1); namespace App\Presenters; use Nette\Application\UI\Presenter; use Sitmpcz\oidc\Security\OpenIDClientService; use Predis\ClientInterface as RedisClient; final class SignPresenter extends Presenter { public function __construct( private OpenIDClientService $oidc, private RedisClient $redisSession // Redis klient pro sessions (databáze 1) ) {} public function actionLogin(): void { $this->redirectUrl($this->oidc->getAuthorizationUrl()); } public function actionCallback(): void { $userinfo = $this->oidc->handleCallback(); $this->getUser()->login($userinfo['preferred_username']); $this->redirect('Homepage:'); } public function actionOut(): void { $idToken = $this->oidc->getIdToken(); // Odhlásit lokálně $this->getUser()->logout(); $this->oidc->logout(); // Zničit celou session včetně dat v Redis $this->session->destroy(); // Přesměrovat na OIDC logout endpoint (Single Sign-Out) $this->redirectUrl($this->oidc->getLogoutUrl($idToken)); } /** * Backchannel logout endpoint - vyhledává sessions v Redis podle sid/sub * URL: /sign/out-slo */ public function actionOutSlo(): void { $this->getHttpResponse()->setContentType('application/json'); try { $logoutToken = $this->getHttpRequest()->getPost('logout_token'); if (!$logoutToken) { $this->getHttpResponse()->setCode(\Nette\Http\Response::S400_BadRequest); $this->sendJson(['error' => 'logout_token parameter is required']); } // Dekóduj logout token a získej sid/sub $parts = explode('.', $logoutToken); if (count($parts) !== 3) { $this->getHttpResponse()->setCode(\Nette\Http\Response::S400_BadRequest); $this->sendJson(['error' => 'Invalid logout token format']); } $payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true); $sid = $payload['sid'] ?? null; $sub = $payload['sub'] ?? null; if (!$sid && !$sub) { $this->getHttpResponse()->setCode(\Nette\Http\Response::S400_BadRequest); $this->sendJson(['error' => 'logout_token must contain sid or sub']); } // Najdi všechny session klíče v Redis $sessionKeys = $this->redisSession->keys('*'); $deletedSessions = 0; foreach ($sessionKeys as $sessionKey) { $sessionData = $this->redisSession->get($sessionKey); if (!$sessionData) { continue; } // Zkontroluj, zda session obsahuje OIDC data if (strpos($sessionData, 'oidc') === false) { continue; } // Extrahuj idToken ze serializovaných session dat // Formát: oidc|a:3:{s:8:"userInfo";a:...;s:7:"idToken";s:NNN:"..."; if (preg_match('/s:7:"idToken";s:\d+:"([^"]+)"/', $sessionData, $matches)) { $idToken = $matches[1]; // Dekóduj ID token $idParts = explode('.', $idToken); if (count($idParts) === 3) { $idPayload = json_decode(base64_decode(strtr($idParts[1], '-_', '+/')), true); $sessionSid = $idPayload['sid'] ?? null; $sessionSub = $idPayload['sub'] ?? null; // Porovnej sid nebo sub if (($sid && $sessionSid === $sid) || ($sub && $sessionSub === $sub)) { // Smaž tuto session z Redis $this->redisSession->del($sessionKey); $deletedSessions++; } } } } if ($deletedSessions > 0) { $this->getHttpResponse()->setCode(\Nette\Http\Response::S200_OK); $this->sendJson([ 'status' => 'logged_out', 'deleted_sessions' => $deletedSessions ]); } else { $this->getHttpResponse()->setCode(\Nette\Http\Response::S200_OK); $this->sendJson(['status' => 'no_matching_session']); } } catch (\Exception $e) { $this->getHttpResponse()->setCode(\Nette\Http\Response::S400_BadRequest); $this->sendJson(['error' => $e->getMessage()]); } } }
Důležité poznámky pro Redis
-
Oddělené databáze: Používejte samostatnou Redis databázi pro sessions (např. databáze 1) oddělenou od cache (databáze 0)
-
Backchannel logout vyžaduje vyhledávání: Na rozdíl od standardních Nette sessions, kde je aktivní session dostupná v kontextu requestu, u backchannel logout musíte:
- Projít všechny session klíče v Redis
- Deserializovat session data
- Najít ID token v sekci
oidc - Porovnat
sidnebosubz logout tokenu s ID tokenem v session - Smazat odpovídající session z Redis
-
Výkon: Pro velký počet aktivních sessions může být vyhledávání pomalé. Zvažte:
- Index sessions podle
sidv samostatné Redis struktuře - TTL pro Redis session klíče odpovídající session expiraci
- Monitoring počtu aktivních sessions
- Index sessions podle
-
Bezpečnost: Backchannel endpoint neověřuje JWT logout token - v produkčním prostředí zvažte přidání validace tokenu pomocí
OpenIDClientService::handleBackchannelLogout()před vyhledáváním v Redis.
Dostupné metody
getAuthorizationUrl(): string
Vrací URL pro přesměrování na přihlašovací stránku OIDC providera.
handleCallback(): array
Zpracuje callback z OIDC providera a vrátí informace o uživateli.
refreshToken(): bool
Obnoví access token pomocí refresh tokenu. Vrací true při úspěchu, false při selhání.
getLogoutUrl(?string $idToken = null): string
Vrací URL pro odhlášení z OIDC providera. Při zadání ID tokenu poskytuje lepší single sign-out.
logout(): void
Vyčistí lokální session (userInfo, refreshToken, idToken).
getIdToken(): ?string
Vrací uložený ID token ze session, pokud existuje.
handleBackchannelLogout(string $logoutToken): bool
Zpracuje backchannel logout požadavek z OIDC providera (např. Keycloak). Validuje JWT logout token a odhlásí lokální session, pokud token odpovídá aktuálnímu uživateli. Vrací true pokud byla session odhlášena.
Backchannel Logout (Single Sign-Out)
Backchannel logout umožňuje OIDC provideru automaticky odhlásit uživatele z vaší aplikace, když se odhlásí z jiné aplikace připojené ke stejnému provideru.
Konfigurace v Keycloak
- V Keycloak administraci přejděte na Client Settings vašeho klienta
- Nastavte Backchannel Logout URL:
https://vase-domena.cz/sign/out-slo - Zapněte Backchannel Logout Session Required
Tip: V config.neon stačí uvést relativní cestu (backchannelLogoutUri: "/sign/out-slo"), knihovna automaticky sestaví plnou URL.
Jak to funguje
- Uživatel se odhlásí z aplikace A připojené ke Keycloak
- Keycloak pošle POST požadavek na backchannel logout endpoint aplikace B
- Aplikace B validuje JWT
logout_tokena odhlásí uživatele - Uživatel je nyní odhlášen ze všech aplikací (Single Sign-Out)
Token je validován podle OIDC Back-Channel Logout specifikace a session je spárována podle sid (session ID) nebo sub (subject/user ID).
Klíčové vlastnosti
- Automatické sestavování absolutních URL z relativních cest
- Podpora reverse proxy a Kubernetes Ingress
- Front-channel a backchannel logout
- Správa session v Nette session storage
- Automatická obnova tokenů přes refresh token
- JWT validace podle OIDC standardů
Licence
MIT