YARP authentication: how to refresh auth token - c#

YARP-based service(just YARP) proxies a request to Service1. And what is important YARP authenticates on Service1. What is the best way to implement token refresh (see scheme)? In middleware? Another way? Would be grateful for an example.
scheme
I tried to do something like this:
endpoints.MapReverseProxy(proxyPipeline =>
{
proxyPipeline.Use((context, next) =>
{
await _next(context);
if (context.Response.StatusCode == StatusCodes.Status401Unauthorized)
{
// requset NEW_Yarp-Service1_Token on Service1
// set "Bearer NEW_Yarp-Service1_Token" to Header
await _next(context);
}
});
proxyPipeline.UseSessionAffinity();
proxyPipeline.UseLoadBalancing();
proxyPipeline.UsePassiveHealthChecks();
});
This leads to 502 Bad Gateway.

Related

How to reset AuthorizationHeader from HttpClientFactory?

I'm developing a Blazor App which consumes three different APIs, and everything is going fine until the API token expires.
My problem is about having the token with the HTTPClientFactory. Right now, I'm using this approach but feel free to tell me I'm completely wrong about it
services.AddHttpClient("first-api", options =>
{
options.BaseAddress = new Uri(first_api_uri);
});
First, I inject the HttpClient for any of the APIs. Let´s say it´s the first one.
After that, I configure the HttpClientFactory to my APIService and set the Default Header (bearer token) reading from my Protected Local Storage.
services.AddScoped<IAPIOneService, APIOneService>(context =>
{
var httpClientFactory = context.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient("first-api");
var storage = context.GetRequiredService<ProtectedLocalStorage>();
AddHeader(httpClient, storage);
var apiClient = new APIClient(httpClient);
return new APIOneService(apiClient);
});
As I said before, the AddHeader set the Authorization Header to the HttpClient, which goes inside of a "handler" of requests (built by me), called APIClient.
If the token expires, I get a 401 from the server and send the user to the login page, but, when it logs in again, the HttpClient keeps the old token, probably because it does not pass through the DI section where the AddHeader writes the new Authentication Header.
I'm probably doing it wrong, but writing a HttpMessageHandler just does not work because AddHeader cannot read from my Protected Local Storage. It's something from Blazor rules.
Can someone help me and give me the right way to do it?
You can create a custom TokenRefreshHandler and add it to the HttpClient pipeline. The handler could check the response status code for each request and determine if it is a 401 Unauthorized error. If so, it can refresh the token and add this new token to the header before re-sending the request.
TokenRefreshHandler.cs
public class TokenRefreshHandler : DelegatingHandler
{
private readonly ProtectedLocalStorage _protectedLocalStorage;
public TokenRefreshHandler(ProtectedLocalStorage protectedLocalStorage)
{
_protectedLocalStorage = protectedLocalStorage;
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
// Refresh the token
string newToken = await RefreshToken();
_protectedLocalStorage.Set("token", newToken);
// Add the new token to the header
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
// Re-send the request with the new token
response = await base.SendAsync(request, cancellationToken);
}
return response;
}
private async Task<string> RefreshToken()
{
// Refresh the token and return the new token value
// ...
}
}
Register
services.AddHttpClient("first-api", options =>
{
options.BaseAddress = new Uri(first_api_uri);
})
.AddHttpMessageHandler<TokenRefreshHandler>();

SignalR connection falls back to longpolling because of 401

I have a .net 6 backend with an Angular 13 backend, that uses JWT tokens for auth. For some reason SignalR always falls back to longpolling, both on prod and on dev machine, it seems that it's call to negotiate?negotiateVersion=1 is successful, and it chooses WebSockets, but afterwards it's call to localhost:PORT/hubs/myhub?id=[ID]&access_token=[JWTTOKEN] is returned with a 401.
The Angular part is using NGRX to get the JWT token, and the JWT token expires after 5 minutes. When it receives a 401 after connection is established it disconnects, makes a normal renew call, and connects again with the new JWT token. However the request described above will always return 401 even with a valid token.
My SignalR service:
export class NotificationSignalrService {
private connection: signalR.HubConnection;
connectionClosedRefreshTokenSubscription: Subscription | undefined;
startConnectionRefreshTokenSubscription: Subscription | undefined;
constructor(#Inject(APP_CONFIG) private appConfig: any, private store: Store) {
this.connection = new signalR.HubConnectionBuilder()
.withUrl(`${this.appConfig.SIGNALR}/hubs/notificationhub`, this.hubConnectionOptions)
.configureLogging(signalR.LogLevel.Debug)
//.withAutomaticReconnect()
.build();
this.connection.onclose(error => {
console.log(`Forbindelse lukket pga: ${error}`);
this.store.dispatch(AuthActions.renewNoLoading());
this.connectionClosedRefreshTokenSubscription = this.store.select(AuthSelectors.selectTokenRefreshed).subscribe({
next: tokenRefreshed => {
if (tokenRefreshed) {
this.connectionClosedRefreshTokenSubscription?.unsubscribe();
this.startSignalRConnection();
}
}
})
});
this.startSignalRConnection();
this.startListening();
}
startSignalRConnection() {
this.connection.start().catch(error => {
console.log(`Der skete en fejl ved start af signalR ${error}`);
this.startConnectionRefreshTokenSubscription = this.store.select(AuthSelectors.selectTokenRefreshed).subscribe({
next: tokenRefreshed => {
if (tokenRefreshed) {
this.startConnectionRefreshTokenSubscription?.unsubscribe();
this.connection.start().catch(error => console.log(`Kunne ikke starte forbindelsen efter renew ${error}`));
}
}
})
});
}
#HostListener('window:beforeunload', ['$event'])
beforeunloadHandler() {
this.connection.stop();
}
protected get hubConnectionOptions(): IHttpConnectionOptions {
// NOTE: The auth token must be updated for each request. So using headers option is not true.
// Also for websockets and some other protocols signalr cannot set auth headers.
// See https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0#bearer-token-authentication
return {
/*headers,*/
accessTokenFactory: () => {
return this.store.select(AuthSelectors.getLoggedInToken)
.pipe(take(1), filter(x => x !== null), map(x => x === null ? "" : x)).toPromise();
// this.authService.refreshLogin()
// .pipe(map(_ => this.authService.accessToken)).toPromise();
}
};
// NOTE:
// The access token function you provide is called before every HTTP request made by SignalR. If you need to renew the token in order to keep the connection active (because it may expire during the connection), do so from within this function and return the updated token.
// In standard web APIs, bearer tokens are sent in an HTTP header. However, SignalR is unable to set these headers in browsers when using some transports. When using WebSockets and Server - Sent Events, the token is transmitted as a query string parameter.
}
getAuthToken() {
let token = '';
this.store.select(AuthSelectors.getLoggedInToken).pipe(take(1))
.subscribe(authToken => token = authToken ?? "");
return {
Authorization: `Bearer ${token}`
};
}
startListening() {
this.connection.on("NewNotificationForUser", (notification: NotificationsEntity) =>
this.store.dispatch(NotificationsState.NotificationsActions.newNotification({ notification }))
);
}
in .net under Startup i have services.AddSignalR(); under ConfigureServices and
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<NotificationHub>("/hubs/notificationhub");
});
in Configure
My Hub has a [Authorize] attribute.
You probably aren't handling the access_token query string parameter. This is required when using WebSockets because the browser APIs for WebSockets do not support setting headers.
The docs explain how to handle the query string
https://learn.microsoft.com/aspnet/core/signalr/authn-and-authz?view=aspnetcore-7.0#built-in-jwt-authentication

401 Unauthorized with Azure b2c on Xamarin.Forms

I have a Xamarin.Forms application that I'm using to connect to an App Service backend, and I'm attempting to authenticate using Auzre B2C JWT tokens.
Through various tutorials I have managed to get B2C setup using microsoft accounts, and I am able to create users, change passwords, and generate access tokens.
My next step was to add the [Authorize] attribute to my controller and attempt to pass that token to my app service and authorize users, but no matter what I try I get a 401 Unauthorized response from my service.
I'm adding the JWT token to the Authorization header of my HttpClient, and it's getting to the service.
I can paste my token into https://jwt.ms/, and it correctly tells me what's in my token.
I've implemented this code in an attempt to figure out what's wrong.
ConfigureServices in startup.cs looks like this:
public void ConfigureServices(IServiceCollection services) {
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options => {
options.Audience = Configuration["Authentication:AzureAd:ClientId"];
options.Events = new JwtBearerEvents {
OnAuthenticationFailed = AuthenticationFailed
};
options.Authority = $"https://{tenant name}.b2clogin.com/{tenant id}/{Configuration["Authentication:AzureAd:Policy"]}";
options.Events = new JwtBearerEvents {
OnAuthenticationFailed = ctx =>
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
message += "From OnAuthenticationFailed:\n";
message += FlattenException(ctx.Exception);
return Task.CompletedTask;
},
OnChallenge = ctx =>
{
message += "From OnChallenge:\n";
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
ctx.Response.ContentType = "text/plain";
return ctx.Response.WriteAsync(message);
},
OnMessageReceived = ctx =>
{
message = "From OnMessageReceived:\n";
ctx.Request.Headers.TryGetValue("Authorization", out var BearerToken);
if (BearerToken.Count == 0)
BearerToken = "no Bearer token sent\n";
message += "Authorization Header sent: " + BearerToken + "\n";
return Task.CompletedTask;
},
OnTokenValidated = ctx =>
{
Debug.WriteLine("token: " + ctx.SecurityToken.ToString());
return Task.CompletedTask;
}
};
});
services.AddMvc();
}
Configure looks like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
IdentityModelEventSource.ShowPII = true;
} else {
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
}
And I've also added this call to AuthenticationFailed, so I'll know if my authentication is working or not:
Task AuthenticationFailed(AuthenticationFailedContext arg) {
Console.WriteLine(arg.Exception.Message);
return Task.FromResult(0);
}
With my current setup I'm getting a 401 error from the server, and that's right after it hits the OnChallenge event wired up in Startup.cs. According to the link above, that's what gets called right before it returns a 401 to the user, so it seems like the service is receiving the proper token, and authenticating, but maybe I don't have the correct rights set up?
I'm not sure where to go from here, but any guidance would be appreciated.
Edit:
As mentioned in a comment below, I was able to curl my website using the access token generated after logging in through my app like this:
curl https://mywebsite.azurewebsites.net/api/Values -i --header "Authorization: Bearer [TOKEN]"
And that seems to work with no issue, so it seems like it's something with how I'm making a call to the controller through my app, not the authentication itself.
Edit 2 (solution):
So, as per Edit 1, I was correct in that it was just how I was adding the token to the authorization header. It wasn't my brightest moment, but I wasn't calling .Value on the claim that contained my Access Token. I was only calling .ToString() on the claim itself, so the "token" was actually the entire claim text "Access Token: ". I didn't think much of it at the time when I was debugging my service, because I didn't realize it shouldn't have that text there.
Once I corrected that issue the service started working as expected.
So, in the end, I guess it was all working as expected. I was, in fact, not sending the expected token, so I was ... unauthorized.
As requested the line of code that I had to change was this:
So, this won't be 100% applicable to most because I'm using a business library called CSLA, but the idea is the same regardless.
After my b2c call returns the token I store it in the ApplicationContext.User.Identity that's built into the CSLA library. That allows me to get the access token claim later. The important part to take away from this is that I'm storing the token some place that I can access it later when I want to add it to the authorization header.
Later, when I'm making the call with my httpclient I need to get that token, so originally, I was doing this:
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ((ClaimsIdentity)ApplicationContext.User.Identity).Claims.FirstOrDefault(c => c.Type == "AccessToken").ToString());
This isn't correct. This was sending the "token" as with value "Access Token: [token value]. Essentially, it was adding the words "Access Token" to the token I needed to authenticate, and that was failing, because the words "Access Token" are not actually supposed to be part of the token you use to authenticate.
After I changed my call to this:
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ((ClaimsIdentity)ApplicationContext.User.Identity).Claims.FirstOrDefault(c => c.Type == "AccessToken").Value);
It started getting only the token value, and when that was added to the authorization header, it worked just fine.
Edit 2 explains the answer to my problem.
I wasn't adding the token correctly to the authorization header, so the service wasn't able to authenticate the token, or rather, it saw the token as invalid.

UseWindowsAzureActiveDirectoryBearerAuthentication middleware is not triggered on routes that don't exists: app returns 404 instead of 401

I'm using the WindowsAzureActiveDirectoryBearerAuthenticationOptions middleware in a web api project and the important parts of my Startup.cs look like this:
public static void ConfigureApp(IAppBuilder appBuilder)
{
HttpConfiguration config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
appBuilder.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = "xx-xx-xx",
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudience = "yy-yy-yy",
ValidateAudience = true
}
});
config.Filters.Add(new AadAuthorizeAttribute());
appBuilder.UseWebApi(config);
}
The problem is that if I try to access http://localhost/api/404route (which does not exist) I get a 404 when I should have gotten a 401 (since the request from browser does not have any bearer token, etc. and is unauthenticated). If I go to a route that exists, I get 401 as expected. I believe this is because the AadAuthorizeAttribute triggers the middleware execution, which does not happen when webapi cannot find the controller/action.
How do I trigger the authentication for any request even if the route does not exist while using this simple middleware (preferably don't want to write my own)?
Authentication middleware is always run. But it won't throw 401s, that's just not its job. It only checks for identity and adds it to the request if any are found.
What you need is something like this:
app.Use(async (ctx, next) =>
{
if (ctx.Authentication.User.Identity.IsAuthenticated == false)
{
ctx.Response.StatusCode = 401;
return;
}
await next();
});
Put this after your authentication middleware, and it'll send back a 401 for any unauthenticated requests, going to a valid path or not.

How to tell [Authorize] attribute to use Basic authentication with custom message handler?

I'm following along Chapter 8 in Pro ASP .NET Web API Security by Badri L., trying to implement basic authentication for a web application that will be consumed by HTTP/JS clients.
I've added the following Authentication Handler to my WebAPI project:
public class AuthenticationHandler : DelegatingHandler
{
private const string SCHEME = "Basic";
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
System.Threading.CancellationToken
cancellationToken)
{
try
{
// Request Processing
var headers = request.Headers;
if (headers.Authorization != null && SCHEME.Equals(headers.Authorization.Scheme))
{
Encoding encoding = Encoding.GetEncoding("iso-8859-1");
// etc
When I decorate methods in my API with [Authorize] and set a breakpoint at the if statement above, headers.Authorization is null upon the first request. If I continue on this break, the if statement gets hit again, this time with headers.Authorization.Scheme as "Negotiate", instead of "Basic":
I have registered my Handler in WebApiConfig:
config.MessageHandlers.Add(new AuthenticationHandler());
But I'm at a loss as to why the Authorize attribute is not respecting basic authentication, or why - since the scheme is not "basic" and the if() in my handler returns false - I'm getting data from my API controller when I should be getting 401 Unauthorized.
I have not specified any authenticationType in my web.config.
Any idea what I'm doing wrong?
Edit: Full Handler:
public class AuthenticationHandler : DelegatingHandler
{
private const string SCHEME = "Basic";
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
System.Threading.CancellationToken
cancellationToken)
{
try
{
// Request Processing
var headers = request.Headers;
if (headers.Authorization != null && SCHEME.Equals(headers.Authorization.Scheme))
{
Encoding encoding = Encoding.GetEncoding("iso-8859-1");
string credentials = encoding.GetString(Convert.FromBase64String(headers.Authorization.Parameter));
string[] parts = credentials.Split(':');
string userId = parts[0].Trim();
string password = parts[1].Trim();
// TODO: Authentication of userId and Pasword against credentials store here
if (true)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, userId),
new Claim(ClaimTypes.AuthenticationMethod, AuthenticationMethods.Password)
};
var principal = new ClaimsPrincipal(new[] {new ClaimsIdentity(claims, SCHEME)});
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
HttpContext.Current.User = principal;
}
}
var response = await base.SendAsync(request, cancellationToken);
// Response processing
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue(SCHEME));
}
return response;
}
catch (Exception)
{
// Error processing
var response = request.CreateResponse(HttpStatusCode.Unauthorized);
response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue(SCHEME));
return response;
}
}
}
When I decorate methods in my API with [Authorize] and set a breakpoint at the if statement above, headers.Authorization is null upon the first request.
This is expected. This is how it is supposed to work. Browser will show the popup to get credentials from the user only when it receives a 401. Subsequent request will have the authorization header with credentials in the basic scheme.
If I continue on this break, the if statement gets hit again, this time with headers.Authorization.Scheme as "Negotiate", instead of "Basic":
Yes, as answered by Dominick (is it Dominick?), you have Windows Authentication enabled and that is the reason for you getting the Negotiate scheme back from the browser. You must disable all authentication methods either in the config or using IIS manager.
But I'm at a loss as to why the Authorize attribute is not respecting basic authentication, or why - since the scheme is not "basic" and the if() in my handler returns false - I'm getting data from my API controller when I should be getting 401 Unauthorized.
Authorize attribute knows nothing about basic authentication. All it cares is if the identity is authenticated or not. Since you have anonymous authentication enabled (I guess that is the case), Authorize attribute is happy and there is no 401 for the message handler response handling part to add the WWW-Authenticate response header indicating that web API expects credentials in the Basic scheme.
Looks like you have Windows authentication enabled for your app in IIS. Disable all authentication methods in config (system.web and system.webServer) and allow anonymous since you do your own authentication in the message handler.
i think you need to register the handler on global.asa
This article looks good: http://byterot.blogspot.com.br/2012/05/aspnet-web-api-series-messagehandler.html
your global.asa.cs would have something like this:
public static void Application_Start(GlobalFilterCollection filters) {
//...
GlobalConfiguration.Configuration.MessageHandlers.Add(new AuthenticationHandler());
}

Categories