Recently I have migrated an API from .net core 2.2 to .net core 3.0 in order to implement an authentication using certificates.
I have followed this Microsoft documentations : https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.0 to do my work. But I'm facing an issue :
When I call a controller Method decorated with an [Authorize] attribute, the certificate validation is never performed. If there is no certificate in the request's header, I get a 403 which is the required behavior, but If I put a certificate the supposed behavior should be the verification by the thumbprint, but nothing...
Here my certificate validation service :
public class CertificateValidationService
{
public bool ValidateCertificate(X509Certificate2 clientCertificate, string thumbprintToChek) =>
clientCertificate.Thumbprint.Equals(thumbprintToChek);
}
As said in the documentation, in StartUp.cs I have set up the following lines of code inside the ConfigureServices method
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options => // code from ASP.NET Core sample
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService = context.HttpContext.RequestServices.GetService<CertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate, Configuration.GetValue<string>("Apim:CertificateThumbprint")))
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
else
{
context.Fail("Invalid certificate.");
}
return Task.CompletedTask;
}
};
options.Events = new CertificateAuthenticationEvents
{
OnAuthenticationFailed = context =>
{
context.Fail("Certificate not valid.");
return Task.CompletedTask;
}
};
});
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-ARR-ClientCert";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if (!string.IsNullOrWhiteSpace(headerValue))
{
byte[] bytes = headerValue.ToByteArray();
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
I have also populated the Configure method with the following code :
app.UseCertificateForwarding();
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
So when I make a request to the API, the CertificateForwarding delegation is well called, but Authentication delegation, never.
However, I have tried to implement this authentication in a brand new project (just for tests) it worked perfectly.
Your assignment to options.Events is done twice. This way the default implementation of OnCertificateValidated overwrites your eventhandler and your CertificateValidationService will never be called.
Combining the two eventhandlers should give the expected result:
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
// Your implementation here.
},
OnAuthenticationFailed = context =>
{
// Your implementation here.
}
};
Related
We have a WebAPI webapp which should support authentication via client certificate and authentication via Microsoft Identity/OAuth2. With our current implementation, it seems that the Microsoft Identity Authentication overrules the Client Certificate Authentication. If we just add the Microsoft Identity Authentication in first place, then the overruling behaves the opposite round.
The Program.cs for authentication looks currently like this:
var builder = WebApplication.CreateBuilder(args);
// 1. setup Certificate Authentication
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options =>
{
// https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth
options.AllowedCertificateTypes = CertificateTypes.All;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
// since with that authentication we have no retrieved authorization from anywhere, we set it explicitly - for now ...
new Claim("scp", "GeneralScope")
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
},
OnChallenge = x =>
{
return Task.CompletedTask;
}
};
});
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(opts =>
{
opts.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
opts.ClientCertificateValidation = (cert, chain, policyErrors) =>
{
// TODO validate certificate
return true;
};
});
});
// 2. setup Microsoft Identity Authentication
var jwtAuthentication = builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
jwtAuthentication.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
jwtAuthentication.AddAppServicesAuthentication();
....
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Dont be confused about the sloppy client certificate validation, this is still work in progress.
How do we need to change the code so we can have the two authentication modes along each other with a either-or way?
I previously have a working project that uses IdentityServer and .Net Core 2.2
I recently did an upgrade on the project's .Net version from Core 2.2 to .Net 5. Also updated every packages into the latest version. Did some tweaks on my code and finally got rid of all the errors. I'm testing my Identity server functionality on my WEB API, and for some reason, the HttpContext.User.Claims return empty.
Here's my code for it.
public Guid? CurrentUserId
{
get
{
var claimNameIdentifier = User.Claims.FirstOrDefault(a => a.Type == ClaimTypes.NameIdentifier)?.Value;
return claimNameIdentifier != null ? Guid.Parse(claimNameIdentifier) : (Guid?)null;
}
}
I have this on a base controller to be implemented by my endpoint controllers to get the user id on the JWT accessing my endpoint. Not sure if this is due to the updates as this was working before.
Here's my Config for my identity server
public static IEnumerable<IdentityResource> IdentityResources =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
public static IEnumerable<ApiResource> ApiResources =>
new List<ApiResource>
{
new ApiResource
{
Name = "ssi.api",
DisplayName = "Standing Settlement Instructions API"
}
};
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope
{
Name = "ssi.api",
DisplayName = "Standing Settlement Instructions API"
}
};
public static IEnumerable<Client> Clients =>
new List<Client>
{
new Client
{
ClientId = "ssi.front",
ClientName = "SSI Client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
// secret for authentication
ClientSecrets =
{
new Secret("my secret".Sha256())
},
// scopes that client has access to
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"ssi.api",
JwtClaimTypes.Role
},
AccessTokenLifetime = 14400,
AllowOfflineAccess = true,
}
};
Here's how my ConfigureServices look like:
// Database
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer("Server=localhost; Database = SsiDB; Integrated Security = SSPI; MultipleActiveResultSets=true;"));
#region Identity
services.AddIdentity<User, Role>(options =>
{
options.Password.RequiredLength = 8;
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.User.RequireUniqueEmail = true;
})
.AddRoles<Role>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
var identityBuilder = services.AddIdentityServer()
.AddInMemoryPersistedGrants()
.AddInMemoryClients(IdentityConfig.Clients)
.AddInMemoryApiResources(IdentityConfig.ApiResources)
.AddInMemoryApiScopes(IdentityConfig.ApiScopes)
.AddAspNetIdentity<User>();
identityBuilder.Services.AddTransient<IResourceOwnerPasswordValidator, OwnerPasswordValidator>();
identityBuilder.Services.AddTransient<IProfileService, IdentityProfileService>();
identityBuilder.AddSigningCredential(new X509Certificate2("StandingSettlementInstructionsIdentityAuth.pfx", "", X509KeyStorageFlags.MachineKeySet)); //release
#endregion
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = appSettings.ApiUrl;
options.Audience = "ssi.api";
options.RequireHttpsMetadata = false;
});
And this is my configure
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHsts();
string appDataPath = Directory.GetCurrentDirectory() + #"\AppData";
if (!Directory.Exists(appDataPath))
Directory.CreateDirectory(appDataPath);
app.UseSwaggerUI(o =>
{
o.DocumentTitle = "Standing Settlement Instructions Api Documentation";
o.RoutePrefix = "api-docs";
o.SwaggerEndpoint("/swagger/v1/swagger.json", "Version 1");
});
app.UseSwagger();
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<NotificationHubService>("/NotificationHubService");
});
app.Run((context =>
{
context.Response.Redirect("api-docs");
return Task.CompletedTask;
}));
}
Things I tried:
added this on the ConfigureService services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); and use it on my base controller. same result
Re-arrange the calls on Configure method based on some google searches
Added claims on API Resource and API Scopes. Same result
Some observation. Accessing localhost/.well-known/openid-configuration displays empty supported claims, when I added claims on either API Resource or API Scopes, it does appear in there but still getting the same issue when extracting claims. Still empty.
Any help will be highly appreciated. Been pulling my hair out on this. Thanks!
The first step to debug claims issues is to actually look at what does the access token actually contain? Use a tool like https://jwt.io/ to do that.
Then Microsoft and IdentityServer have different opinion on what the name of the claims should be, so you need to point out, which claim is the name claim, by using:
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters.RoleClaimType = "roles";
opt.TokenValidationParameters.NameClaimType = "name";
...
I am making a POC of a small website that uses Keycloak as an OIDC provider, for now I am just using the "standard" scaffolded website that .NET Core generates. The Privacy page has an authorize attribute so that it can only be accessed if the user is authenticated.
My StartUp.cs looks like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(i => new CookieAuthenticationOptions
{
Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
return OnValidatePrincipal(context);
}
}
})
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "http://localhost:8080/auth/realms/WatchList/";
options.RequireHttpsMetadata = false;
options.ClientId = "ClientID";
options.ClientSecret = "20ea1950-af47-4251-85e9-7c4f33189c77";
options.ResponseType = OpenIdConnectResponseType.Code;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "groups",
ValidateIssuer = true
};
});
services.AddAuthorization();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
private async Task OnValidatePrincipal(CookieValidatePrincipalContext context)
{
const string accessTokenName = "access_token";
const string refreshTokenName = "refresh_token";
const string expirationTokenName = "expires_at";
if (context.Principal.Identity.IsAuthenticated)
{
var exp = context.Properties.GetTokenValue(expirationTokenName);
if (exp != null)
{
var expires = DateTime.Parse(exp, CultureInfo.InvariantCulture).ToUniversalTime();
if (expires < DateTime.UtcNow)
{
// If we don't have the refresh token, then check if this client has set the
// "AllowOfflineAccess" property set in Identity Server and if we have requested
// the "OpenIdConnectScope.OfflineAccess" scope when requesting an access token.
var refreshToken = context.Properties.GetTokenValue(refreshTokenName);
if (refreshToken == null)
{
context.RejectPrincipal();
return;
}
var cancellationToken = context.HttpContext.RequestAborted;
// Obtain the OpenIdConnect options that have been registered with the
// "AddOpenIdConnect" call. Make sure we get the same scheme that has
// been passed to the "AddOpenIdConnect" call.
//
// TODO: Cache the token client options
// The OpenId Connect configuration will not change, unless there has
// been a change to the client's settings. In that case, it is a good
// idea not to refresh and make sure the user does re-authenticate.
var serviceProvider = context.HttpContext.RequestServices;
var openIdConnectOptions = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenIdConnectOptions>>().Get("Cookies");
var configuration = openIdConnectOptions.Configuration ?? await openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(cancellationToken).ConfigureAwait(false);
// Set the proper token client options
var tokenClientOptions = new TokenClientOptions
{
Address = configuration.TokenEndpoint,
ClientId = openIdConnectOptions.ClientId,
ClientSecret = openIdConnectOptions.ClientSecret
};
var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
using var httpClient = httpClientFactory.CreateClient();
var tokenClient = new TokenClient(httpClient, tokenClientOptions);
var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken, cancellationToken: cancellationToken).ConfigureAwait(false);
if (tokenResponse.IsError)
{
context.RejectPrincipal();
return;
}
// Update the tokens
var expirationValue = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn).ToString("o", CultureInfo.InvariantCulture);
context.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = refreshTokenName, Value = tokenResponse.RefreshToken },
new AuthenticationToken { Name = accessTokenName, Value = tokenResponse.AccessToken },
new AuthenticationToken { Name = expirationTokenName, Value = expirationValue }
});
// Update the cookie with the new tokens
context.ShouldRenew = true;
}
}
}
}
}
This all works as I expect it would: i can access the Home page without having to authenticate but if I want access to the Privacy page I am redirected to the Keycloak login page and after successfully logging in I have access to the Privacy Page as well.
However I want to access the Acces Token and Refresh Token (because I want to use the access token to access an api) as well, this in itself isn't a problem either:
[Authorize]
public IActionResult Privacy()
{
var accessToken = HttpContext.GetTokenAsync("access_token").Result;
Debug.WriteLine(accessToken);
var refreshToken = HttpContext.GetTokenAsync("refresh_token").Result;
Debug.WriteLine(refreshToken);
return View();
}
The problem is that these might be expired and I want to retrieve a new access token, but this should happen automatically.
I've tried the solution that was proposed here, and like you can see in my StartUp.cs, I "catch" the OnValidatePrincipalEvent and execute the code below.
But here is the problem: for some reason this event is never called. I would expect it to be called everytime I access the Privacy page. However this isn't the case, the event seems not be thrown ever.
Things I've tried:
I've followed this comment: https://stackoverflow.com/a/61396951/9784279, that seems to do exactly what I want, except for the fact that the event isn't thrown.
Checked if other events work like expected: I have confirmed that OnTokenValidated and OnTokenResponseReceived are called like expected, however these are OpenIdConnectEvents and not CookieAuthenticationEvents.
Tried playing with the StartUp.cs file, mainly the order/place of app.UseAuthentication() and app.UseAuthorization(). But this didn't change anything.
I thought maybe I could create an extension method GetOrUpdateTokenAsync on HttpContext that retrieves the access token and if it is expired it will retrieve a new one. The problem is that I don't think I have access to StoreTokens in
CookieValidatePrincipalContext
[1]: https://stackoverflow.com/a/61396951/9784279
Similar to how an app can offer authentication via either in-built forms or an external Identity Provider, I'd like to have two authentication options for my web site; In this case a custom token (eg API key) passed in the Authorization header that will be used if verified, and if none found or not valid then Open ID Connect. Cookie Auth for either will then keep a session.
I can get either working separately, but how do I combine them to achieve the above?
The following are the separate implementations.
Both have app.UseAuthentication(); in the Configure method in Startup.cs.
The Open ID implementation has only the following in the ConfigureServices method in Startup.cs, plus the [Authorize] attribute on each controller method I want auth on:
services.AddAuthorization(options =>
{
options.AddPolicy(OpenIdConnectDefaults.AuthenticationScheme, policy =>
policy.RequireClaim(ClaimTypes.Authentication, OpenIdConnectDefaults.AuthenticationScheme));
options.AddPolicy(CookieAuthenticationDefaults.AuthenticationScheme, policy =>
policy.RequireClaim(ClaimTypes.Authentication, CookieAuthenticationDefaults.AuthenticationScheme));
});
services.AddAuthentication(o =>
{
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(o =>
{
o.LoginPath = "/security/accessdenied";
o.AccessDeniedPath = "/security/accessdenied";
})
.AddOpenIdConnect(o =>
{
o.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.ClientId = oidcClientId;
o.Authority = oidcAuthority;
o.Scope.Add("openid");
o.Scope.Add("profile");
o.Scope.Add("email");
o.TokenValidationParameters = new TokenValidationParameters
{
// Set what is populated in User.Identity.Name
NameClaimType = ClaimTypes.Email
};
});
That's the end of the Open ID Connect implementation.
For the custom SAS Token implementation, there's a few pieces...
I have a custom AuthenticationHandler like so:
public class SasTokenAuthHandler : AuthenticationHandler<SasTokenAuthOptions>
{
public SasTokenAuthHandler(IOptionsMonitor<SasTokenAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// get token and parse
// ...
if (tokenExists)
{
// verify token
// ...
if (isVerifiedAndCurrent)
{
var identity = new ClaimsIdentity(new[] {
new Claim(ClaimTypes.Authentication, SasTokenAuthOptions.Scheme)
});
var claimsPrincipal = new ClaimsPrincipal(identity);
result = AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, SasTokenAuthOptions.Scheme));
// Create cookie
var authProperties = new AuthenticationProperties()
{
ExpiresUtc = DateTime.UtcNow.AddDays(10)
};
await Context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, authProperties);
}
else
{
result = AuthenticateResult.Fail("Could not verify signed data");
}
}
return await Task.FromResult(result);
}
}
In the Startup.cs file:
services.AddScheme<SasTokenAuthOptions, SasTokenAuthHandler>(SasTokenAuthOptions.Scheme, options =>
{
var provider = services.BuildServiceProvider();
options.Logger = provider.GetService<Serilog.ILogger>();
options.SasTokenService = provider.GetService<SasTokenService>();
options.CustomApiAuthSettings = provider.GetService<SasTokenAuthSettings>();
});
services.AddAuthorization(options =>
{
options.AddPolicy(SasTokenAuthOptions.Scheme, policy =>
policy.RequireClaim(ClaimTypes.Authentication, SasTokenAuthOptions.Scheme));
options.AddPolicy(CookieAuthenticationDefaults.AuthenticationScheme, policy =>
policy.RequireClaim(ClaimTypes.Authentication, CookieAuthenticationDefaults.AuthenticationScheme));
});
services.AddAuthentication(o =>
{
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultAuthenticateScheme = SasTokenAuthOptions.Scheme;
})
.AddCookie(o =>
{
o.LoginPath = "/security/accessdenied";
o.AccessDeniedPath = "/security/accessdenied";
})
And on each controller method I require authentication on:
[Authorize(SasTokenAuthOptions.Scheme)]
Perhaps of note: I've noticed if I just have [Authorize] then the SAS Token auth doesn't work but I'm not sure why.
That's the end of the custom SAS Token implementation.
I've tried adding both sets of code in Startup.cs but Open ID Connect is always what is used.
Authorize constructor take policy name but you want custom authentication.
Use AuthenticationSchemes property
Update:
Try this
[Authorize(AuthenticationSchemes= SasTokenAuthOptions.Scheme)]
All the auth handler code has changed from that in the question, but once corrected, then instead of using policies configure the auth services as specified here. Thatis, don't include a default scheme but instead specify all schemes in the Authorize attribute with comma separation. Eg.
[Authorize(AuthenticationSchemes = AuthSchemes)]
public class MixedController : Controller
private const string AuthSchemes = CookieAuthenticationDefaults.AuthenticationScheme + "," + JwtBearerDefaults.AuthenticationScheme + "," + OpenIdConnectDefaults.AuthenticationScheme;
I've noticed that the scheme you want to challenge the user (eg redirect) should be the last one.
I had the following code which worked at 1.1.
public static IServiceCollection RegisterRepositoryServices(this IServiceCollection services)
{
services.AddIdentity<ApplicationUser, IdentityRole<int>>(
config => { config.User.RequireUniqueEmail = true;
config.Cookies.ApplicationCookie.LoginPath = "/Account/Login";
config.Cookies.ApplicationCookie.AuthenticationScheme = "Cookie";
config.Cookies.ApplicationCookie.AutomaticAuthenticate = false;
config.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = async ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/visualjobs") && ctx.Response.StatusCode == 200)
{
ctx.Response.StatusCode = 401;
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
}
await Task.Yield();
}
};
}).AddEntityFrameworkStores<VisualJobsDbContext, int>()
.AddDefaultTokenProviders();
services.AddEntityFrameworkSqlServer().AddDbContext<VisualJobsDbContext>();
services.AddScoped<IRecruiterRepository, RecruiterRepository>();
services.AddSingleton<IAccountRepository, AccountRepository>();
return services;
}
It now doesn't like the section that makes reference to the config.Cookies....
I've been searching the net, but I cant really find anything to replace this.
You need to call AddIdentity to add the cookie authentication services and then configure the settings using ConfigureApplicationCookie as follows:
services.AddIdentity<ApplicationUser, IdentityRole<int>>(config => {
config.User.RequireUniqueEmail = true;
})
.AddUserStore<UserStore<ApplicationUser, IdentityRole<int>, VisualJobsDbContext, int>>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(o => {
o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = async ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/visualjobs") && ctx.Response.StatusCode == 200)
{
ctx.Response.StatusCode = 401;
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
}
await Task.Yield();
}
};
});
In addition, in your Configure() method, remember to call the following:
app.UseAuthentication();
Additional reading:
Migrating Authentication and Identity to ASP.NET Core 2.0
Note:
The following section in this article regards the AutomaticAuthenticate option:
Setting Default Authentication Schemes
In 1.x, the AutomaticAuthenticate and AutomaticChallenge
properties of the AuthenticationOptions base class 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
properties on the individual AuthenticationOptions instance. They
can be configured in the AddAuthentication method call within the
ConfigureServices method of Startup.cs:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);