Ì 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.
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.