Introducing the OpenIddict-powered providers · Issue #694 · aspnet-contrib/AspNet.Security.OAuth.Providers (original) (raw)
Earlier today, the first OpenIddict 4.0 preview was pushed to NuGet.org.
As part of this release, a new client stack was introduced alongside an OpenIddict.Client.WebIntegration
package that aims at offering an alternative to the aspnet-contrib providers offered in this repository (that will still be developed and maintained).
As I suspect many users will wonder whether these new providers could be a nice fit for their applications, here's a list of things that differ between the aspnet-contrib providers and their equivalent in the OpenIddict world:
- Instead of being built on top of the ASP.NET Core OAuth 2.0 base handler, these providers are based on the new OpenIddict client, which is a modern dual-protocol client stack that supports both OAuth 2.0 and OpenID Connect and thus is able to adapt its security checks to the protocol(s) supported by the provider (while we've accepted OpenID Connect providers in aspnet-contrib, not all the security checks normally required by the standard have been implemented).
- The OpenIddict providers are compatible with more .NET environments than the aspnet-contrib providers: they don't just work on ASP.NET Core (2.1 on .NET Framework, 3.1 on .NET Core, 6.0 and 7.0 on .NET) but they are also natively compatible with OWIN/Katana so they can be used in legacy ASP.NET >= 4.6.1 applications. Starting in OpenIddict 4.1, all the offered providers can also be used in desktop Linux and Windows applications thanks to the
OpenIddict.Client.SystemIntegration
package. For more information, read Introducing system integration support for the OpenIddict client. - Unlike the aspnet-contrib providers, most of the code behind the OpenIddict providers is generated using Roslyn Source Generators (e.g the settings, the builder methods, the environments, etc.), which makes them much easier to maintain and will eventually allow supporting more providers while greatly reducing the maintainance burden.
- Unlike the ASP.NET Core OAuth 2.0 base handler, the OpenIddict client fully supports OpenID Connect discovery/OAuth 2.0 authorization server metadata, which allows discovering endpoint URLs dynamically, making the OpenIddict-based providers that support discovery more resilient to arbitrary endpoint changes.
- The OpenIddict client is - by default - a stateful client that requires configuring a database for two reasons (note: if you already use the server feature, you can share the same DB):
- By storing the status of state tokens in a database, the OpenIddict client is able to detect when they are used multiple times and protect against replay attacks, which is not something the ASP.NET Core OAuth 2.0 or OpenID Connect handlers offer by default.
- By storing the content of state tokens in a database (what we often call "reference tokens"), the OpenIddict client is not impacted by the
state
size limits enforced by some services (like Twitter).
- The OpenIddict providers use a
System.Net.Http
integration that relies on IHttpClientFactory and integrates Polly by default to automatically retry failed HTTP requests based on a built-in policy and thus be less prone to transient network errors. The aspnet-contrib providers use an authentication scheme per provider, which means you can do.[Authorize(AuthenticationSchemes = "Facebook")]
to trigger an authentication dance. In contrast, the OpenIddict client uses a single authentication scheme and requires setting the issuer as an AuthenticationProperties item if multiple providers are registeredFor the same reason, the providers registered via the OpenIddict client are not listed by Identity's.SignInManager.GetExternalAuthenticationSchemesAsync()
and so don't appear in the "external providers" list returned by the default Identity UI. In practice, many users will prefer customizing this part to be more user-friendly, for instance by using localized provider names or logos, which is not something you can natively do withSignInManager.GetExternalAuthenticationSchemesAsync()
anyway- The OpenIddict client doesn't have the "delegate that
ClaimsPrincipal
instance to the cookie handler so it can create an authentication cookie based on it" logic you have in the aspnet-contrib handlers. Instead, you're encouraged to handle the external authentication data -> local authentication cookie creation in your own code, which gives you full control over what's stored exactly in the final authentication cookie:
// Note: this controller uses the same callback action for all providers
// but for users who prefer using a different action per provider,
// the following action can be split into separate actions.
[HttpGet("/callback/login/{provider}"), HttpPost("/callback/login/{provider}"), IgnoreAntiforgeryToken]
public async Task LogInCallback()
{
// Retrieve the authorization data validated by OpenIddict as part of the callback handling.
var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
// Multiple strategies exist to handle OAuth 2.0/OpenID Connect callbacks, each with their pros and cons:
//
// * Directly using the tokens to perform the necessary action(s) on behalf of the user, which is suitable
// for applications that don't need a long-term access to the user's resources or don't want to store
// access/refresh tokens in a database or in an authentication cookie (which has security implications).
// It is also suitable for applications that don't need to authenticate users but only need to perform
// action(s) on their behalf by making API calls using the access token returned by the remote server.
//
// * Storing the external claims/tokens in a database (and optionally keeping the essential claims in an
// authentication cookie so that cookie size limits are not hit). For the applications that use ASP.NET
// Core Identity, the UserManager.SetAuthenticationTokenAsync() API can be used to store external tokens.
//
// Note: in this case, it's recommended to use column encryption to protect the tokens in the database.
//
// * Storing the external claims/tokens in an authentication cookie, which doesn't require having
// a user database but may be affected by the cookie size limits enforced by most browser vendors
// (e.g Safari for macOS and Safari for iOS/iPadOS enforce a per-domain 4KB limit for all cookies).
//
// Note: this is the approach used here, but the external claims are first filtered to only persist
// a few claims like the user identifier. The same approach is used to store the access/refresh tokens.
// Important: if the remote server doesn't support OpenID Connect and doesn't expose a userinfo endpoint,
// result.Principal.Identity will represent an unauthenticated identity and won't contain any claim.
//
// Such identities cannot be used as-is to build an authentication cookie in ASP.NET Core (as the
// antiforgery stack requires at least a name claim to bind CSRF cookies to the user's identity) but
// the access/refresh tokens can be retrieved using result.Properties.GetTokens() to make API calls.
if (result.Principal is not ClaimsPrincipal { Identity.IsAuthenticated: true })
{
throw new InvalidOperationException("The external authorization data cannot be used for authentication.");
}
// Build an identity based on the external claims and that will be used to create the authentication cookie.
var identity = new ClaimsIdentity(authenticationType: "ExternalLogin");
// By default, OpenIddict will automatically try to map the email/name and name identifier claims from
// their standard OpenID Connect or provider-specific equivalent, if available. If needed, additional
// claims can be resolved from the external identity and copied to the final authentication cookie.
identity.SetClaim(ClaimTypes.Email, result.Principal.GetClaim(ClaimTypes.Email))
.SetClaim(ClaimTypes.Name, result.Principal.GetClaim(ClaimTypes.Name))
.SetClaim(ClaimTypes.NameIdentifier, result.Principal.GetClaim(ClaimTypes.NameIdentifier));
// Preserve the registration identifier to be able to resolve it later.
identity.SetClaim(Claims.Private.RegistrationId, result.Principal.GetClaim(Claims.Private.RegistrationId));
// Build the authentication properties based on the properties that were added when the challenge was triggered.
var properties = new AuthenticationProperties(result.Properties.Items)
{
RedirectUri = result.Properties.RedirectUri ?? "/"
};
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
//
// To make cookies less heavy, tokens that are not used are filtered out before creating the cookie.
properties.StoreTokens(result.Properties.GetTokens().Where(token => token switch
{
// Preserve the access, identity and refresh tokens returned in the token response, if available.
{
Name: OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or
OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken or
OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken
} => true,
// Ignore the other tokens.
_ => false
}));
// Ask the default sign-in handler to return a new cookie and redirect the
// user agent to the return URL stored in the authentication properties.
//
// For scenarios where the default sign-in handler configured in the ASP.NET Core
// authentication options shouldn't be used, a specific scheme can be specified here.
return SignIn(new ClaimsPrincipal(identity), properties);
}
The OpenIddict client doesn't do any claims mapping: all the claims resolved from the identity token/userinfo response are flowed exactly as they were returned and it's up to the user to implement a custom mapping if necessary.- The OpenIddict client supports token refreshing so you can easily get new access tokens via
OpenIddictClientService
for providers that enabledgrant_type=refresh_token
:
var response = await _service.AuthenticateWithRefreshTokenAsync(new() { ProviderName = Providers.Twitter, RefreshToken = "the refresh token previously issued by Twitter" });
If you're interested in giving the OpenIddict providers a try, feel free to take a look at the sample in the OpenIddict repository.
The following providers will be available in OpenIddict 4.0, but if you'd like to see additional providers supported, please don't hesitate to contribute to the effort 😄
Provider name | |
---|---|
Apple | PayPal |
Amazon Cognito | Pro Santé Connect |
Deezer | |
GitHub | StackExchange |
Trakt | |
Keycloak | |
WordPress | |
Microsoft Accounts/Azure AD | Yahoo |
Mixcloud |
Cheers!