401 Unauthorized using IdentityServer3.AccessTokenValidation - c#

When using IdentityServer3.AccessTokenValidation in my .Net 4.8 (not core) WebAPI to secure the endpoints, the WebAPI always returns a 401 Not Authorized to the client app. The token is supplied by IDS4. When the client is calling my Asp.Net Core API, Authorization is successful. But when calling the .Net 4.8 AspNet WebApi, it is not.
Client MVC App Startup.ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "TestMvc";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.Scope.Add("testApi");
options.CallbackPath = "/auth";
options.SignedOutRedirectUri = "~/home";
});
}
The .Net Core API Startup that works:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.ApiName = "testApi";
options.RequireHttpsMetadata = false;
options.RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role";
});
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Startup.cs of the .Net 4.8 WebApi that always returns 401:
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.EnableSystemDiagnosticsTracing();
var cors = new EnableCorsAttribute("*", "*", "*");
config.EnableCors(cors);
var idsOptions = new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:5000",
RequiredScopes = new string[] { "testApi" },
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role
};
app.UseIdentityServerBearerTokenAuthentication(idsOptions);
app.UseWebApi(config);
}
The Asp.Net Core Controller Endpoint that executes successfully
[Route("test")]
[Authorize(Roles = "[A role claim that is definitely present in access token]")]
public class TestController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok("Success");
}
}
The Asp.Net 4.8 Controller Endpoint that returns 401
[Route("test")]
[Authorize(Roles = "[A role claim that is definitely present in access token]")]
public class TestController : ApiController
{
[HttpGet]
public IHttpActionResult Get()
{
return Ok("Success");
}
}
MVC Client App Calling API:
public async Task<IActionResult> AccessApi()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var content = await client.GetStringAsync("http://localhost:5001/test");
ViewBag.Json = content;
return View("json");
}
The last lines of the IDS server log:
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET http://localhost:5000/.well-known/openid-configuration dbug:
IdentityServer4.Hosting.EndpointRouter[0]
Request path /.well-known/openid-configuration matched to endpoint type Discovery dbug:
IdentityServer4.Hosting.EndpointRouter[0]
Endpoint enabled: Discovery, successfully created handler: IdentityServer4.Endpoints.DiscoveryEndpoint info:
IdentityServer4.Hosting.IdentityServerMiddleware[0]
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for
/.well-known/openid-configuration dbug:
IdentityServer4.Endpoints.DiscoveryEndpoint[0]
Start discovery request info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished in 17.3987ms 200 application/json; charset=UTF-8 info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET http://localhost:5000/.well-known/openid-configuration/jwks dbug:
IdentityServer4.Hosting.EndpointRouter[0]
Request path /.well-known/openid-configuration/jwks matched to endpoint type Discovery dbug:
IdentityServer4.Hosting.EndpointRouter[0]
Endpoint enabled: Discovery, successfully created handler: IdentityServer4.Endpoints.DiscoveryKeyEndpoint info:
IdentityServer4.Hosting.IdentityServerMiddleware[0]
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryKeyEndpoint for
/.well-known/openid-configuration/jwks dbug:
IdentityServer4.Endpoints.DiscoveryKeyEndpoint[0]
Start key discovery request info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished in 16.6027ms 200 application/jwk-set+json; charset=UTF-8
The token is issued successfully and passed in the request headers to the API.
The API seems to be calling the discover endpoint correctly, but then nothing after that. I would expect the API to validate the incoming token with the IDS server, but there is no call to the IDS server for validation. Confirmed with both the IDS log and using Fiddler to check the traffic.

This can be caused by couple of things, to be sure we need the log of ASP.NET API project. Here is some suggested fixes for similar issues:
On Statup.cs class of IdentityServer project
Change AccessTokenJwtType to JWT, default value on IdentityServer4 is at+jwt but .Net Framework Api (OWIN/Katana) requires JWT.
Add /resources aud by setting EmitLegacyResourceAudienceClaim to true, this is removed on IdentityServer4.
You can verify the access_token on https://jwt.ms/ by checking "typ" and "aud" .
var builder = services.AddIdentityServer(
options =>
{
options.AccessTokenJwtType = "JWT";
options.EmitLegacyResourceAudienceClaim = true;
});
On Statup.cs class of .Net Framework Api project, set ValidationMode to ValidationMode.Local, access token validation endpoint used by this method is removed on IdentityServer4.
var idsOptions = new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:5000",
RequiredScopes = new string[] { "testApi" },
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
ValidationMode = ValidationMode.Local,
};
I have sample working implementation here, and also a blog post contains more details https://nahidfa.com/posts/identityserver4-and-asp-.net-web-api/
I strongly suggest you to gather logs on API, this helps to find the actual issue in your case and finding the fix. here is a sample to turn on OWIN log on Api.

Related

Confusion using JWT when calling API

I wrote an ASP.NET Core 3.1 API which uses the JWTBearer authentication system. This system works well when I call the API from Postman, but I can't figure out how to call it throught my own application or ASP.NET Core 3.1 MVC Website. Here is the configuration of the API :
API Configuration :
In the Startup.cs ConfigrationServices method I added this classical piece of code :
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = true;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5),
};
});
Then I added the middleware app.UseAuthentication(); to the Configure method.
Now I have a UsersController.cs with a SignIn method which returns a JWT as string if the credentials are corrects.
Finally, I added a simple GetUsers() method with an [Authorize] tag to test the JWT authentication as following :
// GET: api/Users
[Authorize(Roles = "Administrator")]
[HttpGet]
public async Task<ActionResult<IEnumerable<User>>> GetUsers()
{
return await _context.Users.ToListAsync();
}
Throught Postman, everything works fine. I call the api/Users/SignIn url in POST passing my credentials as JSON. I get back my token in response with the 200 StatusCode.
Then I call the api/Users in GET passing the JWT previously obtained to the Postman settings Authorization > Type : Bearer Token. My API returns a successful code with all the data I asked for. Everything works as expected at the API side.
MVC Website Configuration :
To simplify the discussion with the API, I wrote a Class Library with a ClientService.cs class.
Int the ClientService.cs, I have this simplified piece of code which successfully gets the data from the API :
public async Task<string> GetPage(string model)
{
var request =
new HttpRequestMessage(
HttpMethod.Get,
_baseAddress +
model);
var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
If I use it, let's say to get the informations from api/something as following : var smth = await GetPage("something") or any other AllowAnonymous method, it will properly works.
But if I want to make it working with an Authorize method, I actually add this piece of code just before sending the request :
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
Where the token variable contains my JWT hardwritten in code for testing purpose. Everything works fine too.
So now I'm obviously trying to avoid hardwritting the JWT. I decided to store it on the client side in an HttpOnly Cookie coupled with the AntiForgeryToken native function from ASP.NET Core. I wrote this code to store the cookie :
private void Authenticate(string token)
{
Response.Cookies.Append(
"JWT",
token,
new CookieOptions()
{
HttpOnly = true,
Secure = true,
}
);
}
And now I'm stucked here. Because I use it as a Service, my ClientService is the same for all my users. So I can't store the token somewhere and pass it to the newly created client for each request.
I tried to add the JWT to the header before calling the ClientService as following :
public async Task<ActionResult> Index()
{
Request.Headers.Add("Authorization", "Bearer " + Request.Cookies["JWT"]);
var users = await ClientService.GetUsersAsync();
//GetUsersAsync() simply call GetPage("users") method and deserialize the JSON returned as a List<User>
return View(users);
}
But because my ClientService create its own client which sends its own request each time I ask for some data from the API with this code :
var request =
new HttpRequestMessage(
HttpMethod.Get,,
_baseAddress +
model);
var client = _clientFactory.CreateClient();
The headers I added before aren't passed to the API.
A simple solution could be to rewrite all my ClientService methods to accept a Token parameter but it seems redundant and painful.
Which is the best and simpliest solution to pass the token to my API ?
The case you are describing is actually Cookie Authentication :
1. Sign in
ClaimsIdentity claimsIdentity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
//Not mandatory: you can add the token to claim for future usage, like API request from server to server
claimsIdentity.AddClaim(new Claim("token", tokenId));
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal);
2. Add Cookie authentication
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = true;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5),
};
}).AddCookieAuthentication();
AddCookieAuthentication looks like:
public static AuthenticationBuilder AddCookieAuthentication(this IServiceCollection services)
{
var authBuilder = services.AddAuthentication(sharedOptions =>
{
sharedOptions.RequireAuthenticatedSignIn = false;
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, opts =>
{
opts.CookieManager = new ChunkingCookieManager();
opts.Cookie = new CookieBuilder()
{
Domain = "CookieDomain",
Name = "CookieName",
Path = "CookiePath",
SecurePolicy = CookieSecurePolicy.Always,
HttpOnly = true,
SameSite = SameSiteMode.Lax,
};
opts.ExpireTimeSpan = TimeSpan.FromMinutes(20);
opts.ForwardDefaultSelector = ctx =>
{
var authHeader = ctx.Request.Headers["Authorization"].FirstOrDefault();
if (authHeader?.StartsWith(JwtBearerDefaults.AuthenticationScheme) == true)
{
return JwtBearerDefaults.AuthenticationScheme;
}
else
{
return CookieAuthenticationDefaults.AuthenticationScheme;
}
};
});
return authBuilder;
}
A browser through which the user signed-in will receive the cookie cookie created in SignIn method and will be able to populate request via client as well.
Ok I figured it out myself, here is how I solved the problem :
1) On Startup.cs, add an HttpContextAccessor :
The Context from our ASP.NET Core website is only accessible by the controllers. The Class libraries can't access it.
To make it available to a class library, we need to use an HttPContextAccessor. It's a service we can use as a dependancy injection to our class library. We just have to add
services.AddHttpContextAccessor();
In the ConfigureServices method from the Startup.cs.
2) Adapt the Class Library
The ClientService needs to be able to receive the Dependancy Injection as following :
public class ClientService : IClientService
{
private readonly IHttpClientFactory _clientFactory;
private readonly IHttpContextAccessor _contextAccessor;
private readonly string _baseAddress = $#"https://localhost:[PORT]/api/";
public ClientService(IHttpClientFactory clientFactory, IHttpContextAccessor contextAccessor)
{
_clientFactory = clientFactory;
_contextAccessor = contextAccessor;
}
}
Now we can access the current context from the HttpContextAccessor _contextAccessor variable.
Next we have to modify our GetPage(string model) method :
public async Task<string> GetPage(string model)
{
var request =
new HttpRequestMessage(
HttpMethod.Get,
_baseAddress +
model);
var client = _clientFactory.CreateClient();
var authorization = _contextAccessor.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == "Authorization").Value.FirstOrDefault();
if(authorization != null)
{
client.DefaultRequestHeaders.Add("Authorization", authorization);
}
var response = await client.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
The code added will try to get the "Authorization" header from the request and if it's not null, we add it to our own request. This allows the request to the API with the JWT.
3) Add a middleware to add the JWT to each request
Now the final step is to add the JWT to each request from our ASP.NET Core application. If needed, the JWT will be consumed by our Class Library without changing any piece of code, either from the library or from the application.
In order to realise that, we have to add a middleware to the Startup.cs > Configure() method.
The middleware looks like that :
// Add header:
app.Use((context, next) =>
{
var jwt = context.Request.Cookies["JWT"];
if(jwt != null)
{
context.Request.Headers.Add("Authorization", "Bearer " + jwt);
}
return next.Invoke();
});
This just reads the Cookie from the request headers and if the JWT HttpOnly Cookie isn't null, we add the token to our request. This request will then be captured by our Class Library in order to use it to talk with the API.
A few things to note :
Adding the JWT to each request even if we don't need it could consume more bandwidth overtime. It could be also less safe (I'm not a security expert)
Adding the JWT to our main request, then create a local client with its own request in our class library seems not optimal. But it works as expected and I'm actually unaware of how to optimize it.
Feel free to point any issue in my solution or modify it to add some clarifications or even other more efficient ways to achieve it.

Blazor WebAssembly 401 Unauthorized even when I am authorized

I am using Blazor WebAssembly Asp.Net Core hosted PWAand integrated the AspNetCore.Identity into it. I created the AuthenticationStateProvider in the Client-Side and now I want to allow the user access to a controller where he needs to be authorized.
I have tested via postman, the users were been created and stored in DB as aspnetusers with the right credentials. The Login/Account Controller work as I wanted it.
When the user is authorized it tells this exception in the browser when accessing the authorized controller request:
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: Response status code does not indicate success: 401 (Unauthorized).
System.Net.Http.HttpRequestException: Response status code does not
indicate success: 401 (Unauthorized).
Startup.cs (ConfigureServices-Method):
...
serviceCollection.AddDbContext<SQLiteTestDbContext>(options =>
{
options.UseSqlite(config["ConnectionStrings:SQLiteTestConnection"]);
});
serviceCollection.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<SQLiteTestDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
};
});
services.AddHttpContextAccessor();
services.Configure<IdentityOptions>(options =>
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
...
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseAuthentication();
app.UseAuthorization();
...
}
Program.cs Client-Side
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Logging.SetMinimumLevel(LogLevel.Warning);
//Registering Shared-Library models
builder.Services.AddScoped<ObjectModel>();
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ApiAuthenticationStateProvider>();
builder.Services.AddScoped<IAuthService, AuthService>();
//Registered BlazorContextMenu Service
builder.Services.AddBlazorContextMenu();
//Registering FileReader service, for image upload -> Azure
builder.Services.AddFileReaderService(options => options.UseWasmSharedBuffer = true);
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
My Controller with authorize attribute:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("api/[controller]")]
[ApiController]
public class ObjectController : ControllerBase
{
....
Note:
When your user tries to access a protected (annotated with the Authorize attribute) page on the client he should login first or register.
In order to register, he should be redirected to an Account Controller where you should create a new user, and add it to the database (You said you " integrated the AspNetCore.Identity into it"), which is fine...and should be used to authenticate and verify the user's identity. You account controller should also produce a Jwt Token that should be passed to the client app, and stored in the local storage.
Now, whenever your user tries to access protected resources on your Web Api endpoints, you should retrieve the Jwt Token from the local storage, and add it to the request header. If you do so, the Unauthorized response would be something of the past.
Custom AuthenticationStateProvider can be a good place from which you can manage storing the Jwt Token in the local storage and retrieving it for outbound HTTP request calls.
Here's some sample code to clarify what you should do:
#code {
WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
var token = await TokenProvider.GetTokenAsync();
forecasts = await Http.GetJsonAsync<WeatherForecast[]>(
"api/WeatherForecast",
new AuthenticationHeaderValue("Bearer", token));
}
}
Note: TokenProvider is a custom AuthenticationStateProvider that defines a method called GetTokenAsync that provides (reading the Jwt Token from the local storage and passing it to the calling code) the Jwt Token
Hope this helps...
In case of Linux App Service in combination with ID Server the Authority needs to be set according to Microsoft documentation: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-5.0#azure-app-service-on-linux-1
services.Configure<JwtBearerOptions>(
IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
options =>
{
options.Authority = "{AUTHORITY}";
});
Example: options.Authority = "https://contoso-service.azurewebsites.net";

JWT Authentication Failed with SignalR

I have a REST API (core 2.1) which need to support a SPA, providing restful endpoints and some live-interactive features using SignalR;
The Hub/MVC routes are running on the same server, which is also the provider of the JWT token.
After loggin in, the client-side receives a JWT token, which places on the header for every REST request, otherwise it gets a 401 (This is working with the [Authorize] attribute).
At the client-side, the code below tries to connect to my /hub endpoint:
new HubConnectionBuilder().withUrl(HUB_URL, { accessTokenFactory: () => this.getToken() })
And if I place [Authorize] at my Hub class, I get the following error (Without the authorization, the client can send and listen correctly):
WebSocket connection to 'wss://localhost:5001/hub?id=MY_ID&access_token=MY_TOKEN' failed: HTTP Authentication failed; no valid credentials available
The server logged failed authentications:
(Triggered a console.log on AddJwtBearerOptions.Events.OnMessageReceived)
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5001/hub?
id=MY_ID&access_token=MY_TOKEN
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12]
AuthenticationScheme: Bearer was challenged.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 0.3658ms 401
Differently from the those requests, using the SAME JWT TOKEN with the REST ones (Using [Authorize]), with the Header: Bearer XXXX instead of querystring, triggers the OnTokenValidated. The OnAuthenticationFailed is never triggered, even if the authentication fails:
(Triggered a console.log on AddJwtBearerOptions.Events.OnMessageReceived)
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5001/api/products application/json
(Triggered a console.log on AddJwtBearerOptions.Events.OnTokenValidated)
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[2]
Successfully validated the token.
Check below my ´Startup.cs`
ConfigureServices(IServiceCollection)
services.AddSignalR();
services.AddCors(option => option.AddPolicy("CorsPolicy", p => p.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod().AllowCredentials()));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters{
ValidateIssuer = true,
ValidIssuer = Configuration["JWT:Issuer"],
ValidateLifetime = true,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:SecurityKey"]))
};
});
Configure(IApplicationBuilder)
app.UseAuthentication();
app.UseCors("CorsPolicy");
app.UseHttpsRedirection();
app.UseMvc();
app.UseSignalR(routes => {
routes.MapHub<ApplicationHub>("/hub");
});
You also need to add this block inside the .AddJwtBearer section:
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hub")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
This can be found here in the docs.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12]
AuthenticationScheme: Bearer was challenged.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 0.3658ms 401
Spend 20 hours for fixing. :(
Reason was in:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class ChatHub : Hub<IChatClient>
My service configuration:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => ...)
UPD:
Full work example with JS client here.
To expand on what #marcusturewicz already have stated, one should also check for the token inside the Authorization header in case if the access token is sent in the header instead of query.
Although, this may vary from how e.g., the client implementer sends the token.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// If no token is present in the query, we can
// try to get it from the Authorization header.
StringValues accessToken = context.Request.Query["access_token"] == StringValues.Empty
? context.Request.Headers["Authorization"]
: context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hub")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};

ASP.NET Core WebAPI Cookie + JWT Authentication

we have a SPA (Angular) with API backend (ASP.NET Core WebAPI):
SPA is listens on app.mydomain.com, API on app.mydomain.com/API
We use JWT for Authentication with built-in Microsoft.AspNetCore.Authentication.JwtBearer; I have a controller app.mydomain.com/API/auth/jwt/login which creates tokens. SPA saves them into local storage. All works perfect. After a security audit, we have been told to switch local storage for cookies.
The problem is, that API on app.mydomain.com/API is used by SPA but also by a mobile app and several customers server-2-server solutions.
So, we have to keep JWT as is, but add Cookies. I found several articles which combines Cookies and JWT on different controllers, but I need them work side-by-side on each controller.
If client sends cookies, authenticate via cookies. If client sends JWT bearer, authenticate via JWT.
Is this achievable via built-in ASP.NET authentication or DIY middleware?
Thanks!
Okay, I have been trying achieving this for a while and i solved same issue of using jwt Authentication Tokens and Cookie Authentication with the following code.
API Service Provider UserController.cs
This Provide Different Services to the User with Both (Cookie and JWT Bearer)Authentication Schemes
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
[Route("[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private readonly IUserServices_Api _services;
public UsersController(IUserServices_Api services)
{
this._services = services;
}
[HttpGet]
public IEnumerable<User> Getall()
{
return _services.GetAll();
}
}
My Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication(options => {
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Home/Error";
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = " you site link blah blah",
ValidIssuer = "You Site link Blah blah",
IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(sysController.GetSecurityKey()))
,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
}
And further if you want custom Authentication for a specific Controller
then you have to specify the Authentitcation Type for the Authorization
like:
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public IActionResult Index()
{
return View(); // This can only be Access when Cookie Authentication is Authorized.
}
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Index()
{
return View(); // And this one will be Access when JWT Bearer is Valid
}
I've been having the same issue and i just found what it seems to be the solution in another question here in stackoverflow.
Please take a look at this.
I'll try that solution myself and update this answer with the results.
Edit: It seems it's not possible to achieve double authentication types in a same method but the solution provided in the link i mentioned says:
It's not possible to authorize a method with two Schemes Or-Like, but you can use two public methods, to call a private method
//private method
private IActionResult GetThingPrivate()
{
//your Code here
}
//Jwt-Method
[Authorize(AuthenticationSchemes = $"{JwtBearerDefaults.AuthenticationScheme}")]
[HttpGet("bearer")]
public IActionResult GetByBearer()
{
return GetThingsPrivate();
}
//Cookie-Method
[Authorize(AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme}")]
[HttpGet("cookie")]
public IActionResult GetByCookie()
{
return GetThingsPrivate();
}
Anyway you should take a look at the link, it sure helped me.
Credit goes to Nikolaus for the answer.
I have not been able to find much information on a good way to do this - having to duplicate the API is a pain just to support 2 authorization schemes.
I have been looking into the idea of using a reverse proxy and it looks to me like a good solution for this.
User signs into Website (use cookie httpOnly for session)
Website uses Anti-Forgery token
SPA sends request to website server and includes anti-forgery token in header: https://app.mydomain.com/api/secureResource
Website server verifies anti-forgery token (CSRF)
Website server determines request is for API and should send it to the reverse proxy
Website server gets users access token for API
Reverse proxy forwards request to API: https://api.mydomain.com/api/secureResource
Note that the anti-forgery token (#2,#4) is critical or else you could expose your API to CSRF attacks.
Example (.NET Core 2.1 MVC with IdentityServer4):
To get a working example of this I started with the IdentityServer4 quick start Switching to Hybrid Flow and adding API Access back. This sets up the scenario I was after where a MVC application uses cookies and can request an access_token from the identity server to make calls the API.
I used Microsoft.AspNetCore.Proxy for the reverse proxy and modified the quick start.
MVC Startup.ConfigureServices:
services.AddAntiforgery();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
MVC Startup.Configure:
app.MapWhen(IsApiRequest, builder =>
{
builder.UseAntiforgeryTokens();
var messageHandler = new BearerTokenRequestHandler(builder.ApplicationServices);
var proxyOptions = new ProxyOptions
{
Scheme = "https",
Host = "api.mydomain.com",
Port = "443",
BackChannelMessageHandler = messageHandler
};
builder.RunProxy(proxyOptions);
});
private static bool IsApiRequest(HttpContext httpContext)
{
return httpContext.Request.Path.Value.StartsWith(#"/api/", StringComparison.OrdinalIgnoreCase);
}
ValidateAntiForgeryToken (Marius Schulz):
public class ValidateAntiForgeryTokenMiddleware
{
private readonly RequestDelegate next;
private readonly IAntiforgery antiforgery;
public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery)
{
this.next = next;
this.antiforgery = antiforgery;
}
public async Task Invoke(HttpContext context)
{
await antiforgery.ValidateRequestAsync(context);
await next(context);
}
}
public static class ApplicationBuilderExtensions
{
public static IApplicationBuilder UseAntiforgeryTokens(this IApplicationBuilder app)
{
return app.UseMiddleware<ValidateAntiForgeryTokenMiddleware>();
}
}
BearerTokenRequestHandler:
public class BearerTokenRequestHandler : DelegatingHandler
{
private readonly IServiceProvider serviceProvider;
public BearerTokenRequestHandler(IServiceProvider serviceProvider, HttpMessageHandler innerHandler = null)
{
this.serviceProvider = serviceProvider;
InnerHandler = innerHandler ?? new HttpClientHandler();
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
request.Headers.Authorization =new AuthenticationHeaderValue("Bearer", accessToken);
var result = await base.SendAsync(request, cancellationToken);
return result;
}
}
_Layout.cshtml:
#Html.AntiForgeryToken()
Then using your SPA framework you can make a request. To verify I just did a simple AJAX request:
<a onclick="sendSecureAjaxRequest()">Do Secure AJAX Request</a>
<div id="ajax-content"></div>
<script language="javascript">
function sendSecureAjaxRequest(path) {
var myRequest = new XMLHttpRequest();
myRequest.open('GET', '/api/secureResource');
myRequest.setRequestHeader("RequestVerificationToken",
document.getElementsByName('__RequestVerificationToken')[0].value);
myRequest.onreadystatechange = function () {
if (myRequest.readyState === XMLHttpRequest.DONE) {
if (myRequest.status === 200) {
document.getElementById('ajax-content').innerHTML = myRequest.responseText;
} else {
alert('There was an error processing the AJAX request: ' + myRequest.status);
}
}
};
myRequest.send();
};
</script>
This was a proof of concept test so your mileage may very and I'm pretty new to .NET Core and middleware configuration so it could probably look prettier. I did limited testing with this and only did a GET request to the API and did not use SSL (https).
As expected, if the anti-forgery token is removed from the AJAX request it fails. If the user is has not logged in (authenticated) the request fails.
As always, each project is unique so always verify your security requirements are met. Please take a look at any comments left on this answer for any potential security concerns someone might raise.
On another note, I think once subresource integrity (SRI) and content security policy (CSP) is available on all commonly used browsers (i.e. older browsers are phased out) local storage should be re-evaluated to store API tokens which will lesson the complexity of token storage. SRI and CSP should be used now to help reduce the attack surface for supporting browsers.
I think the easiest solution is one proposed by David Kirkland:
Create combined authorization policy (in ConfigureServices(IServiceCollection services)):
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
CookieAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
And add middleware that will redirect to login in case of 401 (in Configure(IApplicationBuilder app)):
app.UseAuthentication();
app.Use(async (context, next) =>
{
await next();
var bearerAuth = context.Request.Headers["Authorization"]
.FirstOrDefault()?.StartsWith("Bearer ") ?? false;
if (context.Response.StatusCode == 401
&& !context.User.Identity.IsAuthenticated
&& !bearerAuth)
{
await context.ChallengeAsync("oidc");
}
});
while looking for combined firebase authorization with net core web api (cookie for web site and authorization header for mobile app ) end with the following solution.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://securetoken.google.com/xxxxx";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = options.Authority,
ValidateAudience = true,
ValidAudience = "xxxxx",
ValidateLifetime = true
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
if (context.Request.Cookies.ContainsKey(GlobalConst.JwtBearer))
{
context.Token = context.Request.Cookies[GlobalConst.JwtBearer];
}
else if (context.Request.Headers.ContainsKey("Authorization"))
{
var authhdr = context.Request.Headers["Authorization"].FirstOrDefault(k=>k.StartsWith(GlobalConst.JwtBearer));
if (!string.IsNullOrEmpty(authhdr))
{
var keyval = authhdr.Split(" ");
if (keyval != null && keyval.Length > 1) context.Token = keyval[1];
}
}
return Task.CompletedTask;
}
};
});
where
public static readonly string JwtBearer = "Bearer";
seems working fine.
checked it from mobile & postman (for cookie )
with this code you can use cookie and header in the same time.
if cookie is null then check the header automatically.
add this code in AddJwtBearer options.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["Authorization"];
return Task.CompletedTask;
}
};
full usage is:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = Configuration["JwtToken:Audience"],
ValidIssuer = Configuration["JwtToken:Issuer"],
IssuerSigningKey =
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtToken:Key"]))
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["Authorization"];
return Task.CompletedTask;
}
};
});
Header => Authorization: Bearer Your-Token
or
Cookie => Authorization=Your-Token
//dont add Bearer in Cookie
I found this nice article by Rick Strahl. It is the best solution that I found so far and I used it in .NET 5
Here is the key code in order to combining JWT token and cookie authentication in .NET applications:
services.AddAuthentication(options =>
{
options.DefaultScheme = "JWT_OR_COOKIE";
options.DefaultChallengeScheme = "JWT_OR_COOKIE";
})
.AddCookie( options =>
{
options.LoginPath = "/login";
options.ExpireTimeSpan = TimeSpan.FromDays(1);
})
.AddJwtBearer( options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = config.JwtToken.Issuer,
ValidateAudience = true,
ValidAudience = config.JwtToken.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.JwtToken.SigningKey))
};
})
.AddPolicyScheme("JWT_OR_COOKIE", "JWT_OR_COOKIE", options =>
{
options.ForwardDefaultSelector = context =>
{
string authorization = context.Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
return JwtBearerDefaults.AuthenticationScheme;
return CookieAuthenticationDefaults.AuthenticationScheme;
};
});
ASP.NET Core 2.0 Web API
Please follow this post for implementing JWT Token based authentication
https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login
If you are using visual studio make sure apply the Bearer type athentication type with the filter
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
for controller or actions.

Asp.net Core Authorize user with Policy

I am trying to authorize an user with a bearer token send from the request header.
I added this code in startup file of resource server.
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
});
Here is my method in controller.
[Authorize("Bearer")]
[HttpGet]
[Route("list")]
public IEnumerable<Products> List()
{
string Authorization = Request.Headers["Authorization"];
}
Application showing me error 401 Unauthorized even if i had token
I am sending this Token in the header request
Authorization:Bearer "xyz"
To work with Bearer token you have to add the following code on your Configure method
public void Configure(IApplicationBuilder app) {
app.UseJwtBearerAuthentication(options => {
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.Audience = "OAuth:Audience";
options.Authority = "OAuth:Authority";
options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
metadataAddress: options.Authority + ".well-known/openid-configuration",
configRetriever: new OpenIdConnectConfigurationRetriever(),
docRetriever: new HttpDocumentRetriever() { RequireHttps = false });
});
}
You will also need a middleware to handle the authorization process.
Have a look at AspNet.Security.OpenIdConnect.Server and OpenIddict

Categories

Resources