Polhem.OAuth2.AspNet 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Polhem.OAuth2.AspNet --version 1.0.0
                    
NuGet\Install-Package Polhem.OAuth2.AspNet -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Polhem.OAuth2.AspNet" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Polhem.OAuth2.AspNet" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Polhem.OAuth2.AspNet" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Polhem.OAuth2.AspNet --version 1.0.0
                    
#r "nuget: Polhem.OAuth2.AspNet, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Polhem.OAuth2.AspNet@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Polhem.OAuth2.AspNet&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Polhem.OAuth2.AspNet&version=1.0.0
                    
Install as a Cake Tool

Polhem.OAuth2

English | 繁體中文

Build CI

Lightweight OAuth2 sign-in for .NET. Desktop and console applications sign in through the system browser with a loopback redirect and PKCE, on Windows, macOS and Linux. ASP.NET Core and ASP.NET (System.Web) applications use the authorization code flow with PKCE, and keep each sign-in in a protected cookie.

Supported providers: Google, Facebook, LINE, Microsoft Entra ID, Auth0 and Okta.

Packages

Package Target frameworks Use it for
Polhem.OAuth2 netstandard2.0, net10.0 The providers, sign-in from desktop and console applications, and other server frameworks
Polhem.OAuth2.AspNetCore net10.0 ASP.NET Core applications
Polhem.OAuth2.AspNet net472 ASP.NET Web Forms and MVC applications on System.Web
dotnet add package Polhem.OAuth2

Options

Each provider has its own options type: GoogleOAuth2Options, FacebookOAuth2Options, LineOAuth2Options, AzureOAuth2Options (Microsoft Entra ID), Auth0OAuth2Options and OktaOAuth2Options.

  • ClientId and RedirectUri are required. A client copies and checks the options when it is created, so later changes to them have no effect, and invalid options throw ArgumentException right away.
  • Auth0 and Okta need Domain, such as your-tenant.auth0.com. Okta uses the default authorization server unless AuthorizationServerId names another one; an empty value selects the org authorization server.
  • Microsoft Entra ID uses the common tenant. An application registered for a single tenant sets Tenant to the tenant ID or domain name.
  • Every endpoint must be an absolute https URI.
  • UsePkce is true by default.

Desktop and console applications

LoopbackOAuth2Client listens on the redirect URI, opens the authorization URL in the default browser, waits for the provider to redirect back, and exchanges the authorization code.

using Polhem.OAuth2;

var options = new GoogleOAuth2Options
{
    ClientId = "your-client-id",
    ClientSecret = "your-client-secret",
    RedirectUri = "http://127.0.0.1:0/callback"
};

var client = new LoopbackOAuth2Client(options);
AuthorizationResult result = await client.SignInAsync();

if (result.IsSuccess)
    Console.WriteLine($"{result.UserInfo.UserId} {result.UserInfo.UserName} {result.UserInfo.Email}");
else
    Console.WriteLine($"The sign-in failed: {result.Exception.Message}");
  • The redirect URI must be an http URI on localhost or a loopback address, and it must be registered with the provider. Port 0 picks a free port for each sign-in, which only works with providers that accept any loopback port.
  • The client always uses PKCE. A client secret shipped with a desktop application can be extracted, so it is not sent, except to Google.
  • A timeout (Timeout, 5 minutes by default), cancellation, or an error from the provider becomes a failed result. A port that cannot be listened on throws SocketException, and a missing default browser throws Win32Exception.
  • Set OpenBrowser to open the URL another way, for example uri => launcher.LaunchUriAsync(uri) with the launcher of a UI framework.
  • The snippet uses top-level statements. The OAuthWinForms sample shows the same sign-in in a Windows Forms application on .NET Framework.
  • An application that targets .NET Framework 4.7.2 and runs on a machine with FIPS mode enabled needs the setting described under "Before deploying" in the ASP.NET (System.Web) section.

The reasons behind this design, and how each provider handled loopback redirects, are recorded in ADR-004.

Registering a loopback redirect URI

These registrations were tested with each provider on 2026-09-14.

Provider Application type Redirect URI Port
Google Desktop app http://127.0.0.1:0/callback Any port was accepted
Microsoft Entra ID Mobile and desktop applications, registered as http://localhost http://localhost:0 Ignored
Auth0 Native http://127.0.0.1:53682/callback Must match
Okta Native, with client authentication None and PKCE required http://localhost:53682/callback Must match
LINE LINE Login channel, Callback URL http://localhost:53682/callback Must match
Facebook Facebook Login, Valid OAuth Redirect URIs http://localhost:53682/callback Register the port you use
  • Google: the tested client had its redirect URIs registered. Google's documentation differs on whether a desktop app needs them, and lists the client secret as optional for installed applications.
  • Okta: the authorization server needs an access policy with a rule that allows the authorization code grant. Without one, the sign-in fails with a policy evaluation error.
  • Facebook refused 127.0.0.1; use localhost. Whether the tested app was in development or live mode was not recorded.
  • A redirect URI without a port means port 80, which usually needs administrator rights to listen on. Name the port.

ASP.NET Core

using Polhem.OAuth2;

builder.Services.AddControllers();
builder.Services.AddOAuth2Client("Google", new GoogleOAuth2Options
{
    ClientId = "your-client-id",
    ClientSecret = "your-client-secret",
    RedirectUri = "https://localhost:7032/auth/callback"
});

var app = builder.Build();
app.MapControllers();
using Microsoft.AspNetCore.Mvc;
using Polhem.OAuth2;
using Polhem.OAuth2.AspNetCore;

public class AuthController(OAuth2Manager oauth2Manager) : ControllerBase
{
    [HttpGet("/auth/login")]
    public IActionResult Login()
    {
        return Redirect(oauth2Manager.CreateAuthorizationUrl(HttpContext, "Google"));
    }

    [HttpGet("/auth/callback")]
    public async Task<IActionResult> Callback()
    {
        AuthorizationResult result = await oauth2Manager.CompleteAuthorizationAsync(HttpContext, HttpContext.RequestAborted);
        return result.IsSuccess
            ? Content($"{result.UserInfo.UserId} {result.UserInfo.UserName} {result.UserInfo.Email}")
            : Content($"The sign-in failed: {result.Exception.Message}");
    }
}
  • AddOAuth2Client registers the client, OAuth2Manager and ASP.NET Core data protection. The client is created by the call, so invalid options stop the application at startup. It takes an optional HttpClient.
  • oauth2Manager.GetClient("Google") returns the client, for example to call RefreshTokenAsync.

ASP.NET (System.Web)

There is no runnable sample for System.Web, because its projects cannot be built with the dotnet CLI. The package is used the same way, through the static OAuth2Manager:

using Polhem.OAuth2;
using Polhem.OAuth2.AspNet;

// Global.asax.cs
protected void Application_Start()
{
    OAuth2Manager.RegisterClient("Google", new GoogleOAuth2Options
    {
        ClientId = "your-client-id",
        ClientSecret = "your-client-secret",
        RedirectUri = "https://localhost:44300/auth/callback"
    });
}
// An MVC controller. In Web Forms, call OAuth2Manager.RedirectToAuthorization("Google") from the sign-in page and return,
// and await OAuth2Manager.CompleteAuthorizationAsync() on the callback page, which needs Async="true".
public class AuthController : Controller
{
    public ActionResult Login()
    {
        return Redirect(OAuth2Manager.CreateAuthorizationUrl(HttpContext, "Google"));
    }

    public async Task<ActionResult> Callback()
    {
        AuthorizationResult result = await OAuth2Manager.CompleteAuthorizationAsync(HttpContext);
        if (result.IsSuccess)
            return Content(result.UserInfo.UserId + " " + result.UserInfo.UserName + " " + result.UserInfo.Email);

        return Content("The sign-in failed: " + result.Exception.Message);
    }
}

Before deploying:

  • Target .NET Framework 4.7.2 or later, and set <httpRuntime targetFramework="4.7.2" />, or your later version, in web.config. Asynchronous pages and the operating system's TLS defaults depend on it.
  • On .NET Framework the core package depends on System.Text.Json. Keep the binding redirects that NuGet adds for it and its dependencies in web.config.
  • When more than one server can receive the callback, set the same <machineKey> in web.config on each of them.
  • On a machine with FIPS mode enabled, an application that targets .NET Framework 4.7.2 can get a CryptographicException when a sign-in starts: for such applications .NET Framework blocks the managed SHA-256 implementation that PKCE uses. Target .NET Framework 4.8 or later, or set the Switch.System.Security.Cryptography.UseLegacyFipsThrow switch to false. See Managed cryptography classes do not throw a CryptographyException in FIPS mode.

Web applications: how a sign-in is kept

  • Each sign-in keeps its state, PKCE code verifier, redirect URI and client name in a cookie of its own, encrypted and authenticated with ASP.NET Core data protection or MachineKey. No session state is needed, and sign-ins started in several tabs do not replace each other. See ADR-005.
  • The cookie name starts with __Host-, and the cookie is Secure, HTTP-only and SameSite=Lax, so the sign-in must start and end on HTTPS pages.
  • A sign-in must complete within 10 minutes. The callback removes the cookie before it exchanges the code.
  • Every server that can receive the callback must be able to decrypt the cookie: share the data protection key ring in ASP.NET Core, or use the same machine key on System.Web.

Other server frameworks

OAuth2Client in the core package runs the same flow without an HTTP framework. Keep the pending values where only the browser that started the sign-in can present them, such as an encrypted, HTTP-only cookie, and remove them when the callback is handled.

var client = new OAuth2Client(options);

// Start the sign-in. Keep and Redirect stand for code of your framework.
AuthorizationRequest request = client.CreateAuthorizationRequest();
Keep(request.Pending.State, request.Pending.CodeVerifier, request.Pending.RedirectUri);
Redirect(request.Url);

// Complete it in the callback.
var pending = new PendingAuthorization(keptState, keptCodeVerifier, keptRedirectUri);
var callback = new AuthorizationCallback(query["code"], query["state"], query["error"], query["error_description"]);
AuthorizationResult result = await client.CompleteAuthorizationAsync(callback, pending, cancellationToken);

Results, tokens and errors

  • A successful result has ProviderName, UserInfo and Token; a failed result has Exception.
  • Token holds the access token, and the refresh token, ID token, lifetime and scopes when the provider returns them. RefreshTokenAsync on a client obtains new tokens. Keep the refresh token of the latest response, because some providers issue a new one each time. Facebook does not issue refresh tokens.
  • Exception holds only the failures a sign-in is expected to produce, such as a failed HTTP request, a state that does not match, or an error from the provider. Configuration and programming errors, such as an unregistered client name, are thrown. See ADR-003.
  • An error from the provider is an OAuth2Exception: Error holds the error code, such as access_denied, and ErrorDescription the provider's text. Anyone who sends the user a link can set the values of an error in a redirect, so encode them before showing them.

Identifying users

  • Identify a user by the provider name together with UserInfo.UserId, not by Email: an address can change, and providers differ in whether they verify it.
  • The provider name is AuthorizationResult.ProviderName: Google, Facebook, LINE, Azure (Microsoft Entra ID), Auth0 or Okta. It does not depend on the name a client is registered under.
  • For Microsoft Entra ID, UserId is the sub claim, which is different for each application the user signs in to.
  • LINE returns the email address only in the ID token, and only when the channel may read it and the user agreed. The library reads it from the ID token that the token endpoint returned, and checks that the token was issued to the client, but does not check its signature.
  • The library does not validate ID tokens. Token.IdToken is returned as the provider sent it; validate it before relying on its claims.

Migrating from Bee.OAuth2

Bee.OAuth2 package Replacement
Bee.OAuth2 Polhem.OAuth2
Bee.OAuth2.AspNet Polhem.OAuth2.AspNet
Bee.OAuth2.AspNetCore Polhem.OAuth2.AspNetCore
Bee.OAuth2.WinForms, Bee.OAuth2.Desktop LoopbackOAuth2Client in Polhem.OAuth2
  • Namespaces: Bee.OAuth2 becomes Polhem.OAuth2, and so on for each package.
  • Web registration: ASP.NET Core registers each client with AddOAuth2Client, and System.Web with OAuth2Manager.RegisterClient(name, options). Session state and OAUTH2_STATE_KEY are no longer used.
  • Web methods: GetAuthorizationUrl becomes CreateAuthorizationUrl, and ValidateAuthorization becomes CompleteAuthorizationAsync. In ASP.NET Core they take the HttpContext. A sign-in started before the upgrade does not complete after it; the user signs in again.
  • Desktop sign-in moves from an embedded WebView2 window to the system browser. The synchronous Authorization(), Caption, Width, Height, AuthorizationForm, and the desktop OAuth2Client, OAuth2Manager and StateStorage are gone; call LoopbackOAuth2Client.SignInAsync instead.
  • Redirect URIs for desktop applications must be registered again as loopback URIs (see the table above). URIs that only worked inside an embedded browser, such as https://login.microsoftonline.com/common/oauth2/nativeclient, no longer work.
  • Results and tokens: AuthorizationResult is read-only. AccessToken becomes Token.AccessToken, and refreshing moves to RefreshTokenAsync on the client, which returns a TokenResponse.
  • Types that are no longer public: the provider classes, IOAuth2Provider, BaseOAuth2Client, IStateStorage, PkceHelper and OAuth2StateCryptor. Applications use an options type with a client or a manager.
  • PKCE and the client secret: UsePkce is true by default. A web client sends the client secret whenever it is set, also with PKCE.
  • Endpoints must be https. Domain of Auth0 and Okta takes a host name, with or without https://.
  • Exceptions: AuthorizationResult.Exception no longer collects unexpected exceptions; they are thrown.
  • JSON null: a user information field whose value is JSON null is now null; Bee.OAuth2 returned an empty string. Fallback fields therefore take effect, such as nickname when Auth0 returns name as JSON null.
  • Target frameworks: Polhem.OAuth2.AspNet targets .NET Framework 4.7.2, and Polhem.OAuth2.AspNetCore targets net10.0.
  • Dependencies: no Bee.Base or Newtonsoft.Json. JSON is parsed with System.Text.Json, which the netstandard2.0 build references as a package.

Samples

Each sample reads its provider settings from OAuthConfig.json in its folder. Copy OAuthConfig.example.json to OAuthConfig.json and fill it in. OAuthConfig.json is ignored by git; keep credentials out of OAuthConfig.example.json.

Sample Shows
OAuthConsole Desktop sign-in from a console application, on any operating system
OAuthDesktop Desktop sign-in from Windows Forms on .NET
OAuthWinForms Desktop sign-in from Windows Forms on .NET Framework 4.8
OAuthAspNetCore ASP.NET Core

LoopbackRedirectProbe checks whether a provider accepts a loopback redirect URI before you build on it.

Design decisions

The reasons behind the design are recorded in the architecture decision records.

License

MIT. Copyright (c) Polhem contributors.

Polhem.OAuth2 continues Bee.OAuth2.

Product Compatible and additional computed target framework versions.
.NET Framework net472 is compatible.  net48 was computed.  net481 was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.3.0 91 9/26/2026
1.2.0 86 9/25/2026
1.1.0 101 9/19/2026
1.0.0 96 9/14/2026