skip to content

In Laravel Socialite, how do scopes(), setScopes() and with() differ, and why can replacing GitHub's default scope leave getEmail() returning null?

level: middleimportance: should knowfreq 30%

answer

  1. merge versus replace
  2. GitHub's default is user:email
  3. primary and verified address only
  4. with() replaces, never merges
  5. reserved keys: state, response_type

basics

~20 s

scopes() adds to the driver's default scopes, setScopes() replaces them, and with() sets extra query parameters. The GitHub driver fetches the primary verified email only when user:email is requested, so setScopes() without it can leave getEmail() null.

solid answer

~50 s

Each Socialite driver starts with default scopes: `user:email` for GitHub, `openid`, `profile` and `email` for Google, plus any `scopes` array in its `config/services.php` entry. `scopes([...])` **merges** new scopes into that list and removes duplicates; `setScopes([...])` **replaces** the list. The GitHub driver calls the `/user/emails` endpoint and returns the primary, verified address only when `user:email` is in the list; after `setScopes(['read:user'])` it falls back to the profile's public email, which is often null, and if only the redirect dropped the scope, the lookup fails on the callback and also yields null. `with([...])` sets extra parameters such as Google's `hd`; each call replaces the previous array, and the values are merged over the standard fields, so passing `state` or `response_type` overrides them. The docs warn against that. After login, `approvedScopes` on the user shows what the provider actually granted.

go deeper

for a junior

Recall that scopes() adds, setScopes() replaces, and with() passes extra provider parameters before redirect().

for a middle

Explain the default scopes per driver, the GitHub email lookup tied to user:email, and how with() is merged over the standard fields.

for a senior

Catch the regressions: a scope cleanup that nulls emails, a with() call that clobbers state, and code that trusts requested scopes instead of approvedScopes.

for a principal

Keep requested permissions minimal and justified per feature, since every extra scope widens consent screens and stored-token risk.

## Default scopes per driver A **scope** is a permission the OAuth provider asks the member to grant. Each Socialite driver ships a default list, and `SocialiteManager` adds any `scopes` array from the driver's `config/services.php` entry when it builds the provider: | Driver | Default scopes | Separator sent to the provider | |---|---|---| | `github` | `user:email` | comma | | `google` | `openid`, `profile`, `email` | space | The separator is handled by the driver, so you always pass PHP arrays. ## `scopes()` versus `setScopes()` - `scopes(['read:org'])` **adds** to the current list: the result is the old list plus the new entries, de-duplicated. - `setScopes(['read:org'])` **replaces** the whole list with exactly what you pass. Both return the provider, so they chain before `redirect()`: ```php return Socialite::driver('github') ->scopes(['read:org']) ->redirect(); // requests user:email,read:org return Socialite::driver('github') ->setScopes(['read:user']) ->redirect(); // requests read:user only ``` ## Why `getEmail()` can turn null The GitHub driver's profile lookup works in two steps: 1. It calls GitHub's `/user` endpoint and reads the profile, whose `email` field is the member's **public** email and is often empty. 2. **Only if `user:email` is in the scope list**, it also calls `/user/emails` and replaces `email` with the address that is both **primary** and **verified**. If that call fails, for example because the token was granted without `user:email`, or no such address exists, `email` is null. So on a developer-community site, a developer who "tidies up" the request to `setScopes(['read:user'])` silently changes what `getEmail()` returns: many members now arrive with null emails, and the ones that do come through are whatever they chose to publish, not necessarily a verified address. Use `scopes()` to add permissions and keep `user:email`. ## `with()`: extra parameters `with(array $parameters)` adds provider-specific parameters to the authorization request, for example Google's hosted-domain hint: ```php return Socialite::driver('google') ->with(['hd' => 'example.com']) ->redirect(); ``` Three details matter: - **It replaces.** A second `with()` call discards the first array. - **It wins over the standard fields.** Socialite builds `client_id`, `redirect_uri`, `scope`, `response_type`, `state` and the PKCE fields, then `array_merge`s your parameters over them. Passing `state` or `response_type` overwrites Socialite's own values, which is why the docs warn against reserved keywords. - **It is also sent on the token request.** The same parameters are merged into the code-for-token POST. ## Scopes and the cached driver `Socialite::driver('github')` is resolved through Laravel's manager pattern, which caches one provider instance per driver name for the life of the process. `scopes()`, `setScopes()` and `with()` change that instance, so any later `Socialite::driver('github')` call in the same process sees the changed list. Build the chain right before `redirect()` in the route that needs it, and keep permanent extra scopes in `config/services.php` rather than calling `scopes()` from shared code. The callback matters too: the GitHub email lookup runs in `user()`, on the callback's own provider instance, and checks that instance's list. Scopes set only on the redirect are not remembered there. ## Requesting more later A common pattern on a developer-community site is **incremental consent**: sign members in with the defaults, and only when someone opens "import my repositories" send them through the redirect again with `scopes(['repo'])` or a narrower permission. The member sees one extra consent screen at the moment the permission makes sense, and members who never use the feature never grant it. ## Checking what was granted The user returned by `user()` carries `approvedScopes`, split from the provider's `scope` response field. A member may decline an optional permission, so code that later calls a provider API should check `approvedScopes` rather than assume the requested list. ## Summary - `scopes()` merges, `setScopes()` replaces, `with()` adds non-scope parameters and replaces its own previous array. - On GitHub, keep `user:email` if you rely on `getEmail()`. - Never pass `state`, `response_type` or `redirect_uri` through `with()`.

  • Where can you set extra Socialite scopes once for every login instead of on each redirect?
    Add a `scopes` array to the driver's entry in `config/services.php`. `SocialiteManager::buildProvider()` passes it to `scopes()` when the provider is built, so it is merged with the driver's defaults on every request.
  • Why might approvedScopes differ from what you requested with scopes()?
    The member, or the provider's policy, can grant less than was asked for. Socialite fills `approvedScopes` from the `scope` field of the token response, so it reflects what was actually granted; code that calls a provider API later should check it before relying on a permission.

saying these in an interview costs you the question

  • setScopes() adds to the default scopes just like scopes()
  • GitHub always returns a verified email whatever scopes you request
  • Calling with() twice merges both parameter arrays
  • with() is a safe place to override the state parameter
  • Google scopes must be joined with commas by the caller