I am trying to create an app that provide Api methods and SignalR hub connection to authorized clients using microservices architecture. We are using reference token with Identity Server. Identity Server is another project and runs standalone, so my application must authorize a user through Identity Server and get his Claims.
I am getting errors when clients are trying to start a socket connection.
Server:
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder => builder
.WithOrigins("https://localhostClientUrl")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
services.AddAuthentication(OAuth2IntrospectionDefaults.AuthenticationScheme)
.AddOAuth2Introspection(options =>
{
options.Authority = "https://identityserverURL";
options.ClientId = "client";
options.ClientSecret = "secret";
options.Events = new OAuth2IntrospectionEvents
{
OnTokenValidated = async context =>
{
var path = context.HttpContext.Request.Path;
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments(Configuration["MapHub"])))
{
// Read the token out of the query string
context.SecurityToken = accessToken;
}
await Task.CompletedTask;
}
};
});
also
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
endpoints.MapRazorPages();
endpoints.MapHub<ChatHub>(Configuration["MapHub"]);
});
Client:
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(this.hubUrl, { accessTokenFactory: () => "ReferenceToken" })
.withAutomaticReconnect()
.build();
this.hubConnection
.start()
.then(() => {
return true;
})
.catch((err) => {
return false;
});
The client is successfully connected and can get all claims on HttpContext.User for the current connection. Prior successful connection i am receiving many clients errors:
WebSocket connection to 'wss://localhost:2222/myHub?id=ew5CRILwAiTZ955IXCZEkg&access_token=C11235C6ADEA27B5EABC08762E1A1B65077278714' failed: HTTP Authentication failed; no valid credentials available
Error: Failed to start the transport 'WebSockets': Error: WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.
GET https://localhost:2222/myHub?id=XaHhJ66drE0tfIlI3-_OPA&access_token=C11235C6ADEA27B5EABC08762E1A1B65077278714 401
Error: Failed to start the transport 'ServerSentEvents': Error: EventSource failed to connect. The connection could not be found on the server, either the connection ID is not present on the server, or a proxy is refusing/buffering the connection. If you have multiple servers check that sticky sessions are enabled.
Any ideas? I see that client is sending /myHub/negotiate requests to the server. Also OnTokenValidated the context.Request.Query["access_token"] is empty. What do i miss?
If i change the AddOAuth2Introspection and use AddIdentityServerAuthentication which is deprecated is working wihtout any client errors.
.AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme, options =>
{
// base-address of your identityserver
options.Authority ="https://identityserverURL";
options.ApiName = "apiname";
options.ApiSecret = "secret";
options.RequireHttpsMetadata = false;
options.TokenRetriever = new Func<HttpRequest, string>(req =>
{
var fromHeader = TokenRetrieval.FromAuthorizationHeader();
var fromQuery = TokenRetrieval.FromQueryString();
return fromHeader(req) ?? fromQuery(req);
});
});
Thank you
Related
I have been trying to create a sample Test app with SignalR but I have been extremely unsuccessful with authentication.
I am on .NET 6, and my Program.cs code looks like this.
Program.cs
using HubTestApp.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddSignalR();
builder.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;
})
.AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/{TenantId}/";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateLifetime = true,
ValidateAudience = false,
ValidateIssuerSigningKey = false,
};
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
// Sending the access token in the query string is required due to
// a limitation in Browser APIs. We restrict it to only calls to the
// SignalR hub in this code.
// See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
// for more information about security considerations when using
// the query string to transmit the access token.
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("/Test")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapHub<SimpleHub>("/Test");
app.Run();
My hub code is pretty simple:
namespace HubTestApp.Hubs
{
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
internal class SimpleHub : Hub<ISimpleClient>
{
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task EchoMessage(string message) => await Clients.All.ReceiveMessage(message);
}
}
And this is my client code:
namespace HubTestClient
{
using Microsoft.AspNetCore.SignalR.Client;
public class MockClient
{
private const string Token = "Bearer <JWT from AAD>";
private readonly HubConnection hubConnection;
public MockClient()
{
// Notice here I have tried to pass the token in various ways, all to no avail.
this.hubConnection = new HubConnectionBuilder()
.WithUrl($"http://localhost:5110/Test?access_token={MockClient.Token}", options =>
{
options.AccessTokenProvider = () => Task.FromResult(MockClient.Token);
options.Headers.Add("Authorization", MockClient.Token);
})
.Build();
this.hubConnection.On<string>("ReceiveMessage", (message) =>
{
Console.WriteLine($"Received message: '{message}'");
});
}
public async Task StartClient()
{
await hubConnection.StartAsync();
Random rng = new Random();
while (true)
{
string messageToEcho = $"Sending random number '{rng.Next()}'";
Console.WriteLine(messageToEcho);
try
{
await hubConnection.InvokeAsync("EchoMessage", messageToEcho);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
// Delay 5 seconds between hitting the hub.
await Task.Delay(5000);
}
}
}
}
I continuously get the message "Failed to invoke 'X' because user is unauthorized." I have made sure the token I got is valid. So, I'm bashing my head over this, completely confused where I am going wrong. Thank you in advance!
I have been through MS docs and IdentityServer docs. I am still stuck.
I have tried every config option and I've reached breaking point.
I started with the MS SPA template, added AspNet Core Identity, and fiddled with these setting. I'm able to call the Api's using Postman, and get a token, it has authority, and scope, and name set, but the API's show null in User.Identity when I debug.
Currently my error is this:
WWW-Authenticate: Bearer error="invalid_token", error_description="The signature key was not found"
Here is my configureServices:
public void ConfigureServices(IServiceCollection services)
{
// Add ASP.Net Core Identity
// this handles user authentication as well as user registration and management
services.AddDefaultIdentity<User>(options =>
{
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireDigit = true;
options.Password.RequireLowercase = false;
options.User.RequireUniqueEmail = true;
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedAccount = true;
})
.AddEntityFrameworkStores<AccountingDbContext>();
services.AddRazorPages();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Handle authentication and authorization using Open ID Connect.
// The components for authentication and authorization are:
// - user
// - client (the Angular app)
// - identity provider (ASP.Net Core Identity with IdentityServer4), and
// - resource server (the api)
// identity provider and resource server are hosted together.
services.AddIdentityServer()
.AddApiAuthorization<User, AccountingDbContext>(options =>
{
options.ApiResources
.First() // represents the configured default Api
.UserClaims
.Add(JwtClaimTypes.Name);
})
.AddDeveloperSigningCredential();
// Add Controllers with API
services.AddControllers();
.AddMvcOptions(options =>
{
// Adding an empty Authorise filter means that any Authenticated user is valid.
options.Filters.Add(new AuthorizeFilter());
});
// For Azure App Service deployments on Linux
if (!Environment.IsDevelopment())
{
services.Configure<JwtBearerOptions>(
IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
options =>
{
options.Authority = Configuration.GetValue<string>("Authority");
});
}
// Authenticate requests to the Api
// accepts any access token issued by identity server
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = Configuration.GetValue<string>("Authority");
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
})
.AddIdentityServerJwt();
// https://identityserver4.readthedocs.io/en/latest/quickstarts/1_client_credentials.html
// Adds Authorization policy to make sure the token has scope 'Accounting.ApplicationAPI openid profile'
services.AddAuthorization(options =>
{
// Registers the Policy with Authorization middleware
options.AddPolicy("Accounting.ApplicationAPI", policy =>
{
// Policy specifies that any Authenticated user with Accounting.ApplicationAPI scope is valid.
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "Accounting.ApplicationAPI openid profile");
});
});
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "./Accounting.Ng/dist";
});
}
and configure:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers()
.RequireAuthorization("Accounting.ApplicationAPI");
endpoints.MapRazorPages();
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "../Accounting.Ng";
if (env.IsDevelopment())
{
spa.UseProxyToSpaDevelopmentServer(Configuration["AngularServer"]);
}
});
}
I've an Angular Client which needs to be authentcated. This client is hosted in .NetCore SPA. Now using IdentityServer4, I've setup the IDP to authenticate the Client.
Everything works fine for login. Here the client automatically redirects to IDP project to login. Once credentials are entered it will redirect back to the client app.
However, for logout it's not working. On the Angular client which is hosted as a SPA in .netcore, I've a logout button. This logout button event will call the API controller in the same client. In this controller there is code to Signout from the httpContext.
My expectation was, when the logout action is called in the controller it will logout and redirect me back to the IDP. On a positive note, I've implemented the same in MVC client(no SPA and angular) and it works for the logout by redirecting me back to the IDP.
Also another positive, on the angular client on logout it actually tries to logout, but the CORS policy is blocking it. The error message in the browser console has the redirect url as part of the error message. This url works when i click on it by redirecting me to the IDP and logging me out.
I'm not able to figure out what CORS policy I have to add to make this redirect work. I've tried adding CORS to the startup class, but did not work. Help please
Browser error message:
Access to XMLHttpRequest at 'https://localhost:44336/connect/endsession?post_logout_redirect_uri=https%3A%2F%2Flocalhost%3A44374%2Fsignout-callback-oidc&id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6IlMwbFpqUi1QazItS0dLc2xxaFlQQ2ciLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE1NzkzNDc3NDcsImV4cCI6MTU3OTM0ODA0NywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzMzYiLCJhdWQiOiJBY3Rpdml0eVRyYWNrZXJOZ0NsaWVudF9DbGllbnRJZCIsIm5vbmNlIjoiNjM3MTQ5NDQ1NDMyMDYwODM5LllUVmpaV1UyTURjdE5ERmhOeTAwTW1SbExXRm1NVGd0TWpKaVl6VTFOMlJoWkRkaE9EYzNPV1EwTm1VdFpEUmxaUzAwT1RNMkxUZ3hOV0l0T0RJeU5HWTVaR1l4T0RsaSIsImlhdCI6MTU3OTM0Nzc0NywiYXRfaGFzaCI6ImJwc3hLLThSU2Nwb2hKMnJPSlViQlEiLCJzX2hhc2giOiJ0M2R1RmljZDR1VTRTQlQ3S253cG1nIiwic2lkIjoiYTFIWnE5YXFMdnR1WHB6S1FUZUVvUSIsInN1YiI6ImQ4NjBlZmNhLTIyZDktNDdmZC04MjQ5LTc5MWJhNjFiMDdjNyIsImF1dGhfdGltZSI6MTU3OTM0Nzc0NywiaWRwIjoibG9jYWwiLCJhbXIiOlsicHdkIl19.bd35dk-lcolUxgoNAzzc4kKIORQIsmeSu5JaARpyqj1I6cv5P6LSHrcdw3YmZ80q_tF8WLi7ywIml-enEP4JAe-nbYw7gSlFt9qHtw5eSF37dMdBZq7UUXt6EoK29xs9lp6TyIB11pzgRZ8tPVAPw0Y8rNpGSGYtjfWjp7t4FdKthvUchAo_SNh6l40S5oV0Yo_YIWfHtjxM-nLZXia0YCvjNEQChmTmkzSMCIdGnVqawhIzQ_O7jv0c1T7kCwaF5YGyer3ZUyj1UM53JTBbbGpKDrDh2DV-kd4tvhoaLnWQAoUqCQ1Ofl_kHc8vffqE7RRPGmQLQYOM48186hIe0g&state=CfDJ8DRPXADjz9hKioMAFvg6DCP1P37ODZ4R81EV3uFXBpxiOLWoJY6GDEcbYNZzB--zZjv-Z94PSfMJkcoJhQcmHAvmM_9yKL9hPaGqmucpJrO_wv74Fj8bmdm8C7l_MJZ3VaNahF5Bqvi9tWFUikbr-HJ_uI0GiGX6qsj5mkrp8K4x&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.5.0.0' (redirected from 'https://localhost:44374/api/Authorization/Logout') from origin 'https://localhost:44374' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource
And the API controller logout code:
[HttpGet]
public async Task Logout()
{
var httpclient = new HttpClient();
var disco = await httpclient.GetDiscoveryDocumentAsync("https://localhost:44336/");
// get the access token to revoke
var accessToken = await HttpContext
.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
if (!string.IsNullOrWhiteSpace(accessToken))
{
var revokeAccessTokenResponse =
await httpclient.RevokeTokenAsync(new TokenRevocationRequest
{
Address = disco.RevocationEndpoint,
ClientId = "App_ClientId",
ClientSecret = "someSecret",
Token = accessToken
});
if (revokeAccessTokenResponse.IsError)
{
throw new Exception("Problem encountered while revoking the access token."
, revokeAccessTokenResponse.Exception);
}
}
// Clears the local cookie ("Cookies" must match name from scheme)
await HttpContext.SignOutAsync("Cookies");
await HttpContext.SignOutAsync("oidc");
}
Startup.cs with entire client configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<ActivityTrackerAPIHttpClient>();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
}).AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = Configuration["IdentityServer:Authority"];
options.ClientId = Configuration["IdentityServer:ClientId"];
options.ClientSecret = Configuration["IdentityServer:ClientSecret"];
options.ResponseType = "code id_token";
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("roles");
options.Scope.Add(Configuration["IdentityServer:ApiName"]);
options.Scope.Add("offline_access");
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.Remove("amr");
options.ClaimActions.DeleteClaim("sid");
options.ClaimActions.DeleteClaim("idp");
options.ClaimActions.MapJsonKey("role", "role");
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = JwtClaimTypes.Role,
};
});
services.ConfigureLoggerService();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync("oidc");
}
else
{
await next();
}
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
I came up with a temporary solution at that point. However I realized, it is ideal to do the client security in the angular side.
My temp solution below.
Startup.cs of client
options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents
{
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var protocolMessage = context.ProtocolMessage;
var param = "";
foreach (var parameter in protocolMessage.Parameters)
{
param += $"{parameter.Key}={parameter.Value}$";
}
var url = $"{protocolMessage.IssuerAddress}?{param}x-client-SKU={protocolMessage.SkuTelemetryValue}&x-client-ver=5.5.0.0";
context.HttpContext.Session.SetString("LogoutUrl", url);
return Task.FromResult(0);
}
};
Also enable session in the client startup.cs by adding services.AddSession(); in ConfigureServices method and then add app.UseSession(); in the Configure method.
In the controller
[HttpGet]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
var url = HttpContext.Session.GetString("LogoutUrl");
return Ok(url);
}
Wondering if anyone has experience in the azure signalr service(v1.04) and angular cli(v6+). Having some issues understanding how to get the client context after connecting to the hub. Any thoughts?
Here's the context of the hub, any help would be appreciated!
this.getConnectionInfo().subscribe(info => {
console.log("INFO", info);
let options = {
accessTokenFactory: () => info.accessToken
};
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(info.hubUrl, options)
.configureLogging(signalR.LogLevel.Information)
.build();
this.hubConnection.start().then(() => {
console.log('Hub connection started init');
**[GET hubConnectionContext]**
console.log('Connection', this.hubConnection);
})
.catch(err => console.error(err.toString()));
I am actually creating a chat with angular 5 and signalR on an ASP.NET Framework API. I followed the documentation but it's still not work. Here is my hub:
public class ChatHub : Hub
{
public void Hello()
{
Clients.All.hello();
}
}
Here is my startup class:
app.Map("/signalr", map =>
{
// Setup the CORS middleware to run before SignalR.
// By default this will allow all origins. You can
// configure the set of origins and/or http verbs by
// providing a cors options with a different policy.
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
EnableJSONP = true
};
// Run the SignalR pipeline. We're not using MapSignalR
// since this branch already runs under the "/signalr"
// path.
map.RunSignalR(hubConfiguration);
});
and here is my angular part which create the hubconnection:
ngOnInit() {
this._hubConnection = new HubConnection('http://localhost:58525/signalr/hubs');
this._hubConnection
.start()
.then(() => console.log('Connection started!'))
.catch(err => console.log('Error while establishing connection :( : ' + err));
this._hubConnection.on('send', data => {
console.log(data);
});
}
I get this error:
If your ASP.NET page runs on another server, then your URL looks not correct.
https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/hubs-api-guide-javascript-client#crossdomain
You have to connect to:
this._hubConnection = new HubConnection('http://localhost:58525/signalr');