In ASP.NET core (2.1), running on a windows, I am using HttpSys configured with the following authentication schemes:
builder.UseHttpSys(options =>
{
options.Authentication.Schemes = AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM;
options.Authentication.AllowAnonymous = true;
})
Then in my Startup.Configure() method I am attempting to access the user credentials of the client calling the uri "/sensitiveOperation" as follows:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.MapWhen(context => context.Request.Path.Equals("/sensitiveOperation") && context.Request.Method.Equals(HttpMethods.Put), subApp =>
{
subApp.Run(async (context) =>
{
if (context.User.Identity.Name == "admin")
{
await context.Response.WriteAsync("Performing sensitive operation.");
// .. Do Sensitive operation....
}
});
});
The example is slightly vulgarised, but the main point is that context.User.Identity.Name is always empty where I would expect to see the name of the AD account which is making the call. Note that the call is done in powershell as follows:
Invoke-WebRequest -Uri http://localhost:5555/sensitiveOperation -Method Put -UseDefaultCredentials
I could put this code in a controller and use the [Authorize] attribute to get the credentials but I would prefer to do this operation before hitting the Mvc pipeline. Is there any way to get the user at this early stage of the pipeline ?
Change AllowAnonymous
options.Authentication.AllowAnonymous = false;
If you have anonymous on, and you're not prompting for authentication then the browser isn't going to authenticate. Even if you do send creates asp.net isn't going to get them unless there's an Authenticate attribute on the controller/method or, if you're going the function route, you call signin.
If you would not want to set AllowAnonymous as false, you could try context.ChallengeAsync to authenticate the request based on Credential.
Here are the code:
app.MapWhen(context => context.Request.Path.Equals("/sensitiveOperation") && context.Request.Method.Equals(HttpMethods.Put), subApp =>
{
subApp.Run(async (context) =>
{
var authService = context.RequestServices.GetRequiredService<IAuthorizationService>();
if (!context.User.Identity.IsAuthenticated)
{
//await context.ChallengeAsync("Windows"); //Option1
//await context.ChallengeAsync(); //Option2
await context.ChallengeAsync(HttpSysDefaults.AuthenticationScheme); //Option3
}
if (context.User.Identity.Name == "admin")
{
await context.Response.WriteAsync("Performing sensitive operation.");
// .. Do Sensitive operation....
}
});
});
Note, for this way, subApp.Run will run twice, first request is UnAuthenticated, and it will challenge the credentail, second request is Authenticated and context.User.Identity.Name will have value. This process is back-end, this would not be reflected in powershell.
I am using .NET Core 3.0 and in my custom middleware I was able to get the UserId with this code:
httpContext.User.Identity.IsAuthenticated
? new Guid(httpContext.User.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier).First().Value)
: Guid.Empty
Related
I'm trying to make it available for users to Authorize through Steam.
I'm currently working with a Frontend on port 8080 and a backend on port 5000. There is a local SSL on both services.
I'm able to sign in via Steam through the backend and retrieve information, the claims, about the user logged in. The problem is that the cookie or claims that is sent after the successful login is not available from the instance of the frontend, because when the user authorize with Steam it's entirely through the backend and therefore no WebToken, cookie or session is available to the frontend.
I'm using
AspNet.Security.OpenId (6.0.0)
AspNet.Security.OpenId.Steam (6.0.0)
I currently have the following setup in the backend
Startup.cs in ConfigureServices
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Authentication/SteamLogin";
options.LogoutPath = "/Authentication/SteamLogout";
options.Cookie.Domain = "https://localhost:8080"; //Tried to do some shared cookie thing but doesnt work
})
.AddSteam(options =>
{
options.ApplicationKey = "SECRET_APP_KEY";
});
AuthenticationController SteamAuthorize
So for connecting through steam you need to access this location directly
"https://localhost:5000/Authentication/SteamAuthorize".
Normally i would authenticate by sending a GET or POST call from the frontend so the frontend would have a relation to the backend. Here there is none because you access the action directly.
[AllowAnonymous]
[Route("SteamAuthorize")]
[HttpGet]
public IActionResult SteamAuthorize()
{
return Challenge(new AuthenticationProperties { RedirectUri = "/", IsPersistent = true }, "Steam");
}
AuthenticationController Index
So after the user has successfully logged in through steam the response is sent to this location.
In here it's not a problem getting the acquired data, but now i need the claims and identity to be available when my frontend makes a request to the backend.
[HttpGet("~/")]
public async Task<ActionResult> Index()
{
var openIdSteamId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
var steam_userName = User.Identity.Name;
if (steam_userName != null && openIdSteamId != null)
{
var steam_userId = openIdSteamId.Value.Replace("https://steamcommunity.com/openid/id/", "");
//I've tried redirecting to the frontend which then makes a new call afterwards
//The frontend will make a new call to this "/" page and now the Claims are gone
return Redirect("https://localhost:8080/steamauthorizing");
}
return Redirect("https://localhost:8080/");
}
I've tried to set options.Cookie.Domain = "https://localhost:8080"; but that didn't work.
I've generally looked for how to make the principal and claims persist through the two services.
To sum the question up. How do I authenticate a third-party app, with OpenID, through my backend, with the Identity also available to my frontend?
About OAuth.
Frontend SPA react
MVC OAuth backend, signs user's into 3rd party providers, works nicely, return token.
from my SPA I can do window.open and redirect the user to a sign-in page, NB: has to be a new window as xframeoptions is set to deny.
How do I return the token & correlate with SPA, as they are in separate windows/sessions?
options I'm looking at
content security policy - set the caller's domain
set same site cookie
Using aspnet-contrib/AspNet.Security.OAuth.Providers
Samples
Startup.cs
public class Startup
{
private const string policyName = "Cors";
public Startup(IConfiguration configuration, IHostEnvironment hostingEnvironment)
{
Configuration = configuration;
HostingEnvironment = hostingEnvironment;
}
public IConfiguration Configuration { get; }
private IHostEnvironment HostingEnvironment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.AddCors(opt =>
{
opt.AddPolicy(name: policyName, builder =>
{
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyOrigin()
.AllowAnyMethod();
});
});
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/signin";
options.LogoutPath = "/signout";
})
.AddGitHub(options =>
{
options.ClientId = Configuration["GitHub:ClientId"];
options.ClientSecret = Configuration["GitHub:ClientSecret"];
options.Scope.Add("user:email");
options.Scope.Add("read:org");
options.Scope.Add("workflow");
options.SaveTokens=true;
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
if (HostingEnvironment.IsDevelopment())
{
// IdentityModelEventSource.ShowPII = true;
}
// Required to serve files with no extension in the .well-known folder
//var options = new StaticFileOptions()
//{
// ServeUnknownFileTypes = true,
//};
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
});
app.UseCors(policyName);
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
});
Authentication Controller
public class AuthenticationController : Controller
{
[HttpGet("~/signin")]
public async Task<IActionResult> SignIn() => View("SignIn", await HttpContext.GetExternalProvidersAsync());
[HttpPost("~/signin")]
public async Task<IActionResult> SignIn([FromForm] string provider)
{
// Note: the "provider" parameter corresponds to the external
// authentication provider choosen by the user agent.
if (string.IsNullOrWhiteSpace(provider))
{
return BadRequest();
}
if (!await HttpContext.IsProviderSupportedAsync(provider))
{
return BadRequest();
}
// Instruct the middleware corresponding to the requested external identity
// provider to redirect the user agent to its own authorization endpoint.
// Note: the authenticationScheme parameter must match the value configured in Startup.cs
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, provider);
}
[HttpGet("~/signout")]
[HttpPost("~/signout")]
public IActionResult SignOutCurrentUser()
{
// Instruct the cookies middleware to delete the local cookie created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
return SignOut(new AuthenticationProperties { RedirectUri = "/" },
CookieAuthenticationDefaults.AuthenticationScheme);
}
}
Home Controller
public class HomeController : Controller
{
public async Task<IActionResult> IndexAsync()
{
var accessToken = await HttpContext.GetTokenAsync("GitHub", "access_token");
var refreshToken = await HttpContext.GetTokenAsync("GitHub", "refresh_token");
return View();
}
}
Home Page (Index.cshtml)
<div class="jumbotron">
#if (User?.Identity?.IsAuthenticated ?? false)
{
<h1>Welcome, #User.Identity.Name</h1>
<p>
#foreach (var claim in Context.User.Claims)
{
<div><code>#claim.Type</code>: <strong>#claim.Value</strong></div>
}
</p>
<a class="btn btn-lg btn-danger" href="/signout?returnUrl=%2F">Sign out</a>
}
else
{
<h1>Welcome, anonymous</h1>
<a class="btn btn-lg btn-success" href="/signin?returnUrl=%2F">Sign in</a>
}
</div>
Thanks for looking
It seems (correct me if I'm mistaken) that the main issue is launching the window for github auth and sending that token back to your site which is a different window (i.e. the parent window of the popup).
One option is to set up your auth request's redirect_uri parameter so that the 3rd party identity provider redirects back to a URL on your site (within the popup window), i.e. your ~/signin endpoint. This allows your server-side to grab hold of the tokens and do things like create a session, store a cookie, etc. Cookies set within the popup window will be available to your site in the original window (assuming it's the same domain) once the parent window has refreshed.
Next, once the popup window has been redirected back to your ~/signin endpoint and you've created the session or stored a cookie, etc, you may wish to close that popup and refresh the parent window so that it recognises the cookie / new session. You can do this by returning a page from the ~/signin request (still inside the popup) which includes the following JavaScript:
window.opener.document.location.reload();
// alternatively send the user to an authenticated homepage:
window.opener.document.location.href = '/signed-in-user-homepage';
// and then close the popup
window.close();
I don't think it's your goal but for completeness, if you wish to perform OAuth authentication/authorisation from the SPA itself, so the server side doesn't get the tokens, you may wish to let a Javascript library like oidc-client do the heavy lifting. This launches its own window to perform authentication and hands back the token to the calling SPA itself. These tokens are not visible to the server side.
One resource which really helped me figure this out was the IdentityServer4 quickstart for JavaScript clients, worth stepping through if this is your use case:
OpenID Connect auth from JavaScript clients quickstart
Sample code for the above quickstart
Once you've got a token on the client side (i.e. in JavaScript), you could pass it to the server side to use to make requests on behalf of the user but this isn't good practice as it impersonation rather than properly delegated authorisation, and you'd need to work out what to do when the access token expires (passing the refresh token to the server side effectively allows your server to impersonate the user indefinitely).
My preference is to allow the server-side to perform an auth-code authorisation flow using AspNet.Security.OAuth.Providers, receive the auth code to the ~/signin page, perform the back channel request to get the tokens, use the identity information to create my own profile for the user and perform requests on behalf of the user using authorisation delegated to my service.
Let me know if any of this needs more explaining but hope it's useful as-is.
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 have a Xamarin.Forms application that I'm using to connect to an App Service backend, and I'm attempting to authenticate using Auzre B2C JWT tokens.
Through various tutorials I have managed to get B2C setup using microsoft accounts, and I am able to create users, change passwords, and generate access tokens.
My next step was to add the [Authorize] attribute to my controller and attempt to pass that token to my app service and authorize users, but no matter what I try I get a 401 Unauthorized response from my service.
I'm adding the JWT token to the Authorization header of my HttpClient, and it's getting to the service.
I can paste my token into https://jwt.ms/, and it correctly tells me what's in my token.
I've implemented this code in an attempt to figure out what's wrong.
ConfigureServices in startup.cs looks like this:
public void ConfigureServices(IServiceCollection services) {
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options => {
options.Audience = Configuration["Authentication:AzureAd:ClientId"];
options.Events = new JwtBearerEvents {
OnAuthenticationFailed = AuthenticationFailed
};
options.Authority = $"https://{tenant name}.b2clogin.com/{tenant id}/{Configuration["Authentication:AzureAd:Policy"]}";
options.Events = new JwtBearerEvents {
OnAuthenticationFailed = ctx =>
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
message += "From OnAuthenticationFailed:\n";
message += FlattenException(ctx.Exception);
return Task.CompletedTask;
},
OnChallenge = ctx =>
{
message += "From OnChallenge:\n";
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
ctx.Response.ContentType = "text/plain";
return ctx.Response.WriteAsync(message);
},
OnMessageReceived = ctx =>
{
message = "From OnMessageReceived:\n";
ctx.Request.Headers.TryGetValue("Authorization", out var BearerToken);
if (BearerToken.Count == 0)
BearerToken = "no Bearer token sent\n";
message += "Authorization Header sent: " + BearerToken + "\n";
return Task.CompletedTask;
},
OnTokenValidated = ctx =>
{
Debug.WriteLine("token: " + ctx.SecurityToken.ToString());
return Task.CompletedTask;
}
};
});
services.AddMvc();
}
Configure looks like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
IdentityModelEventSource.ShowPII = true;
} else {
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
}
And I've also added this call to AuthenticationFailed, so I'll know if my authentication is working or not:
Task AuthenticationFailed(AuthenticationFailedContext arg) {
Console.WriteLine(arg.Exception.Message);
return Task.FromResult(0);
}
With my current setup I'm getting a 401 error from the server, and that's right after it hits the OnChallenge event wired up in Startup.cs. According to the link above, that's what gets called right before it returns a 401 to the user, so it seems like the service is receiving the proper token, and authenticating, but maybe I don't have the correct rights set up?
I'm not sure where to go from here, but any guidance would be appreciated.
Edit:
As mentioned in a comment below, I was able to curl my website using the access token generated after logging in through my app like this:
curl https://mywebsite.azurewebsites.net/api/Values -i --header "Authorization: Bearer [TOKEN]"
And that seems to work with no issue, so it seems like it's something with how I'm making a call to the controller through my app, not the authentication itself.
Edit 2 (solution):
So, as per Edit 1, I was correct in that it was just how I was adding the token to the authorization header. It wasn't my brightest moment, but I wasn't calling .Value on the claim that contained my Access Token. I was only calling .ToString() on the claim itself, so the "token" was actually the entire claim text "Access Token: ". I didn't think much of it at the time when I was debugging my service, because I didn't realize it shouldn't have that text there.
Once I corrected that issue the service started working as expected.
So, in the end, I guess it was all working as expected. I was, in fact, not sending the expected token, so I was ... unauthorized.
As requested the line of code that I had to change was this:
So, this won't be 100% applicable to most because I'm using a business library called CSLA, but the idea is the same regardless.
After my b2c call returns the token I store it in the ApplicationContext.User.Identity that's built into the CSLA library. That allows me to get the access token claim later. The important part to take away from this is that I'm storing the token some place that I can access it later when I want to add it to the authorization header.
Later, when I'm making the call with my httpclient I need to get that token, so originally, I was doing this:
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ((ClaimsIdentity)ApplicationContext.User.Identity).Claims.FirstOrDefault(c => c.Type == "AccessToken").ToString());
This isn't correct. This was sending the "token" as with value "Access Token: [token value]. Essentially, it was adding the words "Access Token" to the token I needed to authenticate, and that was failing, because the words "Access Token" are not actually supposed to be part of the token you use to authenticate.
After I changed my call to this:
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ((ClaimsIdentity)ApplicationContext.User.Identity).Claims.FirstOrDefault(c => c.Type == "AccessToken").Value);
It started getting only the token value, and when that was added to the authorization header, it worked just fine.
Edit 2 explains the answer to my problem.
I wasn't adding the token correctly to the authorization header, so the service wasn't able to authenticate the token, or rather, it saw the token as invalid.
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;
}