diff --git a/.env.example b/.env.example index 66bf938f..ec858a0e 100644 --- a/.env.example +++ b/.env.example @@ -106,5 +106,8 @@ OIDC_CLIENT_ID=deming OIDC_CLIENT_SECRET=deming OIDC_BASE_URL=http://auth.lan OIDC_SUFFIX="" +OIDC_USE_ID_TOKEN=false # true pour décoder le JWT +OIDC_JWT_ALG=RS256 # RS256 ou HS256. utile uniquement avec OIDC_USE_ID_TOKEN=true +OIDC_JWT_SECRET_OR_KEY="" # secret pour HS256 ou clé au format PEM pour RS256 OIDC_REDIRECT_URI=${APP_URL}auth/callback/oidc APP_VERSION=2025.08.13 diff --git a/app/Providers/Socialite/GenericSocialiteProvider.php b/app/Providers/Socialite/GenericSocialiteProvider.php index a59d6889..bf4e872b 100644 --- a/app/Providers/Socialite/GenericSocialiteProvider.php +++ b/app/Providers/Socialite/GenericSocialiteProvider.php @@ -3,9 +3,12 @@ namespace App\Providers\Socialite; use GuzzleHttp\Exception\GuzzleException; +use Illuminate\Support\Arr; use Laravel\Socialite\Two\AbstractProvider; use Laravel\Socialite\Two\ProviderInterface; use Laravel\Socialite\Two\User; +use Firebase\JWT\JWT; +use Firebase\JWT\Key; use Log; /** @@ -96,6 +99,27 @@ protected function getTokenUrl() return $this->getOIDCUrl() . '/token'; } + /** + * {@inheritdoc} + */ + public function user() + { + if ($this->user) { + return $this->user; + } + + if ($this->hasInvalidState()) { + throw new \Laravel\Socialite\Two\InvalidStateException;; + } + + $response = $this->getAccessTokenResponse($this->getCode()); + + $user = $this->getUserByToken(Arr::get($response, 'access_token'), Arr::get($response, 'id_token')); + + return $this->userInstance($response, $user); + } + + /** * @param string $token * @@ -103,8 +127,17 @@ protected function getTokenUrl() * * @return array|mixed */ - protected function getUserByToken($token) + protected function getUserByToken($token, $idToken = null) { + $useIdToken = config('services.oidc.use_id_token', false); + + if ($useIdToken) { + if (!$idToken) { + throw new \Exception('OIDC_USE_ID_TOKEN=true but id_token not received'); + } + return $this->decodeIdToken($idToken); + } + $base_url = $this->getOIDCUrl() . '/userinfo'; // If userinfo endpoint set, use it instead if (config('services.oidc.userinfo_endpoint')) { @@ -140,4 +173,36 @@ protected function mapUserToObject(array $user) } return (new User())->setRaw($user)->map($socialite_user); } + + protected function decodeIdToken($idToken) + { + $alg = config('services.oidc.jwt_alg', 'RS256'); + $key = config('services.oidc.jwt_secret_or_key'); + + if (!$key) { + throw new \Exception('JWT secret or public key not configured'); + } + + try { + $decoded = JWT::decode($idToken, new Key($key, $alg)); + } catch (\Exception $e) { + throw new \Exception('Failed to decode ID token: '.$e->getMessage(), 0, $e); + } + + $claims = (array) $decoded; + $clientId = config('services.oidc.client_id'); + $expectedIssuer = rtrim(config('services.oidc.issuer', $this->getOIDCUrl()), '/'); + $aud = $claims['aud'] ?? null; + $audiences = is_array($aud) ? $aud : ($aud !== null ? [$aud] : []); + if (($claims['iss'] ?? null) !== $expectedIssuer) { + throw new \Exception('Invalid ID token issuer'); + } + if (!in_array($clientId, $audiences, true)) { + throw new \Exception('Invalid ID token audience'); + } + if (count($audiences) > 1 && ($claims['azp'] ?? null) !== $clientId) { + throw new \Exception('Invalid ID token authorized party'); + } + return $claims; + } } diff --git a/composer.json b/composer.json index 8026f07b..2877f020 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "license": "GPLv3", "require": { "php": "^8.2", + "firebase/php-jwt": "^7.0", "directorytree/ldaprecord-laravel": "^3.4", "erusev/parsedown": "^1.7", "laravel/framework": "^11.9", diff --git a/config/services.php b/config/services.php index ba0bdccf..d21516cb 100644 --- a/config/services.php +++ b/config/services.php @@ -71,6 +71,9 @@ 'authorize_endpoint' => env('OIDC_AUTHORIZE_ENDPOINT', null), 'token_endpoint' => env('OIDC_TOKEN_ENDPOINT', null), 'userinfo_endpoint' => env('OIDC_USERINFO_ENDPOINT', null), + 'use_id_token' => env('OIDC_USE_ID_TOKEN', false), + 'jwt_alg' => env('OIDC_JWT_ALG', 'RS256'), + 'jwt_secret_or_key' => env('OIDC_JWT_SECRET_OR_KEY', ''), 'map_user_attr' => [ 'id' => 'sub', 'name' => 'name',