ASP.NET Core Authentication and External Providers

Khalid Abuhakmeh |

Managing users and their identities is among the most challenging components of application building. That’s why some developers use external identity providers to reduce the maintenance burden and focus on solving new and interesting problems. The most popular external providers include Google, Microsoft, and other social media platforms.

In this post, we’ll show how to create a basic ASP.NET Core web application that defers its authentication to an external provider, in our case, Google. We’ll also discuss the relationship between external authentication and cookie authentication. And finally, why you may want to consider a different option for your production applications.

To follow along, you will need a Google Account to create a new Client with a corresponding Client ID and Client Secret. It only takes a few minutes and is easy to set up.

Set up a new ASP.NET Core application

Let’s start by creating a brand new Razor Pages ASP.NET Core application. We’ll eventually need some UI elements, and the template is best suited for this learning-based tutorial. Use your favorite IDE or run the following command from your preferred terminal.

dotnet new webapp -o ExternalProviders

Once created, we’ll want to set up user secrets to store our ClientId and ClientSecret so the application can access the essential information. From within the project directory, run the following commands.

dotnet user-secrets init
dotnet user-secrets set Google:ClientId "<replace with client id>"
dotnet user-secrets set Google:ClientSecret "<replace with client secret>"

You should see messages stating that you have successfully added these values and a new UserSecretsId element in your project file.

Now let's add a reference to the Google.Apis.Auth.AspNetCore3 package. In your csproj file, add the following package.

<PackageReference Include="Google.Apis.Auth.AspNetCore3" Version="1.70.0" />

Note that other external providers are available to .NET developers, and you can adapt this walkthrough to apply to them. Feel free to choose the one most convenient. Let’s start looking at the code.

Building an Authentication Flow

Our goal by the end of this section is to have a button in our application that challenges users to identify themselves through the external provider. On a successful challenge, we will receive user information in a ClaimsPrincipal for our application.

Let’s start by setting up the authentication plumbing in the Program file.

var config = builder.Configuration;
builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = "cookie";
        options.DefaultChallengeScheme = "google";
    })
    .AddCookie("cookie", options =>
    {
        options.LoginPath = "/SignIn";
        options.LogoutPath = "/SignOut";
    })
    .AddGoogleOpenIdConnect(
        authenticationScheme: "google",
        displayName: "Google",
        configureOptions: options =>
    {
        options.ClientId = config["Google:ClientId"]!;
        options.ClientSecret = config["Google:ClientSecret"]!;
        options.CallbackPath = "/signin-google";
    });

The code may look strange initially because why must we register both Cookie and Google authentication providers?

The Google authentication provider relies on the ability to issue a cookie, which stores our ClaimsPrincipal. If not, the application would redirect the user to our external provider every time they make a request, leading to a frustrating experience. The cookie provides a persistent state during a user’s session.

In the call to AddAuthentication, we set our DefaultScheme to “cookie” and the DefaultChallengeScheme to “google”. The distinction will become important when we start writing our implementation later.

Let’s start on the Index page. We’ll want to add the following markup.

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

@if (User is { Identity.IsAuthenticated: true })
{
    var identity = User.Identity;
    <h1>
        Hello @identity.Name! (@User.Identity.AuthenticationType)
    </h1>
    
    <form asp-page="/SignOut" method="post">
        <button class="btn btn-danger">Sign Out</button>
    </form>
}
else
{
    <p>
        <a asp-page="/SignIn">Sign In Now!</a>
    </p>
}

<p class="mt-3">
    <a asp-page="/Secure">Secure Page</a>
</p>

As you can see, we have a few pages to add to our application: SignIn, SignOut, and Secure. Let’s do that now.

Create a new SignIn page with the following markup.

@page
@model ExternalProviders.Pages.SignIn

<form method="post" asp-page="">
    <button class="btn btn-danger" value="google">Google</button>
</form>

A single form with a submit button will redirect us to Google’s authentication page, but how does that happen? Well, that occurs in the page model.

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;

namespace ExternalProviders.Pages;

public class SignIn : PageModel
{   
    public IActionResult OnPost()
    {
        AuthenticationProperties properties = new()
        {
            RedirectUri = Url.Page("/Index")
        };
        
        return Challenge(properties);
    }
}

You can see we returned a Challenge result. Since our DefaultChallengeScheme is our Google external provider, we were redirected to Google. We should see our name on the Index page when our authentication is successful.

ASP.NET Core login using Google

Let’s implement the SignOut page. Create a new Razor Page, and this time, ignore the markup; we only need to update the page model.

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ExternalProviders.Pages;

public class SignOut : PageModel
{
    public async Task<RedirectToPageResult> OnPost()
    {
        await HttpContext.SignOutAsync();
        return RedirectToPage("/Index");
    }
}

When we submit the SignOut form from our Index page, we immediately sign out the user. Since the DefaultScheme is “cookie,” we clear the cookie set initially by the Google authentication handler.

Now, let’s look at our Secure page implementation, which is the simplest. It uses the Authorize attribute—first, let’s get to the markup.

@page
@model ExternalProviders.Pages.Secure

<h1>This is Secure for @User.Identity?.Name</h1>

Then, the page model code.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ExternalProviders.Pages;

[Authorize]
public class Secure : PageModel
{
    public void OnGet() {}
}

The attribute on our page model issues the same Challenge found on our SignIn page, but ASP.NET Core handles this challenge for us using the DefaultChallengeScheme, which is Google.

There you have it—a working ASP.NET Core application using an external provider. However, we recommend you read the next section, as we explain better options.

The Better way to work with External Providers

While the article has shown you how external providers work with ASP.NET Core, the approach outlined so far can become problematic as the number of solutions within your organization grows. If you manage a single application, the current solution’s complexity will be low. The complexity intensifies as you add more applications, APIs, and background services. The need to manage the same authentication code across multiple projects hinders productivity and limits what is possible.

That is where Duende IdentityServer can significantly simplify authentication. Using Duende IdentityServer, you set up your external providers once at a single identity provider and federate user identity to all relying parties. You can maintain and evolve authentication code in one location while relying parties benefit from relying solely on the OpenID Connect specification.

To learn more about Duende IdentityServer, you can watch our YouTube tutorials.

In practice, federating through a single Identity Provider, such as Duende IdentityServer, allows you to add and remove external providers without affecting downstream relationships. It also allows you to transform and supplement claims that may not be provided initially by the external provider.

As always, you understand your situation best and can make appropriate trade-offs, but we strongly encourage you to consider using an Identity Provider to future-proof your solutions.

Conclusion

This article taught us how to set up an external authentication provider with everyday authentication flow actions such as sign-in, sign-out, and securing resources. We also briefly discussed why you should use Duende IdentityServer to reduce the number of places you add external providers and get the most for your solutions.

As always, please leave a comment below and join us in the Duende Community forums for any further discussions.