misha-in/sso-client

Jednoduchý PHP klient pro integraci s SSO serverem pomocí OAuth 2.0 (Authorization Code + PKCE).

Maintainers

Package info

gitlab.com/misha.in/sso-client

Issues

pkg:composer/misha-in/sso-client

Transparency log

Statistics

Installs: 21

Dependents: 0

Suggesters: 0

Stars: 0

dev-main 2026-04-28 00:40 UTC

This package is not auto-updated.

Last update: 2026-07-31 00:02:32 UTC


README

Jednoduchý PHP klient pro integraci s SSO serverem id.produkce.ai pomocí OAuth 2.0 (Authorization Code + PKCE).

Nevyžaduje žádné závislosti – používá pouze PHP curl a cookies.

Požadavky

  • PHP 8.1+
  • Rozšíření curl

Instalace

Composer (doporučeno)

composer require misha-in/sso-client
use MishaIn\SsoClient\SsoClient;

Bez Composeru

require_once __DIR__ . '/SsoClient.php';

Konfigurace

$sso = new SsoClient([
    'client_id'     => 'my-app',                           // ID klienta registrovaného na SSO
    'client_secret' => 'tajny-klic',                       // Tajný klíč klienta
    'redirect_uri'  => 'https://app.example.com/callback', // Callback URL registrovaná na SSO
    'sso_url'       => 'https://id.produkce.ai',           // Base URL SSO serveru
]);

Poznámka k scopům: Klient neposílá seznam požadovaných scopů. SSO server automaticky vrátí všechna oprávnění, která má přihlášený uživatel přiřazena pro danou aplikaci. Správa oprávnění probíhá výhradně na SSO serveru v administraci.

Použití

1. Přihlášení – přesměrování na SSO

$sso->redirectToLogin();
// Funkce ukončí skript přesměrováním (exit).

Interně se vygeneruje state (ochrana proti CSRF) a code_verifier (PKCE), které se uloží do krátkodobých cookies (platnost 10 minut, SameSite=Lax).

2. Callback – výměna kódu za tokeny

Na callback URL (registrované na SSO) zpracujte parametry z $_GET:

try {
    $tokens = $sso->handleCallback();
    // $tokens['access_token']  – přístupový token (platnost 1 hodina)
    // $tokens['refresh_token'] – obnovovací token (platnost 1 měsíc)
    // $tokens['expires_in']    – platnost access tokenu v sekundách (3600)
    // $tokens['token_type']    – "Bearer"
} catch (\MishaIn\SsoClient\SilentAuthenticationException $e) {
    // Silent auth selhalo (uživatel není přihlášen na SSO)
    // $e->errorCode === 'login_required' | 'interaction_required' | ...
} catch (RuntimeException $e) {
    // Neplatný state (CSRF) nebo chyba serveru
    die('Přihlášení selhalo: ' . $e->getMessage());
}

Tokeny jsou automaticky uloženy do $_SESSION – klient je spravuje automaticky.

3. Informace o uživateli

$user = $sso->getUserInfo($tokens['access_token']);
// $user['sub']    – ID uživatele (int)
// $user['email']  – e-mailová adresa
// $user['name']   – celé jméno
// $user['scopes'] – pole přidělených oprávnění, např. ['dashboard', 'articles.faq']
// $user['role']   – per-klientská role uživatele (vždy vyplněna;
//                   bez přiřazené role uživatel ověřením neprojde)
//   $user['role']['id']   – číselné ID role (int)
//   $user['role']['name'] – název role (string), např. 'admin', 'manager', 'user'
//   $user['role']['rank'] – pořadí v hierarchii rolí (int); nižší číslo = vyšší oprávnění
//
// Pokud byl uživateli odebrán přístup (smazána role), SSO vrátí chybu a
// getUserInfo() vyhodí RuntimeException – zachyťte ji stejně jako expiraci tokenu.

Scopy jsou ve formátu dot-notace: parent.child. Scope bez rodiče je jen identifier (např. dashboard).

4. Kontrola oprávnění (scope)

if ($sso->hasScope('articles.faq')) {
    // uživatel má přidělený přesně scope "articles.faq"
}

Metoda podporuje wildcard suffix .* pro kontrolu celé větve stromu scopů:

$sso->hasScope('dashboard')       // true  – pouze přesná shoda "dashboard"
$sso->hasScope('rag')       // false  – kategorie má děti, je tedy nutná kontrola s hvězdičkou
$sso->hasScope('rag.*')     // true  – uživatel má jakýkoli scope začínající "rag." (např. "rag.faq.edit")
$sso->hasScope('rag.faq.*') // true  – uživatel má jakýkoli scope začínající "rag.faq."
$sso->hasScope('rag.faq')   // false – přesná shoda; "rag.faq.edit" nestačí
VoláníScope uživateleVýsledek
hasScope('rag')rag.faq.editfalse
hasScope('rag')ragtrue
hasScope('rag.*')rag.faq.edittrue
hasScope('rag.faq.*')rag.faq.edittrue
hasScope('rag.faq')rag.faq.editfalse

Metoda interně zavolá getUserInfo() – doporučuje se výsledek cachovat na úrovni aplikace (např. ukládat $user do session po dobu přihlášení).

5. Práce s rolí

Role uživatele pro danou aplikaci je vždy přítomna – uživatel bez přiřazené role neprojde ověřením a volání getUserInfo() vyhodí RuntimeException (stejně jako expirovaný token). Aplikace nikdy nedostane data o uživateli bez přiřazené role. Platí pro každého, včetně administrátorů – pokud mají mít přístup, musí mít roli přiřazenou.

try {
    $user = $sso->getUserInfo($accessToken);
} catch (\RuntimeException $e) {
    // Token expiroval, nebo byla uživateli odebrána role pro tuto aplikaci.
    // Zacházejte stejně jako s vypršenou session.
    $sso->redirectToLogin();
}

$role = $user['role']; // array{id: int, name: string, rank: int}

echo $role['name'];  // 'admin' | 'manager' | 'user' | ... (název definovaný v administraci)
echo $role['id'];    // 5  – numerické ID role (stabilní, vhodné pro strojové porovnání)
echo $role['rank'];  // 1  – pořadí v hierarchii rolí; nižší číslo = vyšší oprávnění

rank umožňuje aplikaci porovnávat oprávnění hierarchicky, aniž by musela znát konkrétní názvy rolí:

// Vpustit jen uživatele s dostatečně vysokou rolí (rank 1 nebo 2)
if ($role['rank'] <= 2) {
    // přístup povolen
}

Pořadí rolí (rank) nastavuje správce aplikace přes administraci SSO drag & dropem.

Poznámka: Pokud je role uživateli odebrána po vydání access tokenu, SSO zablokuje přístup při nejbližším volání /userinfo – aplikace tedy nepracuje se zastaralými daty.

6. Ověření přihlášení

if (!$sso->isAuthenticated()) {
    $sso->redirectToLogin();
}

Metoda automaticky obnoví access token pomocí refresh tokenu, pokud vypršel. Pokud obnova selže (refresh token expiroval), vrátí false.

7. Platný access token

Pro přímou práci s tokenem (např. vlastní API volání):

$accessToken = $sso->getValidAccessToken();
if ($accessToken === null) {
    $sso->redirectToLogin(); // session vypršela
}

8. Obnovení tokenu

Access token se obnovuje automaticky při volání isAuthenticated() a getValidAccessToken(). Pokud potřebujete obnovit token ručně:

try {
    $newTokens = $sso->refreshToken($refreshToken);
} catch (RuntimeException $e) {
    // Refresh token vypršel – přesměrujte na přihlášení
    $sso->redirectToLogin();
}

9. Odhlášení

$sso->logout();
// nebo s přesměrováním po odhlášení:
$sso->logout('https://app.example.com/');

Pro sestavení URL bez okamžitého přesměrování (např. odkaz v šabloně):

$logoutUrl = $sso->getLogoutUrl('https://app.example.com/');

10. Seznam uživatelů aplikace

Endpoint /users vrací seznam všech uživatelů, kteří mají k dané aplikaci přiřazenu roli. Přístup je chráněn Bearer tokenem – token musí patřit k dané aplikaci (client_id).

$accessToken = $sso->getValidAccessToken();

// Všichni uživatelé aplikace
$users = $sso->getUsers($accessToken);

// Pouze uživatelé s rolí 'admin'
$admins = $sso->getUsers($accessToken, role: 'admin');

// Pouze uživatelé mající přiřazen scope 'articles.edit'
$editors = $sso->getUsers($accessToken, scope: 'articles.edit');

// Kombinace filtrů – administrátoři mající scope 'reports'
$result = $sso->getUsers($accessToken, role: 'admin', scope: 'reports');

Každý prvek vráceného pole má tvar:

[
    'sub'   => 42,              // int  – interní ID uživatele (stejné jako v /userinfo)
    'name'  => 'Jan Novák',    // string – celé jméno
    'email' => 'jan@firma.cz', // string – e-mail
]

Poznámka: Vrací se vždy jen uživatelé dané aplikace. Token vystavený pro jiného klienta nemůže získat uživatele vaší aplikace.

Silent Authentication (OIDC prompt=none)

Pokud lokální session vypršela, ale uživatel může stále být přihlášen na SSO serveru, lo៮ní lze obnovit bez jakékoliv interakce uživatele.

SSO server při prompt=none nikdy nezobrazí HTML formulář – buď vrátí authorization code (uživatel přihlášen), nebo přesměruje zpět s error=login_required.

// 1. Inicializace tichého ověření (místo redirectToLogin())
$sso->attemptSilentAuthentication(); // přesměrování na SSO s prompt=none

// 2. Na callback URL
try {
    $tokens = $sso->handleCallback();
    // Úspěch – uživatel byl přihlášen na SSO, lokální session obnovena

} catch (\MishaIn\SsoClient\SilentAuthenticationException $e) {
    // Očekávaný stav: uživatel není přihlášen na SSO
    // $e->errorCode může být:
    //   'login_required'             – není přihlášen
    //   'interaction_required'       – SSO vyžaduje interakci
    //   'consent_required'           – musí udělit souhlas
    //   'account_selection_required' – musí vybrat účet
    // → přesměrujte na běžný login, nebo zobrazte veřejnou stránku

} catch (\RuntimeException $e) {
    // Skutečná chyba (CSRF, network, server error)
}

Tip: Ukladaní sso_auth_hint cookie při úspěšném přihlášení umožňuje rozhodnout, zda tichý pokus smá zahajovat (zamezí neřízeným redirect smyčkám).

Kompletní příklad integrace

<?php
session_start();
require_once __DIR__ . '/SsoClient.php';

use MishaIn\SsoClient\SsoClient;

$sso = new SsoClient([
    'client_id'     => 'my-app',
    'client_secret' => 'tajny-klic',
    'redirect_uri'  => 'https://app.example.com/callback',
    'sso_url'       => 'https://id.produkce.ai',
]);

$action = $_GET['action'] ?? '';

// Přihlášení
if ($action === 'login') {
    $sso->redirectToLogin();
}

// Callback ze SSO
if ($action === 'callback') {
    try {
        $sso->handleCallback();
        header('Location: /dashboard');
        exit;
    } catch (\MishaIn\SsoClient\SilentAuthenticationException $e) {
        // Tiché ověření selhalo – uživatel není přihlášen na SSO
        header('Location: /');
        exit;
    } catch (RuntimeException $e) {
        die('Chyba přihlášení: ' . $e->getMessage());
    }
}

// Odhlášení
if ($action === 'logout') {
    $sso->logout('https://app.example.com/');
}

// Chráněná stránka
if (!$sso->isAuthenticated()) {
    $sso->redirectToLogin();
}

$accessToken = $sso->getValidAccessToken();
$user = $sso->getUserInfo($accessToken);

echo 'Přihlášen jako: ' . htmlspecialchars($user['name']);
echo 'Oprávnění: ' . implode(', ', $user['scopes']);
echo 'Role: ' . htmlspecialchars($user['role']['name']) . ' (rank: ' . $user['role']['rank'] . ')';

// Kontrola konkrétního oprávnění
if ($sso->hasScope('articles.faq')) {
    echo 'Má přístup k FAQ.';
}

Přehled metod

MetodaNávratový typPopis
redirectToLogin(?string $prompt = null): neverPřesměruje uživatele na SSO přihlašovací stránku; $prompt='none' = bez UI
attemptSilentAuthentication(): neverTiché ověření přihlášení (OIDC prompt=none)
handleCallback(): arraytokenyVymění authorization code za tokeny; při silent auth chybě vyhodí SilentAuthenticationException
isAuthenticated(): boolboolVrátí true pokud je uživatel přihlášen (automaticky obnoví token)
getValidAccessToken(): ?stringstring|nullVrátí platný access token nebo null pokud session vypršela
getUserInfo(string $accessToken): arrayuser arrayVrátí data uživatele vl. polí scopes a role (id, name, rank), nebo null
getUsers(string $accessToken, ?string $role = null, ?string $scope = null): arraylist uživatelůVrátí seznam uživatelů aplikace; volitelně filtruje dle role nebo scopu
hasScope(string $scope): boolboolZkontroluje, zda má uživatel dané oprávnění
refreshToken(string $refreshToken): arraytokenyRučně obnoví access token
isTokenValid(string $accessToken): boolboolOvěří platnost tokenu dotazem na SSO
getLogoutUrl(?string $redirectUri = null): stringstringVrátí URL pro odhlášení (bez přesměrování)
logout(?string $redirectUri = null): neverOdhlásí uživatele a přesměruje na SSO

Formát scopů

Oprávnění jsou ve formátu dot-notace odpovídající hierarchii definované na SSO serveru:

dashboard    – přístup k dashboardu
rag.faq      – přístup k faq v rámci rag
rag.files    – přístup k files v rámci rag
rag.prompt   – přístup k prompt v rámci rag

Každý uživatel má oprávnění spravována administrátorem přímo na SSO serveru. Aplikace scopy nežádá – dostane automaticky vše, co má daný uživatel přiřazeno.

Bezpečnostní poznámky

  • CSRF ochrana: state parametr je generován pomocí random_bytes(32) a ověřován přes hash_equals().
  • PKCE: Každý login flow používá unikátní code_verifier (S256) – chrání i při úniku authorization code.
  • Tajný klíč (client_secret) nikdy neopouští server – nikdy ho nevkládejte do front-endového kódu nebo do verzovacího systému.
  • Tokeny jsou ukládány do $_SESSION na straně serveru.