I am using asp.net Core 2.0 with various external authentications (Google, Twitter, etc).
Everything is working, and I get a validated response back with the OnTicketReceived event, however, all subsequent calls to the site do not persist the user.
Here is my startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultTokenProviders();
services.AddAuthentication()
.AddGoogle(googleOptions =>
{
googleOptions.ClientId = Configuration["Google:ClientId"];
googleOptions.ClientSecret = Configuration["Google:ClientSecret"];
googleOptions.CallbackPath = "/auth/google";
googleOptions.SaveTokens = true;
googleOptions.Events = new OAuthEvents {OnTicketReceived = async e => await HandleOnTicketReceived(authService, e),};
})
.AddTwitter(twitterOptions =>
{
twitterOptions.ConsumerKey = Configuration["Twitter:ClientId"];
twitterOptions.ConsumerSecret = Configuration["Twitter:ClientSecret"];
twitterOptions.CallbackPath = "/auth/twitter";
twitterOptions.RetrieveUserDetails = true;
twitterOptions.SaveTokens = true;
twitterOptions.Events = new TwitterEvents {OnTicketReceived = async e => await HandleOnTicketReceived(authService, e),};
});
services.AddMvc();
}
This all seems to work great. I get all the necessary data from each OAuth provider. Here is an example:
properties
private async Task HandleOnTicketReceived(IAuthService authService, TicketReceivedContext e)
{
var identity = e.Principal.Identity as ClaimsIdentity;
// HERE identity is correct for all external auth providers, so it is working this far
}
The problem is every subsequent call to the site has lost the user's external identity. What was there is now gone.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var identity = User.Identity as ClaimsIdentity;
// HERE is the problem -- on all subsequent calls, identity is basically empty
base.OnActionExecuting(filterContext);
}
How can I get the user identity to persist across calls? The cookie "Identity.External" is set and seems correct.
Edit:
I have also added a sql backend with
services.AddDbContext<ApplicationDbContext>();
and
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
The AspNet user tables are created but never populated. I am at a loss for what is missing here.
I should clarify that i am using dbContext.Database.Migrate() to create the authentication tables, so I have confidence the ApplicationDbContext is working. However, there is no error thrown, the tables just don't get populated and subsequent calls just don't get the user's identity set.
Related
I use this repo to implement authentication and authorization with cookie on the Blazor Server according to ^.
I know that I couldn't use HttpContextAccessor because of Microsoft recommendations, However, I use the HttpContext in the .cshtml file for login and logout same as this link, not in the Blazor component.
The problem in production phase on windows server IIS server (2022) is that if the first user logs in to the application with his username, the rest of the users are also authenticated in their computer browser with the same first username. Now, if the second user enters with his username, then all users will be authenticated with the second user's name and so on. Now, if a user presses the logout key, he will be logged out and his cookie will be deleted from the browser, but after refreshing the browser page once, he will log in again with the last verified logged-in user without going through the login process. Even then, there are no cookies in his browser. For better clarification, I have put a gif of how it works(Of course, in order to record the screen in the laboratory environment, I have simulated it on the IIS Express).
I can almost guess that the problem is the use of the HttpContext. But all the implementations that I saw with cookies on the web for Blazor Server, none of them mentioned this problem. So I guessed that I might have made a mistake in the implementation somewhere.
Is there a way to solve my problem with the solution provided?
I have already seen the answers to these questions ^, ^, ^, However, None is the answer to my question.
I use .Net6. It's my authentication configuration on the startup.cs:
services.AddScoped<IUnitOfWork, ApplicationDbContext>();
services.AddScoped<IUsersService, UsersService>();
services.AddScoped<IRolesService, RolesService>(); services.AddScoped<ISecurityService, SecurityService>();
services.AddScoped<ICookieValidatorService, CookieValidatorService>();
services.AddScoped<IDbInitializerService, DbInitializerService>();
services.AddSingleton<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
services
.AddAuthentication(options =>
{
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.SlidingExpiration = false;
options.LoginPath = "/";
options.LogoutPath = "/";
//options.AccessDeniedPath = new PathString("/Home/Forbidden/");
options.Cookie.Name = ".my.app1.cookie";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
return cookieValidatorService.ValidateAsync(context);
}
};
});
Update:(The problem solved)
I had used a class like below to get the current user.
public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
private readonly IServiceScopeFactory _scopeFactory;
public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory)
: base(loggerFactory) =>
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user from a new scope to ensure it fetches fresh data
var scope = _scopeFactory.CreateScope();
try
{
var userManager = scope.ServiceProvider.GetRequiredService<IUsersService>();
return await ValidateUserAsync(userManager, authenticationState?.User);
}
finally
{
if (scope is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
private async Task<bool> ValidateUserAsync(IUsersService userManager, ClaimsPrincipal? principal)
{
if (principal is null)
{
return false;
}
var userIdString = principal.FindFirst(ClaimTypes.UserData)?.Value;
if (!int.TryParse(userIdString, out var userId))
{
return false;
}
var user = await userManager.FindUserAsync(userId);
return user is not null;
}
}
This class is injected into the project in the following way.
services.AddSingleton<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
And the problem is exactly from here. This class should not be injected into Blazor Server project as AddSingleton because I used the .cshtml file to log in and logout. If I change it to AddScoped the problem will be solved. You can see the reason for this here. However, The previous problem that was in this question came back again :((.
I think the problem with static. Try this way to add your policy:
services.AddAuthorization(config =>
{
config.AddPolicy("Admin", policy => policy.RequireClaim("IsAdmin", "true"));
});
In controller you can use this:
[Authorize(Policy = “IsAdmin”)]
public IActionResult LogIn()
{
// ...
}
In Component:
#page "/somepage"
#attribute [Authorize(Policy = "IsAdmin")]
Or
<AuthorizeView Policy="IsAdmin">
<p>You can only see this if you are admin</p>
</AuthorizeView>
In order to get group membership functionality in our Blazor app based on Azure B2C, I add some claims in the Server project in Startup.cs. When I inspect the User object in the razor pages, the claims I added are not present.
Startup.cs, public void Configure():
app.Use(async (context, next) =>
{
if (context.User != null && context.User.Identity.IsAuthenticated)
{
var groups = GetGroupsFromAzureB2c(context.User);
// Attempt A, separate Identity
ClaimsIdentity i = new(context.User.Identity);
i.AddClaims(groups.Select(g => new Claim("Group", g)));
context.User.AddIdentity(i);
// Attempt B: Adding claims to the existing Identity
((ClaimsIdentity)context.User.Identity).AddClaims(groups.Select(g => new Claim("Group", g));
}
await next();
});
page.razor, protected override async Task OnInitializedAsync():
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
var _claims = user.Claims;
}
_claims only holds the 12 claims that were added automatically, never the 4 claims I added in Startup.cs.
Are there different instances of the User object? Am I adding the claims incorrectly?
Apparently there are two separate Identities at play in a Blazor app. One in the Client code and a separate one in the Server code.
We fixed this by adding a CustomUserFactory that inherits from AccountClaimsPrincipalFactory<RemoteUserAccount>. We retrieve the AzureB2C roles from a central method using the Graph API for the client side which we call in Program.cs with
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("xxx");
})
// here we hook up our custom factory
.AddAccountClaimsPrincipalFactory<CustomUserFactory>();
For the server side code, we use the same central method to get the roles from AzureB2C which we call from Startup.cs with
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options => {
options.Events.OnTokenValidated = async context => {/*add roles here*/}
i've created an API and set up JWT auth from the same API (I chose not to use IdentityServer4).
I did this through services.AddAuthentication
And then I created tokens in the controller and it works.
However I now want to add registration etc. But i prefer not to write my own code for hashing passwords, handling registration emails etc.
So I came across ASP.NET Core Identity and it seems like what I need, except from that it adds some UI stuff that I dont need (because its just an API and the UI i want completely independent).
But on MSDN is written:
ASP.NET Core Identity adds user interface (UI) login functionality to
ASP.NET Core web apps. To secure web APIs and SPAs, use one of the
following:
Azure Active Directory
Azure Active Directory B2C (Azure AD B2C)
IdentityServer4
So is it really a bad idea to use Core Identity just for hashing and registration logic for an API? Cant I just ignore the UI functionality? It's very confusing because I'd rather not use IdentityServer4 or create my own user management logic.
Let me just get off my chest that the bundling Identity does with the UI, the cookies and the confusing various extension methods that add this or that, but don't add this or that, is pretty annoying, at least when you build modern web APIs that need no cookies nor UI.
In some projects I also use manual JWT token generation with Identity for the membership features and user/password management.
Basically the simplest thing to do is to check the source code.
AddDefaultIdentity() adds authentication, adds the Identity cookies, adds the UI, and calls AddIdentityCore(); but has no support for roles:
public static IdentityBuilder AddDefaultIdentity<TUser>(this IServiceCollection services, Action<IdentityOptions> configureOptions) where TUser : class
{
services.AddAuthentication(o =>
{
o.DefaultScheme = IdentityConstants.ApplicationScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies(o => { });
return services.AddIdentityCore<TUser>(o =>
{
o.Stores.MaxLengthForKeys = 128;
configureOptions?.Invoke(o);
})
.AddDefaultUI()
.AddDefaultTokenProviders();
}
AddIdentityCore() is a more stripped down version that only adds basic services, but it doesn't even add authentication, and also no support for roles (here you can already see what individual services are added, to change/override/remove them if you want):
public static IdentityBuilder AddIdentityCore<TUser>(this IServiceCollection services, Action<IdentityOptions> setupAction)
where TUser : class
{
// Services identity depends on
services.AddOptions().AddLogging();
// Services used by identity
services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
// No interface for the error describer so we can add errors without rev'ing the interface
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser>>();
services.TryAddScoped<UserManager<TUser>>();
if (setupAction != null)
{
services.Configure(setupAction);
}
return new IdentityBuilder(typeof(TUser), services);
}
Now that kind of makes sense so far, right?
But enter AddIdentity(), which appears to be the most bloated, the only one that supports roles directly, but confusingly enough it doesn't seem to add the UI:
public static IdentityBuilder AddIdentity<TUser, TRole>(
this IServiceCollection services,
Action<IdentityOptions> setupAction)
where TUser : class
where TRole : class
{
// Services used by identity
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddCookie(IdentityConstants.ApplicationScheme, o =>
{
o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
})
.AddCookie(IdentityConstants.ExternalScheme, o =>
{
o.Cookie.Name = IdentityConstants.ExternalScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
})
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
o.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
};
})
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
});
// Hosting doesn't add IHttpContextAccessor by default
services.AddHttpContextAccessor();
// Identity services
services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
// No interface for the error describer so we can add errors without rev'ing the interface
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
services.TryAddScoped<UserManager<TUser>>();
services.TryAddScoped<SignInManager<TUser>>();
services.TryAddScoped<RoleManager<TRole>>();
if (setupAction != null)
{
services.Configure(setupAction);
}
return new IdentityBuilder(typeof(TUser), typeof(TRole), services);
}
All in all what you probably need is the AddIdentityCore(), plus you have to use AddAuthentication() on your own.
Also, if you use AddIdentity(), be sure to run your AddAuthentication() configuration after calling AddIdentity(), because you have to override the default authentication schemes (I ran into problems related to this, but can't remember the details).
(Another tidbit of information that might be interesting for people reading this is the distinction between SignInManager.PasswordSignInAsync(), SignInManager.CheckPasswordSignInAsync() and UserManager.CheckPasswordAsync(). These are all public methods you can find and call for authorization purposes. PasswordSignInAsync() implements two-factor signin (also sets cookies; probably only when using AddIdentity() or AddDefaultIdentity()) and calls CheckPasswordSignInAsync(), which implements user lockout handling and calls UserManager.CheckPasswordAsync(), which just checks the password. So to get a proper authentication it's better not to call UserManager.CheckPasswordAsync() directly, but to do it through CheckPasswordSignInAsync(). But, in a single-factor JWT token scenario, calling PasswordSignInAsync() is probably not needed (and you can run into redirect issues). If you have included UseAuthentication()/AddAuthentication() in the Startup with the proper JwtBearer token schemes set, then the next time the client sends a request with a valid token attached, the authentication middleware will kick in, and the client will be 'signed in'; i.e. any valid JWT token will allow client to access controller actions protected with [Authorize].)
And IdentityServer is thankfully completely separate from Identity. In fact the decent implementation of IdentityServer is to use it as a standalone literal identity server that issues tokens for your services. But since ASP.NET Core has no token generation capability built-in, a lot of people end up running this bloated server inside their apps just to be able to use JWT tokens, even though they have a single app and they have no real use for a central authority. I don't mean to hate on it, it's a really great solution with a lot of features, but it would be nice to have something simpler for the more common use cases.
You just configure Identity to use JWT bearer tokens. In my case I'm using encrypted token, so depending on your use-case you may want to adjust the configuration:
// In Startup.ConfigureServices...
services.AddDefaultIdentity<ApplicationUser>(
options =>
{
// Configure password options etc.
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Configure authentication
services.AddAuthentication(
opt =>
{
opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
TokenDecryptionKey =
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my key")),
RequireSignedTokens = false, // False because I'm encrypting the token instead
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
// Down in Startup.Configure add authn+authz middlewares
app.UseAuthentication();
app.UseAuthorization();
Then generate a token when the user wants to sign in:
var encKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my key"));
var encCreds = new EncryptingCredentials(encKey, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512);
var claimsIdentity = await _claimsIdentiyFactory.CreateAsync(user);
var desc = new SecurityTokenDescriptor
{
Subject = claimsIdentity,
Expires = DateTime.UtcNow.AddMinutes(_configuration.Identity.JwtExpirationMinutes),
Issuer = _configuration.Identity.JwtIssuer,
Audience = _configuration.Identity.JwtAudience,
EncryptingCredentials = encCreds
};
var token = new JwtSecurityTokenHandler().CreateEncodedJwt(desc);
// Return it to the user
You can then use the UserManager to handle creating new users and retrieving users, while SignInManager can be used to check for valid login/credentials before generating the token.
Summary
This is my first try with OAuth2 and External Login Mechanisms.
I'm creating a WebApp that will expose API features through a user-friendly UI.
In order to make API calls, I need to receive an access token from QBO that grants access to resources.
So, my WebApp has an external login option which I use to authenticate against QBO, and then authorize my app.
Everything works fine until...
Services Configuration
Based on a tutorial for GitHub authentication, I came up with this.
services.AddAuthentication(o => {
o.DefaultAuthenticateScheme = IdentityConstants.ExternalScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
o.DefaultChallengeScheme = IdentityConstants.ExternalScheme;
})
.AddOAuth("qbo", "qbo", o => {
o.CallbackPath = new PathString("/signin-qbo");
o.ClientId = Configuration["ecm.qbo.client-id"];
o.ClientSecret = Configuration["ecm.qbo.client-secret"];
o.SaveTokens = true;
o.Scope.Add("openid");
o.Scope.Add("profile");
o.Scope.Add("email");
o.Scope.Add("com.intuit.quickbooks.accounting");
o.AuthorizationEndpoint = Configuration["ecm.qbo.authorization-endpoint"];
o.TokenEndpoint = Configuration["ecm.qbo.token-endpoint"];
o.UserInformationEndpoint = Configuration["ecm.qbo.user-info-endpoint"];
o.Events.OnCreatingTicket = async context => {
var companyId = context.Request.Query["realmid"].FirstOrDefault() ?? throw new ArgumentNullException("realmId");
var accessToken = context.AccessToken;
var refreshToken = context.RefreshToken;
Configuration["ecm.qbo.access-token"] = accessToken;
Configuration["ecm.qbo.refresh-token"] = refreshToken;
Configuration["ecm.qbo.realm-id"] = companyId;
context.Backchannel.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
context.Backchannel.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await context.Backchannel.GetStringAsync(context.Options.UserInformationEndpoint);
var result = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
var user = (ClaimsIdentity)context.Principal.Identity;
user.AddClaims(new Claim[] {
new Claim("access_token", accessToken),
new Claim("refresh_token", refreshToken),
new Claim(ClaimTypes.GivenName, result["givenName"]),
new Claim(ClaimTypes.Surname, result["familyName"]),
new Claim(ClaimTypes.Email, result["email"]),
new Claim(ClaimTypes.Name, result["givenName"]+" "+result["familyName"])
});
};
});
This works. I can add my claims based on user information, the context.Principal.Identity indicates that it's authenticated.
For some reasons, it seems to try and redirect to `/Identity/Account/Login?returnUrl=%2F. Why is that?
Login page redirection
Here, I don't get why I get the redirection and this confuses me a lot. So, I added the AccountController just to try and shut it up.
namespace ecm.backoffice.Controllers {
[Authorize]
[Route("[controller]/[action]")]
public class AccountController : Controller {
[AllowAnonymous]
[HttpGet]
public IActionResult Login(string returnUrl = "/") {
return Challenge(new AuthenticationProperties { RedirectUri = returnUrl });
}
[Authorize]
[HttpGet]
public async Task<IActionResult> Logout(string returnUrl = "/") {
await Request.HttpContext.SignOutAsync("qbo");
return Redirect(returnUrl);
}
}
}
And this creates more confusion than it solves, actually. I'm lost here...
Apply Migrations
This Individual User Authentication WebApp seems to use Identity which looks like it creates a lot of behind the scene mechanisms. I first tried to register to my app, and had to "Apply Migrations", which I totally get, since the data model wasn't initialized.
So, I clicked the Apply Migrations button. I though I was okay with this...
Entity Framework Core
I am aware that the app is using Entity Framework Core for its persistence mechanism, hence the registration process, etc. And to configure it, I needed to add these lines to the services configs.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
It looks like it just won't work at all.
Thoughts
At this point, I think that the message says it Entity Framework just can't load the user information from its underlying data store. I totally understand that, and I don't want to register this user. I just want to take for granted that if QBO authenticated the user, it's fine by me and I grant open bar access to the WebApp features, even if the user ain't registered.
How to tell that to my WebApp?
Related Q/A I read prior to ask
Prevent redirect to /Account/Login in asp.net core 2.2
ASP.NET Core (2.1) Web API: Identity and external login provider
asp.net core 2.2 redirects to login after successful sign in
External Login Authentication in Asp.net core 2.1
And many more...
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.