|
3 | 3 | namespace App\Providers\Socialite; |
4 | 4 |
|
5 | 5 | use GuzzleHttp\Exception\GuzzleException; |
| 6 | +use Illuminate\Support\Arr; |
6 | 7 | use Laravel\Socialite\Two\AbstractProvider; |
7 | 8 | use Laravel\Socialite\Two\ProviderInterface; |
8 | 9 | use Laravel\Socialite\Two\User; |
| 10 | +use Firebase\JWT\JWT; |
| 11 | +use Firebase\JWT\Key; |
9 | 12 | use Log; |
10 | 13 |
|
11 | 14 | /** |
@@ -96,15 +99,45 @@ protected function getTokenUrl() |
96 | 99 | return $this->getOIDCUrl() . '/token'; |
97 | 100 | } |
98 | 101 |
|
| 102 | + /** |
| 103 | + * {@inheritdoc} |
| 104 | + */ |
| 105 | + public function user() |
| 106 | + { |
| 107 | + if ($this->user) { |
| 108 | + return $this->user; |
| 109 | + } |
| 110 | + |
| 111 | + if ($this->hasInvalidState()) { |
| 112 | + throw new \Laravel\Socialite\Two\InvalidStateException;; |
| 113 | + } |
| 114 | + |
| 115 | + $response = $this->getAccessTokenResponse($this->getCode()); |
| 116 | + |
| 117 | + $user = $this->getUserByToken(Arr::get($response, 'access_token'), Arr::get($response, 'id_token')); |
| 118 | + |
| 119 | + return $this->userInstance($response, $user); |
| 120 | + } |
| 121 | + |
| 122 | + |
99 | 123 | /** |
100 | 124 | * @param string $token |
101 | 125 | * |
102 | 126 | * @throws GuzzleException |
103 | 127 | * |
104 | 128 | * @return array|mixed |
105 | 129 | */ |
106 | | - protected function getUserByToken($token) |
| 130 | + protected function getUserByToken($token, $idToken = null) |
107 | 131 | { |
| 132 | + $useIdToken = config('services.oidc.use_id_token', false); |
| 133 | + |
| 134 | + if ($useIdToken) { |
| 135 | + if (!$idToken) { |
| 136 | + throw new \Exception('OIDC_USE_ID_TOKEN=true but id_token not received'); |
| 137 | + } |
| 138 | + return $this->decodeIdToken($idToken); |
| 139 | + } |
| 140 | + |
108 | 141 | $base_url = $this->getOIDCUrl() . '/userinfo'; |
109 | 142 | // If userinfo endpoint set, use it instead |
110 | 143 | if (config('services.oidc.userinfo_endpoint')) { |
@@ -140,4 +173,36 @@ protected function mapUserToObject(array $user) |
140 | 173 | } |
141 | 174 | return (new User())->setRaw($user)->map($socialite_user); |
142 | 175 | } |
| 176 | + |
| 177 | + protected function decodeIdToken($idToken) |
| 178 | + { |
| 179 | + $alg = config('services.oidc.jwt_alg', 'RS256'); |
| 180 | + $key = config('services.oidc.jwt_secret_or_key'); |
| 181 | + |
| 182 | + if (!$key) { |
| 183 | + throw new \Exception('JWT secret or public key not configured'); |
| 184 | + } |
| 185 | + |
| 186 | + try { |
| 187 | + $decoded = JWT::decode($idToken, new Key($key, $alg)); |
| 188 | + } catch (\Exception $e) { |
| 189 | + throw new \Exception('Failed to decode ID token: '.$e->getMessage(), 0, $e); |
| 190 | + } |
| 191 | + |
| 192 | + $claims = (array) $decoded; |
| 193 | + $clientId = config('services.oidc.client_id'); |
| 194 | + $expectedIssuer = rtrim(config('services.oidc.issuer', $this->getOIDCUrl()), '/'); |
| 195 | + $aud = $claims['aud'] ?? null; |
| 196 | + $audiences = is_array($aud) ? $aud : ($aud !== null ? [$aud] : []); |
| 197 | + if (($claims['iss'] ?? null) !== $expectedIssuer) { |
| 198 | + throw new \Exception('Invalid ID token issuer'); |
| 199 | + } |
| 200 | + if (!in_array($clientId, $audiences, true)) { |
| 201 | + throw new \Exception('Invalid ID token audience'); |
| 202 | + } |
| 203 | + if (count($audiences) > 1 && ($claims['azp'] ?? null) !== $clientId) { |
| 204 | + throw new \Exception('Invalid ID token authorized party'); |
| 205 | + } |
| 206 | + return $claims; |
| 207 | + } |
143 | 208 | } |
0 commit comments