Getting started with the OpenIddict web providers

Note: this blog post was updated to use the OpenIddict 5.1 packages.

Earlier this year, I unveiled the new web providers that will ship as part of the OpenIddict 4.0 release.

To help users understand the differences between the existing aspnet-contrib providers and the new OpenIddict-based providers, I also posted additional information on the aspnet-contrib repository ; so if you're considering replacing the aspnet-contrib providers by their equivalent in OpenIddict, don't miss it.

Today, we're going to see how to easily get started by creating a minimal application that uses the OpenIddict.Client.WebIntegration package.

At the time of writing, more than 60 providers are already available, but to keep things simple, we'll focus our attention on a single one: GitHub.

Create a new ASP.NET Core application

For this step, we could use one of the Visual Studio templates, but since there's a trend towards doing things the "minimalistic" way, we're only going to add two files: a .csproj file containing the package references we'll need and a Program.cs file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="5.1.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="5.1.0" />
</ItemGroup>

</Project>
1
2
3
4
5
6
7
8
9
10
11
12
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("https://localhost:44381/");

builder.Services.AddAuthentication();
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

await app.RunAsync();

If you run this application as-is, all you'll get is a 404 response as we didn't register any HTTP handler. We'll get back to that in a few minutes.

Create a new GitHub OAuth application

Whether you're using the new OpenIddict GitHub integration or any other OAuth 2.0 client, this step is unavoidable. But luckily, it's not that complicated and GitHub has an excellent guide to help you create your own OAuth app.

You can of course use any application name for this sample, but you'll need to use an authorization callback URL that points to your application. In this case, we'll use https://localhost:44381/callback/login/github (GitHub doesn't require that the redirect_uri specified in authorization requests have the same exact port as the one configured in the application registration, so if you specified a different port in your Program.cs, it should work too).

If everything went smoothly, GitHub should give you a client_id and a client_secret: note them down as we'll need them in the next step.

Add the OpenIddict services

Like the rest of OpenIddict, the OpenIddict client and its web providers are registered using IServiceCollection extensions and builders:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
builder.Services.AddOpenIddict()

// Register the OpenIddict client components.
.AddClient(options =>
{
// Allow the OpenIddict client to negotiate the authorization code flow.
options.AllowAuthorizationCodeFlow();

// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();

// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options.UseAspNetCore()
.EnableRedirectionEndpointPassthrough();

// Register the GitHub integration.
options.UseWebProviders()
.AddGitHub(options =>
{
options.SetClientId("[your client identifier]")
.SetClientSecret("[your client secret]")
.SetRedirectUri("callback/login/github");
});
});

Don't forget to replace the placeholders in the previous snippet by your actual client_id and client_secret.

Since the OpenIddict client is stateful (unlike the ASP.NET Core OAuth 2.0 or OpenID Connect handlers), we'll also need to configure a database that will be used to store the state tokens OpenIddict will create to prevent cross-site request forgery, session fixation and replay attacks.

To keep things simple, we'll use the OpenIddict Entity Framework Core stores with an in-memory database:

1
2
3
4
5
6
7
8
9
// Note: the OpenIddict client is stateful by default and relies on a database to persist
// things like state tokens and provide additional protection against replay attacks.
//
// For a simple single-instance demo application, an in-memory database can be used safely.
builder.Services.AddDbContext<DbContext>(options =>
{
options.UseInMemoryDatabase("db");
options.UseOpenIddict();
});
1
2
3
4
5
6
7
8
9
builder.Services.AddOpenIddict()

// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
});

Add HTTP handlers to start the GitHub authorization process and handle the callback request

Last but not least, we'll also add two minimal actions that will respectively be used to:

  1. Initiate a challenge that will redirect the user to GitHub's authorization endpoint.
  2. Handle the authorization response returned by GitHub to the callback/redirection endpoint of our application. For now, this endpoint will simply return a text message indicating how many public repositories the user has:
1
2
3
// Add a minimal action responsible for triggering a GitHub challenge
// and redirecting the user agent to the GitHub authorization endpoint.
app.MapGet("challenge", () => Results.Challenge(properties: null, authenticationSchemes: [Providers.GitHub]));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Add a minimal action responsible for handling the GitHub redirection callback.
//
// Note: the OpenIddict client automatically validates the request before this action
// is invoked (e.g by ensuring the state token is valid) and takes care of redeeming
// the authorization code and retrieving the user information automatically. All the
// claims returned by the user information endpoint are available as Claim instances.
app.MapMethods("callback/login/github", [HttpMethods.Get, HttpMethods.Post], async (HttpContext context) =>
{
var result = await context.AuthenticateAsync(Providers.GitHub);

return Results.Text(string.Format("{0} has {1} public repositories.",
result.Principal!.FindFirst("name")!.Value,
result.Principal!.FindFirst("public_repos")!.Value));
});

With these handlers in place, if you visit https://localhost:44381/challenge, you should be immediately redirected to GitHub and asked to approve the authorization request. Once you do so, you'll be redirected to https://localhost:44381/callback/login/github and a message similar to this one should be returned:

Kévin Chalet has 21 public repositories.

Implement different callback handling strategies

What happens in your callback endpoint is entirely up to you. While the previous snippet simply returned some text containing the name and the number of public repositories exposed by OpenIddict via AuthenticateResult.Principal.Claims, nothing prevents you from adopting a different logic.

You could, for instance, use the Octokit SDK to retrieve the public repositories associated with the logged in user from GitHub's API and redirect the browser to the most popular one very easily:

1
2
3
<ItemGroup>
<PackageReference Include="Octokit" Version="4.0.3" />
</ItemGroup>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.MapMethods("callback/login/github", [HttpMethods.Get, HttpMethods.Post], async (HttpContext context) =>
{
var result = await context.AuthenticateAsync(Providers.GitHub);

var client = new GitHubClient(new ProductHeaderValue("Sample-App"))
{
// Attach the access token returned by GitHub's token endpoint:
Credentials = new Credentials(result.Properties!.GetTokenValue(
OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken))
};

return Results.Redirect(
(from repository in await client.Repository.GetAllForCurrent()
orderby repository.StargazersCount descending
select repository.HtmlUrl).FirstOrDefault() ?? "https://github.com/");
});

Returning a local authentication cookie based on the GitHub identity of the user is also quite easy.

If you're not using ASP.NET Core Identity, you'll need to add an instance of the ASP.NET Core cookie handler to store the user identity and configure it as the default scheme in the ASP.NET Core authentication options.

When using ASP.NET Core Identity, it is not necessary because multiple cookie handlers are automatically registered for you by Identity.

1
2
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
app.MapMethods("callback/login/github", [HttpMethods.Get, HttpMethods.Post], async (HttpContext context) =>
{
// Retrieve the authorization data validated by OpenIddict as part of the callback handling.
var result = await context.AuthenticateAsync(Providers.GitHub);

// 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 details to be able to resolve them later.
identity.SetClaim(Claims.Private.RegistrationId, result.Principal!.GetClaim(Claims.Private.RegistrationId))
.SetClaim(Claims.Private.ProviderName, result.Principal!.GetClaim(Claims.Private.ProviderName));

// 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 ?? "/whoami"
};

// 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 Results.SignIn(new ClaimsPrincipal(identity), properties);
});

It's important to note that the the callback/redirection endpoint can only be called once with the same authorization code or state token: any subsequent request with the same parameters will result in an error being returned. Keeping these sensitive parameters in a visible URL also has security concerns.

As such, it's always a good idea to redirect the user agent to a different page – for instance, a confirmation page – once the needed actions are performed, as shown in the last snippet.

1
2
3
4
5
6
7
8
9
10
app.MapGet("whoami", async (HttpContext context) =>
{
var result = await context.AuthenticateAsync();
if (result is not { Succeeded: true })
{
return Results.Text("You're not logged in.");
}

return Results.Text(string.Format("You are {0}.", result.Principal.FindFirst(ClaimTypes.Name)!.Value));
});

If you use ASP.NET Core Identity and its default UI, the web providers registered via options.UseWebProviders().Add*() are automatically returned without any additional configuration needed:

If needed, the display name can be customized using the .SetProviderDisplayName() API:

1
2
3
4
5
6
7
options.UseWebProviders()
.AddGitHub(options =>
{
// ...

options.SetProviderDisplayName("My provider");
});

Voilà, that's all for today! 🎋