11

I have tried to use the windows authentication and JWT together with .NET Core 2.1.

I have following startup settings of the authentication:

services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = IISDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,

                    ValidIssuer = "Test",
                    ValidAudience = "Test",
                    IssuerSigningKey = JwtSecurityKey.Create("677efa87-aa4d-42d6-adc8-9f866e5f75f7")
                };

                options.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = OnAuthenticationFailed
                };
            });

IIS settings:

"iisSettings": {
    "windowsAuthentication": true, 
    "anonymousAuthentication": true, 
    ..
  }

I have tried following code snippet to create the JWT token with windows authentication:

[Route("api/[controller]")]
    [ApiController]
    [Authorize(AuthenticationSchemes = "Windows")]
    public class AuthController : ControllerBase
    {
        [HttpPost("token")]
        public IActionResult Token()
        {
            //Setup claims
            var claims = new[]
            {
                new Claim(ClaimTypes.Name, User.Identity.Name),
                //Add additional claims
            };

            //Read signing symmetric key
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("677efa87-aa4d-42d6-adc8-9f866e5f75f7"));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            //Create a token
            var token = new JwtSecurityToken(
                issuer: "Test",
                audience: "Test",
                claims: claims,
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: creds);

            //Return signed JWT token
            return Ok(new
            {
                token = new JwtSecurityTokenHandler().WriteToken(token)
            });
        }
    }

And in another controller I need use only JWT authentication:

[Route("api/[controller]")]
    [ApiController]
    [Authorize(AuthenticationSchemes = "Bearer")]
    public class ProductController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            var userName = User.Identity.Name;

            var claims = User.Claims.Select(x => new { x.Type, x.Value });

            return Ok(new { userName, claims });
        }
    }

If the JWT token is expired then I correctly received the response code 401 but I still get the dialog in the browser for putting the credentials.

How can I configure the windows authentication only for a part when I want to create the JWT token and disable response which is responsible for showing the browser dialog with credentials? How to correctly combine these things?

Jenan
  • 3,408
  • 13
  • 62
  • 105
  • 1
    I'm strugging with the exact same issue. It seems the problem is that when using Windows Authentication, IIS will always add "Negotiate, NTLM" to the Authenticate Response Header value. If you inspect the reponse in Middleware in your app, you'll only see "WWW-Authenticate Bearer", but if you inspect the response in the browser it has became "WWW-Authenticate Bearer, Negotiate, NTLM". The browser sees this and pops-up the login like its supposed to. This seems to get done by IIS once the response has passed out your application. – Valuator Apr 25 '19 at 18:30

7 Answers7

11

The way I would handle this is to create two different web applications: one for Windows Authentication and one that uses JWT Token Authentication.

The Windows Authentication web application would be very small and only does one thing. Authenticate the user via Windows Authentication at an endpoint and return a JWT Token.

Then, that token can be used for the main application. As long as your signing key and audience is the same, it doesn't matter if the token is created on a different web application.

You won't need to struggle with trying to handle both at the same time.

Todd Skelton
  • 6,839
  • 3
  • 36
  • 48
  • This approach has a very important side effect - introducing the dedicated app for authenticating, we go towards our own identity service :) – Bronek Dec 13 '22 at 13:34
6

To work with both windows and JWT bearer authentication-- windows authentication by default get applied to all pages and it over ride the functionality of JWT Bearer. For combining both into one single application :-

  1. Apply Windows authentication on the provider which are used to generate Token of JWt Bearer using [AllowAnonymous] tag on it, which is using windows authentication by default
  2. Apply Jwt Bearer authentication on rest of pages in the application using [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] on it

So, In above case we are using windows authentication for generating JWT Token and using that JWT Token we are authenticating rest of pages in application.

For Testing scenario, You can try postman with NTLM(windows authentication) for token generation controller and BearerToken for page which are having JWTAuthentication

5

Here is how I did it in .net 5 to use both Windows authentication and Jwt on a web api. I first let windows authentication get identity name, then check against a database table for authorization to get roles etc, then return a jwt token. the client web site uses the jwt token to access jwt protected resources.

  1. enable both Anonymous and windows authentication in launchingsettings when debug. in iis if deployed on the server.

  2. if you want to debug through Kestrel, at the time of this writing, in startup, DON'T use services.AddAuthentication(IISDefaults.AuthenticationScheme);
    instead, use

     services.AddAuthentication(NegotiateDefaults.AuthenticationScheme).AddNegotiate();
    
  3. standard set up in startup configureservices to add jwt authentication. in configure function, add authentication.

  4. decorate your function that you want to use windows authentication with this:

    [Authorize(AuthenticationSchemes = NegotiateDefaults.AuthenticationScheme)]
    [Route("GetToken")]
    [HttpGet]
    public IActionResult GetToken() 
    {}
  1. decorate your controller that you want to use jwt token with this:
    [Authorize(AuthenticationSchemes =JwtBearerDefaults.AuthenticationScheme)]
    [ApiController]
    [Route("[controller]")]
    public class ReportController : ControllerBase
    {
    }

that's all.

wxm146
  • 151
  • 1
  • 3
2

In .Net 5.0 I have been able to successfully handle Windows, OIDC, Cookie, and JWT Schemes in a single application. IIS/LaunchSettings.json configuration must designate both Anonymous and Windows authentication as true, because bearer authentication will fail without server configuraton for Anonymous and Windows authentication will not work without server configuration for Windows.

Begin with an AllowAnonymous api and issue a response redirect (redirect is key here to force the non-anonymous auth scheme to issue a challenge and in the case of windows seamlessly receive the desired negotiate authorization header) to whichever additional routes you need to support, decorating them with the appropriate authorization scheme attribute.

I prefer to harvest the pertinent data from each non-jwt scheme, generate a new claims identity with the token as a claim and have all other routes aside from the authentication controller used by the application utilize the bearer scheme. I found it necessary to override both the AllowAnonymous and Authorize attribute on authorization implementations in order to ensure that all requests are always assured to be translated into ClaimsIdentity's with a valid bearer token to achieve consistency across all of the contexts.

To support an anonymous context for windows authenticated contexts, I offer a header that governs whether to try and seamlessly authenticate or otherwise simply geneate the ClaimsIdentity as an anonymous user. Take care to set the authentication type as an empty string when initializing a claims identity to accomplish this.

2

This is desired behaviour of IIS. As soon as the application returns 401, IIS append WWW-Authenticate to the header which is why we see browser dialog to enter credentials.

For more: https://github.com/aspnet/Security/issues/1853

To fix this behaviour, we need to change the status from 401 to something else so through client side framework (blazor or angular) we can redirect the user to the login module(that call Authenticate Endpoint to get token) instead of showing the credentials dialog.

The easiest way is to override the OnChallenge behaviour of JWT Bearer and change the status from 401 to 600 (or whatever you like).

services.AddAuthentication(options =>
{
  options.DefaultAuthenticateScheme = IISDefaults.AuthenticationScheme;
  options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddNegotiate()
 .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme,
        options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = Configuration["Jwt:Issuer"],
                ValidAudience = Configuration["Jwt:Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])),
                ClockSkew = TimeSpan.FromMinutes(25)
            };
            options.Events = new JwtBearerEvents();
            // override the challenge behaviour and change the status to 600
            options.Events.OnChallenge = context =>
            {

                context.HandleResponse();
                context.Response.StatusCode = (int)CustomHttpStatusCode.JwtTokenExpire;
                return Task.CompletedTask;
            };

        });
Angry Coder
  • 145
  • 2
  • 10
  • 1
    So I am having a controller which is generating tokens based on the windows account of an user. And all other controllers only need to use the bearer token generated based on the windows account, this is not possible due to the restrictions of IIS? – Wilko van der Veen Dec 24 '21 at 09:13
  • @WilkovanderVeen I'm trying to do the same thing. Any luck with doing so? – Kurtis Jungersen Feb 28 '22 at 23:59
  • Not perfect solution in terms of standards but as a workaround - very smart one. – Bronek Dec 13 '22 at 13:32
0

This answer might help: https://stackoverflow.com/a/51055082/1212994

You need to ensure, that you NOT setting Authorization: Bearer HTTP header when you trying to use Windows Auth. The key point here is how "Windows Auth" actually works. Let's look how it works with browser for example.

Todd Skelton
  • 6,839
  • 3
  • 36
  • 48
0

The only way I found to get this done so far is using Kestrel. It allows me to control which authentication schemes are sent for every request. On IIS, I only control whether Bearer is sent or not, NTLM and Negotiate are sent by default as pointed out above.

Stephan Steiner
  • 1,043
  • 1
  • 9
  • 22