I'm trying to build an Azure AD B2C web app that allows users to authenticate/authorize using B2C user flows and OpenIDConnect, based off of boilerplate code from Microsoft.Identity.Web. We would like to validate the id_token that's returned from the implicit flow that we configured in Azure. In most of the Microsoft documentation I looked through, they recommend configuring the options for OpenIDConnect alongside the rest of the services. My ConfigureServices() and Configure() are as follows, with some creative liberties:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
// Add B2C Authentication
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, Constants.AzureAdB2C);
services.AddOptions();
// Specify Cookie policy options
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
// Handling SameSite cookie according to https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1
options.HandleSameSiteCookieCompatibility();
});
// Specify configuration options for OpenIDConnect using appsettings.json
services.Configure<OpenIdConnectOptions>(AzureADB2CDefaults.OpenIdScheme, options => {
Configuration.GetSection("AzureAdB2C");
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.ResponseMode = "fragment";
options.TokenValidationParameters.ValidateAudience = true;
options.TokenValidationParameters.ValidAudience = "my-app-id";
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = ctx =>
{
return Task.CompletedTask;
},
OnTokenResponseReceived = ctx =>
{
return Task.CompletedTask;
},
OnAccessDenied = ctx =>
{
return Task.CompletedTask;
},
OnMessageReceived = ctx =>
{
return Task.CompletedTask;
},
OnAuthorizationCodeReceived = ctx =>
{
return Task.CompletedTask;
},
OnAuthenticationFailed = ctx =>
{
return Task.CompletedTask;
},
OnRedirectToIdentityProvider = ctx =>
{
return Task.CompletedTask;
},
};
options.Validate();
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapRazorPages();
});
}
Now, most of the code for options is completely unused/unnecessary, as I was just trying to get anything to trigger (none of my breakpoints will trigger unless it's on the Configure() line, it seems like nothing in the lambda is being executed). That being said, I've tried many combinations for these options, including moving the Configuration.GetSection("AzureAdB2C");
to it's own Configure() call to make sure the params are correct. However, it seems like nothing is being triggered at all for these options. For example, I added the part about options.ResponseMode = "fragment";
to see if it changed from the default 'form_post', but it still specifies the response_mode as 'form_post' in the HTTP request when triggering the /SignIn action. In fact, I don't even think it's being applied at all; I can comment out this entire section, and it seems like it doesn't even affect anything at all, including sign-in and sign-out.
The AccountController is just a very slightly modified version of https://github.com/AzureAD/microsoft-identity-web/blob/master/src/Microsoft.Identity.Web.UI/Areas/MicrosoftIdentity/Controllers/AccountController.cs (my version included below for reference), and the only notable thing in my appsettings.json is that I specified the CallbackPath as the default '/signin-oidc'.
[AllowAnonymous]
[Route("[controller]/[action]")]
public class AccountController : Controller
{
private readonly IOptionsMonitor<MicrosoftIdentityOptions> _optionsMonitor;
/// <summary>
/// Constructor of <see cref="AccountController"/> from <see cref="MicrosoftIdentityOptions"/>
/// This constructor is used by dependency injection.
/// </summary>
/// <param name="microsoftIdentityOptionsMonitor">Configuration options.</param>
public AccountController(IOptionsMonitor<MicrosoftIdentityOptions> microsoftIdentityOptionsMonitor)
{
_optionsMonitor = microsoftIdentityOptionsMonitor;
}
/// <summary>
/// Handles user sign in.
/// </summary>
/// <param name="scheme">Authentication scheme.</param>
/// <param name="redirectUri">Redirect URI.</param>
/// <returns>Challenge generating a redirect to Azure AD to sign in the user.</returns>
[HttpGet("{scheme?}")]
public IActionResult SignIn(
[FromRoute] string scheme,
[FromQuery] string redirectUri)
{
scheme ??= OpenIdConnectDefaults.AuthenticationScheme;
string redirect;
if (!string.IsNullOrEmpty(redirectUri) && Url.IsLocalUrl(redirectUri))
{
redirect = redirectUri;
}
else
{
redirect = Url.Content("~/")!;
}
return Challenge(
new AuthenticationProperties { RedirectUri = redirect },
scheme);
}
/// <summary>
/// Challenges the user.
/// </summary>
/// <param name="redirectUri">Redirect URI.</param>
/// <param name="scope">Scopes to request.</param>
/// <param name="loginHint">Login hint.</param>
/// <param name="domainHint">Domain hint.</param>
/// <param name="claims">Claims.</param>
/// <param name="policy">AAD B2C policy.</param>
/// <param name="scheme">Authentication scheme.</param>
/// <returns>Challenge generating a redirect to Azure AD to sign in the user.</returns>
[HttpGet("{scheme?}")]
public IActionResult Challenge(
string redirectUri,
string scope,
string loginHint,
string domainHint,
string claims,
string policy,
[FromRoute] string scheme)
{
scheme ??= OpenIdConnectDefaults.AuthenticationScheme;
Dictionary<string, string?> items = new Dictionary<string, string?>
{
{ Constants.Claims, claims },
{ Constants.Policy, policy },
};
Dictionary<string, object?> parameters = new Dictionary<string, object?>
{
{ Constants.LoginHint, loginHint },
{ Constants.DomainHint, domainHint },
};
OAuthChallengeProperties oAuthChallengeProperties = new OAuthChallengeProperties(items, parameters);
oAuthChallengeProperties.Scope = scope?.Split(" ");
oAuthChallengeProperties.RedirectUri = redirectUri;
return Challenge(
oAuthChallengeProperties,
scheme);
}
/// <summary>
/// Handles the user sign-out.
/// </summary>
/// <param name="scheme">Authentication scheme.</param>
/// <returns>Sign out result.</returns>
[HttpGet("{scheme?}")]
public IActionResult SignOut(
[FromRoute] string scheme)
{
if (AppServicesAuthenticationInformation.IsAppServicesAadAuthenticationEnabled)
{
return LocalRedirect(AppServicesAuthenticationInformation.LogoutUrl);
}
else
{
scheme ??= OpenIdConnectDefaults.AuthenticationScheme;
var callbackUrl = Url.Page("/SignedOut", pageHandler: null, values: null, protocol: Request.Scheme);
return SignOut(
new AuthenticationProperties
{
RedirectUri = callbackUrl,
},
CookieAuthenticationDefaults.AuthenticationScheme,
scheme);
}
}
/// <summary>
/// In B2C applications handles the Reset password policy.
/// </summary>
/// <param name="scheme">Authentication scheme.</param>
/// <returns>Challenge generating a redirect to Azure AD B2C.</returns>
[HttpGet("{scheme?}")]
public IActionResult ResetPassword([FromRoute] string scheme)
{
scheme ??= OpenIdConnectDefaults.AuthenticationScheme;
var redirectUrl = Url.Content("~/");
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
properties.Items[Constants.Policy] = _optionsMonitor.Get(scheme).ResetPasswordPolicyId;
return Challenge(properties, scheme);
}
}
I feel like I may be overlooking or misunderstanding something obvious, so my questions are, what is the correct way to receive the id_token in code (if at all), and how can I validate the token and its claims with the OpenIdConnectOptions or a related library like System.IdentityModel.Tokens.Jwt? Also, is this validation step necessary or is it handled by OpenIdConnect/B2C?
Thanks for the help.