# Adding an OAuth provider

> Wire up an external OAuth provider (Google, GitHub, ...) alongside the cookie sessions, end to end.

## How OAuth fits in

Slicekit authenticates with cookie sessions (see [Authentication](/docs/authentication)). OAuth sits beside
that: a provider proves who the user is, and Slicekit either signs in an existing identity or links the external
account to the current one. Every provider is an `IOAuthProvider` implementation registered in DI. The generic
endpoints under `Slicekit.Api/Endpoints/v1/Auth/` (`OAuthStartEndpoint`, `OAuthCallbackEndpoint`,
`LinkOAuthStartEndpoint`, `LinkOAuthCallbackEndpoint`) drive every provider, so adding one never means writing a
new endpoint.

Adding a provider is four touch-points: a settings block, a provider class, one DI line, and a frontend entry.
This guide adds **Discord** as the worked example.

## 1. Add provider settings

In `api/src/Slicekit.Core/Settings/OAuthSettings.cs`, add a property for the new provider next to `Google` and
`GitHub`:

```csharp
public OAuthProviderSettings Discord { get; set; } = new();
```

Each provider reuses the shared `OAuthProviderSettings` record (`Enabled`, `ClientId`, `ClientSecret`,
`RedirectUri`), whose `IsConfigured` check guards the three credentials. Add a matching rule to `Validate()` so a
misconfigured provider fails at boot rather than at first login:

```csharp
if (Discord.Enabled && !Discord.IsConfigured)
    yield return new ValidationResult(
        "OAuth:Discord requires ClientId, ClientSecret, and RedirectUri when Enabled",
        [nameof(Discord)]);
```

Add the section to `api/src/Slicekit.Api/appsettings.json` with placeholder values. The dev `ClientSecret` stays
empty: real secrets come from env vars, never from the committed file.

```json
"Discord": {
  "Enabled": false,
  "ClientId": "",
  "ClientSecret": "",
  "RedirectUri": "https://localhost:5077/api/v1/auth/discord/callback"
}
```

List the production env vars in `.env.prod.example`. The double-underscore maps onto the `OAuth:Discord:*` config
keys:

```sh
OAuth__Discord__Enabled=true
OAuth__Discord__ClientId=
OAuth__Discord__ClientSecret=
OAuth__Discord__RedirectUri=
```

## 2. Implement `IOAuthProvider`

Create `api/src/Slicekit.Api/Auth/OAuth/DiscordOAuthProvider.cs`. The interface (in `IOAuthProvider.cs`) is small:
build the authorization URL, exchange the code for tokens, and read back user info. `GitHubOAuthProvider` is the
closest template for a non-OIDC provider.

```csharp
public sealed class DiscordOAuthProvider(
    IHttpClientFactory httpClientFactory,
    IOptions oauth) : IOAuthProvider
{
    public string Provider => "Discord";

    public (string url, string codeVerifier) BuildAuthorizationUrl(string state)
    {
        var cfg = oauth.Value.Discord;
        var (verifier, challenge) = PkceHelper.Generate();

        var url = $"https://discord.com/oauth2/authorize?response_type=code" +
                  $"&client_id={Uri.EscapeDataString(cfg.ClientId)}" +
                  $"&redirect_uri={Uri.EscapeDataString(cfg.RedirectUri)}" +
                  $"&scope={Uri.EscapeDataString("identify email")}" +
                  $"&state={Uri.EscapeDataString(state)}" +
                  $"&code_challenge={challenge}" +
                  $"&code_challenge_method=S256";

        return (url, verifier);
    }

    public async Task<OAuthTokens?> ExchangeCodeAsync(string code, string codeVerifier, CancellationToken ct)
    {
        // POST to the provider token endpoint; return OAuthTokens, or null on failure.
    }

    public async Task<OAuthUserInfo?> GetUserInfoAsync(string accessToken, CancellationToken ct)
    {
        // GET userinfo; return OAuthUserInfo(externalId, email), or null.
        // externalId must be the provider's stable user ID, not the email.
    }
}
```

The records the methods hand back live alongside the interface:

```csharp
public sealed record OAuthTokens(string AccessToken, string? RefreshToken, string? IdToken = null);
public sealed record OAuthUserInfo(string ExternalId, string Email);
```

Three invariants matter:

- **`Provider` is the storage key.** The string is persisted in the `OAuthProviderLinks` table and is the lookup
  key in `OAuthProviderFactory` (it lower-cases on read, so casing is cosmetic but the value is not). Once used in
  production, never rename it, or existing links break. Capitalise it as a proper noun (`"GitHub"`, not `"github"`).
- **`ExternalId` must be stable.** Use the provider's opaque or numeric user ID, never the email address, which a
  user can change.
- **OIDC providers validate the ID token.** Override the default `ValidateIdTokenAsync` and check the ID token
  against the provider's JWKS using `OidcIdTokenValidator`. See `GoogleOAuthProvider` for that pattern. Non-OIDC
  providers like GitHub and Discord leave the default no-op in place.

`PkceHelper.Generate()` returns a `(verifier, challenge)` pair. Use the challenge in the URL when the provider
supports PKCE; if it does not (GitHub, for example), still generate the verifier for interface consistency and
ignore the challenge.

## 3. Register in DI

In `api/src/Slicekit.Api/Configuration/Auth.cs`, inside the `if (oauthSettings.Enabled)` block, add one line beside
the existing Google and GitHub registrations:

```csharp
if (oauthSettings.Discord.Enabled)
    builder.Services.AddScoped<IOAuthProvider, DiscordOAuthProvider>();
```

`OAuthProviderFactory` is registered right after and discovers every `IOAuthProvider` from the container, so no
further wiring is needed. A provider that is configured but not `Enabled` is simply never registered.

## 4. Update the frontend

Add the provider to the union type and the static list in `frontend/src/features/oauth/types.ts`:

```ts

  { id: 'google' as OAuthProviderName, label: 'Google' },
  { id: 'github' as OAuthProviderName, label: 'GitHub' },
  { id: 'discord' as OAuthProviderName, label: 'Discord' },
];
```

Give it an icon in `frontend/src/features/oauth/components/OAuthButtons.tsx`:

```ts

const ICONS: Record<OAuthProviderName, IconType> = {
  google: FcGoogle,
  github: FaGithub,
  discord: FaDiscord,
};
```

The button components filter to the providers returned by `GET /api/v1/auth/features`, so the Discord button
appears only when `OAuth:Discord:Enabled` is true. No conditional rendering beyond what is already there.

## 5. Whitelist the redirect URI

In the provider's own OAuth app console, register the exact redirect URI from your settings. For local dev that
is:

```sh
https://localhost:5077/api/v1/auth/discord/callback
```

The generic `OAuthCallbackEndpoint` handles the return leg for every provider, so there is no new endpoint file.

## Verify

- `dotnet build api/slicekit.slnx` passes.
- With `OAuth:Enabled` and `OAuth:Discord:Enabled` true and the three credentials set,
  `dotnet run --project api/src/Slicekit.Api` starts cleanly. ValidateOnStart rejects a half-configured provider at
  boot.
- Log in via the new provider and confirm the account appears under Settings, Security, Linked accounts.
- Repeat from an existing session to confirm account-linking attaches the provider to the current identity.

See [Getting started](/docs/getting-started) for bringing the API and frontend up together, and
[Authentication](/docs/authentication) for how the resulting session cookie behaves.

## Checklist

- [ ] `OAuthProviderSettings` property added to `OAuthSettings` with a matching `Validate()` rule.
- [ ] `appsettings.json` placeholder block and `.env.prod.example` env vars added.
- [ ] `IOAuthProvider` implementation created, with a stable `Provider` name and `ExternalId`.
- [ ] OIDC providers override `ValidateIdTokenAsync`; non-OIDC leave the default.
- [ ] DI line added inside the `oauthSettings.Enabled` block in `Auth.cs`.
- [ ] Frontend `types.ts` union, list, and `OAuthButtons.tsx` icon updated.
- [ ] Redirect URI whitelisted in the provider's OAuth app console.
- [ ] Build passes; login and account-linking both verified.
