Integrar Azure Active Directory B2C con ASP.NET MVC

Revisando la documentación oficial para integrar Azure Active Directory B2C con una aplicación ASP.NET MVC quizás no quede muy claro cuál es el proceso, ya que se basa en un proyecto ya creado y preconfigurado. En este post te voy a mostrar los pasos que necesitas para integrarlo desde el principio, con el fin de que entiendas bien cada uno de ellos y puedas adaptarlo a cualquier proyecto que tengas e  incluso en uno nuevo. En este articulo se obvia la creación del directorio, ya que puedes guiarte a través de aquí, y la creación de las políticas que puedes configurarlas siguiendo este otro apartado.

Crea un proyecto ASP.NET MVC sin ningún tipo de autenticación predefinida

En este ejemplo partimos un de proyecto nuevo, aunque puedes integrarlo con uno que ya tengas. Para este primer paso sólo necesitas asegurarte de que no hay seleccionado ningún método de autenticación durante su creación:

B2CWebApp – No Authentication

Habilita SSL para el proyecto y modifica la URL de arranque

Para este tipo de integraciones es necesario que la comunicación sea segura de extremo a extremo. Por ello, debes habilitar SSL en el proyecto web que acabas de crear. Haz clic sobre el proyecto web y en sus propiedades cambia el valor de  SSL Enabled a True.

B2CWebApp – SSL Enabled

Copia la URL que aparece en el apartado SSL URL, justo debajo de SSL Enabled, y abre las propiedades del proyecto haciendo clic  sobre él con el botón derecho y seleccionando Properties. Accede al apartado Web y copia la URL en el campo Project Url.

B2CWebApp – SSL URL to Project URL

Instalar los nugets de Owin

Es necesario que instales los siguientes paquetes para Owin, ya que te ayudarán tanto a recuperar los token como validar los mismos dentro de tu aplicación:

  • Microsoft.Owin.Security.OpenIdConnect
  • Microsoft.Owin.Security.Cookies
  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.IdentityModel.Protocol.Extensions (Actualizalo).

Valores en el web.config

Dentro del apartado App Settings es necesario que crees algunas claves con los valores propios de tu tenant de AAD B2C:

  <appSettings>
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />

    <!-- Azure Active Directory B2C configuration -->
    <add key="ida:Tenant" value="returngisb2c.onmicrosoft.com" />
    <add key="ida:ClientId" value="fcc40767-1d1a-474f-9d86-c514c5cfa986" />
    <add key="ida:AadInstance" value="https://login.microsoftonline.com/{0}/v2.0/.well-known/openid-configuration?p={1}" />
    <add key="ida:SignUpPolicyId" value="B2C_1_SignUpWebApp" />
    <add key="ida:SignInPolicyId" value="B2C_1_SignInWebApp" />
    <add key="ida:UserProfilePolicyId" value="B2C_1_EditingProfileWebApp" />
    <add key="ida:RedirectUri" value="https://localhost:44365/" />
    <!-- End Azure Active Directory B2C configuration-->
    
  </appSettings>

Para que te hagas una idea de cuáles son los valores para dichas claves he dejado unos de ejemplo:

  • Tenant: el nombre que elegiste para tu directorio.
  • ClientId: el Id de la aplicación que debes crear para asociar tu aplicación web con tu directorio. Puedes ver los pasos aquí.
  • AddInstance: este valor puedes dejarlo tal cual está, ya que se concatenará en el siguiente paso.
  • SignUpPolicyId: se trata del nombre de la política de tipo SignUp, puedes crearla siguiendo estos pasos.
  • SignInPolicyId: nombre de la política de tipo SignIn.
  • UserProfilePolicyId: nombre de la política de tipo Profile Editing.
  • RedirectUri: debe coincidir con la URL de tu sitio local (puedes recuperarla del apartado SSL URL visto anteriormente).

Clase StartUp

Para poder gestionar la autenticación con Owin y AAD B2C es necesario configurar la aplicación a través de un archivo llamado StartUp.cs. Crea este nuevo archivo dentro de la carpeta App_Start y no te olvides de eliminar .App_Start del namespace de la clase para que siga la convención de Owin y no sea necesario un paso adicional. Este es el contenido de la clase:

using Microsoft.IdentityModel.Protocols;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Notifications;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Configuration;
using System.IdentityModel.Tokens;
using System.Threading.Tasks;

namespace B2CWebApp
{
    public class Startup
    {
        // B2C settings
        private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
        private static string aadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
        private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
        private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];

        // B2C policies
        public static string SignUpPolicyId = ConfigurationManager.AppSettings["ida:SignUpPolicyId"];
        public static string SignInPolicyId = ConfigurationManager.AppSettings["ida:SignInPolicyId"];
        public static string ProfilePolicyId = ConfigurationManager.AppSettings["ida:UserProfilePolicyId"];

        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }

        public void ConfigureAuth(IAppBuilder app)
        {
            //Authetication type: cookies
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            //Default cookie configuration
            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            // Configure OpenID Connect middleware for each policy
            app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignUpPolicyId));
            app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(ProfilePolicyId));
            app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignInPolicyId));
        }


        private Task AuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
        {
            notification.HandleResponse();
            if (notification.Exception.Message == "access_denied")
            {
                notification.Response.Redirect("/");
            }
            else
            {
                notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
            }

            return Task.FromResult(0);
        }

        private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
        {
            return new OpenIdConnectAuthenticationOptions
            {
                // For each policy, give OWIN the policy-specific metadata address, and
                // set the authentication type to the id of the policy
                MetadataAddress = String.Format(aadInstance, tenant, policy),
                AuthenticationType = policy,

                // These are standard OpenID Connect parameters, with values pulled from web.config
                ClientId = clientId,
                RedirectUri = redirectUri,
                PostLogoutRedirectUri = redirectUri,
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthenticationFailed = AuthenticationFailed
                },
                Scope = "openid",
                ResponseType = "id_token",

                // It is used for displaying the user's name in the navigation bar.
                TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    SaveSigninToken = true
                }
            };
        }
    }
}

Como puedes ver, se recuperan los valores que definimos en el web.config y se configura el tipo de autenticación, las políticas y el comportamiento que debe tener si la autenticación falla.

Controlador Account Controller

Por último vas a agregar un controlador que te permita llamar a cada una de las políticas de Azure Active Directory B2C con el fin de que tus usuarios puedan registrarse, iniciar sesión, modificar su perfil y cerrar sesión.

using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace B2CWebApp.Controllers
{
    public class AccountController : Controller
    {
        public void SignIn()
        {
            if (!Request.IsAuthenticated)
            {
                HttpContext.GetOwinContext().Authentication.Challenge(
                    new Microsoft.Owin.Security.AuthenticationProperties() { RedirectUri = "/" }, Startup.SignInPolicyId);
            }
        }

        public void SignUp()
        {
            if (!Request.IsAuthenticated)
            {
                HttpContext.GetOwinContext().Authentication.Challenge(
                    new Microsoft.Owin.Security.AuthenticationProperties() { RedirectUri = "/" }, Startup.SignUpPolicyId);
            }
        }

        public void Profile()
        {
            if (Request.IsAuthenticated)
            {
                HttpContext.GetOwinContext().Authentication.Challenge(
                    new Microsoft.Owin.Security.AuthenticationProperties() { RedirectUri = "/" }, Startup.ProfilePolicyId);
            }
        }

        public void SignOut()
        {
            //To Sign out the user, you should issue an OpenIDConnect sign out request.
            if (Request.IsAuthenticated)
            {
                var authTypes = HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes();
                HttpContext.GetOwinContext().Authentication.SignOut(authTypes.Select(t => t.AuthenticationType).ToArray());
            }
        }
    }
}

Enlazamos estas acciones en el Layout de nuestra aplicación:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("Home", "Index", "Home")</li>
                    <li>@Html.ActionLink("About", "About", "Home")</li>
                    <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
                </ul>
                @if (Request.IsAuthenticated)
                {
                    <text>
                        <ul class="nav navbar-nav navbar-right">
                            <li>
                                <a>@User.Identity.Name</a>
                            </li>
                            <li>@Html.ActionLink("Edit Profile", "Profile", "Account")</li>
                            <li>
                                @Html.ActionLink("Sign out", "SignOut", "Account")
                            </li>
                        </ul>
                    </text>
                }
                else
                {
                    <ul class="nav navbar-nav navbar-right">
                        <li>@Html.ActionLink("Sign up", "SignUp", "Account")</li>
                        <li>@Html.ActionLink("Sign in", "SignIn", "Account")</li>
                    </ul>
                }
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
</body>
</html>

Si no lo habías hecho antes, añade la URL del proyecto dentro de la sección Reply URL de la aplicación creada dentro de Azure Active Directory B2C:

B2CWebApp – Reply URL

Por último, ejecuta la aplicación e intenta registrar un nuevo usuario con uno de los proveedores que hayas elegido:

B2CWebApp – Logged in

¡Saludos!