External OAuth
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.
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
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 metadataAddCompleter<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):
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /login/github | Challenge; rate-limited; returnUrl | |
GET | /login/github/callback | Provision + completer; errors redirect to ErrorPath | |
GET | /login/google | Challenge; rate-limited | |
GET | /login/google/callback | Provision + completer | |
GET | /logins | yes | List linked external logins |
DELETE | /logins/{loginProvider}/{providerKey} | yes | Unlink; antiforgery + ReAuth; refuses the last sign-in method |
GET | /link/{scheme} | yes | Start link challenge |
GET | /link/{scheme}/callback | yes | Complete link |
IdP callback paths
| Provider | Middleware path |
|---|---|
| GitHub | /signin-github |
/signin-google |
Security behavior
- Verified email (default): new accounts require a verified provider email. Google reads
email_verifiedfrom the userinfo payload after the host configures the handler, so a host stamp cannot stick. GitHub callsGET https://api.github.com/user/emailswith 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 (SaveTokensis false). - Auto-link by email (default off): when
AutoLinkByEmailis true, the provider email must be verified and the local account'sEmailConfirmedmust be true, even ifRequireVerifiedEmailis false. Otherwise the callback denies withauto_link_disabled,email_unverified, oremail_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 whenRequireVerifiedEmailis true. The callback scheme must match the route. - Unlink: requires an authenticated application user, the core antiforgery filter, and a ReAuth principal (
AuthEndpoints.ReAuthorAuthEndpoints.ReAuthBearerwith claimReauth=true). ReAuth is checked without replacingHttpContext.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 islast_signin_method. Passkeys are counted when the Identity store schema is version 3 (AddAuthEndpointssets 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/dashboardstay local on Linux.~/, protocol-relative//, backslash, control characters, and encoded CR/LF/NUL/tab are rejected. Absolute URLs must matchAllowedReturnUrlOriginsby scheme, host, and port (same rule as email-confirmation redirects). Illegal values fall back toDefaultReturnUrl.- Errors / cancel: browser redirects to
ErrorPath?error=&error_description=; clients that preferapplication/jsonovertext/htmlget 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
| Property | Default | Notes |
|---|---|---|
SignInScheme | null | Cookie completer scheme override |
IsPersistent | true | Application cookie persistence |
DefaultReturnUrl | / | Rooted local path |
RequireVerifiedEmail | true | Block unverified provider emails on create and explicit link |
AutoLinkByEmail | false | Opt-in link of an existing confirmed user by verified provider email |
AllowedReturnUrlOrigins | empty | Absolute http(s) origins allowlist |
ErrorPath | /auth/external/error | Relative path for error redirects |
Custom providers
- Register an OAuth handler with
SignInScheme = IdentityConstants.ExternalScheme. - Implement
IExternalAuthProvider(scheme + login/callback paths + endpoint name). builder.AddProvider<MyProvider>()thenMapExternalAuthProvider<TUser>("MyScheme")orMapExternalAuthEndpoints.
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:
| Variable | Provider |
|---|---|
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET | GitHub |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
Gotchas
- Pipeline: authentication +
UseRateLimiter()(viaUseAuthEndpoints). - Host an error page at
ErrorPath(or change the option). - Install
AuthEndpoints.OAuth.GitHuborAuthEndpoints.OAuth.Google(or both). The core package alone does not register those handlers. - Cookie completion skips two-factor. See Security behavior.