I am using JWT bearer authentication, configured as follows.
My problem is that the middleware is executing before the token is validated.
How do I configure the middleware to run afterwards?
services.AddAuthentication()
.AddCookie(_ => _.SlidingExpiration = true)
.AddJwtBearer(
_ =>
{
_.Events = new JwtBearerEvents
{
// THIS CODE EXECUTES AFTER THE MIDDLEWARE????
OnTokenValidated = context =>
{
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(context.Principal.Claims, "local"));
return Task.CompletedTask;
}
};
_.RequireHttpsMetadata = false;
_.SaveToken = false;
_.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = this.Configuration["Tokens:Issuer"],
ValidAudience = this.Configuration["Tokens:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.Configuration["Tokens:Key"])),
};
});
I am attempting to add middleware into the pipeline that accesses the current user. This code unfortunately executes BEFORE the token is validated. How do I make it execute afterwards?
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseIdentityServer();
app.UseAuthentication();
app.Use(async (httpContext, next) =>
{
// THIS CODE EXECUTES BEFORE THE TOKEN IS VALIDATED IN OnTokenValidated.
var userName = httpContext.User.Identity.IsAuthenticated
? httpContext.User.GetClaim("email")
: "(unknown)";
LogContext.PushProperty("ActiveUser", !string.IsNullOrWhiteSpace(userName) ? userName : "(unknown)");
await next.Invoke();
});
It looks like you've found a good solution to your problem but I thought I'd add an answer to explain the behavior you're seeing.
Since you have multiple authentication schemes registered and none is the default, authentication does not happen automatically as the request goes through the pipeline. That's why the HttpContext.User was empty/unauthenticated when it went through your custom middleware. In this "passive" mode, the authentication scheme won't be invoked until it is requested. In your example, this happens when the request passes through your AuthorizeFilter. This triggers the JWT authentication handler, which validates the token, authenticates and sets the Identity, etc. That's why (as in your other question) the User is populated correctly by the time it gets to your controller action.
It probably doesn't make sense for your scenario (since you're using both cookies and jwt)... however, if you did want the Jwt authentication to happen automatically, setting HttpContext.User for other middleware in the pipeline, you just need to register it as the default scheme when configuring authentication:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
based on #leppie's comment, here is a solution that works.
public class ActiveUserFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
var userName = context.HttpContext.User.Identity.IsAuthenticated
? context.HttpContext.User.GetClaim("email")
: "(unknown)";
using (LogContext.PushProperty("ActiveUser", !string.IsNullOrWhiteSpace(userName) ? userName : "(unknown)"))
await next();
}
}
Inserted as follows...
services.AddMvc(
_ =>
{
_.Filters.Add(
new AuthorizeFilter(
new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme,
IdentityConstants.ApplicationScheme)
.RequireAuthenticatedUser()
.Build()));
_.Filters.Add(new ActiveUserFilter());
...
Related
I am creating CookieAutentecation signin for my Web API.
I have read and followed the official article here and I have done everything correctly as far as I am concerned.
But when I put breakpoints in my controllers and inspect HttpContext.User, everything is always null, no Username, no claims, nothing.
What else do I need to make this work? Are additional steps needed for Web API vs MVC app?
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, act => {
act.LoginPath = "/api/login";
act.AccessDeniedPath = "/api/login";
act.SlidingExpiration = true;
});
services.AddControllers();
services.AddServices(); // <- Own app domain services
services.AddDataAccess(); // <- Own app domain data access
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseCors(
options => options.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
api/login
var user = new SecurityUser()
{
UserID = 123,
CompleteName = "Test user",
FirstName = "Test",
Email = "test.user#123.com"
};
var identity = user.ToClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme, 123);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), new AuthenticationProperties()
{
AllowRefresh = true,
ExpiresUtc = DateTime.UtcNow.AddDays(7),
IsPersistent = true,
});
ToClaimsIdentity extension method:
public static ClaimsIdentity ToClaimsIdentity(this SecurityUser user, string authenticantionType, int auditUserID)
{
var claims = new List<Claim>()
{
new Claim(ClaimTypes.NameIdentifier, user.UserID.ToString()),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Name, user.FirstName),
new Claim(SecurityUserClaimTypes.AuditUserID, auditUserID.ToString())
};
var identity = new ClaimsIdentity(claims, authenticantionType);
return identity;
}
Any help would be greatly appreciated.
Edit - This is what I am taking about 👇
Thanks for your help guys!
I finally realised it was a client thing, I did three things:
CORS was an issue, in my .UseCors method call my my Api I allowed credentials:
.AllowCredentials()
My client app in using Blazor, I found this article here which told me I needed to set the http request configuration to include credentials, so in my client side app startup.cs:
WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include;
I am using Http not Https on my local, and Chrome was complaining about SameSite, so im my Api StartUp.cs, where I call AddAuthentication...AddCookie I added this:
options.Cookie.SameSite = SameSiteMode.Unspecified;
I don't fully understand the SameSite... and I have also come across JSON Web Tokens (JWT).
But I'm not interested, as long as it's working. ;-)
I'm creating applications that are authorized by a personal run OIDC server.
The server is using Openiddict library, and the applications are using OWIN for the configuration. this is because the OIDC server is running on .Net core and the applications on .Net framework.
When trying to log out from these applications I redirect to the OIDC server /Account/Logout, this will in turn get all the logged in applications and open an iframe with the front channel logout url (/signout-oidc).
When logging out, it will give a 404 not found, meaning that the url "example.com/signout-oidc" has not been created.
The used libraries for the application are:
Microsoft.Owin.Security
Microsoft.Owin.Security.Cookies
Microsoft.Owin.Security.OpenIdConnect
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.Use(async (Context, next) =>
{
Debug.WriteLine("1 ==>request, before cookie auth");
await next.Invoke();
Debug.WriteLine("6 <==response, after cookie auth");
});
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.Use(async (Context, next) =>
{
Debug.WriteLine("2 ==>after cookie, before OIDC");
await next.Invoke();
Debug.WriteLine("5 <==after OIDC");
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions()
{
ClientId = "application-client-id",
ClientSecret = "application-client-secret",
Scope = "openid profile email",
Authority = "personal-oidc-link",
AuthenticationMode = AuthenticationMode.Active,
ResponseType = OpenIdConnectResponseType.IdToken,
RedirectUri = "https://example.com/signin-oidc",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = ClaimTypes.NameIdentifier
},
});
app.Use(async (Context, next) =>
{
Debug.WriteLine("3 ==>after OIDC, before leaving the pipeline");
await next.Invoke();
Debug.WriteLine("4 <==after entering the pipeline, before OIDC");
});
The server configuration:
services.AddOpenIddict()
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and entities.
options.UseEntityFrameworkCore()
.UseDbContext<DataContext>()
.ReplaceDefaultEntities<CompanyApplication, CompanyAuthorization, CompanyScope, CompanyToken, Guid>();
})
.AddServer(options =>
{
options.UseMvc();
options.UseJsonWebTokens();
options.AddEphemeralSigningKey("RS512");
// options.AddDevelopmentSigningCertificate();
if (this.environment.IsDevelopment())
{
options.DisableHttpsRequirement();
}
// Enable the authorization, logout, token and userinfo endpoints.
options
.EnableAuthorizationEndpoint("/oidc/authorize")
.EnableLogoutEndpoint("/Account/Logout")
.EnableTokenEndpoint("/oidc/token")
.EnableUserinfoEndpoint("/oidc/userinfo");
options
.AllowAuthorizationCodeFlow()
.AllowImplicitFlow()
.AllowRefreshTokenFlow();
options.RegisterClaims(
CompanyClaims.FriendlyName,
CompanyClaims.Email,
CompanyClaims.EmailVerified,
CompanyClaims.Sub,
CompanyClaims.Group,
CompanyClaims.GivenName,
CompanyClaims.MiddleName,
CompanyClaims.FamilyName);
// Mark the "email", "profile" and "roles" scopes as supported scopes.
options.RegisterScopes(
OpenIddictConstants.Scopes.Email,
OpenIddictConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Roles);
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(
CookieAuthenticationDefaults.AuthenticationScheme,
o =>
{
o.AccessDeniedPath = "/Home/Denied";
o.LoginPath = "/Account/Login";
o.LogoutPath = "/Account/Logout";
o.Cookie.SameSite = SameSiteMode.Strict;
o.Cookie.Name = "session";
o.Cookie.Expiration = TimeSpan.FromHours(24);
o.ExpireTimeSpan = TimeSpan.FromHours(24);
});
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseResponseCaching();
app.UseMvcWithDefaultRoute();
}
I expect that OWIN generates the /signout-oidc route, and when called it deletes the authentication cookie.
Edit: Added some more configuration files.
You need to use
PostLogoutRedirectUri = ""
I've no experience with the openiddict library here. But I'm using the IdentityServer library.
I've learned that in the OpenId Connect flow to remove the cookies using the FrontChannel logout you need to:
o.Cookie.SameSite = SameSiteMode.None;
By doing this, the GET request to /signout-oidc, initiated by your OpenId server will contain the authentication cookie of the user currently logging out. So, the middleware handling the /signout-oidc request knows which user is doing the logout and is able to logout that user.
You can use the Developer Tools in your browser (don't forget to enable 'Preserve log' when using Chrome) to find the GET /signout-oidc request and see if it contains the authentication cookie.
A second option is to choose using a backchannel logout flow, don't know if the openiddict supports this.
recently we have migrated our project from asp.net to asp.net core, the project was working fine in asp.net, We have followed the migration document of Microsoft for migrating "asp.net to asp.net core" and modified accordingly, unfortunately, OnRedirectToIdentityProvider method in startup class is not working on challenge call (from the controller). It would be great if someone helps me to figure out where my code is wrong. I have been looping on the same issue for a while. thanks in advance.
StartUp.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
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.None;
});
//Add a strongly-typed options class to DI
services.Configure<AuthOptions>(Configuration.GetSection("Authentication"));
services.AddAuthentication(opt => {
opt.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie("MiddleWareCookie")
.AddOpenIdConnect(options => Configuration.Bind("Authentication", options));
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = Configuration["Authentication:Authority"];
options.ClientId= Configuration["Authentication:ClientId"];
options.ClientSecret= Configuration["Authentication:ClientSecret"];
options.TokenValidationParameters = new TokenValidationParameters
{
// Instead of using the default validation (validating against a single issuer value, as we do in
// line of business apps), we inject our own multitenant validation logic
ValidateIssuer = false,
// If the app is meant to be accessed by entire organizations, add your issuer validation logic here.
//IssuerValidator = (issuer, securityToken, validationParameters) => {
// if (myIssuerValidationLogic(issuer)) return issuer;
//}
};
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = (context) =>
{
object obj = null;
var request = context.Request;
if (context.HttpContext.Items.TryGetValue("Authority", out obj))
{
string authority = obj as string;
if (authority != null)
{
context.ProtocolMessage.IssuerAddress = authority;
}
}
//string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
string appBaseUrl = #"https://localhost:44359/";//UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase);
Debug.WriteLine($"appBaseUrl: {appBaseUrl}");
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
context.ProtocolMessage.Prompt = "select_account";
context.ProtocolMessage.Resource = Configuration["Authentication:AzureResourceManagerIdentifier"];
return Task.FromResult(0);
},
OnAuthorizationCodeReceived = async (context) =>
{
var request = context.HttpContext.Request;
var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);
var credential = new ClientCredential(context.Options.ClientId, context.Options.ClientSecret);
string tenantId = context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
//Comment
Debug.WriteLine($"tenantID: {tenantId}");
// Revisit
string signedInUserUniqueName = context.Principal.FindFirst(ClaimTypes.Name).Value.Split('#')[context.Principal.FindFirst(ClaimTypes.Name).Value.Split('#').Length - 1];
//Comment
Debug.WriteLine($"tenantID: {signedInUserUniqueName}");
var tokenCache = new ADALTokenCache(signedInUserUniqueName);
tokenCache.Clear();
// revisit
AuthenticationContext authContext = new AuthenticationContext(string.Format("https://login.microsoftonline.com/{0}", tenantId), tokenCache);
// var items = authContext.TokenCache.ReadItems().ToList();
// revisit
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(
context.ProtocolMessage.Code, new Uri(currentUri), credential);
//Tell the OIDC middleware we got the tokens, it doesn't need to do anything
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
},
OnTokenValidated = (context) => {
string issuer = context.Principal.FindFirst("iss").Value;
if (issuer != null)
{
if (!issuer.StartsWith("https://sts.windows.net/"))
throw new SecurityTokenValidationException();
}
return Task.FromResult(0);
},
OnTicketReceived = context =>
{
// If your authentication logic is based on users then add your logic here
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Response.Redirect("/Error");
context.HandleResponse(); // Suppress the exception
return Task.CompletedTask;
},
// If your application needs to do authenticate single users, add your user validation below.
//OnTokenValidated = context =>
//{
// return myUserValidationLogic(context.Ticket.Principal);
//}
};
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/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.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
Subscription Controller Method
HttpContext.Items.Add("Authority", string.Format(_authOptions.Authority + "OAuth2/Authorize", _directoryId));
Dictionary<string, string> dict = new Dictionary<string, string>();
dict["prompt"] = "select_account";
var userIdentity = new ClaimsIdentity(User.Claims, "login");
ClaimsPrincipal principal = new ClaimsPrincipal(userIdentity);
await HttpContext.AuthenticateAsync("MiddleWareCookie");
I can see two things that explain why OnRedirectToIdentityProvider is not called based on your controller code:
you're calling the AuthenticateAsync method where your question states you're calling Challenge;
you're referring to the cookie authentication scheme (MiddleWareCookie), where I think your intent is to trigger an OpenID Connect login.
I believe you need to replace HttpContext.AuthenticateAsync("MiddleWareCookie") with HttpContext.ChallengeAsync() to have the OIDC login request be triggered.
Another potential issue I can see is that you set, in the AddAuthentication method, the DefaultScheme to CookieAuthenticationDefaults.AuthenticationScheme, but the name of your cookie authentication scheme is MiddleWareCookie. These two need to be in sync.
I'm attempting to authorize requests to my API which bear a JWT token attached to it, however, none of the tutorials, blog posts, and documentation have helped avoiding a constant 403 - Unauthorized error.
This is the -skimmed- current configuration:
Class which generates the token: TokenManagement.cs:
// Add the claims to the token
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, credentials.Username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim("claimName", "claimValue")
};
Configuring the services: Startup.cs - ConfigureServices():
services.Configure<GzipCompressionProviderOptions>(options => options.Level = System.IO.Compression.CompressionLevel.Optimal);
services.AddResponseCompression();
services.AddAuthentication()
.AddJwtBearer(config => {
config.RequireHttpsMetadata = false;
config.SaveToken = true;
config.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = "Issuer",
ValidAudience = "Audience",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(symmetricKey))
};
});
services.AddAuthorization(options => {
options.AddPolicy("myCustomPolicy", policy => {
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim("claimName", "claimValue");
});
});
services.AddMvc();
General Configuration: Startup.cs - Configure():
app.UseAuthentication();
app.Use(async (context, next) => {
await next();
if (context.Response.StatusCode == 404 &&
!Path.HasExtension(context.Request.Path.Value)) {
context.Request.Path = "/index.html";
await next();
}
});
app.UseMvc();
app.UseResponseCompression();
app.UseDefaultFiles();
app.UseStaticFiles();
Controller which should be authorized: ActionsController.cs:
[Authorize(Policy = "myCustomPolicy")]
[Route("api/[controller]")]
public class ActionsController : Controller
Any request I send to the server (which carries a JWT token with the proper claim), returns as a 403.
Any methods which have the [AllowAnonymous] attribute, work just fine.
Is there a way to -at least- debug and see what's going on?
I found out that some claim types changed to different values from my identity server config.
for example , In my Identity Server i am using role claim type:
UserClaims = new []
{
JwtClaimTypes.Role , user.role // "JwtClaimTypes.Role" yield "role"
};
But when i debuged my web api , the role claim type has changed to (see my snapshot below, under watch section):
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
Solution:
To "workaround" (is this desired behavior?) the issue, you need to check your claim type
value are planning use in web api, and use the correct claim type value in your policy.
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdmin", policy =>
{
//policy.RequireClaim(IdentityModel.JwtClaimTypes.Role, "Admin"); // this doesn't work
policy.RequireClaim(ClaimTypes.Role, "Admin"); // this work
});
});
my web api debug snapshot:
Try to enable CORS in Startup.cs File
public void ConfigureAuth(IAppBuilder app) {
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
// Rest of code
}
After upgrading my ASP.NET Core project to 2.0, attempts to access protected endpoints no longer returns 401, but redirects to an (non-existing) endpoint in an attempt to let the user authenticate.
The desired behaviour is for the application simply to return a 401. Previously I would set AutomaticChallenge = false when configuring authentication, but according to this article the setting is no longer relevant (in fact it doesn't exist anymore).
My authentication is configured like this:
Startup.cs.ConfigureServices():
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.Cookie.Name = options.CookieName;
o.Cookie.Domain = options.CookieDomain;
o.SlidingExpiration = true;
o.ExpireTimeSpan = options.CookieLifetime;
o.TicketDataFormat = ticketFormat;
o.CookieManager = new CustomChunkingCookieManager();
});
Configure():
app.UseAuthentication();
How can I disable automatic challenge, so that the application returns 401 when the user is not authenticated?
As pointed out by some of the other answers, there is no longer a setting to turn off automatic challenge with cookie authentication. The solution is to override OnRedirectToLogin:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.Headers["Location"] = context.RedirectUri;
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
This may change in the future: https://github.com/aspnet/Security/issues/1394
After some research, I found we can deal with this problem though the bellow approach:
We can add two Authentication scheme both Identity and JWT; and use Identity scheme for authentication and use JWT schema for challenge, JWT will not redirect to any login route while challenge.
services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication((cfg =>
{
cfg.DefaultScheme = IdentityConstants.ApplicationScheme;
cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})).AddJwtBearer();
Similiar to #Serverin, setting the OnRedirectToLogin of the Application Cookie worked, but must be done in statement following services.AddIdentity in Startup.cs:ConfigureServices:
services.ConfigureApplicationCookie(options => {
options.Events.OnRedirectToLogin = context => {
context.Response.Headers["Location"] = context.RedirectUri;
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
According to this article:
In 1.x, the AutomaticAuthenticate and AutomaticChallenge properties were intended to be set on a single authentication scheme. There was no good way to enforce this.
In 2.0, these two properties have been removed as flags on the individual AuthenticationOptions instance and have moved into the base AuthenticationOptions class. The properties can be configured in the AddAuthentication method call within the ConfigureServices method of Startup.cs
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
Alternatively, use an overloaded version of the AddAuthentication method to set more than one property. In the following overloaded method example, the default scheme is set to CookieAuthenticationDefaults.AuthenticationScheme. The authentication scheme may alternatively be specified within your individual [Authorize] attributes or authorization policies.
services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});
Define a default scheme in 2.0 if one of the following conditions is true:
You want the user to be automatically signed in
You use the [Authorize] attribute or authorization policies without specifying
schemes
An exception to this rule is the AddIdentity method. This method adds cookies for you and sets the default authenticate and challenge schemes to the application cookie IdentityConstants.ApplicationScheme. Additionally, it sets the default sign-in scheme to the external cookie IdentityConstants.ExternalScheme.
Hope this help you.
This is the source code of CookieAuthenticationEvents.OnRedirectToLogin :
public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToLogin { get; set; } = context =>
{
if (IsAjaxRequest(context.Request))
{
context.Response.Headers["Location"] = context.RedirectUri;
context.Response.StatusCode = 401;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
};
You can add "X-Requested-With: XMLHttpRequest" Header to the request while making API calls from your client.
I found that in most cases the solution is to override
OnRedirectToLogin
But in my app I was using multiple authentication policies and overriding of the OnRedirectToLogin did not work for me. The solution in my case it was to add a simple middleware to redirect the incoming request.
app.Use(async (HttpContext context, Func<Task> next) => {
await next.Invoke(); //execute the request pipeline
if (context.Response.StatusCode == StatusCodes.Status302Found && context.Response.Headers.TryGetValue("Location", out var redirect)) {
var v = redirect.ToString();
if (v.StartsWith($"{context.Request.Scheme}://{context.Request.Host}/Account/Login")) {
context.Response.Headers["Location"] = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}";
context.Response.StatusCode = 401;
}
}
});
Another way to do this which is more DI/testing-friendly is to use AuthenticationSchemeOptions.EventsType (another answer points at it here). This will allow you to pull other components into the resolution process.
Here's an example including registration and resolution which stops the default redirect to login on an unauthenticated request, and instead just returns with a hard 401. It also has a slot for any other dependencies which may need to know about unauthenticated requests.
In Startup.cs:
services
.AddAuthentication("MyAuthScheme")
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.EventsType = typeof(MyEventsWrapper);
};
...
services.AddTransient<MyEventsWrapper>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
Then, in MyEventsWrapper.cs:
public class MyEventsWrapper : CookieAuthenticationEvents
{
private readonly IHttpContextAccessor _accessor;
private readonly IDependency _otherDependency;
public MyEventsWrapper(IHttpContextAccessor accessor,
IDependency otherDependency)
{
_accessor = accessor;
_otherDependency = otherDependency;
}
public override async Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
{
context.Response.Headers.Remove("Location");
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await _otherDependency.Cleanup(_accessor.HttpContext);
}
}
I'm not sure how to generate the 401 error, however if you use the:
o.AccessDeniedPath = "{path to invalid}";
This will allow you to redirect somewhere when the challenge has failed.