ASP.NET Core WebAPI Cookie + JWT Authentication - c#

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.

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.

Using multiple authentication schemes in ASP.NET Core 3.1?

I have been making a web application using ASP.NET Core 3.1 using clean architecture.
I have some class libraries like Infrastructure, Persistence, Domain, Application, and a MVC application project named "Web" as the startup point of my application.
In the Web layer I have "Areas" in which I have an Admin area containing some controllers and action methods which return JSON as my API endpoints to be used in a React-based app.
I also have some controllers in the Web MVC project in Controllers folder which their action methods return html views.
I also use Identity and JWT for my API endpoints but:
- What if I want to use claims-based Identity in my MVC controllers which their action results return html views?
- What is the best practice for using claims-based Identity in ASP.NET Core 3.1 in such an application?
Any help would be appreciated.
After doing some research, I found the solution in ASP.NET core Authorization documentation in an article with the title "Authorize with a specific scheme in ASP.NET Core".
Based on the mentioned article in Microsoft ASP .NET core documentation, In some scenarios, such as Single Page Applications (SPAs), it's common to use multiple authentication methods. For example, the app may use cookie-based authentication to log in and JWT bearer authentication for JavaScript requests.
An authentication scheme is named when the authentication service is configured during authentication. For example:
public void ConfigureServices(IServiceCollection services)
{
// Code omitted for brevity
services.AddAuthentication()
.AddCookie(options => {
options.LoginPath = "/Account/Unauthorized/";
options.AccessDeniedPath = "/Account/Forbidden/";
})
.AddJwtBearer(options => {
options.Audience = "http://localhost:5001/";
options.Authority = "http://localhost:5000/";
});
In the preceding code, two authentication handlers have been added: one for cookies and one for bearer.
Selecting the scheme with the Authorize attribute
[Authorize(AuthenticationSchemes =
JwtBearerDefaults.AuthenticationScheme)]
public class MixedController : Controller
In the preceding code, only the handler with the "Bearer" scheme runs. Any cookie-based identities are ignored.
This is the solution which solved my problem and I thought it would be good to share it with you guys for those who need this.
Multiple Authentication Schemes in .Net Core 3.1 or .Net 5.0
Startup.cs
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(x =>
{
x.LoginPath = "/";
x.ExpireTimeSpan = TimeSpan.FromMinutes(Configuration.GetValue<int>("CookieExpiry"));
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration.GetValue<string>("JWTSecret"))),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
/api/auth/login
public async Task<AuthenticationResult> Login([FromForm] string userName, [FromForm] string password, [FromHeader] string authmode = "")
{
if (userName != "demo" || password != "demo")
return new AuthenticationResult { HasError = true, Message = "Either the user name or password is incorrect." };
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, userName)
};
if(authmode?.ToLower() == "token")
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_config.GetValue<string>("JWTSecret"));
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims, "JWT"),
Expires = DateTime.UtcNow.AddMinutes(_config.GetValue<int>("JWTExpiry")),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var jwt = tokenHandler.WriteToken(token);
return new AuthenticationResult { Token = jwt };
}
else
{
ClaimsPrincipal princ = new ClaimsPrincipal(new ClaimsIdentity(claims, "COOKIE"));
await HttpContext.SignInAsync(princ);
return new AuthenticationResult();
}
}
Output:

Asp.Net Core 2 Validating bearer token

I am struggling to find an exact way of validating my OAuth bearer token which is passed when a request is sent to the API am working on which is a Asp.Net core project.
The purpose here is to extract the bearer token and Validate it and if all is fine then continue with the request.
So far my findings have come across the following
JWT bear token authorization which mostly talks about access_token
Asp.Net core security middleware
Custom Authorize attribute which handle this.
I am not really sure how I can achieve my validation? Should I extract the bearer token and then create a custom validating method?
Ideally would like the [Authorize] attribute to handle this.
Suggestions please?
Well finally after more research I finally found that custom AuthorizationHandler is a more suitable solution as suppose to using custom Authorize attributes which is not suggested in Asp.Net Core.
It was simple to setup and I am able to extract my Bearer token from the header for further authorization with OAuth.
Here is a my approach:
public class CustomAuthorizationHandler: IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
var authFilterCtx = (Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext)context.Resource;
string authHeader = authFilterCtx.HttpContext.Request.Headers["Authorization"];
if (authHeader != null && authHeader.Contains("Bearer"))
{
var token = authHeader.Replace("Bearer", "");
// Now token can be used for further authorization
}
throw new NotImplementedException();
}
}
Lastly registering the handler in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IAuthorizationHandler, CustomAuthorizationHandler>();
}
I think to put the following code snippet inside ConfigureServices() should be able to validate your access_token after installing Microsoft.AspNetCore.Authentication.JwtBearer NuGet package:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
//options.SaveToken = true;
options.MetadataAddress = ValidationEndPoint;
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = tokenIssuer,
ValidAudiences = new[] { clientId },
ValidAudience = null
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("MyPolicy", policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
});
remember to put app.UseAuthentication() and app.UseAuthorization() in the Configure() method. And add [authorize] to your controller API.

Append Authorization Header Before Authorizing Web Sockets

I'm in the process of refactoring my API to use the built in .Net authentication instead of IdentityServer4
In my old code I would append the authentication token to the websocket address and inject a header using the middleware
public class SignalRQueryStringAuthMiddleware
{
private readonly RequestDelegate _next;
public SignalRQueryStringAuthMiddleware(RequestDelegate next)
{
_next = next;
}
// Convert incomming qs auth token to a Authorization header so the rest of the chain
// can authorize the request correctly
public async Task Invoke(HttpContext context)
{
if (context.Request.Query.TryGetValue("A5S0kT0k", out var token))
{
context.Request.Headers.Add("Authorization", "Bearer " + token.First());
}
await _next.Invoke(context);
}
}
I can see that my middleware is being executed as expected an appending the proper authorization Header.
However in my startup my Authorization never seems to be called and it moves directly to connecting to the websocket'
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(cfg => {
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.Events = new JwtBearerEvents
{
OnMessageReceived = async (ctx) =>
{
Console.WriteLine(ctx.Token);
},
OnTokenValidated = async (ctx) =>
{
Console.WriteLine("BreakPoint");
},
OnAuthenticationFailed = async (ctx) =>
{
Console.WriteLine("Breakpoint");
}
};
cfg.TokenValidationParameters = tokenValidationParameters;
});
Here is the order of execution of my pipeline in the configure
app.UseSignalRQueryStringAuth();
app.UseAuthentication();
app.UseSignalR(routes =>
{
routes.MapHub<DefaultServiceHubBase<MessageDTO>>("/messages");
routes.MapHub<DefaultServiceHubBase<ConversationDTO>>("/conversations");
routes.MapHub<InMemoryHub<UserLocationDTO>>("/user-locations");
});
I configure my pipeline so that the middleware is hit first but the authentication I can never hit any of my breakpoint in the JWTBearer section, However if I make a standard HttpRequest everything works fine?
My OnMessageReceived is ignored and it goes directly to the onconnect function in my hub why is this happening?
Not sure why but it turns out I need to added a default challenge schema as well
services.AddAuthentication(options =>
{
// Identity made Cookie authentication the default.
// However, we want JWT Bearer Auth to be the default.
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
I found this in the Signalr Authentication Docs

ASP.NET Core 2.0 combining Cookies and Bearer Authorization for the same endpoint

I've created a new ASP.NET Core Web Application project in VS17 using the "Web Application (Model-View-Controller)" template and ".Net Framework" + "ASP.NET Core 2" as the configuration. The authentication config is set to "Individual User Accounts".
I have the following sample endpoint:
[Produces("application/json")]
[Route("api/price")]
[Authorize(Roles = "PriceViwer", AuthenticationSchemes = "Cookies,Bearer")]
public class PriceController : Controller
{
public IActionResult Get()
{
return Ok(new Dictionary<string, string> { {"Galleon/Pound",
"999.999" } );
}
}
"Cookies,Bearer" is derived by concatenating CookieAuthenticationDefaults.AuthenticationScheme and JwtBearerDefaults.AuthenticationScheme.
The objective is to be able to configure the authorization for the end point so that it's possible access it using both the token and cookie authentication methods.
Here is the setup I have for Authentication in my Startup.cs:
services.AddAuthentication()
.AddCookie(cfg => { cfg.SlidingExpiration = true;})
.AddJwtBearer(cfg => {
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters() {
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
};
});
So, when I try to access the endpoint using a browser, I get the 401 response with a blank html page.
Then I login and when I try to access the endpoint again, I get the same response.
Then, I try to access the endpoint by specifying the bearer token. And that returns the desired result with the 200 response.
So then, if I remove [Authorize(AuthenticationSchemes = "Cookies,Bearer")], the situation becomes the opposite - cookie authentication works and returns 200, however the same bearer token method as used above doesn't give any results and just redirect to the default AspIdentity login page.
I can see two possible problems here:
1) ASP.NET Core doesn't allow 'combined' authentication.
2) 'Cookies' is not a valid schema name. But then what is the right one to use?
Please advise. Thank you.
If I understand the question correctly then I believe that there is a solution. In the following example I am using cookie AND bearer authentication in a single app. The [Authorize] attribute can be used without specifying the scheme, and the app will react dynamically, depending on the method of authorization being used.
services.AddAuthentication is called twice to register the 2 authentication schemes. The key to the solution is the call to services.AddAuthorization at the end of the code snippet, which tells ASP.NET to use BOTH schemes.
I've tested this and it seems to work well.
(Based on Microsoft docs.)
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://localhost:4991";
options.RequireHttpsMetadata = false;
options.ClientId = "WebApp";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.Scope.Add("api");
options.SaveTokens = true;
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:4991";
options.RequireHttpsMetadata = false;
// name of the API resource
options.Audience = "api";
});
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
CookieAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
EDIT
This works for authenticated users, but simply returns a 401 (unauthorized) if a user has not yet logged in.
To ensure that unauthorized users are redirected to the login page, add the following code to the Configure method in your Startup class. Note: it's essential that the new middleware is placed after the call the app.UseAuthentication().
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");
}
});
If you know a cleaner way to achieve this redirect, please post a comment!
After many hours of research and head-scratching, this is what worked for me in ASP.NET Core 2.2 -> ASP.NET 5.0:
Use .AddCookie() and .AddJwtBearer() to configure the schemes
Use a custom policy scheme to forward to the correct Authentication Scheme.
You do not need to specify the scheme on each controller action and will work for both. [Authorize] is enough.
services.AddAuthentication( config =>
{
config.DefaultScheme = "smart";
} )
.AddPolicyScheme( "smart", "Bearer or Jwt", options =>
{
options.ForwardDefaultSelector = context =>
{
var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith( "Bearer " ) ?? false;
// You could also check for the actual path here if that's your requirement:
// eg: if (context.HttpContext.Request.Path.StartsWithSegments("/api", StringComparison.InvariantCulture))
if ( bearerAuth )
return JwtBearerDefaults.AuthenticationScheme;
else
return CookieAuthenticationDefaults.AuthenticationScheme;
};
} )
.AddCookie( CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.LoginPath = new PathString( "/Account/Login" );
options.AccessDeniedPath = new PathString( "/Account/Login" );
options.LogoutPath = new PathString( "/Account/Logout" );
options.Cookie.Name = "CustomerPortal.Identity";
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromDays( 1 ); //Account.Login overrides this default value
} )
.AddJwtBearer( JwtBearerDefaults.AuthenticationScheme, options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey( key ),
ValidateIssuer = false,
ValidateAudience = false
};
} );
services.AddAuthorization( options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder( CookieAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme )
.RequireAuthenticatedUser()
.Build();
} );
I think you don't need to set the AuthenticationScheme to your Controller. Just use Authenticated user in ConfigureServices like this:
// requires: using Microsoft.AspNetCore.Authorization;
// using Microsoft.AspNetCore.Mvc.Authorization;
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
For Documentation of my sources: registerAuthorizationHandlers
For the part, whether the scheme-Key wasn't valid, you could use an interpolated string, to use the right keys:
[Authorize(AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme},{JwtBearerDefaults.AuthenticationScheme}")]
Edit:
I did further research and came to following conclusion:
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 like this:
//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();
}
If you have tried the above answers and they are not working for you, try replacing CookieAuthenticationDefaults.AuthenticationScheme with "Identity.Application":
services.AddAuthentication()
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => {
// Your bearer token configuration here
});
//Note how there is no `AddCookie` method here
services.AddAuthorization(options => {
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme, "Identity.Application")
.Build();
});
Then you can simply use [Authorize] on your controllers and the app will automatically decide whether to use Bearer or Cookie authentication:
[Authorize]
[ApiController]
[Route("api/something")]
public class MyController : ControllerBase {
// ...
And finally, you can configure the cookie properties (like sliding expiration or login path) using services.ConfigureApplicationCookie:
services.ConfigureApplicationCookie(options => {
options.SlidingExpiration = true;
//Return a 401 error instead of redirecting to login
options.Events.OnRedirectToLogin = async context => {
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes("Cookies: 401 unauthorized"));
};
//Return a 403 error instead of redirecting to login
options.Events.OnRedirectToAccessDenied = async context => {
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes("Cookies: 403 forbidden"));
};
});
Essentially this is all because using signInManager to log in uses the .ASpNetCore.identity.Application cookie rather than the standard cookies.
Credit goes to user Mashtani on this answer.
I was stuck for hours and hours on this going round in circles with 401 errors before I stumbled across the solution.
Tested with Asp.net Core 2.2
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:4991";
options.RequireHttpsMetadata = false;
// name of the API resource
options.Audience = "api";
});
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://localhost:4991";
options.RequireHttpsMetadata = false;
options.ClientId = "WebApp";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.Scope.Add("api");
options.SaveTokens = true;
});
services.AddAuthorization(options =>
{
// Add policies for API scope claims
options.AddPolicy(AuthorizationConsts.ReadPolicy,
policy => policy.RequireAssertion(context =>
context.User.HasClaim(c =>
((c.Type == AuthorizationConsts.ScopeClaimType && c.Value == AuthorizationConsts.ReadScope)
|| (c.Type == AuthorizationConsts.IdentityProviderClaimType))) && context.User.Identity.IsAuthenticated
));
// No need to add default policy here
});
app.UseAuthentication();
app.UseCookiePolicy();
In the controller, add necessary Authorize attribute
[Authorize(AuthenticationSchemes = AuthorizationConsts.BearerOrCookiesAuthenticationScheme, Policy = AuthorizationConsts.ReadPolicy)]
Here is the helper class
public class AuthorizationConsts
{
public const string BearerOrCookiesAuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme + "," + IdentityServerAuthenticationDefaults.AuthenticationScheme;
public const string IdentityProviderClaimType = "idp";
public const string ScopeClaimType = "scope";
public const string ReadPolicy = "RequireReadPolicy";
public const string ReadScope = "data:read";
}
I had a scenario where I need to use Bearer or Cookie only for file download api alone. So following solution works for me.
Configure services as shown below.
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie()
.AddJwtBearer(options =>
{
options.Authority = gatewayUrl;
})
.AddOpenIdConnect(options =>
{
// Setting default signin scheme for openidconnect makes it to force
// use cookies handler for signin
// because jwthandler doesnt have SigninAsync implemented
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://youridp.com";
options.ClientId = "yourclientid";
options.CallbackPath = "/signin-oidc";
options.ResponseType = OpenIdConnectResponseType.Code;
});
Then configure your controller as shown below.
[HttpGet]
[Authorize(AuthenticationSchemes = "Bearer,OpenIdConnect")]
public async Task<IActionResult> Download([FromQuery(Name = "token")] string token)
{
///your code goes here.
///My file download api will work with both bearer or automatically authenticate with cookies using OpenidConnect.
}
Christo Carstens, answer worked perfectly for me.
Just thought I'd share an additional check that I added to his AddPolicyScheme. (see above)
In my case the issue was that I had an Azure Web Service that was handling all my mobile app requests using JWT, but I also needed it to act as a gateway for Google/Apple/Facebook authentication which uses cookies.
I updated my startup as recommended
.AddPolicyScheme( "smart", "Bearer or Jwt", options =>
{
options.ForwardDefaultSelector = context =>
{
var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith( "Bearer " ) ?? false;
// You could also check for the actual path here if that's your requirement:
// eg: if (context.HttpContext.Request.Path.StartsWithSegments("/api", StringComparison.InvariantCulture))
if ( bearerAuth )
return JwtBearerDefaults.AuthenticationScheme;
else
return CookieAuthenticationDefaults.AuthenticationScheme;
};
} )
My only problem was that if a call was made to any of my api calls which had the [Authorize] attribute set, and no "Authorization" key was in the headers, then it would use Cookie authorization and return a Not found (404) instead of Unauthorized (401).
His suggestion to check for the Path worked, but I wanted to enforce JWT on any method which, in the future, may not have that path.
In the end I settled for this code.
.AddPolicyScheme("CookieOrJWT", "Bearer or Jwt", options =>
{
options.ForwardDefaultSelector = context =>
{
var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false;
if (bearerAuth)
return JwtBearerDefaults.AuthenticationScheme;
else
{
var ep = context.GetEndpoint();
var requiresAuth = ep?.Metadata?.GetMetadata<AuthorizeAttribute>();
return requiresAuth != null
? JwtBearerDefaults.AuthenticationScheme
: CookieAuthenticationDefaults.AuthenticationScheme;
}
};
})
By checking the Endpoint metadata (only in rare cases where Authorization is not in the header), I can set JwtBearerDefaults.AuthenticationScheme for any method decorated with the [Authorize] attribute.
This works even if the method is inheriting the [Authorize] attribute from it's class and does not have it explicitly set.
e.g.
[ApiController]
[Route("api/[Controller]")]
[Authorize]
public class MyController : ControllerBase {
[HttpGet]
public ActionResult MyWebRequestThatRequiresAuthorization() {
return true;
}
}
Thanks to Christo Carstens for the solution. I was breaking my head over this. Saved me countless hours.

Categories

Resources