Skip to content

Gestion décodage JWT fix #596#597

Merged
dbarzin merged 4 commits intodbarzin:mainfrom
wouldsmina:patch-oidc-jwk
Apr 3, 2026
Merged

Gestion décodage JWT fix #596#597
dbarzin merged 4 commits intodbarzin:mainfrom
wouldsmina:patch-oidc-jwk

Conversation

@wouldsmina
Copy link
Copy Markdown
Contributor

@wouldsmina wouldsmina commented Apr 3, 2026

Bonjour,

Ce PR ajoute la possibilité de configurer Deming pour utiliser le id_token (JWT) retourné par l'IdP afin d'en extraire directement les claims, au lieu d'appeler l'endpoint /userinfo.

Cette fonctionnalité est utile avec Apereo CAS, qui ne fournit pas l'endpoint /userinfo.

Le fonctionnement est contrôlé par l'option OIDC_USE_ID_TOKEN. Lorsqu'elle est activée, le id_token est décodé et ses claims sont utilisés pour construire l'utilisateur. Sinon, le comportement actuel (appel à /userinfo avec l'access_token) est conservé.

Je n'ai testé que l'algorithme RS256 avec une clé publique au format PEM. Le support de HS256 est également présent et devrait fonctionner correctement.

Deux points importants :

Mon serveur CAS (et probablement d'autres IdP) ne fournit pas la clé publique directement au format PEM mais au format JWKS (RSA). J'ai converti cette clé en PEM à l'aide de ce script Python :
https://akrabat.com/converting-jwks-json-to-pem-using-python/

Je n'ai pas réussi à récupérer et convertir automatiquement la clé depuis l'endpoint /jwks en PHP. Cette clé pourrait être récupérée dynamiquement plutôt que configurée manuellement. Ça pourrait faire l'objet d'une amélioration ultérieure.

Summary by CodeRabbit

  • New Features

    • Optional OIDC ID token decoding for user info retrieval.
    • Selectable JWT algorithm (RS256 or HS256) and runtime option to supply verification key/secret.
    • Configuration flags added to enable ID token usage and provide algorithm/key settings.
  • Chores

    • Added a JWT library dependency to support ID token decoding.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

Warning

Rate limit exceeded

@wouldsmina has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 17 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 0 minutes and 17 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f4d4d153-5b8d-479d-ad72-ca6453110425

📥 Commits

Reviewing files that changed from the base of the PR and between 847b97f and db7bab3.

📒 Files selected for processing (1)
  • app/Providers/Socialite/GenericSocialiteProvider.php

Walkthrough

Adds optional OIDC ID-token decoding to GenericSocialiteProvider: provider captures id_token from token responses, can decode and validate JWTs using configured algorithm/key, and optionally returns ID-token claims instead of calling the userinfo endpoint. New env/config entries and a JWT dependency were added.

Changes

Cohort / File(s) Summary
Configuration & Environment
\.env.example, config/services.php
Added OIDC_USE_ID_TOKEN, OIDC_JWT_ALG, and OIDC_JWT_SECRET_OR_KEY env/config entries to enable ID-token decoding and specify algorithm/key.
OAuth Provider Enhancement
app/Providers/Socialite/GenericSocialiteProvider.php
Added user() override to capture token response, extended getUserByToken($token, $idToken = null) to accept an id_token, added decodeIdToken() to verify/decode JWT using configured alg/key, and conditional logic to return decoded claims when enabled.
Dependencies
composer.json
Added runtime dependency firebase/php-jwt (^7.0) for JWT decoding.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Provider as GenericSocialiteProvider
    participant OAuth as OAuthServer
    participant Decoder as JWTDecoder

    Client->>OAuth: OAuth callback (code)
    OAuth->>Provider: token response (access_token, id_token)
    Provider->>Provider: store access_token + id_token

    alt OIDC_USE_ID_TOKEN = true
        Provider->>Decoder: decode id_token (alg, key)
        Decoder-->>Provider: decoded claims
        Provider-->>Client: return user from claims
    else OIDC_USE_ID_TOKEN = false
        Provider->>OAuth: request /userinfo with access_token
        OAuth-->>Provider: userinfo payload
        Provider-->>Client: return user from userinfo
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibbled tokens in the night, so spry,
Pulled out an id_token, gave it a try.
RS or HS, the claims unfold,
Verified and warm, no extra call told.
Hop, decode, and off I fly.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Gestion décodage JWT fix #596' is partially related to the changeset. It references JWT decoding (the main feature) and a GitHub issue number, but lacks clarity and specificity about what the change accomplishes. Consider a more descriptive title in English or French that clearly explains the feature: e.g., 'Add OIDC ID token decoding support with JWT validation' or 'Support OIDC ID token claims extraction instead of userinfo endpoint'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@wouldsmina
Copy link
Copy Markdown
Contributor Author

Oups! J'ai envoyé dans main! @dbarzin préfères-tu que je recommence en pointant sur dev ?

@dbarzin
Copy link
Copy Markdown
Owner

dbarzin commented Apr 3, 2026

C'est OK, rien en DEV pour l'instant.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Providers/Socialite/GenericSocialiteProvider.php`:
- Around line 108-112: The provider currently caches the ID token in
getAccessTokenResponse() via $this->idToken and then getUserByToken($token)
ignores its $token argument when services.oidc.use_id_token is true; change the
logic so getUserByToken uses the token parameter passed in instead of a cached
$this->idToken, and only read an ID token from getAccessTokenResponse() to
populate a local/flow-scoped value (not shared state) or pass it explicitly into
getUserByToken when calling from the authorization-code flow; if an ID token is
not available, have getUserByToken fall back to calling the /userinfo endpoint
rather than throwing, ensuring code paths referencing getAccessTokenResponse(),
getUserByToken($token) and $this->idToken no longer rely on instance-level
cached state.
- Around line 169-185: The decodeIdToken method currently only verifies the JWT
signature; after JWT::decode($idToken, ...) you must validate standard OIDC
claims: compare the token's 'iss' against a configured expected issuer (use
config('services.oidc.issuer') or construct it if absent), ensure the 'aud'
contains the configured client id (config('services.oidc.client_id')), and if an
'azp' claim exists verify it equals the configured client id; if any check fails
throw an Exception describing which claim failed. Update decodeIdToken to read
these config values and perform the checks on the decoded token array (from
json_decode(json_encode($decoded), true)), failing fast on mismatch to prevent
authentication bypass.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c6f29c13-bd71-415f-9673-d33eed2575c8

📥 Commits

Reviewing files that changed from the base of the PR and between f235745 and 5126141.

📒 Files selected for processing (4)
  • .env.example
  • app/Providers/Socialite/GenericSocialiteProvider.php
  • composer.json
  • config/services.php

Comment thread app/Providers/Socialite/GenericSocialiteProvider.php Outdated
Comment thread app/Providers/Socialite/GenericSocialiteProvider.php
@dbarzin
Copy link
Copy Markdown
Owner

dbarzin commented Apr 3, 2026

@wouldsmina Tu peux vérifier les commentaires de CodeRabbit ?

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/Providers/Socialite/GenericSocialiteProvider.php (1)

177-193: ⚠️ Potential issue | 🔴 Critical

Validate iss, aud, and azp before accepting the ID token.

Lines 186-192 only verify the JWT signature. That still accepts tokens signed by the same issuer key for a different client, which is a relying-party auth bypass.

Suggested fix
     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);
         }

-        return json_decode(json_encode($decoded), true);
+        $claims = json_decode(json_encode($decoded), true);
+        $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 (isset($claims['azp']) && $claims['azp'] !== $clientId) {
+            throw new \Exception('Invalid ID token authorized party');
+        }
+
+        if (count($audiences) > 1 && ! isset($claims['azp'])) {
+            throw new \Exception('Missing ID token authorized party');
+        }
+
+        return $claims;
     }
#!/bin/bash
# Verify decodeIdToken() has no relying-party claim checks today.
sed -n '177,215p' app/Providers/Socialite/GenericSocialiteProvider.php
rg -n "JWT::decode|services\.oidc\.(issuer|client_id)|\biss\b|\baud\b|\bazp\b" app/Providers/Socialite/GenericSocialiteProvider.php
fd '^services\.php$' config -x rg -n "oidc|client_id|issuer" {}

Expected result: JWT::decode is present, but there are no iss / aud / azp comparisons inside decodeIdToken().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Providers/Socialite/GenericSocialiteProvider.php` around lines 177 - 193,
The decodeIdToken method currently only verifies the JWT signature; update
decodeIdToken to also validate relying-party claims by fetching expected values
from config('services.oidc.issuer') and config('services.oidc.client_id') and
then (after JWT::decode) assert that the decoded claims contain 'iss' exactly
equal to the configured issuer, that the 'aud' claim either equals the
configured client_id or (if 'aud' is an array) contains the client_id, and if
'aud' is an array also verify 'azp' equals the configured client_id; on any
mismatch throw a clear Exception (e.g., "Invalid issuer", "Invalid audience",
"Invalid authorized party") so the token is rejected for wrong
issuer/audience/azp. Ensure you reference the decoded payload returned by
JWT::decode in these checks and keep signature validation intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Providers/Socialite/GenericSocialiteProvider.php`:
- Around line 111-112: The thrown InvalidStateException is resolving to the
current namespace instead of Socialite's class; add an import for Socialite's
exception (e.g., add "use Laravel\Socialite\Two\InvalidStateException;") at the
top of GenericSocialiteProvider.php so the throw in the hasInvalidState() check
references the correct Socialite exception class.

---

Duplicate comments:
In `@app/Providers/Socialite/GenericSocialiteProvider.php`:
- Around line 177-193: The decodeIdToken method currently only verifies the JWT
signature; update decodeIdToken to also validate relying-party claims by
fetching expected values from config('services.oidc.issuer') and
config('services.oidc.client_id') and then (after JWT::decode) assert that the
decoded claims contain 'iss' exactly equal to the configured issuer, that the
'aud' claim either equals the configured client_id or (if 'aud' is an array)
contains the client_id, and if 'aud' is an array also verify 'azp' equals the
configured client_id; on any mismatch throw a clear Exception (e.g., "Invalid
issuer", "Invalid audience", "Invalid authorized party") so the token is
rejected for wrong issuer/audience/azp. Ensure you reference the decoded payload
returned by JWT::decode in these checks and keep signature validation intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b45658be-46aa-485f-9b07-3db07bce143a

📥 Commits

Reviewing files that changed from the base of the PR and between 5126141 and 1295f80.

📒 Files selected for processing (1)
  • app/Providers/Socialite/GenericSocialiteProvider.php

Comment thread app/Providers/Socialite/GenericSocialiteProvider.php Outdated
Add OIDC claims validation to prevent authentication bypass.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
app/Providers/Socialite/GenericSocialiteProvider.php (1)

6-12: ⚠️ Potential issue | 🔴 Critical

Missing InvalidStateException import will cause a class-not-found error.

Line 112 throws InvalidStateException, but the class is not imported. PHP will attempt to resolve it within the App\Providers\Socialite namespace, resulting in a fatal error when state validation fails.

Suggested fix
 use GuzzleHttp\Exception\GuzzleException;
 use Illuminate\Support\Arr;
 use Laravel\Socialite\Two\AbstractProvider;
+use Laravel\Socialite\Two\InvalidStateException;
 use Laravel\Socialite\Two\ProviderInterface;
 use Laravel\Socialite\Two\User;
 use Firebase\JWT\JWT;
 use Firebase\JWT\Key;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Providers/Socialite/GenericSocialiteProvider.php` around lines 6 - 12,
The file throws InvalidStateException in the state validation path (see where
InvalidStateException is thrown around the validation logic in
GenericSocialiteProvider) but the class isn't imported; add the proper import
for Laravel\Socialite\Two\InvalidStateException at the top of the file (or
change the throw to a fully-qualified class name) so PHP resolves the exception
instead of looking in the App\Providers\Socialite namespace.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/Providers/Socialite/GenericSocialiteProvider.php`:
- Around line 6-12: The file throws InvalidStateException in the state
validation path (see where InvalidStateException is thrown around the validation
logic in GenericSocialiteProvider) but the class isn't imported; add the proper
import for Laravel\Socialite\Two\InvalidStateException at the top of the file
(or change the throw to a fully-qualified class name) so PHP resolves the
exception instead of looking in the App\Providers\Socialite namespace.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d5e2f254-97e3-4faf-9873-637a63d08f89

📥 Commits

Reviewing files that changed from the base of the PR and between 1295f80 and 847b97f.

📒 Files selected for processing (1)
  • app/Providers/Socialite/GenericSocialiteProvider.php

@dbarzin dbarzin merged commit 1df558b into dbarzin:main Apr 3, 2026
1 check passed
@dbarzin
Copy link
Copy Markdown
Owner

dbarzin commented Apr 3, 2026

Merci !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants