> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dezerx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth Providers

> Let people sign in with an external account, or link one to an account they already have. Discord, Google and GitHub all work this way.

This is the smallest module type.

<Note>
  Read [Fundamentals](/extensions/tutorials/fundamentals) first if you haven't.
</Note>

## Generate it

```bash theme={null}
php artisan make:module Acme --type=provider
php artisan module:enable AcmeProvider
php artisan module:migrate AcmeProvider
php artisan module:doctor AcmeProvider
```

<Warning>
  You also need a logo, or the login button will not render:

  ```text theme={null}
  public/modules-logos/acme-provider.png
  ```
</Warning>

<Accordion title="What you get" icon="folder-tree">
  ```text theme={null}
  Modules/Providers/AcmeProvider/
  ├── module.json                 link_url, link_column, colors
  ├── app/
  │   ├── Providers/
  │   ├── Http/Controllers/
  │   │   ├── AcmeController      redirect, callback, unlink
  │   │   └── SettingsController
  │   └── Models/AcmeCredential
  ├── database/migrations/        credentials table, and users.acme_id
  └── routes/
  ```
</Accordion>

## Three manifest keys do all the work

```json module.json theme={null}
"link_url": "/auth/acme/redirect",
"link_column": "acme_id",
"logo": "/modules-logos/acme-provider.png",
"colors": { "bg": "#FF6B35", "text": "#FFFFFF", "hover": "#E55A2B" }
```

<ParamField path="link_url" type="string" required>
  Where the button sends people. Must match your actual route exactly.
</ParamField>

<ParamField path="link_column" type="string" required>
  The `users` column holding the external account ID. Also how "is this linked?" gets answered.
</ParamField>

<ParamField path="logo" type="string" required>
  Path under `public/`.
</ParamField>

<ParamField path="colors" type="object">
  Button branding: `bg`, `text`, `hover`. Falls back to a built-in table by provider name.
</ParamField>

<Warning>
  A provider missing any of the first three gets skipped in silence, so the button just never shows up.
  `module:doctor` reports it as an error.
</Warning>

`OAuthProviderRegistry` reads them, and the login page, market checkout and profile page all get their
provider list from it.

## Install a Socialite driver

Most providers are on [socialiteproviders.com](https://socialiteproviders.com). Google, GitHub,
Facebook, X, LinkedIn, GitLab and Bitbucket ship with `laravel/socialite` already.

Then uncomment two lines in your service provider:

```php theme={null}
class AcmeProviderServiceProvider extends OAuthProviderServiceProvider
{
    protected ?string $socialiteDriver = 'acme';
    protected ?string $socialiteProvider = \SocialiteProviders\Acme\Provider::class;
}
```

<Note>For a driver built into `laravel/socialite`, leave both out.</Note>

## Credentials come from the database

Admins configure OAuth apps in the UI, not in `.env`, so you pass config per request:

```php theme={null}
Socialite::driver('acme')->setConfig(new Config(
    $credential->client_id,
    $credential->client_secret,
    route('acme.callback'),
));
```

The generated controller has a `driver()` helper that does this and returns `null` when the module
isn't configured yet.

## The callback has three cases

The generated controller handles all three. Worth knowing what each one is.

<Tabs>
  <Tab title="Already signed in">
    Link the account, but refuse if that external ID belongs to someone else:

    ```php theme={null}
    $taken = User::where('acme_id', $oauthUser->getId())
        ->where('id', '!=', $user->id)
        ->exists();
    ```

    <Warning>
      Skip that check and one external account can be attached to several DezerX users, which is an
      account-takeover path.
    </Warning>
  </Tab>

  <Tab title="Known external ID">
    Sign them in. Nothing else to do.
  </Tab>

  <Tab title="Brand new">
    Hand it to core:

    ```php theme={null}
    $user = $this->registration->registerViaOAuth(
        $oauthUser,
        'acme',        // notification channel
        'Acme',        // shown to the user
        'acme_id',     // the users column
        $oauthUser->getAvatar(),
    );
    ```
  </Tab>
</Tabs>

## The one rule you shouldn't relax

`registerViaOAuth()` resolves in this order:

<Steps>
  <Step title="Match the external ID">
    Found, so refresh the avatar and sign them in.
  </Step>

  <Step title="Match the email, but only if that account already has this provider's ID">
    Same result.
  </Step>

  <Step title="No match, so create the user">
    Fires `Registered` and sends the welcome notification.
  </Step>
</Steps>

If the email matches an account that was never linked to this provider, it throws:

> An account with this email already exists but is not linked to Acme. Please log in with your
> password first, then link your Acme account from your profile settings.

<Warning>
  That looks unhelpful and it's deliberate. Auto-linking on email alone means anyone who can set that
  address at the provider takes over the DezerX account, and not every provider verifies email
  addresses.

  Discord and GitHub always refused. Google used to auto-link, and no longer does.
</Warning>

Your catch block should surface that message rather than a generic failure:

```php theme={null}
if (str_contains($e->getMessage(), 'already exists but is not linked to Acme')) {
    return redirect('/login')->with('error', $e->getMessage());
}
```

<Note>
  The generator writes this for you. The exact phrasing is what the check matches on, so don't reword
  it.
</Note>

## Unlinking

Provide the route. The generated controller already refuses to unlink someone's only way back in:

```php theme={null}
if (empty($user->password)) {
    return back()->with('error', 'Set a password before unlinking your only sign-in method.');
}
```

<Tip>
  Fire an event if other modules should react. `DiscordProvider` removes Discord roles on unlink.
</Tip>

## Test it

```bash theme={null}
php artisan module:doctor AcmeProvider
```

<Check>Fresh sign-up creates the account and sends exactly one welcome email</Check>
<Check>Sign out, sign back in: same account, not a duplicate</Check>
<Check>A password account can link from the profile page and shows as linked</Check>
<Check>Linking the same external account to a second user is rejected</Check>
<Check>Unlink clears the column and the button says "Link" again</Check>
<Check>Disabling the module removes the button from login and profile</Check>
<Check>An email matching an unlinked account gives the actionable message, not "Authentication failed"</Check>

## Worth reading

<CardGroup cols={2}>
  <Card title="GoogleProvider" icon="google">
    The minimal version.
  </Card>

  <Card title="GithubProvider" icon="github">
    Same shape, different driver.
  </Card>

  <Card title="DiscordProvider" icon="discord">
    What this can grow into: role sync and 17 listeners sending DMs.
  </Card>
</CardGroup>

<Tip>
  If your provider has a notification channel or a roles concept, `DiscordProvider` is the pattern to
  copy. It's just core events, covered in [Fundamentals](/extensions/tutorials/fundamentals#events).
</Tip>
