
Jeff TsaiPolhem.OAuth2, the successor to Bee.OAuth2, now covers .NET MAUI as well
Adding "Sign in with Google" to a .NET application looks like a problem that was solved long ago, until you have more than one kind of application. A website uses ASP.NET Core's authentication handlers, a desktop tool needs something that can open a browser and catch the callback, a mobile app does it another way again, and none of these help the old Web Forms site that is still in production.
I wanted a small library that handles all of these in the same way, so I built Polhem.OAuth2. It is open source under the MIT license and available on NuGet.
If you have used Bee.OAuth2, Polhem.OAuth2 is its successor. It keeps the idea of one set of options per provider (the sign-in service, such as Google or LINE), and most of the rest has been rebuilt. Section 6 lists what changed.
127.0.0.1 or localhost on the local machine), and uses PKCE. Runs on Windows, macOS and Linux.WebAuthenticator with PKCE, and keeps no client secret in the app. An app with its own ASP.NET Core back end can sign in through that back end instead, so the provider's tokens never reach the device.System.Web.OAuth2Client runs the flow in two steps and does not depend on any HTTP framework.PKCE (Proof Key for Code Exchange) is an extension to OAuth2. The client generates a random value when it starts the sign-in and presents it again when it exchanges the code for tokens, so an intercepted authorization code is useless without that value. For desktop and mobile apps, which cannot keep a client secret, it is essential.
Supported providers: Google, Facebook, LINE, Microsoft Entra ID, Auth0 and Okta. Every provider returns the result in the same shape: the user ID, name and email, plus the access, refresh and ID tokens.
| Package | Target frameworks | Use |
|---|---|---|
| Polhem.OAuth2 | netstandard2.0, net10.0 | The providers; desktop, console and .NET MAUI apps; other server frameworks |
| Polhem.OAuth2.AspNetCore | net10.0 | ASP.NET Core |
| Polhem.OAuth2.AspNet | net472 | ASP.NET Web Forms and MVC on System.Web
|
LoopbackOAuth2Client listens on the redirect URI, opens the sign-in page 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}");
Port 0 picks a free port for each sign-in. This only works with providers that accept any loopback port, such as Google. For other providers, use the port you registered.
Register the client at startup:
builder.Services.AddOAuth2Client("Google", new GoogleOAuth2Options
{
ClientId = "your-client-id",
ClientSecret = "your-client-secret",
RedirectUri = "https://localhost:7032/auth/callback"
});
Then start and complete the sign-in with two actions:
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($"Hello, {result.UserInfo.UserName}")
: Content($"The sign-in failed: {result.Exception.Message}");
}
}
The state, PKCE code verifier and redirect URI of each sign-in are kept in their own cookie, encrypted with ASP.NET Core Data Protection. So no session is needed, sign-ins started in several tabs at once do not overwrite each other, and several servers can work together as long as they share the Data Protection keys.
Anything inside a mobile app can be extracted, so it cannot keep a client secret. AppOAuth2Client therefore always uses PKCE and never sends a client secret. You pass it a function that opens the browser, which is usually WebAuthenticator:
var options = new Auth0OAuth2Options
{
Domain = "your-tenant.auth0.com",
ClientId = "your-native-client-id",
RedirectUri = "com.example.app:/oauth2redirect"
};
var client = new AppOAuth2Client(options, async (url, redirectUri, cancellationToken) =>
(await WebAuthenticator.Default.AuthenticateAsync(url, redirectUri)).CallbackUri);
AuthorizationResult result = await client.SignInAsync();
If the app signs in to its own back end, the back end can complete the sign-in as a web client and hand the app a one-time code. The app exchanges that code for the user's information, and the provider's tokens stay on the server.
A few decisions shape how the library behaves:
result.Exception. An unregistered client name is a programming error and throws.The reasoning behind each decision is written up as an ADR (Architecture Decision Record) in the repo's docs/adr/.
Bee.OAuth2.WinForms and Bee.OAuth2.Desktop are replaced by LoopbackOAuth2Client in the core package.OAUTH2_STATE_KEY environment variable. Each sign-in has its own cookie, protected by ASP.NET Core Data Protection or MachineKey.AddOAuth2Client replaces the OAuth2Manager singleton you used to assemble by hand.Bee.Base or Newtonsoft.Json. JSON is now parsed with System.Text.Json.The README has a migration guide that maps each old type and method to its replacement.
Install the core package:
dotnet add package Polhem.OAuth2
The README covers each kind of application, including how to register the redirect URI with each provider. The repo also includes sample apps: a console app, Windows Forms on .NET and on .NET Framework, ASP.NET Core, and .NET MAUI.
The library only handles signing in and getting the user's information and tokens. It does not validate the ID token's signature, and it does not manage accounts or permissions after sign-in. If your project is already well served by ASP.NET Core's built-in authentication or an OpenID Connect package, you may not need to switch. It fits best when you have desktop, mobile and web applications at the same time and want to write sign-in the same way in all of them. Feedback, issues and pull requests are welcome.
📘 Original article on HackMD, in Traditional Chinese:
👉 https://hackmd.io/@jeff377/polhem-oauth2
📢 You are welcome to republish this, please credit the source.
📬 Follow for more engineering notes:
Facebook | HackMD | GitHub | NuGet