I'm using AspNetCore 2 with HTTP.SYS. I'm trying to implement an authentication scheme that
1. Performs custom (stateless) authentication if a request has a given header, or
2. If not, defaults back to windows auth
My initial attempt was adding a policy scheme that selects the appropriate authentication scheme based on the request. This doesn't quite work though - it looks that Http.sys authentication is done BEFORE my policy selector is even invoked (see comment in the code snippet).
In a desparate and clueless attempt, I've fiddled around with setting AllowAnonymous to true, but that just seems to lead to Windows auth never being used.
Is there any way to select the auth scheme properly?
var host = new WebHostBuilder()
.ConfigureServices(services =>
{
services
.AddAuthentication("DynamicAuthenticationScheme")
.AddScheme<AuthenticationSchemeOptions, CustomAuth>("Custom", _ => { })
.AddPolicyScheme("DynamicAuthenticationScheme", "Default system policy", cfgOptions =>
{
cfgOptions.ForwardDefaultSelector = ctx =>
{
// Auth looks to be done beforehand already; if setting a breakpoint here, ctx.User is already given
if (ctx.Request.Headers.ContainsKey("CUSTOM-AUTH"))
return "Custom";
return HttpSysDefaults.AuthenticationScheme;
};
});
})
.UseHttpSys(options =>
{
options.Authentication.Schemes = AuthenticationSchemes.NTLM | AuthenticationSchemes.Negotiate;
options.Authentication.AllowAnonymous = false;
})
.UseUrls("http://localhost:7200")
.Configure(app =>
{
app.UseAuthentication();
app.Map(new PathString("/test"), cfg =>
cfg.Use(async (context, next) =>
{
await context.Response.WriteAsync($"Hello {context.User.Identity?.Name}");
}));
})
.Build();
await host.StartAsync();
I've "solved" this now by adding a custom middleware:
class MyMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if(ShouldUseCustomAuth(context))
{
var authResult = await context.AuthenticateAsync("Custom");
if(authResult.Succeeded)
{
context.User = auth.Principal;
await next(context);
return;
}
}
await context.ChallengeAsync("Windows");
}
}
I'll leave the question open for a moment though in case anyone has a nicer solution (maybe involving policies after all?)
Related
I am using the below code on .net core 3.1 startups.cs class. It is not hitting the below line. In my case, I am getting a token from Azure AD and want to check if the user exists on our application database(external database). Below is the sample code I am using
services.AddProtectedWebApi(Configuration);
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
await existingOnTokenValidatedHandler(context);
context.Fail("user not avilable in database");
// your code to add extra claims that will be executed after the current event implementation.
};
});
Default authentication scheme in Microsoft.Identity.Web for JwtBearerOptions is JwtBearerDefaults.AuthenticationScheme ("Bearer") as mentioned in documet for AddProtectedWebApi method. The authentication scheme AzureADDefaults.JwtBearerAuthenticationScheme ("AzureADJwtBearer") is not registered so it is ignored.
Update the code by replacing AzureADDefaults.JwtBearerAuthenticationScheme with JwtBearerDefaults.AuthenticationScheme as shown below:
services.AddProtectedWebApi(Configuration);
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
await existingOnTokenValidatedHandler(context);
context.Fail("user not avilable in database");
// your code to add extra claims that will be executed after the current event implementation.
};
});
Another way to extend custom token validation can be found in below code:
services.AddProtectedWebApi(options =>
{
Configuration.Bind("AzureAd", options);
options.Events = new JwtBearerEvents();
options.Events.OnTokenValidated = async context =>
{
//your code for additional validation.
};
},
options =>
{
Configuration.Bind("AzureAd", options);
});
I'm following Identity Server quickstart template, and trying to setup the following
Identity server aspnet core app
Mvc client, that authenticates to is4 and also calls webapi client which is a protected api resource.
The ApplicationUser has an extra column which I add into claims from ProfileService like this:
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
if (user == null)
return;
var principal = await _claimsFactory.CreateAsync(user);
if (principal == null)
return;
var claims = principal.Claims.ToList();
claims.Add(new Claim(type: "clientidentifier", user.ClientId ?? string.Empty));
// ... add roles and so on
context.IssuedClaims = claims;
}
And finally here's the configuration in Mvc Client app ConfigureServices method:
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "mvc-secret";
options.ResponseType = "code";
options.SaveTokens = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("offline_access");
options.Scope.Add("api1");
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapUniqueJsonKey("clientidentifier", "clientidentifier");
});
With GetClaimsFromUserInfoEndpoint set to true I can access the custom claim in User.Identity, but this results in 2 calls for ProfileService.
If I remove or set to false then this claim is still part of access_token, but not part of id_token, and then I can't access this specific claim from context User.
Is there a better way I can access this claim from User principal without resulting in 2 calls (as it's now)? or perhaps reading access_token from context and updating user claims once the token is retrieved?
thanks :)
Turns out that Client object in identity server has this property that does the job:
//
// Summary:
// When requesting both an id token and access token, should the user claims always
// be added to the id token instead of requring the client to use the userinfo endpoint.
// Defaults to false.
public bool AlwaysIncludeUserClaimsInIdToken { get; set; }
As explained in the lib metadata setting this to true for a client, then it's not necessary for the client to go and re-get the claims from endpoint
thanks everybody :)
If you want to access custom claims in client side over those added in identity server just follow these steps, it worked for me. I imagine you implement both client and identity server as separated projects in asp.net core and they are ready, you now want to play with claims or maybe want to authorize by role-claim and so on, alright let's go
create a class that inherits from "IClaimsTransformation" like this:
public class MyClaimsTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var userName = principal.Identity.Name;
var clone = principal.Clone();
var newIdentity = (ClaimsIdentity)clone.Identity;
var user = config.GetTestUsers().Where(p => p.Username == userName).First();
if (user != null)
{
var lstUserClaims = user.Claims.Where(p => p.Type == JwtClaimTypes.Role).ToList();
foreach (var item in lstUserClaims)
if (!newIdentity.Claims.Where(p => p.ValueType == item.ValueType && p.Value == item.Value).Select(p => true).FirstOrDefault())
newIdentity.AddClaim(item);
}
return Task.FromResult(principal);
}
}
But be aware this class will call multiple times over user authentication so i added a simple code to prevent multiple duplicate claim. also you have user name of authenticated user too.
Next create another class like this:
public class ProfileService : IProfileService
{
//private readonly UserManager<ApplicationUser> userManager;
public ProfileService(/*UserManager<ApplicationUser> userManager*/ /*, SignInManager<ApplicationUser> signInManager*/)
{
//this.userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.AddRequestedClaims(context.Subject.Claims);
var collection = context.Subject.Claims.Where(p => p.Type == JwtClaimTypes.Role).ToList();
foreach (var item in collection)
{
var lst = context.IssuedClaims.Where(p => p.Value == item.Value).ToList();
if (lst.Count == 0)
context.IssuedClaims.Add(item);
}
await Task.CompletedTask;
}
public async Task IsActiveAsync(IsActiveContext context)
{
//context.IsActive = true;
await Task.FromResult(0); /*Task.CompletedTask;*/
}
}
This class will call by several context but it's okay cause we added our custom claim(s) at part #1 at this code
foreach (var item in lstUserClaims)
if (!newIdentity.Claims.Where(p => p.ValueType == item.ValueType && p.Value == item.Value).Select(p => true).FirstOrDefault())
newIdentity.AddClaim(item);
This is your basic startup.cs at identity server side:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => options.EnableEndpointRouting = false);
services.AddTransient<Microsoft.AspNetCore.Authentication.IClaimsTransformation, MyClaimsTransformation>();
services.AddIdentityServer().AddDeveloperSigningCredential()
.AddInMemoryApiResources(config.GetApiResources())
.AddInMemoryIdentityResources(config.GetIdentityResources())
.AddInMemoryClients(config.GetClients())
.AddTestUsers(config.GetTestUsers())
.AddInMemoryApiScopes(config.GetApiScope())
.AddProfileService<ProfileService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
Pay attention to .AddProfileService<ProfileService>(); and services.AddTransient<Microsoft.AspNetCore.Authentication.IClaimsTransformation, MyClaimsTransformation>();
Now at client side go to startup.cs and do as follows:
.AddOpenIdConnect("oidc", options =>
{
//other code
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.Add(new JsonKeyClaimAction(JwtClaimTypes.Role, null, JwtClaimTypes.Role));
})
for my sample i tried to use "Role" and authorize users by my custom roles.
Next at your controller class do like this:
[Authorize(Roles = "myCustomClaimValue")] or you can create a class for custom authorization filter.
Note that you define test user in config file in your identity server project and the user has a custom claim like this new claim(JwtClaimTypes.Role, "myCustomClaimValue") and this will be back at lstUserClaims variable.
I am assuming you are passing Authorization header with Bearer JWT token while calling the API. You can read access_token from HttpContext in your API Controller.
var accessToken = await this.HttpContext.GetTokenAsync("access_token");
var handler = new JwtSecurityTokenHandler();
if (handler.ReadToken(accessToken) is JwtSecurityToken jt && (jsonToken.Claims.FirstOrDefault(claim => claim.Type == "sub") != null))
{
var subID = jt.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
}
NOTE : GetClaimsFromUserInfoEndpoint no need to set explicitly.
Here is a bit of extra info on the subject. By default, IdentityServer doesn't include identity claims in the identity token. It is allowed by setting the AlwaysIncludeUserClaimsInIdToken setting on the client configuration to true. But it is not recommended. The initial identity token is returned from the authorization endpoint via front‑channel communication either through a form post or through the URI. If it's returned via the URI and the token becomes too big, you might hit URI length restrictions, which are still dependent on the browser. Most modern browsers don't have issues with long URIs, but older browsers like Internet Explorer might. This may or may not be of concern to you. Looks like my project is similar to yours. Good luck.
I am working on an ASP.NET Core 2 web application. I am handling Access Denied page for [Authorize (roles OR policies)] pages.
By default, Instead of showing the original URL and returning 403 status, ASP.NET Core 2.0 redirects the request to an AccessDenied page with status is 302 -> This is not what I want.
Instead of redirecting AccessDenied page. I want ASP.NET Core throws my custom ForbiddenException exception so I can handle unauthorized accesses like I do for Unhandled exceptions.
Here is my authentication configuration:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; // Cookies
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; // Cookies
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; // Cookies
})
.AddCookie(options => {
options.LoginPath = "/Auth/Login/";
options.LogoutPath = "/Auth/Logout/";
// I want disable this and throw ForbiddenException instead.
options.AccessDeniedPath = "/Auth/AccessDenied/";
});
Anyone has any help? Thank you!
The code for redirecting to AccessDeniedPath is handled by CookieAuthenticationEvents.OnRedirectToAccessDenied, included here for completeness:
public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToAccessDenied { get; set; } = context =>
{
if (IsAjaxRequest(context.Request))
{
context.Response.Headers["Location"] = context.RedirectUri;
context.Response.StatusCode = 403;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
};
In order to override this behavior, you can provide your own implementation of OnRedirectToAccessDenied, that can either write to the response or, in your case, throw an exception. To do that, you can use something like the following:
services.AddAuthentication(...)
.AddCookie(options => {
// ...
options.Events.OnRedirectToAccessDenied = context => {
// Your code here.
// e.g.
throw new YouAreNotWelcomeHereException();
};
});
Here, context is an instance of RedirectContext, which (through its inheritance tree), contains properties such as:
HttpContext
Request
Response
Properties (type: AuthenticationProperties)
Using these properties, you can obtain any information you need about the incoming request. You can also write your own response, etc.
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 am trying to provide an ActiveUser property to Serilog.
Unfortunately I cannot seem to find the correct spot to check for the current user.
In the below code httpContext.User.Identity.IsAuthenticated is always false?
But only when logging in with the bearer token
The bearer token login is working correctly insofar as the user is
authenticated to the controller methods, and the user needs to belong
to the correct roles in order to be authenticated. Though the user name is not correctly set - the claims are present, and IsAuthenticated is set to true.
If I use the cookie login, the user is set correctly, and the claims are set correctly, and the Serilog works correctly. This is true whether using the bearer token or a cookie to call in. Once the user is logged in with a cookie it always works.
When the bearer token is validated, the user is not immediately set?
The project is aspnetcore 2.0
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
... (other configuration items)
app.UseIdentityServer();
app.UseAuthentication();
app.Use(async (httpContext, next) =>
{
// HERE IsAuthenticated IS ALWAYS FALSE
// HERE THE CLAIMS ARE ALWAYS EMPTY, UNLESS
// I LOGIN USING THE COOKIE AS WELL - THEN IT WORKS
var userName = httpContext.User.Identity.IsAuthenticated
? httpContext.User.GetClaim("name")
: "(unknown)";
LogContext.PushProperty(
"ActiveUser",
!string.IsNullOrWhiteSpace(userName)
? userName
: "(unknown)");
await next.Invoke();
});
app.UseMvc(
routes =>
{
routes.MapRoute(
"default",
"{controller=Home}/{action=Index}/{id?}");
});
In my controller method, the User is set correctly, and is authenticated.
[Authorize]
[HttpGet("user")]
public object UserDetail()
{
// HERE THE CLAIMS ARE SET, IsAuthenticated IS ALWAYS TRUE
// AS THE USER MUST BE AUTHENTICATED TO GET HERE
Debug.Assert(this.User.Identity.IsAuthenticated == true)
edit
Digging into the problem further it would appear that the JWTBearer token is validated AFTER my middleware has already executed. The middleware needs to execute AFTER the token is validated.
TL;DR
(the full configuration)
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) =>
{
var userName = httpContext.User.Identity.IsAuthenticated
? httpContext.User.GetClaim("email")
: "(unknown)";
LogContext.PushProperty("ActiveUser", !string.IsNullOrWhiteSpace(userName) ? userName : "(unknown)");
await next.Invoke();
});
app.UseMvc(
routes =>
{
routes.MapRoute(
"default",
"{controller=Home}/{action=Index}/{id?}");
});
}
(more configuration)
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication()
.AddOpenIdConnect(
o =>
{
o.Authority = "https://localhost:44319";
o.ClientId = "api";
o.ClientSecret = "secret";
o.RequireHttpsMetadata = false;
o.ResponseType = "code id_token token";
o.GetClaimsFromUserInfoEndpoint = true;
})
.AddJwtBearer(
o =>
{
o.Authority = "https://localhost:44319";
o.Audience = "api";
o.RequireHttpsMetadata = false;
//o.SaveToken = true;
});
services.AddMemoryCache();
services.AddIdentity<ApplicationUser, ApplicationRole>(
x =>
{
x.Password.RequireNonAlphanumeric = false;
x.Password.RequireUppercase = false;
})
.AddEntityFrameworkStores<FormWorkxContext>()
.AddDefaultTokenProviders()
.AddIdentityServer();
// NB
services.Configure<IdentityOptions>(
options =>
{
options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role;
options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name;
});
services.ConfigureApplicationCookie(
options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
options.Events.OnRedirectToLogin = this.ProcessStatusCodeResponse;
});
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApis())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<ApplicationUser>();
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc(
_ =>
{
_.Filters.Add(
new AuthorizeFilter(
new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme,
IdentityConstants.ApplicationScheme)
.RequireAuthenticatedUser()
.Build()));
_.Filters.Add(new ExceptionFilter());
_.ModelBinderProviders.Insert(0, new PartyModelBinderProvider());
_.ModelBinderProviders.Insert(0, new DbGeographyModelBinder());
_.ModelMetadataDetailsProviders.Add(new KeyTypeModelMetadataProvider());
})
.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>())
.AddJsonOptions(json => json.SerializerSettings.Converters.Add(new DbGeographyJsonConverter()));
}
Copying my answer from your other related question in case anyone comes across this and wonders what's going on:
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 the User is populated correctly
by the time it gets to your controller action.
I have replicated this issue when logging in using a principal set up as follows:
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
Then I login with SignInAsync. This too leads to User.Identity.Name having a value but the User.Identity.IsAuthenticated not being set to true.
Now when I add the authenticationType parameter to ClaimsIdentity like this:
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "local"));
The IsAuthenticated is now set to true.
I am not entirely sure how your sign in would work and you could mention this authenticationType somewhere or you could pass it along while creating the JWT. That is the way I had done it.
Update ok just noticed your comment about the Name not shown either, but you can still try setting the authenticationType. Also as far as your claims are right, you should be able to extract the principle using AuthenticateAsync. Once you can access the principle from the Context.User object, you can always customize the an authentication scheme to force in the principal.
Update 2 In your case, inside your AddJwtBearer, try including this:
o.Events.OnTokenValidated = async (context) => {
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(context.Principal.Claims, "local"));
};
Authenticate the user explicitly in your custom middleware by adding the following line of code:
var result = await context.Request.HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);//AuthenticationOptions.DefaultAuthenticateScheme)
if (result.Succeeded)
{
//context.User.AddIdentity(result.Principal);
context.User = result.Principal;
}