/active-directory-b2c-dotnetcore-webapp

An ASP.NET Core web application that can sign in a user using Azure AD B2C, get an access token using MSAL.NET and call an API.

Primary LanguageC#MIT LicenseMIT

page_type description languages products
sample
How to build an MVC web application that performs identity management with Azure AD B2C using the ASP.Net Core OpenID Connect middleware.
csharp
dotnet-core
aspnet-core
azure
azure-active-directory

An ASP.NET Core web app with Azure AD B2C

This sample shows how to build an MVC web application that performs identity management with Azure AD B2C using the ASP.Net Core OpenID Connect middleware. It assumes you have some familiarity with Azure AD B2C. If you'd like to learn all that B2C has to offer, start with our documentation at aka.ms/aadb2c.

The app is a basic web application that performs three functions: sign-in, sign-up, and sign-out. It is intended to help get you started with Azure AD B2C in a ASP.NET Core application, giving you the necessary tools to execute Azure AD B2C policies & securely identify uses in your application.

How To Run This Sample

To run this sample you will need:

  • To install .NET Core for Windows by following the instructions at dot.net/core, which will include Visual Studio 2017.
  • An Internet connection
  • An Azure subscription (a free trial is sufficient)

Step 1: Clone or download this repository

From your shell or command line:

git clone https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore-b2c.git

[OPTIONAL] Step 2: Get your own Azure AD B2C tenant

You can also modify the sample to use your own Azure AD B2C tenant. First, you'll need to create an Azure AD B2C tenant by following these instructions.

IMPORTANT: if you choose to perform one of the optional steps, you have to perform ALL of them for the sample to work as expected.

[OPTIONAL] Step 3: Create your own policies

This sample uses three types of policies: a unified sign-up/sign-in policy, a profile editing policy and a password reset policy. Create one policy of each type by following the instructions here. You may choose to include as many or as few identity providers as you wish.

If you already have existing policies in your Azure AD B2C tenant, feel free to re-use those. No need to create new ones just for this sample.

[OPTIONAL] Step 4: Create your own Web API

This sample calls an API at https://fabrikamb2chello.azurewebsites.net which has the same code as the sample Node.js Web API with Azure AD B2C. You'll need your own API or at the very least, you'll need to register a Web API with Azure AD B2C so that you can define the scopes that your single page application will request access tokens for.

Your web API registration should include the following information:

  • Enable the Web App/Web API setting for your application.
  • Set the Reply URL to the appropriate value indicated in the sample or provide any URL if you're only doing the web api registration, for example https://myapi.
  • Make sure you also provide a AppID URI, for example demoapi, this is used to construct the scopes that are configured in you single page application's code.
  • (Optional) Once you're app is created, open the app's Published Scopes blade and add any extra scopes you want.
  • Copy the AppID URI and Published Scopes values, so you can input them in your application's code.

[OPTIONAL] Step 5: Create your own Web app

Now you need to register your web app in your B2C tenant, so that it has its own Application ID. Don't forget to grant your application API Access to the web API you registered in the previous step.

Your native application registration should include the following information:

  • Enable the Web App/Web API setting for your application.
  • Set the Reply URL to https://localhost:5000/signin-oidc.
  • Once your app is created, open the app's Keys blade and click on Generate Key and Save, copy this key so that you can used it in the next step.
  • Once your app is created, open the app's API access blade and Add the API you created in the previous step.
  • Copy the Application ID generated for your application, so you can use it in the next step.

[OPTIONAL] Step 6: Configure the sample with your app coordinates

  1. Open the solution in Visual Studio.
  2. Open the appsettings.json file.
  3. Find the assignment for Tenant and replace the value with your tenant name.
  4. Find the assignment for ClientID and replace the value with the Application ID from Step 5.
  5. Find the assignment for each of the policies XPolicyId and replace the names of the policies you created in Step 3.
  6. Find the assignment for ClientSecret and replace the value with App Key you created in Step 5.
  7. Find the assignment for ApiUrl and replace the value with the URL of the API that you registered in Step 4.
  8. Find the assignment for the ApiScopes and replace the scopes with those you created in Step 4.
{
  "Authentication": {
    "AzureAdB2C": {
      "ClientId": "90c0fe63-bcf2-44d5-8fb7-b8bbc0b29dc6",
      "Tenant": "fabrikamb2c.onmicrosoft.com",
      "SignUpSignInPolicyId": "b2c_1_susi",
      "ResetPasswordPolicyId": "b2c_1_reset",
      "EditProfilePolicyId": "b2c_1_edit_profile",
      "RedirectUri": "http://localhost:5000/signin-oidc",
      "ClientSecret" : "v0WzLXB(uITV5*Aq",
      "ApiUrl": "https://fabrikamb2chello.azurewebsites.net/hello",
      "ApiScopes": "https://fabrikamb2c.onmicrosoft.com/demoapi/demo.read"
    }
  }
}

Step 7: Run the sample

Clean the solution, rebuild the solution, and run it. You can now sign up & sign in to your application using the accounts you configured in your respective policies.

About the code

Here there's a quick guide to the most interesting authentication related bits of the sample.

Sign in

As it is standard practice for ASP.NET Core MVC apps, the sign in functionality is implemented with the OpenID Connect OWIN middleware. Here there's a relevant snippet from the middleware initialization:

  // In Startup.cs
  public void ConfigureServices(IServiceCollection services)
  {
    //...
    services.Configure<AzureAdB2COptions>(Configuration.GetSection("Authentication:AzureAdB2C"));
    services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectOptionsSetup>();
  }
  
  // In OpenIdConnectOptionsSetup.cs
  public void Configure(OpenIdConnectOptions options)
  {
    options.ClientId = AzureAdB2COptions.ClientId;
    options.Authority = AzureAdB2COptions.Authority;
    options.UseTokenLifetime = true;
    options.TokenValidationParameters = new TokenValidationParameters() { NameClaimType = "name" };
    options.Events = new OpenIdConnectEvents()
    {
      OnRedirectToIdentityProvider = OnRedirectToIdentityProvider,
      OnRemoteFailure = OnRemoteFailure,
      OnAuthorizationCodeReceived = OnAuthorizationCodeReceived
    };
  }
  
  // In AzureAdB2COptions.cs
  public class AzureAdB2COptions
  {
    public const string PolicyAuthenticationProperty = "Policy";

    public AzureAdB2COptions()
    {
      AzureAdB2CInstance = "https://login.microsoftonline.com/tfp";
    }

    public string ClientId { get; set; }
    public string AzureAdB2CInstance { get; set; }
    public string Tenant { get; set; }
    public string SignUpSignInPolicyId { get; set; }
    public string SignInPolicyId { get; set; }
    public string SignUpPolicyId { get; set; }
    public string ResetPasswordPolicyId { get; set; }
    public string EditProfilePolicyId { get; set; }

    public string DefaultPolicy => SignUpSignInPolicyId;
    public string Authority => $"{AzureAdB2CInstance}/{Tenant}/{DefaultPolicy}/v2.0";
    
    public string ClientSecret { get; set; }
    public string ApiUrl { get; set; }
    public string ApiScopes { get; set; }
  }

Important things to notice:

  • The Authority points is constructed using the tfp path, the tenant name and the default (sign-up/sign-in) policy.
  • The OnRedirectToIdentityProvider notification is used in order to support EditProfile and Password Reset.
  • The OnRemoteFailure notification is used in order to support Password Reset.
  • The OnAuthorizationCodeReceived notification is used to redeem the access code using MSAL.

Initial token acquisition

This sample makes use of OpenId Connect hybrid flow, where at authentication time the app receives both sign in info (the id_token) and artifacts (in this case, an authorization code) that the app can use for obtaining an access token. That token can be used to access other resources - in this sample, the a Demo web API which echoes back the user's name. This sample shows how to use MSAL to redeem the authorization code into an access token, which is saved in a cache along with any other useful artifact (such as associated refresh_tokens) so that it can be used later on in the application. The redemption takes place in the AuthorizationCodeReceived notification of the authorization middleware. Here there's the relevant code:

// Use MSAL to swap the code for an access token
// Extract the code from the response notification
var code = context.ProtocolMessage.Code;

string signedInUserID = context.Principal.FindFirst(ClaimTypes.NameIdentifier).Value;
IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder.Create(AzureAdB2COptions.ClientId)
    .WithB2CAuthority(AzureAdB2COptions.Authority)
    .WithRedirectUri(AzureAdB2COptions.RedirectUri)
    .WithClientSecret(AzureAdB2COptions.ClientSecret)
    .Build();
 new MSALStaticCache(signedInUserID, context.HttpContext).EnablePersistence(cca.UserTokenCache);

try
{
    AuthenticationResult result = await cca.AcquireTokenByAuthorizationCode(AzureAdB2COptions.ApiScopes.Split(' '), code)
        .ExecuteAsync();


    context.HandleCodeRedemption(result.AccessToken, result.IdToken);
}

Important things to notice:

  • The IConfidentialClientApplication is the interface that MSAL uses to model the application. As such, it is initialized with the main application's coordinates.
  • MSALStaticCache is a sample implementation of a custom MSAL token cache, which saves tokens in memory. In a real-life application, you would likely want to save tokens in a long lived store instead, so that you don't need to retrieve new ones more often than necessary. For examples of such caches see ASP.NET Core Web app tutorial | Token caches
  • The scope requested by AcquireTokenByAuthorizationCode is just the one required for invoking the API targeted by the application as part of its essential features. We'll see later that the app allows for extra scopes, but you can ignore those at this point.

Using access tokens in the app, handling token expiration

The Api action in the HomeController class demonstrates how to take advantage of MSAL for getting access to protected API easily and securely. Here there's the relevant code:

// Retrieve the token with the specified scopes
var scope = AzureAdB2COptions.ApiScopes.Split(' ');
string signedInUserID = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

IConfidentialClientApplication cca =
ConfidentialClientApplicationBuilder.Create(AzureAdB2COptions.ClientId)
    .WithRedirectUri(AzureAdB2COptions.RedirectUri)
    .WithClientSecret(AzureAdB2COptions.ClientSecret)
    .WithB2CAuthority(AzureAdB2COptions.Authority)
    .Build();
new MSALStaticCache(signedInUserID, this.HttpContext).EnablePersistence(cca.UserTokenCache);

var accounts = await cca.GetAccountsAsync();
AuthenticationResult result = await cca.AcquireTokenSilent(scope, accounts.FirstOrDefault())
    .ExecuteAsync();

The code creates a new instance of IConfidentialClientApplication with the exact same coordinates as the ones used when redeeming the authorization code at authentication time. In particular, note that the exact same cache is used. That done, all you need to do is to invoke AcquireTokenSilent, asking for the scopes you need. MSAL will look up the cache and return any cached token which match with the requirement. If such access tokens are expired or no suitable access tokens are present, but there is an associated refresh token, MSAL will automatically use that to get a new access token and return it transparently.

In the case in which refresh tokens are not present or they fail to obtain a new access token, MSAL will throw MsalUiRequiredException. That means that in order to obtain the requested token, the user must go through an interactive experience.