SignalR Azure Service with stand alone Identity Server 4 returns 401 on negotiaton - asp.net-core

We have a ASP.Net Core application that authenticates against a standalone Identity Server 4. The ASP.Net Core app implements a few SignalR Hubs and is working fine when we use the self hosted SignalR Service. When we try to use the Azure SignalR Service, it always returns 401 in the negotiation requests. The response header also states that
"Bearer error="invalid_token", error_description="The signature key
was not found"
I thought the JWT-Configuration is correct because it works in the self hosted mode but it looks like, our ASP.Net Core application needs information about the signature key (certificate) that our identity server uses to sign the tokens. So I tried to use the same method like our identity server, to create the certificate and resolve it. Without luck :-(
This is what our JWT-Configuration looks like right now:
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options => {
var appSettings = Configuration.Get<AppSettingsModel>();
options.Authority = appSettings.Authority;
options.RefreshOnIssuerKeyNotFound = true;
if (environment.IsDevelopment()) {
options.RequireHttpsMetadata = false;
}
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters {
ValidateAudience = false,
IssuerSigningKey = new X509SecurityKey(getSigningCredential()),
IssuerSigningKeyResolver = (string token, SecurityToken securityToken, string kid, TokenValidationParameters validationParameters) =>
new List<X509SecurityKey> { new X509SecurityKey(getSigningCredential()) }
};
options.Events = new JwtBearerEvents {
OnMessageReceived = context => {
var accessToken = "";
var headerToken = context.Request.Headers[HeaderNames.Authorization].ToString().Replace("Bearer ", "");
if (!string.IsNullOrEmpty(headerToken) && headerToken.Length > 0) {
accessToken = headerToken;
}
var queryStringToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(queryStringToken) && queryStringToken.ToString().Length > 0) {
accessToken = queryStringToken;
}
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) {
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
Update:
We also have a extended the signalR.DefaultHttpClient in our Angular Client and after playing around a bit, I noticed the application is working fine without it:
export class CustomSignalRHttpClientService extends signalR.DefaultHttpClient {
userSubscription: any;
token: string = "";
constructor(private authService: AuthorizeService) {
super(console); // the base class wants a signalR.ILogger
this.userSubscription = this.authService.accessToken$.subscribe(token => {
this.token = token
});
}
public async send(
request: signalR.HttpRequest
): Promise<signalR.HttpResponse> {
let authHeaders = {
Authorization: `Bearer ${this.token}`
};
request.headers = { ...request.headers, ...authHeaders };
try {
const response = await super.send(request);
return response;
} catch (er) {
if (er instanceof signalR.HttpError) {
const error = er as signalR.HttpError;
if (error.statusCode == 401) {
console.log('customSignalRHttpClient -> 401 -> TokenRefresh')
//token expired - trying a refresh via refresh token
this.token = await this.authService.getAccessToken().toPromise();
authHeaders = {
Authorization: `Bearer ${this.token}`
};
request.headers = { ...request.headers, ...authHeaders };
}
} else {
throw er;
}
}
//re try the request
return super.send(request);
}
}
The problem is, when the token expires while the application is not open (computer is in sleep mode e.g.), the negotiaton process is failing again.

I finally found and solved the problem. The difference of the authentication between "self hosted" and "Azure SignalR Service" is in the negotiation process.
Self Hosted:
SignalR-Javascript client authenticates against our own webserver with
the same token that our Javascript (Angular) app uses. It sends the
token with the negotiation request and all coming requests of the
signalR Http-Client.
Azure SignalR Service:
SignalR-Javascript client sends a negotiation request to our own
webserver and receives a new token for all coming requests against the
Azure SignalR Service.
So our problem was in the CustomSignalRHttpClientService. We changed the Authentication header to our own API-Token for all requests, including the requests against the Azure SignalR Service -> Bad Idea.
So we learned that the Azure SignalR Service is using it's own token. That also means the token can invalidate independently with our own token. So we have to handle 401 Statuscodes in a different way.
This is our new CustomSignalRHttpClientService:
export class CustomSignalRHttpClientService extends signalR.DefaultHttpClient {
userSubscription: any;
token: string = "";
constructor(private authService: AuthorizeService, #Inject(ENV) private env: IEnvironment, private router: Router,) {
super(console); // the base class wants a signalR.ILogger
this.userSubscription = this.authService.accessToken$.subscribe(token => {
this.token = token
});
}
public async send(
request: signalR.HttpRequest
): Promise<signalR.HttpResponse> {
if (!request.url.startsWith(this.env.apiUrl)) {
return super.send(request);
}
try {
const response = await super.send(request);
return response;
} catch (er) {
if (er instanceof signalR.HttpError) {
const error = er as signalR.HttpError;
if (error.statusCode == 401 && !this.router.url.toLowerCase().includes('onboarding')) {
this.router.navigate([ApplicationPaths.Login], {
queryParams: {
[QueryParameterNames.ReturnUrl]: this.router.url
}
});
}
} else {
throw er;
}
}
//re try the request
return super.send(request);
}
}
Our login-Route handles the token refresh (if required). But it could also happen, that our own api-token is still valid, but the Azure SignalR Service token is not. Therefore we handle some reconnection logic inside the service that creates the SignalR Connections like this:
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe(async (page: NavigationEnd) => {
if (page.url.toLocaleLowerCase().includes(ApplicationPaths.Login)) {
await this.restartAllConnections();
}
});
hope this helps somebody

Related

Authenticating with JWT on Signalr hub ends with 401 with Authorize attribute

I am trying to use Signalr with the following JWT options
.AddJwtBearer(options => {
options.TokenValidationParameters =new TokenValidationParameters()
{
ValidateAudience=false,
ValidateIssuer=true,
ValidateLifetime=true,
ValidateIssuerSigningKey=true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience=builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Key"])),
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/ConnectionsHub")))
{
context.Token = accessToken;
context.Response.Headers.Authorization = accessToken;
}
return Task.CompletedTask;
}
};
})
...
builder.Services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
builder.Services.AddSingleton<IUserIdProvider, CustomEmailProvider>();
but when I sending the Authentication token to the signalr client it throws 401 if I used Authorize attribute on top of the Hub but without the Attribute i see that no Identity user is recognized on GetUserId(returns null and there arent any claims)
public class CustomEmailProvider : IUserIdProvider
{
public virtual string GetUserId(HubConnectionContext connection)
{
var res = connection.User?.Claims.FirstOrDefault(x=>x.Type==ClaimTypes.NameIdentifier);
return res?.Value;
}
}
So what could I be possibly doing wrong with the JWT authentication? it is worth noting that I am using a blazor server side project and Identity

razor pages with firebase auth - where to put this token ? :)

i am working on web site with razor pages. part of the site should be accessed only by registred users. decided to go with firebase authentification (now with login and password ).
created everything necessary in firebase.
created backend code for user registration - works well.
created area which requires authorisation
services.AddRazorPages(options =>
{
options.Conventions.AuthorizeAreaFolder("User", "/");
})
added jwt middleware
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
added code to login page to call firebase to get token
function login()
{
firebase.auth().signInWithEmailAndPassword(email, password)
.then((userCredential) => {
// Signed in
var user = userCredential.user;
// ...
alert("signed");
})
.catch((error) => {
var errorCode = error.code;
var errorMessage = error.message;
alert(errorMessage);
});
}
got token from firebase.
if i'd call service next, i'd simply put token in "bearer" header.
tried to find how to add header to current browser for future requests and failed.
as i understand, i need this token to be added to auth header ? how ? :)
feeling dumb ;( tried to google, but most samples are for using this token later with api calls.
or i am going in the wrong direction?
tia
ish
well. it seems that it is not possible to add bearer from js, so i switched to cookies
in startup.cs use cookies
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["bearer"];
return Task.CompletedTask;
}
};
code to login with firebase, put token into the cookie and redirect
function login() {
firebase.auth().signInWithEmailAndPassword(email, password)
.then((userCredential) => {
// Signed in
var user = userCredential.user;
firebase.auth().currentUser.getIdToken(true).then(function (idToken)
{
document.cookie = "bearer" + "=" + idToken;
window.location.href = "/user/index";
}).catch(function (error) {
// Handle error
});
alert("signed");
})
.catch((error) => {
var errorCode = error.code;
var errorMessage = error.message;
alert(errorMessage);
});
}
or the same with firebaseUI
function login1()
{
ui.start('#firebaseui-auth-container', {
signInSuccessUrl: '/User/index',
signInOptions: [
{
provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
requireDisplayName: false
}
],
callbacks:
{
signInSuccessWithAuthResult: function (authResult, redirectUrl)
{
var user = authResult.user;
firebase.auth().currentUser.getIdToken(true).then(function (idToken) {
document.cookie = "bearer" + "=" + idToken;
}).catch(function (error) {
// Handle error
});
return true;
}
}
});
}

Storing claims on cookie while redirecting to other URL and also without identity authentication

I just need advise if this is feasible. I am developing an authorization for my Shopify app and I need to somewhat store the access token from shopify auth for future verification of my front-end app.
So the first end-point the shopify is calling is this one:
[HttpGet("install")]
public async Task<IActionResult> Install()
{
try
{
if (ModelState.IsValid)
{
var queryString = Request.QueryString.Value;
var isValid = _shopifyService.VerifyRequest(queryString);
if (isValid)
{
var shopifyUrl = Request.Query["shop"];
var authUrl = _shopifyService.BuildAuthUrl(shopifyUrl,
$"{Request.Scheme}://{Request.Host.Value}/api/shopify/authorize",
Program.Settings.Shopify.AuthorizationScope);
return Redirect(authUrl);
}
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
This works fine and the result of this api call will actually redirect to same link to my api, but this one will authorize the app:
[HttpGet("authorize")]
public async Task<IActionResult> AuthorizeStore()
{
try
{
if (ModelState.IsValid)
{
var code = Request.Query["code"];
var shopifyUrl = Request.Query["shop"];
var accessToken = await _shopifyService.AuthorizeStore(code, shopifyUrl).ConfigureAwait(false);
var identity = User.Identity as ClaimsIdentity;
identity.AddClaim(new Claim(Constants.Claims.AccessToken, accessToken));
// genereate the new ClaimsPrincipal
var claimsPrincipal = new ClaimsPrincipal(identity);
// store the original tokens in the AuthenticationProperties
var props = new AuthenticationProperties {
AllowRefresh = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(1),
IsPersistent = false,
IssuedUtc = DateTimeOffset.UtcNow,
};
// sign in using the built-in Authentication Manager and ClaimsPrincipal
// this will create a cookie as defined in CookieAuthentication middleware
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, props).ConfigureAwait(false);
Uri uri = new Uri($"{Program.Settings.Shopify.RedirectUrl}?token={accessToken}");
return Redirect(uri.ToString());
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
So the above api will authorize my app in shopify and will return an access token. The accessToken is the one I want to save in the claims identity with Cookie authentication type(this is without authorizing user credentials). Still no errors at that point and after calling the HttpContext.SignInAsync function, I can still view using debugger the newly added claims.
As, you can see in the code, after assigning claims, I call to redirect the app to front-end link(Note: front-end and back-end has different url)
In my front-end app, I have a Nuxt middleware that I put a logic to check the token received from back-end since I only pass the token to the front-end app using query params. Here's my middleware code:
export default function ({ app, route, next, store, error, req }) {
if (process.browser) {
const shopifyAccessToken = store.get('cache/shopifyAccessToken', null)
if (!shopifyAccessToken && route.query.token) {
// if has token on query params but not yet in cache, store token and redirect
store.set('cache/shopifyAccessToken', route.query.token)
app.router.push({
path: '/',
query: {}
})
// verify access token on the route
app.$axios
.get(`/shopify/verifyaccess/${route.query.token}`)
.catch((err) => {
error(err)
})
} else if (!shopifyAccessToken && !route.query.token) {
// if does not have both, throw error
error({
statusCode: 401,
message: 'Unauthorized access to this app'
})
}
} else {
next()
}
}
In my middleware, when the route has query params equal to token= It calls another api to verify the accessToken saved in my claims identity:
[HttpGet("verifyaccess/{accessToken}")]
public async Task<IActionResult> VerifyAccess(string accessToken)
{
try
{
if (ModelState.IsValid)
{
var principal = HttpContext.User;
if (principal?.Claims == null)
return Unauthorized(_localizer["NotAuthenticated"]);
var accessTokenClaim = principal.FindFirstValue(Constants.Claims.AccessToken);
if (accessToken == accessTokenClaim)
{
return Ok();
}
else
{
return Unauthorized(_localizer["NotAuthenticated"]);
}
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
Looking at the code above, it always fails me because the claims identity that I saved on the authorize endpoint was not there or in short the ClaimsIdentity is always empty.
Here's how I register the Cookie config:
private void ConfigureAuthCookie(IServiceCollection services)
{
services.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
option.RequireAuthenticatedSignIn = false;
})
.AddCookie(options => {
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.SlidingExpiration = true;
options.Cookie.Name = "shopifytoken";
});
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
}
and I also put a app.UseAuthentication() and app.UseAuthorization() on my Startup.Configure
Please let me know if this seems confusing so I can revised it. My main goal here is to be able to access that accessToken that I saved in the ClaimsIdentity so that I can verify the token. The reason why I did this because currently the shopify does not have an API for verifying access token. So when a user access my app link like this one http://example.com/?token=<any incorrect token> then they can already access my app.

Blazor WebAssembly SignalR Authentication

I would love to see an example on how to add authentication to a SignalR hub connection using the WebAssembly flavor of Blazor. My dotnet version is 3.1.300.
I can follow these steps to get an open, unauthenticated SignalR connection working: https://learn.microsoft.com/en-us/aspnet/core/tutorials/signalr-blazor-webassembly?view=aspnetcore-3.1&tabs=visual-studio
All the tutorials I find seem older or are for a server-hosted type, and don't use the built-in template.
I have added authentication to the rest of the back-end, using the appropriate template and these instructions, including the database:
https://learn.microsoft.com/en-us/aspnet/core/security/blazor/?view=aspnetcore-3.1
But every time I add [Authenticate] to the chat hub, I get an error returned. Is there any way, extending the first tutorial, that we can authenticate the hub that is created there? It would be great to hitch on to the built-in ASP.NET system, but I am fine just passing a token in as an additional parameter and doing it myself, if that is best. In that case I would need to learn how to get the token out of the Blazor WebAssembly, and then look it up somewhere on the server. This seems wrong, but it would basically fill my needs, as an alternative.
There are all sorts of half-solutions out there, or designed for an older version, but nothing to build off the stock tutorial that MS presents.
Update:
Following the hints in this news release https://devblogs.microsoft.com/aspnet/blazor-webassembly-3-2-0-preview-2-release-now-available/, I now can get a token from inside the razor page, and inject it into the header. I guess this is good?? But then how do I get it and make use of it on the server?
Here is a snippet of the razor code:
protected override async Task OnInitializedAsync()
{
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(UriHelper.BaseUri);
var tokenResult = await AuthenticationService.RequestAccessToken();
if (tokenResult.TryGetToken(out var token))
{
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");
hubConnection = new HubConnectionBuilder()
.WithUrl(UriHelper.ToAbsoluteUri("/chatHub"), options =>
{
options.AccessTokenProvider = () => Task.FromResult(token.Value);
})
.Build();
}
}
Update 2:
I tried the tip in here: https://github.com/dotnet/aspnetcore/issues/18697
And changed my code to:
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chatHub?access_token=" + token.Value))
.Build();
But no joy.
I've come across the same issue.
My solution was 2-sided: I had to fix something in the fronend and in the backend.
Blazor
In your connection builder you should add the AccessTokenProvider:
string accessToken = "eyYourToken";
connection = new HubConnectionBuilder()
.WithUrl("https://localhost:5001/hub/chat", options =>
{
options.AccessTokenProvider = () => Task.FromResult(token.Value);
})
.Build();
options.AccessTokenProvider is of type Func<Task<string>>, thus you can also perform async operations here. Should that be required.
Doing solely this, should allow SignalR to work.
Backend
However! You might still see an error when SignalR attempts to create a WebSocket connection. This is because you are likely using IdentityServer on the backend and this does not support Jwt tokens from query strings. Unfortunately SignalR attempts to authorize websocket requests by a query string parameter called access_token.
Add this code to your startup:
.AddJwtBearer("Bearer", options =>
{
// other configurations omitted for brevity
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("/hubs"))) // Ensure that this path is the same as yours!
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
edit 1: Clarified the usage of the Blazor SignalR code
In my case (Blazor WebAssembly, hosted on ASP.NET Core 5.0 using JWT Bearer Token Auth), I had to add the following:
Blazor WASM Client
When building the connection (in my case: in the constructor of some service proxy class), use IAccessTokenProvider and configure the AccessTokenProvider option like so:
public ServiceProxy(HttpClient httpClient, IAccessTokenProvider tokenProvider) {
HubConnection = new HubConnectionBuilder()
.WithUrl(
new Uri(httpClient.BaseAddress, "/hubs/service"),
options => {
options.AccessTokenProvider = async () => {
var result = await tokenProvider.RequestAccessToken();
if (result.TryGetToken(out var token)) {
return token.Value;
}
else {
return string.Empty;
}
};
})
.WithAutomaticReconnect() // optional
.Build();
}
ASP.NET Core Server
Add the following to Startup.ConfigureServices:
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options => {
// store user's "name" claim in User.Identity.Name
options.TokenValidationParameters.NameClaimType = "name";
// pass JWT bearer token to SignalR connection context
// (from https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0)
options.Events = new JwtBearerEvents {
OnMessageReceived = context => {
var accessToken = context.Request.Query["access_token"];
// If the request is for on of our SignalR hubs ...
if (!string.IsNullOrEmpty(accessToken) &&
(context.HttpContext.Request.Path.StartsWithSegments("/hubs/service"))) {
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
This is my solution and works
[Inject] HttpClient httpClient { get; set; }
[Inject] IAccessTokenProvider tokenProvider { get; set; }
HubConnection hubConnection { get; set; }
(...)
private async Task ConnectToNotificationHub()
{
string url = httpClient.BaseAddress.ToString() + "notificationhub";
var tokenResult = await tokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var token))
{
hubConnection = new HubConnectionBuilder().WithUrl(url, options =>
{
options.Headers.Add("Authorization", $"Bearer {token.Value}");
}).Build();
await hubConnection.StartAsync();
hubConnection.Closed += async (s) =>
{
await hubConnection.StartAsync();
};
hubConnection.On<string>("notification", m =>
{
string msg = m;
});
}
}

SignalR + Web Api 2 Bearer Token Authentication

I have client side app which is running on nodejs server and have Web Api project. I have Bearer Token Authentication in Startup.cs class:
// Configure the application for OAuth based flow
var oAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
AllowInsecureHttp = true
};
app.UseOAuthAuthorizationServer(oAuthOptions);
app.UseOAuthBearerAuthentication(newOAuthBearerAuthenticationOptions());
and I want to start hub on client:
$.connection.hub.url = 'https://localhost:44302/signalr/hubs';
this.proxy = $.connection.chathub;
this.proxy.client.onMessage = function (stock){
};
$.connection.hub.start()
.done(function(error){})
.fail(function(error){});
On client-side I get token like this:
$.post("https://localhost:44302/token", { grant_type: "password", username: user.username, password: user.password})
.done(function (data) {
if (data && data.access_token) {
bearerToken = data.access_token;
}
})
.fail(function (xhr) {
error('Wrong password or username.');
});
So I saved token in bearerToken but how now I can pass this to server side?
In this code:
[HubName("chathub")]
public class ChatHub : Hub
{
public override Task OnConnected()
{
if (Context.User.Identity.IsAuthenticated)
{
var identity = (ClaimsIdentity)Context.User.Identity;
...
IsAuthenticated is false, and there no data in User.