Trying to add Azure/Microsoft AD to my application. I already have JWT token in place, which means 2 JWT tokens should be validated. It's in an Angular + .NET 6 App.
Here is the documentation explaining the Microsoft AD part: Docs1, specific for SPA app: github docs
As per documentation and this stackoverflow answer, I have tried doing so:
Program.cs
:
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
// Add services to the container.
builder.Services.AddDbContext<DatabaseContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DatabaseContext")));
if (builder.Environment.IsDevelopment())
{
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
}
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<DatabaseContext>()
.AddDefaultTokenProviders();
builder.Services.AddSession();
IdentityModelEventSource.ShowPII = true; // For debugging purposes
builder.Services.AddAuthentication()
.AddJwtBearer("InternalBearer", options =>
{
options.Audience = configuration["settings:PortalUrl"];
options.Authority = configuration["settings:PortalUrl"];
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.Configuration = new OpenIdConnectConfiguration();
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["settings:PortalUrl"],
ValidAudience = configuration["settings:PortalUrl"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["settings:SecurityKey"]))
};
})
.AddMicrosoftIdentityWebApi(configuration, "AzureAd", subscribeToJwtBearerMiddlewareDiagnosticsEvents: true);
// Creating policies that wraps the authorization requirements
builder.Services
.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("InternalBearer", JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
builder.Services.AddMvc().AddNewtonsoftJson(o =>
{
o.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
o.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
o.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
//o.SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.Objects;
})
.ConfigureApplicationPartManager(apm =>
apm.FeatureProviders.Add(new ModuleControllerFeatureProvider(configuration)));
....
var app = builder.Build();
app.UseRouting();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
Here is my account controller, remember that the JWT token creation and validation was working before when just using 1 authentication scheme. Before I've set the DefaultAuthenticationScheme
within AddAuthentication()
method.
AccountController
:
[HttpGet("[action]")]
public IActionResult Get()
{
_logger.LogInformation("Get - Retrieving contact details");
var userId = HttpContext.User.FindFirst(ClaimTypes.Name)?.Value;
if (userId == null)
{
return NotFound();
}
return Ok(userId); // for testing purposes now
}
[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginDTO login)
{
var user = await _userManager.FindByEmailAsync(login.Email);
if (user == null)
{
return BadRequest();
}
var succeeded = await _userManager.CheckPasswordAsync(user, login.Password);
if (succeeded)
{
// add claims to token
var roles = await _userManager.GetRolesAsync(user);
List<Claim> claims = new List<Claim> { new Claim(ClaimTypes.Name, user.Id), new Claim(ClaimTypes.Email, user.Email) };
foreach (string role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
ClaimsIdentity identity = new ClaimsIdentity(claims);
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["settings:SecurityKey"]));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256Signature);
var tokenOptions = new SecurityTokenDescriptor()
{
Issuer = _config["settings:PortalUrl"],
Audience = _config["settings:PortalUrl"],
Expires = DateTime.UtcNow.AddDays(7).Date,
SigningCredentials = signinCredentials,
Subject = identity
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenOptions);
string tokenStr = tokenHandler.WriteToken(token);
return Ok(new { Token = tokenStr });
}
return BadRequest();
}
I've re-configured the front-end to now use InternalBearer
as prefix for every authenticated request. Of course I have validated this, in Swagger:
curl -X 'GET' \
'https://localhost:44322/api/Account/Get' \
-H 'accept: */*' \
-H 'Authorization: InternalBearer eyJhb...8Eh0'
When I try to access the Account/get endpoint, I get a 401 and I see this logging:
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request starting HTTP/2 GET https://localhost:44322/api/account/get - -
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: AuthenticationScheme: InternalBearer was challenged.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: AuthenticationScheme: Bearer was challenged.
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request finished HTTP/2 GET https://localhost:44322/api/account/get - - - 401 - - 12.1477ms
When I do use Bearer
as pre-fix in the Authorization header, it seems I can access the Account/Get endpoint! I thought that was it, not perfect but hey, 'it was working'.
Boy was I wrong, because when I try to access another endpoint that has a Role added to it, I was getting the following output:
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed. These requirements were not met:
RolesAuthorizationRequirement:User.IsInRole must be true for one of the following roles: (Portal)
That controller simply has [Authorize(Roles = "Portal")]
added to it, I'm 100% the logged in user has this role, because in my IClaimsTransformation
I can see the role is being present. Yet again: It was working before with just one AuthenticationScheme.
The logging does give me some other issues, yet I think this has to do with the fact that I try to use the Bearer
prefix, so the wrong AuthenticationScheme is used and thus the wrong validation is used. But if interested:
Microsoft.IdentityModel.LoggingExtensions.IdentityLoggerAdapter: Error: IDX10634: Unable to create the SignatureProvider.
Algorithm: 'HS256', SecurityKey: '[PII of type 'Microsoft.IdentityModel.Tokens.RsaSecurityKey' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'
is not supported. The list of supported algorithms is available here: https://aka.ms/IdentityModel/supported-algorithms
Microsoft.IdentityModel.LoggingExtensions.IdentityLoggerAdapter: Information: IDX10243: Reading issuer signing keys from validation parameters.
Microsoft.IdentityModel.LoggingExtensions.IdentityLoggerAdapter: Information: IDX10265: Reading issuer signing keys from configuration.
Microsoft.IdentityModel.LoggingExtensions.IdentityLoggerAdapter: Error: IDX10503: Signature validation failed. Token does not have a kid. Keys tried: '[PII of type 'System.Text.StringBuilder' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. Number of keys in TokenValidationParameters: '14'.
Number of keys in Configuration: '0'.
Exceptions caught:
'[PII of type 'System.Text.StringBuilder' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
token: '[PII of type 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: Failed to validate the token.
Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException: IDX10503: Signature validation failed. Token does not have a kid. Keys tried: '[PII of type 'System.Text.StringBuilder' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. Number of keys in TokenValidationParameters: '14'.
Number of keys in Configuration: '0'.
Exceptions caught:
'[PII of type 'System.Text.StringBuilder' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
token: '[PII of type 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignatureAndIssuerSecurityKey(String token, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateJWS(String token, TokenValidationParameters validationParameters, BaseConfiguration currentConfiguration, SecurityToken& signatureValidatedToken, ExceptionDispatchInfo& exceptionThrown)
--- End of stack trace from previous location ---
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, JwtSecurityToken outerToken, TokenValidationParameters validationParameters, SecurityToken& signatureValidatedToken)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: Bearer was not authenticated. Failure message: IDX10503: Signature validation failed. Token does not have a kid. Keys tried: '[PII of type 'System.Text.StringBuilder' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. Number of keys in TokenValidationParameters: '14'.
Number of keys in Configuration: '0'.
Exceptions caught:
'[PII of type 'System.Text.StringBuilder' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
token: '[PII of type 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
Microsoft.AspNetCore.Routing.EndpointMiddleware: Information: Executing endpoint 'CularBytes.Core.Controllers.V1.LinkController.GetLinkAllowed (CularBytes.Core.Controllers)'
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Information: Route matched with {action = "GetLinkAllowed", controller = "Link", page = ""}. Executing controller action with signature Microsoft.AspNetCore.Mvc.IActionResult GetLinkAllowed(LinkModel) on controller CularBytes.Core.Controllers.V1.LinkController (CularBytes.Core.Controllers).
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Information: Executing action method CularBytes.Core.Controllers.V1.LinkController.GetLinkAllowed (CularBytes.Core.Controllers) - Validation state: Valid
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Information: Executed action method CularBytes.Core.Controllers.V1.LinkController.GetLinkAllowed (CularBytes.Core.Controllers), returned result Microsoft.AspNetCore.Mvc.OkObjectResult in 0.0635ms.
Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor: Information: Executing OkObjectResult, writing value of type 'CularBytes.Core.Controllers.V1.LinkController+LinkResult'.
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Information: Executed action CularBytes.Core.Controllers.V1.LinkController.GetLinkAllowed (CularBytes.Core.Controllers) in 11.8873ms
Microsoft.AspNetCore.Routing.EndpointMiddleware: Information: Executed endpoint 'CularBytes.Core.Controllers.V1.LinkController.GetLinkAllowed (CularBytes.Core.Controllers)'
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request finished HTTP/2 POST https://localhost:44322/api/link/allowed application/json 97 - 200 16 application/json;+charset=utf-8 185.5499ms
When I log in via the Microsoft flow, I also get an error saying:
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: Failed to validate the token.
Microsoft.IdentityModel.Tokens.SecurityTokenUnableToValidateException: IDX10516: Signature validation failed. Unable to match key:
kid: '2ZQ.....TOI'.
Number of keys in TokenValidationParameters: '0'.
Number of keys in Configuration: '1'.
Which is also strange, perhaps it has something to do with each other.
I guess I am doing something wrong in the configuration, hope you can tell me what that is!
UPDATE
I started a GitHub issue here, where I used a simple template project so that should help with taking away all the project-related issues: https://github.com/dotnet/aspnetcore/issues/43467