After a lot of research I wasn't able to find a solution, still happy for someone to provide one. Main thing that I wanted to achieve was to be able to update the AzureADB2C settings at the runtime and with this based on a criteria (url / a dynamic prop etc)to be able to use different settings.
Using this approach you'll need to test all flows from AzureAdB2C (as Forgot password / new user etc) and make sure all of these are covered.
I have used another approach, I've removed the:
builder.Services.AddAuthentication().AddMicrosoftIdentityWebApp(...);
and use Cookies instead:
builder.Services.AddAuthentication(options =>
{
OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultScheme = "TokenLogin";
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie("TokenLogin", options =>
{
options.ExpireTimeSpan = DateTime.Now.Subtract(DateTime.UtcNow).Add(TimeSpan.FromDays(2));
options.Cookie.MaxAge = options.ExpireTimeSpan;
options.SlidingExpiration = true;
});
I have created a Midelware: SignInIdDcMiddleware.cs and register it. The redirect URI needs to be whitelisted in Azure.
app.Map("/signin-oidc", app =>
{
app.UseMiddleware<SignInIdDcMiddleware>();
});
The code for SignInIdDcMiddleware.cs is below, and with the info from here, you can pass the info to a controller that will deal with the authentication using the cookies.
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Text;
namespace TestDynamicAzureB2C.Middleware
{
public class SignInIdDcMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
public SignInIdDcMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context)
{
string responseBody = "";
// Read form data
var formData = await context.Request.ReadFormAsync();
// Create a list to store key-value pairs
var formDataList = new List<KeyValuePair<string, string>>();
// Iterate through form data and add to the list
foreach (var field in formData)
{
foreach (var value in field.Value)
{
formDataList.Add(new KeyValuePair<string, string>(field.Key, value));
}
}
#region Error
if (formDataList.Any(p => p.Key == "error"))
{
foreach (var value in formDataList)
{
responseBody += value + Environment.NewLine;
}
// to check rest of the flows (Forgot Password & New Account)
// Display responseBody as a string
context.Response.ContentType = "text/plain; charset=utf-8";
await context.Response.WriteAsync(responseBody, Encoding.UTF8);
return;
}
#endregion
#region Validate ID Token
bool isTokenValid = false;
string idToken = "";
if (formDataList.Any(p => p.Key == "id_token"))
{
string state = WebUtility.UrlDecode(formDataList.Single(p => p.Key == "state").Value).ToLower().Replace("tn=", "").Replace("cid=", "");
string[] stateData = state.Split("&");
string tenantName = stateData[0],
policyName = "b2c_1_signupsignin",
clientId = stateData[1];
idToken = formDataList.Single(p => p.Key == "id_token").Value;
var openIdConfigUrl = $"https://{tenantName}.b2clogin.com/{tenantName}.onmicrosoft.com/{policyName}/v2.0/.well-known/openid-configuration";
var publicKeys = await GetPublicKeysAsync(openIdConfigUrl);
var issuer = $"https://{tenantName}.b2clogin.com/{tenantName}.onmicrosoft.com/{policyName}/v2.0/";
isTokenValid = ValidateIdToken(idToken, publicKeys, issuer, clientId);
}
#endregion
foreach (var value in formDataList)
{
responseBody += value + Environment.NewLine;
}
responseBody += $"IsTokenValid: {isTokenValid}" + Environment.NewLine;
if (isTokenValid)
{
List<System.Security.Claims.Claim> claims = GetUserClaims(idToken);
if (claims is not null && claims.Any())
{
foreach (var value in claims)
{
responseBody += value + Environment.NewLine;
}
}
else
{
responseBody += "UserClaims: N/A" + Environment.NewLine;
}
}
// Display responseBody as a string
context.Response.ContentType = "text/plain; charset=utf-8";
await context.Response.WriteAsync(responseBody, Encoding.UTF8);
}
public async Task<IEnumerable<SecurityKey>> GetPublicKeysAsync(string openIdConfigUrl)
{
using var httpClient = new HttpClient();
var openIdConfigResponse = await httpClient.GetStringAsync(openIdConfigUrl);
// this will not work due to a bug
//var openIdConfig = JsonConvert.DeserializeObject<OpenIdConnectConfiguration>(openIdConfigResponse);
var openIdConfig = JsonConvert.DeserializeObject<OpenIdConnectConfiguration_Extension>(openIdConfigResponse);
var jwksResponse = await httpClient.GetStringAsync(openIdConfig.jwks_uri);
var jwks = JsonConvert.DeserializeObject<JsonWebKeySet>(jwksResponse);
return jwks.Keys;
}
public bool ValidateIdToken(string idToken, IEnumerable<SecurityKey> publicKeys, string issuer, string audience)
{
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
// not sure why is not working with this set to true
// posible explation https://stackoverflow.com/questions/70927709/issue-validation-failed-error-for-azure-app-registration-even-in-sample-applica
ValidateIssuer = false,
ValidIssuer = issuer,
ValidateAudience = true,
ValidAudience = audience,
IssuerSigningKeys = publicKeys,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
try
{
SecurityToken validatedToken;
tokenHandler.ValidateToken(idToken, validationParameters, out validatedToken);
return true;
}
catch
{
return false;
}
}
public List<System.Security.Claims.Claim> GetUserClaims(string idToken)
{
// Decode the ID Token
var tokenHandler = new JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadJwtToken(idToken);
// Access claims
var userClaims = jwtToken.Claims.ToList();
return userClaims;
}
}
public class OpenIdConnectConfiguration_Extension
{
public string jwks_uri { get; set; }
}
}
The code for the controller AuthController.cs below, please deal with secure the information, this is just a POC.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;
using Newtonsoft.Json;
using System.Text;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
namespace TestDynamicAzureB2C.Controller
{
// added it here for POC
public class LoginData
{
public string Key { get; set; }
public string Value { get; set; }
}
[Route("[controller]/[action]")]
public class AuthController : ControllerBase
{
private readonly IConfiguration _config;
public AuthController(IConfiguration config)
{
_config = config;
}
[HttpGet]
public async Task<IActionResult> Login([FromQuery] string data, [FromQuery] string redirect)
{
try
{
// Decode the base64 string to a JSON string
var jsonString = Encoding.UTF8.GetString(Convert.FromBase64String(data));
// Deserialize the JSON string to a list of LoginData objects
var loginDataList = JsonConvert.DeserializeObject<List<LoginData>>(jsonString);
List<Claim> claims = new List<Claim>();
foreach (LoginData gd in loginDataList)
{
claims.Add(new Claim(gd.Key, gd.Value));
}
var claimsIdentity = new ClaimsIdentity(claims, "TokenLogin");
var authProperties = new AuthenticationProperties
{
AllowRefresh = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddHours(1),
IsPersistent = true
};
await HttpContext.SignInAsync("TokenLogin", new ClaimsPrincipal(claimsIdentity), authProperties);
if (redirect != null)
return Redirect(redirect);
else
return Redirect("/");
}
catch (Exception e)
{
return BadRequest(new { Success = false, Status = 400, Erorr = e.Message });
}
}
[HttpGet]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync("TokenLogin");
// this endpoint below needs to be created and managed by you
return Redirect("/MicrosoftIdentity/Account/SignOut");
}
}
}
And in a *.razor file you can build the AzureADB2C sign in url:
public class B2CData
{
public string URL { get; set; }
public string Policy { get; set; }
public string ClientId { get; set; }
public string RedirectURI { get; set; }
public string ResponseType { get; set; }
public string Scope { get; set; }
public string ResponseMode { get; set; }
public string Nonce { get; set; }
public string XClientSKU { get; set; }
public string XClientVer { get; set; }
public string State { get; set; }
}
public class B2CDataLogout
{
public string URL { get; set; }
public string Policy { get; set; }
public string RedirectURI { get; set; }
}
void LogIn()
{
B2CData data = new B2CData()
{
URL = "https://<tenant-name>.b2clogin.com/<tenant-name>.onmicrosoft.com/{0}/oauth2/v2.0/authorize",
Policy = "b2c_1_signupsignin",
ClientId = "XXXXXX-XXXXX-XXXX-XXX-XXXXX",
RedirectURI = "https://localhost:44321/signin-oidc",
ResponseType = "id_token",
Scope = "openid%20profile",
ResponseMode = "form_post",
Nonce = Guid.NewGuid().ToString(),
XClientSKU = "ID_NET6_0",
XClientVer = "6.32.0.0",
State = "tn=<tenant-name>&cid=XXXXX-XXXX-XXXX-XXXX-XXXXXX"
};
string redirectURL =
$"{string.Format(data.URL, data.Policy)}?" +
$"client_id={data.ClientId}&" +
$"redirect_uri={WebUtility.UrlEncode(data.RedirectURI)}&" +
$"response_type={data.ResponseType}&" +
$"scope={data.Scope}&" +
$"response_mode={data.ResponseMode}&" +
$"nonce={data.Nonce}&" +
$"x-client-SKU={data.XClientSKU}&" +
$"x-client-ver={data.XClientVer}&" +
$"state={WebUtility.UrlEncode(data.State)}";
NavigationManager.NavigateTo(redirectURL, true);
}
void LogOut()
{
B2CDataLogout data = new B2CDataLogout()
{
URL = "https://<tenant-name>.b2clogin.com/<tenant-name>.onmicrosoft.com/{0}/oauth2/v2.0/logout",
Policy = "b2c_1_signupsignin",
RedirectURI = "https%3A%2F%2Flocalhost%3A44321%2F",
};
string redirectURL =
$"{string.Format(data.URL, data.Policy)}?" +
$"post_logout_redirect_uri={data.RedirectURI}";
NavigationManager.NavigateTo(redirectURL, true);
}