Skip to content

Check email_verified when handling missing user in oauth controller - #2434

Merged
Boy132 merged 1 commit into
mainfrom
boy132/oauth-check-email-verified
Jul 8, 2026
Merged

Check email_verified when handling missing user in oauth controller#2434
Boy132 merged 1 commit into
mainfrom
boy132/oauth-check-email-verified

Conversation

@Boy132

@Boy132 Boy132 commented Jul 7, 2026

Copy link
Copy Markdown
Member

No description provided.

@Boy132 Boy132 self-assigned this Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The OAuth callback flow in OAuthController was updated to read error parameters using input() instead of get(). Additionally, handleMissingUser now checks an email_verified attribute and redirects with an error if the OAuth provider indicates the email is unverified.

Changes

OAuth Controller Error Handling

Layer / File(s) Summary
Callback error detection
app/Http/Controllers/Auth/OAuthController.php
callback() now reads error and error_description via $request->input() instead of get(), redirecting through errorRedirect() when an error is present.
Email verification guard
app/Http/Controllers/Auth/OAuthController.php
handleMissingUser() adds a check on the email_verified attribute, redirecting with "Email not verified on OAuth provider." if it is present but not truthy, before proceeding with user lookup/creation.

Compact Metadata

  • Files changed: 1
  • Lines changed: +7/-3
  • Estimated review effort: Medium
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive No pull request description was provided, so there is no meaningful description to evaluate. Add a brief description of the OAuth email_verified handling change and any related callback behavior.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change to check email_verified in the OAuth missing-user flow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 614153a and ed499e7.

📒 Files selected for processing (1)
  • app/Http/Controllers/Auth/OAuthController.php

Comment on lines +80 to +83
if (isset($oauthUser->email_verified) && !filter_var($oauthUser->email_verified, FILTER_VALIDATE_BOOLEAN)) {
return $this->errorRedirect('Email not verified on OAuth provider.');
}

@coderabbitai coderabbitai Bot Jul 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 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 app

Repository: 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 || true

Repository: 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 || true

Repository: 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:


🏁 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 || true

Repository: 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:


🌐 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:


🌐 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:


🌐 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:


🌐 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:


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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is not true, oauth users provide all their attributes via __get.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 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();
}

@Boy132
Boy132 merged commit b8bbf29 into main Jul 8, 2026
16 checks passed
@Boy132
Boy132 deleted the boy132/oauth-check-email-verified branch July 8, 2026 11:13
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants