Check email_verified when handling missing user in oauth controller - #2434
Conversation
📝 WalkthroughWalkthroughThe OAuth callback flow in OAuthController was updated to read error parameters using ChangesOAuth Controller Error Handling
Compact Metadata
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/Http/Controllers/Auth/OAuthController.php`:
- Around line 80-83: The email verification check in
OAuthController::handleOAuthCallback is reading email_verified from the
Socialite OAuthUser object, but that field may not be mapped onto the contract.
Update the check to read email_verified from the raw Socialite payload (for
example via getRaw() or equivalent array access) and keep the existing boolean
validation and errorRedirect flow so provider-only data is actually enforced.
🪄 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: 89fe8096-5d1c-4757-a815-29ceccc18f4d
📒 Files selected for processing (1)
app/Http/Controllers/Auth/OAuthController.php
| if (isset($oauthUser->email_verified) && !filter_var($oauthUser->email_verified, FILTER_VALIDATE_BOOLEAN)) { | ||
| return $this->errorRedirect('Email not verified on OAuth provider.'); | ||
| } | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Locate the OAuthUser alias/import and any driver code that sets email_verified.
rg -n 'use .*OAuthUser' app/Http/Controllers/Auth/OAuthController.php
rg -n 'email_verified' -g '*.php' -C3
rg -n 'class .*OAuthUser|extends .*User' -g '*.php' -C3 appRepository: pelican-dev/panel
Length of output: 209
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the controller and nearby code.
ast-grep outline app/Http/Controllers/Auth/OAuthController.php --view expanded || true
wc -l app/Http/Controllers/Auth/OAuthController.php
sed -n '1,180p' app/Http/Controllers/Auth/OAuthController.php
# Inspect the Socialite contract and concrete user classes if present in the repo.
rg -n "interface User|class (User|TwoUser|AbstractUser)|function getRaw|ArrayAccess|email_verified" vendor app -g '*.php' -C 3 || trueRepository: pelican-dev/panel
Length of output: 11240
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find all places that mention OAuthController and OAuthUser.
rg -n "OAuthController|OAuthUser|email_verified" app -g '*.php' -C 3 || true
# If the Socialite package is vendored, inspect the contract and user implementation.
fd -a "User.php|AbstractUser.php|Two" vendor -E node_modules 2>/dev/null || trueRepository: pelican-dev/panel
Length of output: 6082
🌐 Web query:
Laravel Socialite AbstractUser ArrayAccess __get raw attributes email_verified
💡 Result:
In Laravel Socialite, the AbstractUser class (and its concrete implementations like Laravel\Socialite\Two\User) is designed to normalize user data across different OAuth providers [1][2]. To access raw attributes—such as email_verified or other provider-specific fields that are not automatically mapped to Socialite's standardized properties—you should use the getRaw() method [3][4]. Key points regarding access: 1. Accessing Raw Data: The getRaw() method returns the full array of data received from the OAuth provider [4]. You cannot access these values directly as public properties or through array-style access (e.g., $user['email_verified']) because the raw user data is stored in a protected property [4]. Example: $emailVerified = $user->getRaw['email_verified']?? null; 2. ArrayAccess and get: While Socialite's AbstractUser provides some abstraction, it does not function as a generic ArrayAccess object for raw response data [4]. If you are implementing a custom driver, you typically use the setRaw() method within your mapUserToObject() function to store the provider's response, which then populates the internal data available via getRaw() [5][6][7]. 3. Mapping Attributes: If you need a specific field like email_verified to be a first-class property of the user object, you must map it during the mapUserToObject phase of your custom provider implementation [5][6]. Example of mapping in a custom driver: return (new User)->setRaw($user)->map([ 'id' => $user['sub'], 'email' => $user['email'], 'email_verified' => $user['email_verified'], ]); If you are using a standard, built-in Socialite driver, the recommended way to retrieve non-standard fields remains using the getRaw() method to access the original provider response array [4].
Citations:
- 1: https://laravel.com/docs/13.x/socialite
- 2: https://laravel.com/docs/13.x/socialite.md
- 3: Add getRaw method to the User contract laravel/socialite#499
- 4: https://www.volcengine.com/article/883688
- 5: https://kirschbaumdevelopment.com/insights/implement-a-custom-driver-for-laravel-socialite
- 6: https://stackoverflow.com/questions/77053775/abstractprovider-error-when-creating-custom-driver-for-sso-via-socialite
- 7: https://github.com/laravel/socialite/blob/20f076bd86e7f6e8d85c2ac4728dc960dd87b0b2/src/Two/AbstractProvider.php
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# See whether the repository pins Socialite and whether vendor sources are present.
rg -n '"laravel/socialite"|Socialite' composer.json composer.lock -C 2 || true
# Read the controller around the relevant lines.
nl -ba app/Http/Controllers/Auth/OAuthController.php | sed -n '1,130p'Repository: pelican-dev/panel
Length of output: 6768
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check whether Socialite is a dependency and whether its source is vendored here.
python3 - <<'PY'
from pathlib import Path
for p in [Path('composer.json'), Path('composer.lock')]:
print(f'## {p}')
if p.exists():
text = p.read_text()
print('present')
if 'laravel/socialite' in text:
idx = text.index('laravel/socialite')
print(text[max(0, idx-200):idx+500])
else:
print('missing')
PY
# Search for any custom wrapper/transform that assigns email_verified onto the object.
rg -n "email_verified\s*=" app config routes -g '*.php' -C 2 || true
rg -n "->email_verified|email_verified" app/Http/Controllers/Auth/OAuthController.php app -g '*.php' -C 2 || trueRepository: pelican-dev/panel
Length of output: 2012
🌐 Web query:
Laravel Socialite v5.28 AbstractUser getRaw public property email_verified ArrayAccess source
💡 Result:
In Laravel Socialite, the AbstractUser class (from which the concrete Laravel\Socialite\Two\User and Laravel\Socialite\One\User classes inherit) includes a getRaw() method that returns the raw user data array provided by the OAuth provider [1][2]. Regarding your specific query: 1. getRaw() Method: The getRaw() method is available on the user instance returned by Socialite. It returns the entire array of user information received from the OAuth provider [1]. If the provider includes a field like email_verified in its raw response, you can access it via the array returned by getRaw() [3][4]. 2. ArrayAccess Interface: AbstractUser implements PHP's ArrayAccess interface [5]. This allows you to access properties on the user object as if it were an array (e.g., $user['email_verified'] if the implementation maps it there) [5]. However, note that getRaw() itself returns an array, so if you are accessing nested data from the raw response, you would typically do something like $user->getRaw()['email_verified'] [6][1]. 3. Public Properties vs. Methods: While AbstractUser maintains raw data, developers are generally encouraged to use the provided getter methods (like getEmail(), getId(), etc.) for standard profile fields [7]. If you need non-standard data (like email_verified which might not have a dedicated getter), accessing the raw data array via getRaw() is the standard approach [6][3]. If you are encountering issues with static analysis (like Larastan) when using getRaw(), it is often because the base Socialite\Contracts\User contract does not strictly define the getRaw() method [6]. If type-hinting is required, you can cast or type-hint against the concrete Laravel\Socialite\Two\User class instead of the contract [6].
Citations:
- 1: https://stackoverflow.com/questions/50375389/save-object-within-an-array-from-api-response
- 2: Additional Getter Methods For Retrieving User Information laravel/socialite#710
- 3: https://kirschbaumdevelopment.com/insights/implement-a-custom-driver-for-laravel-socialite
- 4: https://stackoverflow.com/questions/77053775/abstractprovider-error-when-creating-custom-driver-for-sso-via-socialite
- 5: valorin/socialite@b36860a
- 6: Add getRaw method to the User contract laravel/socialite#499
- 7: https://laravel.com/docs/13.x/socialite
🌐 Web query:
site:github.com laravel/socialite AbstractUser __get __isset email_verified
💡 Result:
In Laravel Socialite, the Laravel\Socialite\AbstractUser class acts as a base for user objects returned by OAuth providers [1]. It is designed to hold user data retrieved from the authentication provider, typically populated via a mapping process within individual provider classes [2][3]. Regarding your specific query on how get, isset, and email_verified interact: 1. Dynamic Access (get and isset): The AbstractUser class and the user objects extending it generally rely on dynamic property access to retrieve data that is stored in the underlying raw user array [2]. When you access a property like $user->email_verified or check isset($user->email_verified), PHP triggers the get or isset magic methods if those properties are not explicitly defined as public class properties [4]. These magic methods typically look up the requested key within the user's attributes or the raw response data provided by the OAuth provider [2]. 2. email_verified: This is not a standard getter method (like getEmail() or getName()) defined in the AbstractUser class [5]. Instead, email_verified is a data attribute often provided by OpenID Connect or specific OAuth providers (such as Google or LinkedIn) [2][6]. To access it, you typically retrieve it from the user object's attributes, either by accessing the property directly (if supported by the provider's mapping) or by accessing the raw data array [5][2]. For example, many developers access such attributes via the raw data returned by the provider [5]: $user = Socialite::driver('google')->user; $isVerified = $user->user['email_verified']?? null; While some providers explicitly map email_verified into their user objects [2][3], it is not a universally standardized method across all Socialite drivers, and you should check the raw user data returned by your specific provider to verify its availability [5].
Citations:
- 1: https://github.com/laravel/socialite/blob/5.x/src/One/User.php
- 2: https://github.com/laravel/socialite/blob/5.x/src/Two/GoogleProvider.php
- 3: laravel/socialite@c654af8
- 4: https://github.com/laravel/framework/blob/11.x/src/Illuminate/Auth/GenericUser.php
- 5: https://github.com/laravel/docs/blob/13.x/socialite.md
- 6: Add "email_verified" to linkedin openid laravel/socialite#725
🌐 Web query:
Laravel Socialite AbstractUser implements ArrayAccess __get __isset source code
💡 Result:
In Laravel Socialite, the AbstractUser class implements the ArrayAccess interface [1][2], which allows user objects to be accessed as arrays (e.g., $user['email']). However, the native Laravel Socialite AbstractUser implementation does not include magic methods such as get or isset for dynamic property access in the way classes like Illuminate\Support\Fluent do [3]. Key details regarding AbstractUser: 1. Interface Implementation: AbstractUser implements ArrayAccess [1][2]. This requires the implementation of the four methods defined by the interface: offsetExists, offsetGet, offsetSet, and offsetUnset. 2. Magic Methods: The core AbstractUser class provided by the Laravel Socialite package does not define get or isset [1]. While some developers might extend or wrap this class to include such functionality, it is not part of the standard library implementation. 3. Contrast with Fluent: The behavior you might be looking for—where attributes are accessible dynamically via property access—is characteristic of the Illuminate\Support\Fluent class [3]. Fluent explicitly implements both ArrayAccess and provides the get, set, isset, and unset magic methods to allow for fluent, dynamic attribute manipulation [3]. 4. Accessing Data: In standard Socialite usage, user data is typically accessed via defined getter methods (e.g., getEmail, getId) [4] or by accessing the raw array returned by the getRaw method [5][6]. If you are seeing get or isset in an implementation of AbstractUser, it is likely a custom modification or part of a third-party fork rather than the official Laravel Socialite package [7].
Citations:
- 1: valorin/socialite@b36860a
- 2: https://gitd3ti.vokasi.uns.ac.id/Meilinda/website-konseling-dan-kelas-online/-/blob/75cb97a9bf8d2d359112a0821c2c3605df12b5c6/vendor/laravel/socialite/src/AbstractUser.php
- 3: https://github.com/laravel/framework/blob/10.x/src/Illuminate/Support/Fluent.php
- 4: Additional Getter Methods For Retrieving User Information laravel/socialite#710
- 5: Add getRaw method to the User contract laravel/socialite#499
- 6: https://stackoverflow.com/questions/50375389/save-object-within-an-array-from-api-response
- 7: hypervel/socialite@8155921
🌐 Web query:
Laravel Socialite ArrayAccess offsetGet raw attributes user object email_verified
💡 Result:
In Laravel Socialite, the User object allows access to raw user data returned by the OAuth provider through the use of the ArrayAccess interface [1][2] or the getRaw method [3]. When you retrieve a user via Socialite, the returned user object (typically an instance of Laravel\Socialite\Two\User) contains a protected $user property holding the raw array of data from the provider [4]. Because the class implements ArrayAccess, you can retrieve fields from this raw data using array-style syntax [1][2]. For example, if you need to access a field such as email_verified, which is often included in the raw provider response but not mapped to a standardized Socialite getter method, you can use: $user = Socialite::driver('provider')->user; $emailVerified = $user->offsetGet('email_verified'); // Or using array access syntax: $emailVerified = $user['email_verified']; Additionally, you can use the getRaw method to retrieve the entire array of raw attributes: $raw = $user->getRaw; $emailVerified = $raw['email_verified']?? null; Note that the availability of specific fields like email_verified depends entirely on the OAuth provider you are using [5][6]. While some providers include this in their user profile endpoint response, others may not, or they may use a different key name [7][8]. Always inspect the raw data returned by your specific provider to ensure the key is present [3].
Citations:
- 1: https://stackoverflow.com/questions/32407604/laravel-socialite-first-and-last-name
- 2: https://stackoverflow.com/questions/77356223/laravel-socialite-user-creation-validation
- 3: Add getRaw method to the User contract laravel/socialite#499
- 4: https://github.com/laravel/socialite/blob/647ee1b1b7520e16c661b4df5cdff1d7f6902eee/src/Two/AbstractProvider.php
- 5: https://laravel.com/docs/13.x/socialite.md
- 6: https://laravel.com/docs/13.x/socialite
- 7: Add "email_verified" to linkedin openid laravel/socialite#725
- 8: Add "email_verified" to linkedin openid laravel/socialite#725
🌐 Web query:
"Laravel\Socialite\Two\AbstractUser" "__get" "__isset"
💡 Result:
The Laravel Socialite Laravel\Socialite\Two\AbstractUser class (and its parent Laravel\Socialite\AbstractUser) does not implement the magic get or isset methods [1]. In PHP, if a class does not define get or isset, accessing undefined public properties or checking them with isset() follows standard PHP behavior: accessing an undefined property will return null (and potentially trigger a notice, depending on configuration), and isset() will return false if the property is not defined as a public member of the class [1]. Historically, developers have encountered issues when using utility functions like object_get() or data_get() on Socialite user objects [1]. Because object_get() internally uses isset() to verify property existence before accessing it, it may fail to retrieve data that is not explicitly defined as a public property on the User object, even if that data exists within the user's raw attributes array [1]. To access such data, developers should use the getRaw() method provided by AbstractUser to retrieve the underlying attribute array directly [2][3].
Citations:
- 1: Test on PHP 8.2 laravel/socialite#607
- 2: Add getRaw method to the User contract laravel/socialite#499
- 3: https://stackoverflow.com/questions/50375389/save-object-within-an-array-from-api-response
Read email_verified from the raw Socialite payload. OAuthUser is the Socialite contract here, and isset($oauthUser->email_verified) won’t see provider-only fields unless a driver maps them onto the object first. Use getRaw()['email_verified'] or array access instead; otherwise this check stays inert and unverified emails can slip through.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Http/Controllers/Auth/OAuthController.php` around lines 80 - 83, The
email verification check in OAuthController::handleOAuthCallback is reading
email_verified from the Socialite OAuthUser object, but that field may not be
mapped onto the contract. Update the check to read email_verified from the raw
Socialite payload (for example via getRaw() or equivalent array access) and keep
the existing boolean validation and errorRedirect flow so provider-only data is
actually enforced.
There was a problem hiding this comment.
This is not true, oauth users provide all their attributes via __get.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## composer manifests"
for f in composer.json composer.lock; do
if [ -f "$f" ]; then
echo "### $f"
rg -n -C2 'laravel/socialite|socialiteproviders/' "$f" || true
fi
done
echo
echo "## OAuthController lines around the review"
nl -ba app/Http/Controllers/Auth/OAuthController.php | sed -n '70,90p'Length of output: 8339
🤖 web_search executed:
Laravel Socialite AbstractUser __get email_verified site:github.com/laravel/socialite
Length of output: 8367
🤖 web_search executed:
Laravel Socialite 5.x AbstractUser source __get ArrayAccess raw attributes
Length of output: 7229
🤖 web_search executed:
Laravel Socialite v5.28.0 AbstractUser __get __isset attributes source site:github.com/laravel/socialite
Length of output: 4324
🤖 web_search executed:
site:github.com/laravel/socialite "class AbstractUser" "__get(" "__isset(" "attributes"
Length of output: 4677
🏁 Script executed:
#!/bin/bash
set -euo pipefail
repo="laravel/socialite"
ref="v5.28.0"
echo "## AbstractUser.php"
gh api \
-H "Accept: application/vnd.github.raw+json" \
"repos/$repo/contents/src/AbstractUser.php?ref=$ref" | sed -n '1,240p'
echo
echo "## Contracts/User.php"
gh api \
-H "Accept: application/vnd.github.raw+json" \
"repos/$repo/contents/src/Contracts/User.php?ref=$ref" | sed -n '1,220p'
echo
echo "## Two/User.php"
gh api \
-H "Accept: application/vnd.github.raw+json" \
"repos/$repo/contents/src/Two/User.php?ref=$ref" | sed -n '1,220p'Length of output: 7761
@Boy132 You're right that direct property reads are supported here via __get() in Socialite v5.28.0, so my earlier wording was too broad.
The problem is more specifically the isset($oauthUser->email_verified) part: Laravel\Socialite\AbstractUser has __get(), but it does not implement __isset(). That means this guard can still evaluate as not set for provider-only attributes even when __get() would return a value.
So I don't think this one should be withdrawn, but the rationale should be narrowed to the isset(...) check rather than property access in general.
🧬 Code Graph Analysis Results
app/Http/Controllers/Auth/OAuthController.php
Lines 41-70 (callback: where the Socialite user is obtained and used)
public function callback(Request $request, string $driver): RedirectResponse
{
$driver = $this->oauthService->get($driver);
if (!$driver || !$driver->isEnabled()) {
return redirect()->route('auth.login');
}
// Check for errors (https://www.oauth.com/oauth2-servers/server-side-apps/possible-errors/)
if ($request->input('error')) {
report($request->input('error_description') ?? $request->input('error'));
return $this->errorRedirect($request->input('error'));
}
$oauthUser = Socialite::driver($driver->getId())->user();
if ($request->user()) {
$this->oauthService->linkUser($request->user(), $driver, $oauthUser);
return redirect(EditProfile::getUrl(['tab' => 'oauth::data::tab'], panel: 'app'));
}
$user = User::whereJsonContains('oauth->'. $driver->getId(), $oauthUser->getId())->first();
if ($user) {
return $this->loginUser($user);
}
return $this->handleMissingUser($driver, $oauthUser);
}Lines 72-112 (handleMissingUser: where OAuth user attributes/properties are read)
private function handleMissingUser(OAuthSchemaInterface $driver, OAuthUser $oauthUser): RedirectResponse
{
$email = $oauthUser->getEmail();
if (!$email) {
return $this->errorRedirect('No email was linked to your account on the OAuth provider.');
}
if (isset($oauthUser->email_verified) && !filter_var($oauthUser->email_verified, FILTER_VALIDATE_BOOLEAN)) {
return $this->errorRedirect('Email not verified on OAuth provider.');
}
$user = User::whereEmail($email)->first();
if ($user) {
if (!$driver->shouldLinkMissingUser($user, $oauthUser)) {
return $this->errorRedirect();
}
$user = $this->oauthService->linkUser($user, $driver, $oauthUser);
} else {
if (!$driver->shouldCreateMissingUser($oauthUser)) {
return $this->errorRedirect();
}
try {
$user = $this->userCreation->handle([
'username' => $oauthUser->getNickname(),
'email' => $email,
'oauth' => [
$driver->getId() => $oauthUser->getId(),
],
]);
} catch (Exception $exception) {
report($exception);
return $this->errorRedirect();
}
}
return $this->loginUser($user);
}app/Extensions/OAuth/OAuthSchemaInterface.php
Lines 13-13
public function getId(): string;Lines 37-37
public function isEnabled(): bool;Lines 39-39
public function shouldCreateMissingUser(OAuthUser $user): bool;Lines 41-41
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool;app/Extensions/OAuth/OAuthService.php
Lines 49-57 (linkUser: persists the provider id -> oauth user id mapping)
public function linkUser(User $user, OAuthSchemaInterface $schema, OAuthUser $oauthUser): User
{
$oauth = $user->oauth ?? [];
$oauth[$schema->getId()] = $oauthUser->getId();
$user->update(['oauth' => $oauth]);
return $user->refresh();
}
No description provided.