In Laravel Socialite, how do scopes(), setScopes() and with() differ, and why can replacing GitHub's default scope leave getEmail() returning null?
answer
- merge versus replace
- GitHub's default is user:email
- primary and verified address only
- with() replaces, never merges
- reserved keys: state, response_type
basics
~20 sscopes() 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 sEach 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
Recall that scopes() adds, setScopes() replaces, and with() passes extra provider parameters before redirect().
Explain the default scopes per driver, the GitHub email lookup tied to user:email, and how with() is merged over the standard fields.
Catch the regressions: a scope cleanup that nulls emails, a with() call that clobbers state, and code that trusts requested scopes instead of approvedScopes.
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