Search code examples
.netazure-ad-b2c.net-4.8

Is it possible to integrate Azure AD B2C into a legacy .net framework 4.8 application?


Ì want to to integrate Azure AD B2C into a legacy application which is programmed with .Net Framework v4.8. The thing is that I really can't find anything on the internet that works for that framework.

I also tried solutions from ChatGPT but surprise they didn't work either.

Does anyone did this already and can give me some tips and tricks for the integration? Thank you.

Edit: The answer from Jeff was exactly what I needed. It's also the same as in the Tutorial which was mentioned from Panagiotis in the comments. Have a look here.


Solution

  • Yes - you can. I still have an ASP.Net Framework MVC application running that uses B2C. Here's a quick overview of how I added B2C to my existing application.

    Add an OWIN startup class following the instructions here - https://learn.microsoft.com/en-us/aspnet/aspnet/overview/owin-and-katana/getting-started-with-owin-and-katana#host-owin-in-iis

    The example I followed (years ago) set up a class named Startup as a partial class, split between the Owin startup class in the example (mine is Startup.cs) and a file named Startup.Auth.cs in the App_Start folder. (If I could still find the example - I would just link to it, but I can't - so I'm including the relevant code.)

    My Startup.cs looks like this:

    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // this controls the claim type that is used to populate User.Identity.Name
            System.Web.Helpers.AntiForgeryConfig.UniqueClaimTypeIdentifier = "emails";
            
            ConfigureAuth(app);
        }
    }
    

    My Startup.Auth.cs looks like this (almost completely from the example):

    public partial class Startup
    {
        /*
        * Configure the OWIN middleware
        */
    
        public void ConfigureAuth(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
    
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                // ASP.NET web host compatible cookie manager
                CookieManager = new SystemWebChunkingCookieManager()
            });
    
            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    // Generate the metadata address using the tenant and policy information
                    MetadataAddress = String.Format(Globals.WellKnownMetadata, Globals.Tenant, Globals.DefaultPolicy),
    
                    // These are standard OpenID Connect parameters, with values pulled from web.config
                    ClientId = Globals.ClientId,
                    RedirectUri = Globals.RedirectUri,
                    PostLogoutRedirectUri = Globals.RedirectUri,
    
                    // Specify the callbacks for each type of notifications
                    Notifications = new OpenIdConnectAuthenticationNotifications
                    {
                        RedirectToIdentityProvider = OnRedirectToIdentityProvider,
                        AuthorizationCodeReceived = OnAuthorizationCodeReceived,
                        AuthenticationFailed = OnAuthenticationFailed,
                    },
    
                    // Specify the claim type that specifies the Name property.
                    TokenValidationParameters = new TokenValidationParameters
                    {
                        NameClaimType = "emails",
                        ValidateIssuer = false
                    },
    
                    // Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
                    Scope = $"openid profile offline_access {Globals.ReadTasksScope} {Globals.WriteTasksScope}",
    
                    // ASP.NET web host compatible cookie manager
                    CookieManager = new SystemWebCookieManager()
                }
            );
        }
    
        /*
         *  On each call to Azure AD B2C, check if a policy (e.g. the profile edit or password reset policy) has been specified in the OWIN context.
         *  If so, use that policy when making the call. Also, don't request a code (since it won't be needed).
         */
        private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
        {
            var policy = notification.OwinContext.Get<string>("Policy");
    
            if (!string.IsNullOrEmpty(policy) && !policy.Equals(Globals.DefaultPolicy))
            {
                notification.ProtocolMessage.Scope = OpenIdConnectScope.OpenId;
                notification.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
                notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(Globals.DefaultPolicy.ToLower(), policy.ToLower());
            }
    
            return Task.FromResult(0);
        }
    
        /*
         * Catch any failures received by the authentication middleware and handle appropriately
         */
        private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
        {
            notification.HandleResponse();
    
            // Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
            // because password reset is not supported by a "sign-up or sign-in policy"
            if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
            {
                // If the user clicked the reset password link, redirect to the reset password route
                notification.Response.Redirect("/Account/ResetPassword");
            }
            else if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90091"))
            {
                notification.Response.Redirect("/Home/Index");
            }
            else if (notification.Exception.Message == "access_denied")
            {
                notification.Response.Redirect("/");
            }
            else
            {
                notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
            }
    
            return Task.FromResult(0);
        }
    
        /*
         * Callback function when an authorization code is received
         */
        private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
        {
            try
            {
                /*
                 The `MSALPerUserMemoryTokenCache` is created and hooked in the `UserTokenCache` used by `IConfidentialClientApplication`.
                 At this point, if you inspect `ClaimsPrinciple.Current` you will notice that the Identity is still unauthenticated and it has no claims,
                 but `MSALPerUserMemoryTokenCache` needs the claims to work properly. Because of this sync problem, we are using the constructor that
                 receives `ClaimsPrincipal` as argument and we are getting the claims from the object `AuthorizationCodeReceivedNotification context`.
                 This object contains the property `AuthenticationTicket.Identity`, which is a `ClaimsIdentity`, created from the token received from
                 Azure AD and has a full set of claims.
                 */
                IConfidentialClientApplication confidentialClient = MsalAppBuilder.BuildConfidentialClientApplication(new ClaimsPrincipal(notification.AuthenticationTicket.Identity));
    
                // Upon successful sign in, get & cache a token using MSAL
                AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(Globals.Scopes, notification.Code).ExecuteAsync();
            }
            catch (Exception ex)
            {
                throw new HttpResponseException(new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.BadRequest,
                    ReasonPhrase = $"Unable to get authorization code {ex.Message}."
                });
            }
        }
    }
    

    I have these values in my web.config appSettings section (obviously - replace with your values from the App Registration):

    <add key="ida:Tenant" value="tenantname.onmicrosoft.com" />
    <add key="ida:TenantId" value="hidden" />
    <add key="ida:ClientId" value="hidden" />
    <add key="ida:ClientSecret" value="hidden" />
    <add key="ida:AadInstance" value="https://tenantname.b2clogin.com/tfp/{0}/{1}" />
    <add key="ida:RedirectUri" value="https://homepageurl" />
    <add key="ida:SignUpSignInPolicyId" value="b2c_1_susi" />
    <add key="ida:EditProfilePolicyId" value="b2c_1_edit_profile" />
    <add key="ida:ResetPasswordPolicyId" value="b2c_1_reset" />
    

    You can see a couple of places that I'm using "emails" to populate User.Identity.Name. You can specify a different claim if you want to.