2

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

CularBytes
  • 9,924
  • 8
  • 76
  • 101

0 Answers0