How to route JWT Token request from client app, formed with double slashes? - asp.net-core

Currently we have a WPF client app that request bearer tokens to an ASP MVC app using OAuth.
This configuration takes place at startup when an endpoint is defined:
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = true
};
The client WPF application requests tokens by issuing a POST request to [resourceBaseAddress/Token]. The request content I will assume is not relevante to my question.
The problem is that my "resourceBaseAddress" contains a trailing forward slash, so the request is formed in a way such as http://contoso.com//Token (note the double slash). And it currently works.
But now that we are migrating to ASP.NET Core and by consequence I had to add a controller with (Jwt) Token action, I am experiencing problems to route this request with double forward slashes.
As I cannot change code on client app, how can I configure this controller method properly?

How about a middleware that rewrites the request path? Did you try something like this?
Example:
app.Use(async (context, next) =>
{
var url = context.Request.Path.Value;
if (url.EndsWith("//Token"))
{
// rewrite and continue processing
context.Request.Path = "/Token";
}
await next();
});
app.UseRouting();
And a simple controller like this:
[Route("[controller]")]
public class TokenController : ControllerBase
{
[HttpPost]
public string Post()
{
return "Token";
}
}

Related

Setup JWT from headers to be used by default in HttpClient

I use .NET 6 and add HttpClient in Program.cs like this:
builder.Services.AddHttpClient<IUserClient, UserClient>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Clients:UserBaseUrl"]);
});
In my onion architecture when I want to create an order a request is coming to OrderService.API and to check userId is correct in IUserService from OrderService.BL I call user microservice API with the help of registered HttpClient. So the problem is that for now, I need to transfer JWT to the business logic layer via method parameters.
var createdOrder = await _orderService.Add(model.MapToDto(), HttpContext.Request.Headers["Authorization"]);
I don't like it because, for every method using the HttpClient, it's necessary to provide an extra parameter. I think maybe there is a way to set up HttpClient default authentication during the current request.
I tried to setup default request headers during HttpClient registration:
builder.Services.AddHttpClient<IUserClient, UserClient>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Clients:UserBaseUrl"]);
client.DefaultRequestHeaders.Add("Authorization", token); // setup token
});
but I don't know how to get JWT from HttpRequest headers there.
Additionally, I thought maybe I can set up the header for HttpClient in some additional BaseController which would be nested by any other my controller but it doesn't seem to be a great solution.
Maybe there is a way for middleware use but as I understand we handle an incoming request to OrderService and can't handle outcoming requests from HttpClient.
So would be grateful for any of your ideas!
Thanks to #Rena who provided a link to an existing similar problem: https://stackoverflow.com/a/62324677/11398810
So I created a message helper like this and it seems work for me:
public sealed class HttpClientsAuthHelper : DelegatingHandler
{
private readonly IHttpContextAccessor _accessor;
public HttpClientsAuthHelper(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = _accessor.HttpContext.Request.Headers["Authorization"].First();
request.Headers.Add("Authorization", token);
return await base.SendAsync(request, cancellationToken);
}
}
And added these lines to Program.cs:
builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddTransient<HttpClientsAuthHelper>();
builder.Services.AddHttpClient<IUserClient, UserClient>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Clients:UserBaseUrl"]);
}).AddHttpMessageHandler<HttpClientsAuthHelper>();
builder.Services.AddHttpClient<IProductClient, ProductClient>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Clients:ProductBaseUrl"]);
}).AddHttpMessageHandler<HttpClientsAuthHelper>();
I'm not sure how correct such approach so I'll dive a bit into this logic later =)

In ASP.NET Core, can the application generate a new request to itself without doing IO?

I'm implementing a custom ASP.NET Core middleware to handle the ETag/If-Match pattern to prevent lost updates. My HTTP GET operations will return an ETag value in the header and every PUT operation will be required to include that ETag in the If-Match header. I'm hashing the body of the GET responses to generate the ETag value. I've currently implemented the middleware using the HttpClient to perform the GET operation when I need to check the If-Match header. This works but requires a network/out-of-process call. Shouldn't there be a better performing way to call the ASP.NET Core HTTP pipeline without leaving the process? Can the application generate a new request to itself without doing IO? Here's the code currently for my middleware:
public class ETagIfMatchMiddleware : IMiddleware
{
//client for my asp.net core application
public static HttpClient client = new HttpClient { BaseAddress = new Uri("https://localhost:5001") };
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var request = context.Request;
if (request.Method == HttpMethods.Put)
{
var ifMatch = request.Headers[HeaderNames.IfMatch];
//requires network out of process call
var response = await client.GetAsync(request.GetEncodedUrl());
string eTag = response.Headers.ETag.Tag;
if (eTag != ifMatch)
{
context.Response.StatusCode = StatusCodes.Status412PreconditionFailed;
return;
}
}
await next(context);
}
}

How to simply authenticate a request in middleware?

I am developing a Web API in ASP.NET Core 2.2. I want to authenticate every request by any user at any time based on token stored in Authorization header of HTTP Request so the user can call controllers and actions which are annotated with [AuthorizeAttribute]. This is my middleware:
public class TokenBasedAuthenticationMiddleware
{
private RequestDelegate nextDelegate;
public TokenBasedAuthenticationMiddleware(RequestDelegate next) => nextDelegate = next;
public async Task Invoke(HttpContext httpContext, IRegistrationsRepository registrationsRepository)
{
if (registrationsRepository.IsAuthorized(httpContext.Request.Headers["Authorization"]))
{
//Code To Authenticate this request?
}
await nextDelegate.Invoke(httpContext);
}
}
How can I simply authenticate the request (i.e set HttpContext.User.Identity.IsAuthenticated to true) without going into any complexity?
You can try something like :
var claims = new[] { new Claim("name", "YourName"), new Claim(ClaimTypes.Role, "Admin") };
var identity = new ClaimsIdentity(claims, "JWT");
httpContext.User = new ClaimsPrincipal(identity);
await nextDelegate.Invoke(httpContext);
But you can directly use the AddJwtBearer extension if using JWT bearer token authentication . The JwtBearer middleware looks for tokens (JSON Web Tokens or JWTs) in the HTTP Authorization header of incoming requests. If a valid token is found, the request is authorized. You then add the [Authorize] attribute on your controllers or routes you want protected:
Related code samples :
https://developer.okta.com/blog/2018/03/23/token-authentication-aspnetcore-complete-guide
https://garywoodfine.com/asp-net-core-2-2-jwt-authentication-tutorial/

IdentityServer4 How can I redirect after login to the revious url page without registering all routes at IdP

As recommend I would have register the authorize callback url/redirect_url at IdP, which it works.
But what if a client using MVC app tries to access a page with an unauthorized state, will be redirect to idsrv login page.
The redirect_url is always (Home page entry point) as configured.
To change this behavior I would have to register all possible routes at IdP.
That can not a be solution!
On idsrv Login method I have tried:
Login(string returnUrl)
checking the value from returnUrl it gives /connect/authorize/callback?client_id=...
Shouldn't returnUrl have the url of the previous page? Like in a normal mvc app has..
I have tried to get Referer store it on session and then redirect..
if (!string.IsNullOrEmpty(Request.Headers["Referer"].ToString()))
{
this.httpContextAccessor.HttpContext.Session.SetString("Referer", Request.Headers["Referer"].ToString());
}
But that doesn't work Referer comes null...
I have checked what's coming on context from interation services
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
context.RedirectUri
And returns /signin-oidc/ this is the automated way for returning (Home page entry point).
Any chance to get the previous url, so that the user can be redirect?
So what can I do else?
I'm using Hybrid flow to manage the following clients : mvc-app, classic-asp, web api
Here's an example of implementation allowing you to achieve what you want. Keep in mind that there's other ways of doing it.
All the code goes on your client, the server never knows anything about the end url.
First, you want to create a custom attribute that will be decorating all your actions/controllers that you want to protect:
using System;
using System.Web.Mvc;
namespace MyApp
{
internal class MyCustomAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
if (filterContext.Result is HttpUnauthorizedResult)
{
filterContext.RequestContext.HttpContext.Session["oidc-returnUrl"] = filterContext.RequestContext.HttpContext.Request.UrlReferrer?.PathAndQuery;
}
}
}
}
And then you are going to create a login route/action that will handle all your authorize requests:
using System.Web.Mvc;
namespace MyApp
{
public class AccountController : Controller
{
[MyCustomAuthorize]
public ActionResult Login()
{
returnUrl = Session["oidc-returnUrl"]?.ToString();
// clean up
Session["oidc-returnUrl"] = null;
return Redirect(returnUrl ?? "/");
}
}
}
The login path can be changed in your startup code:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = "/my-login"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
// setting up your client
});
}
}

How to Consume/Validate Token Issued by AspNet.Security.OpenIdConnect.Server (RC1)?

I have followed everything I know from posts regarding how to implement AspNet.Security.OpenIdConnect.Server.
Pinpoint, do you hear me? ;)
I've managed to separate token issuing and token consumption. I won't show the "auth server side" because I think that part is all set, but I'll show how I built the authentication ticket inside my custom AuthorizationProvider:
public sealed class AuthorizationProvider : OpenIdConnectServerProvider
{
// The other overrides are not show. I've relaxed them to always validate.
public override async Task GrantResourceOwnerCredentials(GrantResourceOwnerCredentialsContext context)
{
// I'm using Microsoft.AspNet.Identity to validate user/password.
// So, let's say that I already have MyUser user from
//UserManager<MyUser> UM:
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
//identity.AddClaims(await UM.GetClaimsAsync(user));
identity.AddClaim(ClaimTypes.Name, user.UserName);
(await UM.GetRolesAsync(user)).ToList().ForEach(role => {
identity.AddClaim(ClaimTypes.Role, role);
});
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Some new stuff, per my latest research
ticket.SetResources(new[] { "my_resource_server" });
ticket.SetAudiences(new[] { "my_resource_server" });
ticket.SetScopes(new[] { "defaultscope" });
context.Validated(ticket);
}
}
And startup at the auth server:
using System;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.Data.Entity;
using Microsoft.Extensions.DependencyInjection;
using MyAuthServer.Providers;
namespace My.AuthServer
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication();
services.AddCaching();
services.AddMvc();
string connectionString = "there is actually one";
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<MyDbContext>(options => {
options.UseSqlServer(connectionString).UseRowNumberForPaging();
});
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<MyDbContext>().AddDefaultTokenProviders();
}
public void Configure(IApplicationBuilder app)
{
app.UseIISPlatformHandler();
app.UseOpenIdConnectServer(options => {
options.ApplicationCanDisplayErrors = true;
options.AllowInsecureHttp = true;
options.Provider = new AuthorizationProvider();
options.TokenEndpointPath = "/token";
options.AccessTokenLifetime = new TimeSpan(1, 0, 0, 0);
options.Issuer = new Uri("http://localhost:60556/");
});
app.UseMvc();
app.UseWelcomePage();
}
public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}
}
Sure enough, when I have this HTTP request, I do get an access token, but I'm not sure if that access token has all the data that the resource server expects.
POST /token HTTP/1.1
Host: localhost:60556
Content-Type: application/x-www-form-urlencoded
username=admin&password=pw&grant_type=password
Now, At the resource server side, I'm using JWT Bearer Authentication. On startup, I've got:
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.Data.Entity;
using Microsoft.Extensions.DependencyInjection;
namespace MyResourceServer
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
string connectionString = "there is actually one";
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<MyDbContext>(options => {
options.UseSqlServer(connectionString).UseRowNumberForPaging();
});
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<MyDbContext>().AddDefaultTokenProviders();
}
public void Configure(IApplicationBuilder app)
{
app.UseIISPlatformHandler();
app.UseMvc();
app.UseWelcomePage();
app.UseJwtBearerAuthentication(options => {
options.Audience = "my_resource_server";
options.Authority = "http://localhost:60556/";
options.AutomaticAuthenticate = true;
options.RequireHttpsMetadata = false;
});
}
public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}
}
When I make this HTTP request to the resource server, I get a 401 Unauthorized:
GET /api/user/myroles HTTP/1.1
Host: localhost:64539
Authorization: Bearer eyJhbGciOiJS...
Content-Type: application/json;charset=utf-8
The controller who has a route to /api/user/myroles is decorated with a plain [Authorize] with no parameters.
I feel like I'm missing something in both auth and resource servers, but don't know what they are.
The other questions that ask "how to validate token issued by AspNet.Security.OpenIdConnect.Server" don't have an answer. I would appreciate some help in this.
Also, I've noticed that there is OAuth Introspection commented out in the sample provider, and have read somewhere that Jwt is not going to be supported soon. I can't find the dependency that gives me the OAuth Instrospection.
UPDATE I've included both of my startup.cs, from each of auth and resource servers. Could there be anything wrong that would cause the resource server to always return a 401 for every request?
One thing I didn't really touch throughout this whole endeavor is signing. It seems to generate a signature for the JWT at the auth server, but the resource server (I guess) doesn't know the signing keys. Back in the OWIN projects, I had to create a machine key and put on the two servers.
Edit: the order of your middleware instances is not correct: the JWT bearer middleware must be registered before MVC:
app.UseIISPlatformHandler();
app.UseJwtBearerAuthentication(options => {
options.Audience = "my_resource_server";
options.Authority = "http://localhost:60556/";
options.AutomaticAuthenticate = true;
options.RequireHttpsMetadata = false;
});
app.UseMvc();
app.UseWelcomePage();
Sure enough, when I have this HTTP request, I do get an access token, but I'm not sure if that access token has all the data that the resource server expects.
Your authorization server and resource server configuration look fine, but you're not setting the "destination" when adding your claims (don't forget that to avoid leaking confidential data, AspNet.Security.OpenIdConnect.Server refuses to serialize the claims that don't explicitly specify a destination):
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(ClaimTypes.Name, user.UserName, destination: "id_token token");
(await UM.GetRolesAsync(user)).ToList().ForEach(role => {
identity.AddClaim(ClaimTypes.Role, role, destination: "id_token token");
});
Also, I've noticed that there is OAuth Introspection commented out in the sample provider, and have read somewhere that Jwt is not going to be supported soon. I can't find the dependency that gives me the OAuth Instrospection.
Starting with the next beta (ASOS beta5, not yet on NuGet.org when writing this answer), we'll stop using JWT as the default format for access tokens, but of course, JWT will still be supported OTB.
Tokens now being opaque by default, you'll have to use either the new validation middleware (inspired from Katana's OAuthBearerAuthenticationMiddleware) or the new standard introspection middleware, that implements the OAuth2 introspection RFC:
app.UseOAuthValidation();
// Alternatively, you can also use the introspection middleware.
// Using it is recommended if your resource server is in a
// different application/separated from the authorization server.
//
// app.UseOAuthIntrospection(options => {
// options.AutomaticAuthenticate = true;
// options.AutomaticChallenge = true;
// options.Authority = "http://localhost:54540/";
// options.Audience = "resource_server";
// options.ClientId = "resource_server";
// options.ClientSecret = "875sqd4s5d748z78z7ds1ff8zz8814ff88ed8ea4z4zzd";
// });
You can find more information about these 2 middleware here: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/issues/185