I'm trying to integration test my web api controllers. The application uses JWTs to authenticate users against the resource server.
To spool up the application, I'm using the TestServer found in Microsoft.OWIN.Testing.
I can obtain a valid JWT by performing a login as a browser would do. I then proceed to add the JWT to the request as follows:
request.AddHeader("Authorization", "Bearer " + accessToken.RawData);
That header also arrives in the OWIN pipeline. However, all controllers protected with the [Authorize]-attribute return 401 Unauthorized when invoked.
The API is protected using IdentityServer3 by Thinktecture, the relevant section looks like this:
var authority = "http://localhost:8080/idsrv/";
var parameters = new TokenValidationParameters() { ValidAudiences = new[] { "implicitclient" } };
var options = new IdentityServerBearerTokenAuthenticationOptions
{
Authority = authority,
TokenValidationParameters = parameters
};
app.UseIdentityServerBearerTokenAuthentication(options);
var configuration = new WebApiConfiguration(this.container);
configuration.Configuration(app);
I don't really know where to look for any pointers to the problem, so any help is appreciated.
Do you want to really test with the token middleware? I mean - you are not testing the token middleware itself - but the controller logic based on certain authentication outcomes.
Just write a small inline middleware that sets Context.Authentication.User to some ClaimsPrincipal you want to test with.
app.Use(async (ctx, next) => { ctx.Authentication.User = somePrincipal; await next() };
Related
I am trying to add multiple ways to authenticate users in our app. All but one are working flawlessly. I am able to log in with ASP.NET Core Identity, GitHub, Azure AD, and even API auth, but JWT is giving me a bit of a headache as I always get a 401 response when I pass in an bearer token to the authorization header.
This might have something to do with a custom middleware class that works on this header:
app.UseMiddleware<JwtAuthMiddleware>()
.UseAuthentication()
.UseAuthorization()
The middleware class in question:
public class JwtAuthMiddleware
{
private readonly RequestDelegate _next;
public JwtAuthMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext context)
{
string authHeader = context.Request.Headers["Authorization"];
if (authHeader != null)
{
string jwtEncodedString = authHeader[7..]; // Fetch token without Bearer
JwtSecurityToken token = new(jwtEncodedString: jwtEncodedString);
ClaimsIdentity identity = new(token.Claims, "jwt");
context.User = new ClaimsPrincipal(identity);
}
return _next(context);
}
}
The identity setup is fairly simple.
services
.AddAuthentication()
.AddCookie(/*config*/)
.AddGitHub(/*config*/)
.AddJwtBearer(/*config*/)
.AddAzureAd(/*config*/);
string[] authSchemes = new string[]
{
IdentityConstants.ApplicationScheme,
CookieAuthenticationDefaults.AuthenticationScheme,
GitHubAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme,
"InHeader"
};
AuthorizationPolicy authnPolicy = new AuthorizationPolicyBuilder(authSchemes)
.RequireAuthenticatedUser()
.Build();
services.AddAuthorization(options =>
{
options.FallbackPolicy = authnPolicy;
});
I also set a filter in AddMvc:
.AddMvc(options =>
{
AuthorizationPolicy policy = new AuthorizationPolicyBuilder(authSchemes)
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
JWT works when options.FallbackPolicy is not set. Otherwise, I get a 401 response. Inspecting the DenyAnonymousAuthorizationRequirement in debug mode confirms this as there is no principal set with the right claims filled out. So it looks like context.User is ignored or reset.
Ideally, I would want to get rid of JwtAuthMiddleware altogether, but I still have to figure out how to combine JWT with cookie authentication in this particular setup. Any thoughts?
You don't need to use this custom JwtAuthMiddleware and define the custom ClaimsPrincipal like that. The problem is that you are just extracting the claims from the token. Setting the ClaimsPrincipal like that wouldn't automatically authenticate the user i.e. Context.User.Identity.IsAuthenticated would be false. Hence you get 401.
The token needs to be validated. You are already using AddJwtBearer which you can customize the code like below if you haven't done that.
JWT bearer authentication performs authentication automatically by extracting and validating a JWT token from the Authorization request header. However, you need to set the TokenValidationParameters which tells the handler what to validate.
services
.AddAuthentication()
.AddJwtBearer("Bearer", options =>
{
// you don't have to validate all of the parameters below
// but whatever you need to, but see the documentation for more details
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = config.JwtToken.Issuer, // presuming config is your appsettings
ValidateAudience = true,
ValidAudience = config.JwtToken.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.JwtToken.SigningKey))
};
})
However, there is a caveat. When combining multiple authentication mechanisms, you need to utilise multiple authentication schemes. They play a role in defining the logic to select the correct authentication mechanism. That is because you can call AddJwtBearer or AddCookie multiple times for different scenarios. If the scheme name is not passed, it would try to utilize the default scheme and the authentication will fail, as it would have no way of knowing which mechanism to use JWT or AzureAd as they both use the Bearer token.
You mentioned that JWT works when FallbackPolicy is not set but you get 401 otherwise. That is because your authorization policy requires a user to be authenticated. The user was never authenticated as I mentioned in the beginning. If FallbackPolicy is not set it would work in the context of JWT being passed in the alright, but there is no requirement to check e.g user is authenticated and has a specific claim or role, so it works.
You would need to define an authentication policy scheme and set the ForwardDefaultSelector property of the PolicySchemeOptions. ForwardDefaultSecltor is used to select a default scheme for the current request that authentication handlers should forward all authentication operations to by default.
So basically you need to set the ForwardDefaultSecltor delegate which uses some logic to forward the request to the correct scheme to handle the authentication.
So the above code would change to:
// scheme names below can be any string you like
services
.AddAuthentication("MultiAuth") // virtual scheme that has logic to delegate
.AddGitHub(GitHubAuthenticationDefaults.AuthenticationScheme, /*config*/) // uses GitHub scheme
.AddAzureAd("AzureAd", /*config*/) // uses AzureAd scheme
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, /*config*/) // uses Cookies scheme
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { // above code for AddJwtBearer }) // uses Bearer scheme
// this is quite important to use the same virtual scheme name below
// which was used in the authentication call.
.AddPolicyScheme("MultiAuth", null, options =>
{
// runs on each request should return the exact scheme name defined above
options.ForwardDefaultSelector = context =>
{
// if the authorization header is present, use the Bearer scheme
string authorization = context.Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
{
return "Bearer";
}
// custom logic for GitHub
...
return GitHubAuthenticationDefaults.AuthenticationScheme;
// custom logic for AzureAd
...
return "AzureAd";
// otherwise fallback to Cookies or whichever is the default authentication scheme
return CookieAuthenticationDefaults.AuthenticationScheme;
};
});
That way, you just remove the JwtAuthMiddleware. AddJwtBearer would automatically add the claims in the Claims object when the token is validated, which you can then use to define custom authorization policies to authorize the user.
I hope that helps.
#Shazad Hassan made some good points in his answer. The custom middleware was in fact not necessary in the end because the System.IdentityModel.Tokens.XXXX assemblies were throwing errors that the tokens were invalid. I never really knew the token was invalid because the middleware assumed it was correct (and content-wise, it was), while all this time the context was running for an unauthenticated user, which became apparent when I applied the fallback policy.
So I rewrote the token generation code and now I no longer get this issue, and I can safely remove the custom middleware. So far so good.
While I still have to bone up on authentication schemes and all that, for the moment it doesn't even seem necessary to have multiple calls to AddAuthentication because this works:
services
.AddAuthentication()
.AddCookie(configureCookieAuthOptions)
.AddGitHub(configureGithub)
.AddAzureAd(config)
.AddJwtBearer(configureJwtBearerOptions)
.AddApiKeyInHeader<ApiKeyProvider>("InHeader", options =>
{
options.Realm = "My realm";
options.KeyName = "X-API-KEY";
});
With this, I am able to log in with every method in the list. Looks promising, but might need to be reviewed by someone who knows the ins and outs of authentication in ASP.NET Core.
So: some clients will be sending a cookie, and some will be sending Authorize header with a bearer token.
These are authentication schemes that you set up with AddAuthentication.
After that you need to set up authorization (AddAuthorization) to grant access to users who are authentication with either of these schemes.
(Authentication means checking that you are who you say you are (users), authorization means what you are and aren't allowed to do (roles) -- it's confusing.)
I have an ASP.NET Core Web API that uses AzureAd registered app service for authentication.
Its purpose will be to act as a backend service to UI front ends.
I have tested security functionality using postman, headers, JWT Authorization, etc., and that works successfully.
Now, I have created a front end in ASP.NET Core 2.1 as an MVC web app.
I have AzureAd login working on the front end.
As a test, I have tried an Ajax request to the backend Web API.
But, I get a 401 Unauthorized response because I do not know how to pass the Authorization Bearer in the request headers, I presume this is my problem.
My question is how is this done?
How do I authenticate to an ASP.NET Core Web API from an ASP.NET Core Web app?
I would like to use a streamlined approach like this, if possible.
And/Or, is there a better approach?
I found my own answer to this problem after reading about middleware and HttpClient requests.
In case anyone finds their self in the same boat here is how I solved it.
Capture the azure "Bearer" token and store token server-side as an identity claim of the user.
Make a method to send http requests to the Web Api.
Like this as follows:
GETTING AND STORING THE TOKEN
This goes in your startup.cs where you configure servcies for AzureAd/OpenIdConnect.
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
builder.AddOpenIdConnect(o =>
{
//Additional config snipped
o.Events = new OpenIdConnectEvents
{
OnTokenValidated = async context =>
{
ClaimsIdentity identity = context.Principal.Identity as ClaimsIdentity;
if (identity != null)
{
identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData));
}
System.Diagnostics.Debug.WriteLine(context.SecurityToken.RawData + "\n\n");
}
};
});
METHOD FOR SENDING ALL HTTP REQUESTS WITH THE AUTHORIZATION TOKEN.
public async Task<IActionResult> AjaxAction(string url)
{
if (User.Claims == null) return null;
System.Security.Claims.Claim claim = User.Claims.SingleOrDefault(s => s.Type == "access_token");
if (claim == null) return null;
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", claim.Value);
string url_e = System.Web.HttpUtility.UrlEncode(url);
HttpResponseMessage response = await httpClient.GetAsync(url);
// Here we ask the framework to dispose the response object a the end of the user resquest
HttpContext.Response.RegisterForDispose(response);
return new HttpResponseMessageResult(response);
}
I'm struggling to implement a custom auth flow with OAuth and JWT.
Basically it should go as follows:
User clicks in login
User is redirect to 3rd party OAuth login page
User logins into the page
I get the access_token and request for User Info
I get the user info and create my own JWT Token to be sent back and forth
I have been following this great tutorial on how to build an OAuth authentication, the only part that differs is that Jerrie is using Cookies.
What I Have done so far:
Configured the AuthenticationService
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "3rdPartyOAuth";
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie() // Added only because of the DefaultSignInScheme
.AddJwtBearer(options =>
{
options.TokenValidationParameters = // Ommited for brevity
})
.AddOAuth("3rdPartyOAuth", options =>
{
options.ClientId = securityConfig.ClientId;
options.ClientSecret = securityConfig.ClientSecret;
options.CallbackPath = new PathString("/auth/oauthCallback");
options.AuthorizationEndpoint = securityConfig.AuthorizationEndpoint;
options.TokenEndpoint = securityConfig.TokenEndpoint;
options.UserInformationEndpoint = securityConfig.UserInfoEndpoint;
// Only this for testing for now
options.ClaimActions.MapJsonKey("sub", "sub");
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
// Request for user information
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JObject.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
};
});
Auth Controller
[AllowAnonymous]
[HttpGet("login")]
public IActionResult LoginIam(string returnUrl = "/auth/loginCallback")
{
return Challenge(new AuthenticationProperties() {RedirectUri = returnUrl});
}
[AllowAnonymous]
[DisableRequestSizeLimit]
[HttpGet("loginCallback")]
public IActionResult IamCallback()
{
// Here is where I expect to get the user info, create my JWT and send it back to the client
return Ok();
}
Disclaimer: This OAuth flow is being incorporated now. I have a flow for creating and using my own JWT working and everything. I will not post here because my problem is before that.
What I want
In Jerrie's post you can see that he sets DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;. With that, when the /auth/loginCallback is reached I have the user claims in the HttpContext.
The problem is my DefaultAuthenticateScheme is set to JwtBearersDefault and when the loginCallback is called I can't see the user claims nowhere in the Request.
How can I have access to the information gained on the OnCreatingTicketEvent in my callback in this scenario?
Bonus question: I don't know much about OAuth (sure that is clear now). You may have noted that my options.CallbackPath differs from the RedirectUri passed in the Challenge at the login endpoint. I expected the option.CallbackPath to be called by the 3rd Part OAuth provider but this is not what happens (apparently). I did have to set the CallbackPath to the same value I have set in the OAuth provider configuration (like Jerries tutorial with GitHub) for it to work. Is that right? The Callback is used for nothing but a match configuration? I can even comment the endpoint CallbackPath points to and it keep working the same way...
Thanks!
Auth
As Jerrie linked in his post, there is a great explanation about auth middlewares:
https://digitalmccullough.com/posts/aspnetcore-auth-system-demystified.html
You can see a flowchart in the section Authentication and Authorization Flow
The second step is Authentication middleware calls Default Handler's Authenticate.
As your default auth handler is Jwt, the context is not pupulated with the user data after the oauth flow,
since it uses the CookieAuthenticationDefaults.AuthenticationScheme
Try:
[AllowAnonymous]
[DisableRequestSizeLimit]
[HttpGet("loginCallback")]
public IActionResult IamCallback()
{
//
// Read external identity from the temporary cookie
//
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("Nein");
}
var oauthUser = result.Principal;
...
return Ok();
}
Great schemes summary: ASP.NET Core 2 AuthenticationSchemes
You can persist your user with
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.signinasync?view=aspnetcore-2.2
Bonus
I did have to set the CallbackPath to the same value I have set in the OAuth provider configuration (like Jerries tutorial with GitHub) for it to work. Is that right?"
Yes.
For security reasons, the registered callback uri (on authorization server) and the provided callback uri (sent by the client) MUST match.
So you cannot change it randomly, or if you change it, you have to change it on the auth server too.
If this restriction was not present, f.e. an email with a mailformed link (with modified callback url) could obtain grant.
This is called Open Redirect, the rfc refers to it too: https://www.rfc-editor.org/rfc/rfc6749#section-10.15
OWASP has a great description: https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md
I can even comment the endpoint CallbackPath points to and it keep working the same way..."
That is because your client is trusted (you provide your secret, and you are not a fully-frontend Single Page App). So it is optional for you to send the callback uri.
But IF you send it, it MUST match with the one registered on the server. If you don't send it, the auth server will redirect to the url, that is registered on its side.
https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
redirect_uri
OPTIONAL. As described in Section 3.1.2.
https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2
The authorization server redirects the user-agent to the
client's redirection endpoint previously established with the
authorization server during the client registration process or when
making the authorization request.
https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.2
The authorization server MUST require the following clients to register their redirection endpoint:
Public clients.
Confidential clients utilizing the implicit grant type.
Your client is confidential and uses authorization code grant type (https://www.rfc-editor.org/rfc/rfc6749#section-1.3.1)
https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.3
If multiple redirection URIs have been registered, if only part of
the redirection URI has been registered, or if no redirection URI has
been registered, the client MUST include a redirection URI with the
authorization request using the "redirect_uri" request parameter.
You have registered your redirect uri, that's why the auth server does not raise an error.
change [AllowAnonymous]
to [Authorize]
on the 'loginCallback' endpoint (AuthController.IamCallback method)
We have a web app built on Asp.Net core. It doesn't contain any authentication middleware configured in it.
We are hosting on Azure App Service and using the Authentication/Authorization option (EasyAuth) to authenticate against Azure AD.
The authentication works well - we get the requisite headers inserted and we can see the authenticated identity at /.auth/me. But the HttpContext.User property doesn't get populated.
Is this a compatibility issue for Asp.Net core? Or am I doing something wrong?
I've created a custom middleware that populates the User property until this gets solved by the Azure Team.
It reads the headers from the App Service Authentication and create a a user that will be recognized by the [Authorize] and has a claim on name.
// Azure app service will send the x-ms-client-principal-id when authenticated
app.Use(async (context, next) =>
{
// Create a user on current thread from provided header
if (context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
{
// Read headers from Azure
var azureAppServicePrincipalIdHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
var azureAppServicePrincipalNameHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];
// Create claims id
var claims = new Claim[] {
new System.Security.Claims.Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", azureAppServicePrincipalIdHeader),
new System.Security.Claims.Claim("name", azureAppServicePrincipalNameHeader)
};
// Set user in current context as claims principal
var identity = new GenericIdentity(azureAppServicePrincipalIdHeader);
identity.AddClaims(claims);
// Set current thread user to identity
context.User = new GenericPrincipal(identity, null);
};
await next.Invoke();
});
Yes, this is a compatibility issue. ASP.NET Core does not support flowing identity info from an IIS module (like Easy Auth) to the app code, unfortunately. This means HttpContext.User and similar code won't work like it does with regular ASP.NET.
The workaround for now is to invoke your web app's /.auth/me endpoint from your server code to get the user claims. You can then cache this data as appropriate using the x-ms-client-principal-id request header value as the cache key. The /.auth/me call will need to be properly authenticated in the same way that calls to your web app need to be authenticated (auth cookie or request header token).
I wrote a small basic middleware to do this. It will create an identity based off of the .auth/me endpoint. The identity is created in the authentication pipeline so that [authorize] attributes and policies work with the identity.
You can find it here:
https://github.com/lpunderscore/azureappservice-authentication-middleware
or on nuget:
https://www.nuget.org/packages/AzureAppserviceAuthenticationMiddleware/
Once added, just add this line to your startup:
app.UseAzureAppServiceAuthentication();
The following code decrypts the AAD token from the Azure App Service HTTP header and populates HttpContext.User with the claims. It's rough as you'd want to cache the configuration rather than look it up on every request:
OpenIdConnectConfigurationRetriever r = new OpenIdConnectConfigurationRetriever();
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.Endpoint, r);
OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync();
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKeys = config.SigningKeys.ToList(),
ValidateIssuer = true,
ValidIssuer = config.Issuer,
ValidateAudience = true,
ValidAudience = options.Audience,
ValidateLifetime = true,
ClockSkew = new TimeSpan(0, 0, 10)
};
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
ClaimsPrincipal principal = null;
SecurityToken validToken = null;
string token = context.Request.Headers["X-MS-TOKEN-AAD-ID-TOKEN"];
if (!String.IsNullOrWhiteSpace(token))
{
principal = handler.ValidateToken(token, tokenValidationParameters, out validToken);
var validJwt = validToken as JwtSecurityToken;
if (validJwt == null) { throw new ArgumentException("Invalid JWT"); }
if (principal != null)
{
context.User.AddIdentities(principal.Identities);
}
}
It only works for Azure AD. To support other ID providers (Facebook, Twitter, etc) you'd have to detect the relevant headers and figure out how to parse each provider's token. However, it should just be variations on the above theme.
You can give this library a try. I faced a similar problem and created this to simplify the use.
https://github.com/dasiths/NEasyAuthMiddleware
Azure App Service Authentication (EasyAuth) middleware for ASP.NET
CORE with fully customizable components with support for local
debugging
It hydrates the HttpContext.User by registering a custom authentication handler. To make things easier when running locally, it even has the ability to use a json file to load mocked claims.
I am using ASP.Net 5 MVC 6 with JWT tokens that are created while the user is on another site which this site is a subdomain of. My goal is to pass the token along with the request to this subdomain. If a user happens to try to come to this subdomain url without the proper token in the header then I want to redirect them to the main site login page.
After much frustration with the newest RC-1 release and using JWT tokens with a SecureKey instead of certificates. I finally got my code working by using the RC-2 nightly build version. Now my problem is that I want to be able to redirect to an outside url in the case of unauthenticated users. Here is an example of my authentication code:
var key = "mysupersecretkey=";
var encodedkey2 = Convert.FromBase64String(key);
app.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.TokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(encodedkey2);
options.TokenValidationParameters.ValidIssuer = "https://tv.alsdkfalsdkf.com/xxx/yyy";
options.TokenValidationParameters.ValidateIssuer = true;
options.TokenValidationParameters.ValidAudience = "https://www.sdgfllfsdkgh.com/";
options.TokenValidationParameters.ValidateAudience = true;
options.Configuration = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration()
{
Issuer = "https://tv.sdfkaysdf.com/xxx/yyy"
};
});
now I see other examples which are using OpedId and they have it pretty easy , there is a parameter called RedirectUrl
app.UseOpenIdConnectAuthentication(options => {
...
options.RedirectUri = "https://localhost:44300";
...
});
any idea how to set RedirectUrl when using JwtBearerAuthentication ???
There's no such property for a simple reason: the JWT bearer middleware (like the more generic OAuth2 middleware in Katana) has been designed for API authentication, not for interactive authentication. Trying to trigger a redirection in this case wouldn't make much sense for headless HTTP clients.
That said, it doesn't mean that you can't redirect your unauthenticated users at all, at some point. The best way to handle that is to catch the 401 response returned by the JWT middleware at the client level and redirect the user to the appropriate login page. In JS applications for instance, this is usually done using an HTTP interceptor.
If you're really convinced breaking the OAuth2 bearer specification is the right thing to do, you can do that using the OnChallenge notification:
app.UseJwtBearerAuthentication(options => {
options.Events = new JwtBearerEvents {
OnChallenge = context => {
context.Response.Redirect("http://localhost:54540/login");
context.HandleResponse();
return Task.FromResult(0);
}
};
});