Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing the OpenIddict-powered providers #694

Closed
kevinchalet opened this issue Jun 22, 2022 · 4 comments
Closed

Introducing the OpenIddict-powered providers #694

kevinchalet opened this issue Jun 22, 2022 · 4 comments

Comments

@kevinchalet
Copy link
Member

kevinchalet commented Jun 22, 2022

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 registered.

  • For 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 with SignInManager.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<ActionResult> 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 enabled grant_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 Reddit
GitHub StackExchange
Google Trakt
Keycloak Twitter
LinkedIn WordPress
Microsoft Accounts/Azure AD Yahoo
Mixcloud

Cheers!

@kevinchalet kevinchalet pinned this issue Jun 22, 2022
@SDP190
Copy link

SDP190 commented Mar 28, 2023

Hi @kevinchalet, I am a bit lost in confusion what you mention OpenIDConnect in comparison to OAuth handlers

Isn't OAuth just for granting authorization tokens for resources? how would one use OAuth protocol when he needs Authentication which works via OpenIdConnect protocol only. as far as I understand OpenIDConnect was invented on top of OAuth for the purpose of authentication.

How can same thing be achieved via the OAuth and OpenIdConnect protocols?

@kevinchalet
Copy link
Member Author

Hey,

Isn't OAuth just for granting authorization tokens for resources? how would one use OAuth protocol when he needs Authentication which works via OpenIdConnect protocol only. as far as I understand OpenIDConnect was invented on top of OAuth for the purpose of authentication.

While it's true that the OAuth 2.0 standard itself is an authorization-only protocol, in practice almost all OAuth 2.0 servers also provide a custom API endpoint that provides information about the user represented by an access token, which serves the same exact purpose as the standard "userinfo endpoint" in the OpenID Connect specification. Just like the Facebook, Microsoft Account or Google providers developed by Microsoft and based on OAuthHandler, OpenIddict leverages this endpoint to implement authentication and expose a list of claims to the app developer.

For the extremely rare cases where no user information is available, the OpenIddict client can still be used for pure authorization (i.e performing actions on behalf of the user), but in this case, no Claim is available to determine who the user is.

@kevinchalet
Copy link
Member Author

Update: a majority of providers have been ported to OpenIddict: openiddict/openiddict-core#1801. If you're interested in helping port the remaining services (that I can't port myself due to the painful registration process they all require), feel free to chime in.

@kevinchalet
Copy link
Member Author

I updated the OP to take the recent improvements made to the OpenIddict client and its web providers into account.

Closing.

@kevinchalet kevinchalet unpinned this issue Aug 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants