Changelog
Modules

External OAuth

Separate preview package for GitHub and Google OAuth login with Identity cookie or JWT completion.

External OAuth (GitHub, Google) ships as separate NuGet packages. The core package AuthEndpoints.External.OAuth has no GitHub or Google handler dependency. Install AuthEndpoints.OAuth.GitHub and/or AuthEndpoints.OAuth.Google; each depends on the core package. It is compose-only (not wired into MapAuthEndpoints). Default completion issues an Identity application cookie; replace the completer for JWT. Passkeys use the same completer idea via IPasskeySignInCompleter in the core package — see Passkeys.

Preview. Independent versioning from the core AuthEndpoints package — see the changelog. GitHub and Google are their own packages at the same preview version.

Install

dotnet add package AuthEndpoints
dotnet add package AuthEndpoints.OAuth.GitHub --prerelease
dotnet add package AuthEndpoints.OAuth.Google --prerelease

nuget

Use --prerelease for the OAuth preview package. Requires ASP.NET Core Identity (UserManager / SignInManager) already configured, typically via AddAuthEndpoints. It does not call Identity management HTTP APIs; those remain optional alongside External.

Hosts must call UseRateLimiter() (included in UseAuthEndpoints). AddExternalAuthEndpoints registers the login rate-limit policy.

DI

using AuthEndpoints.External.OAuth;
using AuthEndpoints.OAuth.GitHub;
using AuthEndpoints.OAuth.Google;

builder.Services.AddExternalAuthEndpoints<AppUser>(o =>
{
    o.RequireVerifiedEmail = true;   // default
    o.AutoLinkByEmail = false;       // default; opt in requires verified provider email and confirmed local email
    o.DefaultReturnUrl = "/";        // rooted local path
    o.ErrorPath = "/auth/external/error";
    // o.AllowedReturnUrlOrigins.Add("https://app.example.com"); // optional absolute allowlist
})
.AddGitHub(o =>
{
    o.ClientId = builder.Configuration["Authentication:GitHub:ClientId"]!;
    o.ClientSecret = builder.Configuration["Authentication:GitHub:ClientSecret"]!;
})
.AddGoogle(o =>
{
    o.ClientId = builder.Configuration["Authentication:Google:ClientId"]!;
    o.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]!;
});

ClientId/Secret are validated on startup. Missing values fail fast.

AddExternalAuthBuilder methods:

  • AddGitHub / AddGoogle — OAuth handlers (SignInScheme = Identity.External) + provider metadata
  • AddCompleter<T> — replace cookie completion (e.g. JwtExternalLoginCompleter<AppUser>)
  • AddProvider / AddProvider<T> — custom providers (see Custom providers)

JWT completion

Requires JWT services registered (AddJwtEndpoints):

builder.Services.AddExternalAuthEndpoints<AppUser>()
    .AddCompleter<JwtExternalLoginCompleter<AppUser>>()
    .AddGitHub(...);

After OAuth, a refresh cookie is written and the browser redirects to returnUrl. The client should obtain an access token via the JWT refresh flow (CSRF + refresh cookie) — the access token is not placed in the redirect URL.

Map

var external = app.MapGroup("/auth/external").WithTags("External");
external.MapGitHubAuthEndpoints<AppUser>();
external.MapGoogleAuthEndpoints<AppUser>();
external.MapExternalAccountEndpoints<AppUser>(); // link / unlink while signed in

Or map every registered sign-in provider:

app.MapGroup("/auth/external").MapExternalAuthEndpoints<AppUser>();

Routes

Relative to the group prefix (example /auth/external):

MethodPathAuthNotes
GET/login/githubChallenge; rate-limited; returnUrl
GET/login/github/callbackProvision + completer; errors redirect to ErrorPath
GET/login/googleChallenge; rate-limited
GET/login/google/callbackProvision + completer
GET/loginsyesList linked external logins
DELETE/logins/{loginProvider}/{providerKey}yesUnlink; antiforgery + ReAuth; refuses the last sign-in method
GET/link/{scheme}yesStart link challenge
GET/link/{scheme}/callbackyesComplete link

IdP callback paths

ProviderMiddleware path
GitHub/signin-github
Google/signin-google

Security behavior

  • Verified email (default): new accounts require a verified provider email. Google reads email_verified from the userinfo payload after the host configures the handler, so a host stamp cannot stick. GitHub calls GET https://api.github.com/user/emails with the access token and keeps the verified primary address, or any verified address when the primary is unverified. An unverified profile email is dropped. The access token is not saved on the ticket (SaveTokens is false).
  • Auto-link by email (default off): when AutoLinkByEmail is true, the provider email must be verified and the local account's EmailConfirmed must be true, even if RequireVerifiedEmail is false. Otherwise the callback denies with auto_link_disabled, email_unverified, or email_unconfirmed.
  • EmailConfirmed on new users matches whether the provider email was verified.
  • Link: the challenge stores the signed-in user id as the external-login XSRF token (ConfigureExternalAuthenticationProperties). The callback accepts the external login only when that token matches. Explicit link also requires a verified email when RequireVerifiedEmail is true. The callback scheme must match the route.
  • Unlink: requires an authenticated application user, the core antiforgery filter, and a ReAuth principal (AuthEndpoints.ReAuth or AuthEndpoints.ReAuthBearer with claim Reauth=true). ReAuth is checked without replacing HttpContext.User, so the antiforgery token still matches the application user. Removal is refused when it would delete the last sign-in method (a password, a passkey, or another external login must remain). A missing login returns 404. The problem title is last_signin_method. Passkeys are counted when the Identity store schema is version 3 (AddAuthEndpoints sets this).
  • External cookie is signed out after success and on login/link failure, including remote authentication failure.
  • returnUrl: rooted local paths are accepted before any absolute-URI parse, so / and /dashboard stay local on Linux. ~/, protocol-relative //, backslash, control characters, and encoded CR/LF/NUL/tab are rejected. Absolute URLs must match AllowedReturnUrlOrigins by scheme, host, and port (same rule as email-confirmation redirects). Illegal values fall back to DefaultReturnUrl.
  • Errors / cancel: browser redirects to ErrorPath?error=&error_description=; clients that prefer application/json over text/html get Problem details instead.
  • Cookie completion calls SignInAsync, the same path as Identity UI. It does not issue a two-factor challenge. Hosts that need 2FA after OAuth should use a completer that does.

Options

PropertyDefaultNotes
SignInSchemenullCookie completer scheme override
IsPersistenttrueApplication cookie persistence
DefaultReturnUrl/Rooted local path
RequireVerifiedEmailtrueBlock unverified provider emails on create and explicit link
AutoLinkByEmailfalseOpt-in link of an existing confirmed user by verified provider email
AllowedReturnUrlOriginsemptyAbsolute http(s) origins allowlist
ErrorPath/auth/external/errorRelative path for error redirects

Custom providers

  1. Register an OAuth handler with SignInScheme = IdentityConstants.ExternalScheme.
  2. Implement IExternalAuthProvider (scheme + login/callback paths + endpoint name).
  3. builder.AddProvider<MyProvider>() then MapExternalAuthProvider<TUser>("MyScheme") or MapExternalAuthEndpoints.

Built-in GitHub and Google providers set email and email_verified themselves. A custom provider must put a real verified email on the principal when RequireVerifiedEmail is true. A host event that stamps email_verified before the provider seal does not count.

Configuration

OAuth ClientId / ClientSecret come from host configuration (environment variables, user secrets, or a configuration provider). They are not ExternalAuthOptions properties. Typical environment variable names:

VariableProvider
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRETGitHub
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETGoogle

Gotchas

  • Pipeline: authentication + UseRateLimiter() (via UseAuthEndpoints).
  • Host an error page at ErrorPath (or change the option).
  • Install AuthEndpoints.OAuth.GitHub or AuthEndpoints.OAuth.Google (or both). The core package alone does not register those handlers.
  • Cookie completion skips two-factor. See Security behavior.