Skip to content

Master ASP.NET Core Identity in .NET10: External Authentication Providers (OAuth)

Tutor: Frank Liu

Course Link

Logging in with External/Social Media Accounts (OAuth)

All logins via social media accounts follow OAuth. The flow follows something like this:

sequenceDiagram
    autonumber
    actor User
    participant Client as Frontend (e.g., React)
    participant AuthServer as OAuth Provider (e.g., Auth0 / Entra ID)
    participant API as .NET Core API

    Note over User,AuthServer: 1. The Login Flow (Handled by Client & Auth Server)
    User->>Client: Clicks "Login"
    Client->>AuthServer: Redirects to Authorization Endpoint (with PKCE challenge)
    AuthServer-->>User: Displays Login Page
    User->>AuthServer: Enters Credentials & Consents
    AuthServer-->>Client: Redirects back with Authorization Code
    Client->>AuthServer: POST Code + PKCE verifier to Token Endpoint
    AuthServer-->>Client: Returns JWT (Access Token & ID Token)

    Note over Client,API: 2. The API Access Flow (Handled by .NET Core)
    Client->>API: HTTP Request (Header: Authorization: Bearer )

    Note over API: .NET Middleware automatically 
fetches public keys (JWKS) from AuthServer Note over API: .NET validates JWT Signature, Issuer, Audience, and Expiry alt Token is Valid API->>API: Executes Endpoint Logic API-->>Client: 200 OK + Protected Data else Token is Invalid or Expired API-->>Client: 401 Unauthorized end

When the React frontend sends the token to your API, the .NET JwtBearer middleware takes over. You do not have to write the validation logic manually. Under the hood, .NET does the following:

  1. Fetches the Keys: It reaches out to the Auth Server's .well-known/openid-configuration endpoint to download the public keys used to sign the token. It caches these keys in memory.

  2. Validates the Signature: It mathematically verifies that the token was actually created by your Auth Server and hasn't been tampered with.

  3. Validates the Claims: It checks the iss (Issuer), aud (Audience), and exp (Expiration) claims to ensure the token is meant for this specific API and hasn't expired.

Example using Facebook

To get started with this we first need to create an application in Facebook.

When a user tries to log in to a web application (in general), the user needs to have an account in the web application, this holds the user credentials (username and password).

When a web application needs to communicate with Facebook to use Facebook login, the web application itself is the user. In this scenario Facebook is the server, the web app is the user, so the web app also needs an account on the server, hence why we need to create the application account inside Facebook. It is an account for this web application to use in order to use the services that Facebook provides. In this case we're using the Facebook login service.

To create the application account:

  1. Log into Facebook at https://developers.facebook.com.
  2. Select My Apps (probably at the top-right of the page).
  3. Click on the Create App button.
  4. Fill in the information and click Next Facebook Create App Dialog
  5. Select the Authenticate and request data from users with Facebook Login and click Next.
  6. Select I don't want to connect a business portfolio yet. option and click Next
  7. Click Next on the Publishing Requirements page, this won't stop us from testing Facebook login.
  8. Review the Overview page, and if OK click Go to dashboard.
  9. Re-enter your password when prompted.

The application account is now created and you will be on the dashboard.

The easiest way to configure this applicant is to click on the Customize the Authenticate and request data from users with Facebook Login use case. This takes us to the next dashboard. Public Profile will be part of the default permissions, public profile is ready to be exposed from Facebook to our application. For authentication we will also need to add email to the permissions by clicking the + Add button.

Once Ready for testing appears we navigate to Settings on the left panel.

In the Client OAuth settings section there is a setting for Valid OAuth Redirect URIs. After Facebook verifies you do have a Facebook account and then wants to provide your Facebook account information (which includes your name, your email address, and anything else you may have specified previously when we added the email permission) back to your application, this is the redirect URL that Facebook is going to for the response. Our application connects to Facebook through a HTTP request.

After Facebook verifies your application's app ID and app secret, then it's going to expose the information back to your application. In the HTTP response, there is a location header, and this is the URL that is going to be used in that location header.

When Facebook uses the redirect URL, it's going to append the user information to the URL, so the application will be able to receive the information from the location header.

The redirect URL to use here is found by looking at our running web application and using that https URL (Make sure this is the https one not the HTTP one because only https works for Facebook login), with /signin-facebook appended, for example:

https://localhost:7226/

Because we're using Facebook for external login provider we use /signin-facebook, if we were using Google authentication it would be /signin-google. This is a convention defined by the NuGet package itself.

Everything else can be left as the default values, so click save-changes. Clicking on My Apps should show this new application on the dashboard page.

Delegate Login to Facebook

On the Facebook developers dashboard list of My Apps, click into the web application you created and select App Settings from the nav bar, then select Basic.

Within here you will see the App ID and App secret (visible after clicking Show). These values represent the username and password for our web application (App ID being username, Secret being the password). We need to store this information in our web application, to do this:

  • In Visual Studio right click on the WebApp project node in solution explorer, and click Manage User Secrets.
  • Secrets.json will open.Add the following (using the information from your Facebook application):
  "Facebook": {
    "AppId": "1234567890",
    "AppSecret": "your_facebook_app_secret"
  }
  • We need to add a NuGet package to the project to allow communication with Facebook, Microsoft.AspNetCore.Authentication.Facebook.
  • Now we need to update Program.cs to allow us to use Facebook authentication

We haven't configured our authentication yet because we have been using identities all the time so far.

...

builder.Services.AddAuthentication().AddFacebook(options =>
{
    options.AppId = builder.Configuration["Facebook:AppId"] ?? string.Empty;
    options.AppSecret = builder.Configuration["Facebook:AppSecret"] ?? string.Empty;
});

var app = builder.Build();

...

Now we add a facebook button to the login page:

Login.cshtml.cs

[BindProperty]
public IEnumerable<AuthenticationScheme> ExternalLoginProviders { get; set; } = Enumerable.Empty<AuthenticationScheme>();

...

public async Task OnGetAsync()
{
    // Get list of external providers (authentication schemes)
    ExternalLoginProviders = await _signInManager.GetExternalAuthenticationSchemesAsync();
}

Login.cshtml

<form method="post">
   ...
    <br />
    <p>
        Social Media Logins
        <div class="form-group">
            @foreach (var provider in Model.ExternalLoginProviders)
            {
                <button type="submit"
                        asp-page-handler="LoginExternally"
                        name="provider"
                        class="btn btn-secondary"
                        value="@provider.Name">
                    Login with @provider.DisplayName
                </button>
            }
        </div>
    </p>
</form>

Have a quick test to check this renders correctly.

We then need to implement a callback controller action in order to actually log in the user. Facebook only can provide the user information from Facebook to us, it is still up to us to use ASP.Net Core Identity to log the user in.

Implement the callback functionality

The web application redirects the user to Facebook to login. After this completes a callback function to redirect to a URL with the information in the header, as specified in the Valid OAuth Redirect URIs earlier, to complete the flow by passing back to the WebApp. The returned information is handled by ASP.Net Core Identity NuGet package which automatically decrypts the user information and puts it into the claims.

We need to use the sign-in manager to call the provider and retrieve those returned claims.

In order to provide that callback URL we will need to create a controller. We need to add controller ability to our web app to do this. In Program.cs we only have MapRazorPages() defined so far. To add controller support we need to add app.MapControllers() before app.Run(), and after builder.Services.AddRazorPages() we simply add builder.Services.AddControllers().

Add a new root folder Controllers to contain any code for controllers we add, and into this add a new controller (via the pop-up menu in the usual way, choosing API Controller - Empty, named AccountController, with the following code:

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using WebApp.Data.Accounts;

namespace WebApp.Controllers;

public class AccountController : Controller
{
    private readonly SignInManager<User> _signInManager;
    private readonly UserManager<User> _userManager;

    public AccountController(SignInManager<User> signInManager, UserManager<User> userManager)
    {
        _signInManager = signInManager;
        _userManager = userManager;
    }

    [HttpGet]
    [Route("Account/ExternalLoginCallback")]
    public async Task<IActionResult> ExternalLoginCallback(string? returnUrl = null, string? remoteError = null)
    {
        if (remoteError != null)
        {
            TempData["ErrorMessage"] = $"Error from external provider: {remoteError}";
            return RedirectToPage("/Account/Login");
        }

        var loginInfo = await _signInManager.GetExternalLoginInfoAsync();
        if (loginInfo == null)
        {
            TempData["ErrorMessage"] = "Error loading external login information.";
            return RedirectToPage("/Account/Login");
        }

        // Try to sign in with the external login provider
        var result = await _signInManager.ExternalLoginSignInAsync(
            loginInfo.LoginProvider, 
            loginInfo.ProviderKey, 
            isPersistent: false, 
            bypassTwoFactor: true);

        if (result.Succeeded)
        {
            return RedirectToPage("/Index");
        }

        if (result.IsLockedOut)
        {
            TempData["ErrorMessage"] = "Account is locked out.";
            return RedirectToPage("/Account/Login");
        }

        // If the user doesn't have an account, create one
        var emailClaim = loginInfo.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email);

        if (emailClaim == null)
        {
            TempData["ErrorMessage"] = "Email claim not received from provider.";
            return RedirectToPage("/Account/Login");
        }

        var user = await _userManager.FindByEmailAsync(emailClaim.Value);

        if (user == null)
        {
            // Create new user
            user = new User
            {
                Email = emailClaim.Value,
                UserName = emailClaim.Value,
                EmailConfirmed = true
            };

            var createResult = await _userManager.CreateAsync(user);
            if (!createResult.Succeeded)
            {
                TempData["ErrorMessage"] = "Error creating user account.";
                return RedirectToPage("/Account/Login");
            }
        }

        // Add the external login to the user
        var addLoginResult = await _userManager.AddLoginAsync(user, loginInfo);
        if (!addLoginResult.Succeeded)
        {
            TempData["ErrorMessage"] = "Error adding external login.";
            return RedirectToPage("/Account/Login");
        }

        // Sign in the user
        await _signInManager.SignInAsync(user, isPersistent: false);

        return RedirectToPage("/Index");
    }
}

Now we have the ExternalLoginCallback defined, we add the following in Login.cshtml.cs:

public IActionResult OnPostLoginExternally(string provider)
{
    var redirectUrl = Url.Action("ExternalLoginCallback", "Account");
    var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
    return Challenge(properties, provider);
}

This implements the button ball from the page design. Now this is ready to test.

OAuth Deep Dive

Watch this video for a deep dive into the details of OAuth.