question

Maxima-5691 avatar image
0 Votes"
Maxima-5691 asked FeiXue-MSFT answered

SignalR authorization not working out of the box in asp.net core angular SPA with Identity

Github complete code: https://github.com/maxima120/signalr-test

What I did:

Installed

dotnet new angular --auth Individual
npm i @microsoft/signalr

Modified Startup.cs

         services.AddCors(options =>
         {
             options.AddPolicy("CorsPolicy", builder => builder
             .WithOrigins("http://localhost:4200")
             .AllowAnyMethod()
             .AllowAnyHeader()
             .AllowCredentials());
         });
         services.AddSignalR();

 . . .

         app.UseCors("CorsPolicy");

         app.UseAuthentication();
         app.UseIdentityServer();
         app.UseAuthorization();
         app.UseEndpoints(endpoints =>
         {
             . . .
             endpoints.MapHub<NewsHub>("/newshub");
         });

Added Hub class to the server

 [Authorize]
 public class NewsHub : Hub
 {
 }

Modified WeatherForecastController:

     private IHubContext<NewsHub> _hub;

     public WeatherForecastController(ILogger<WeatherForecastController> logger, IHubContext<NewsHub> hub)
     {
         _hub = hub;
         _logger = logger;
     }

     [HttpGet]
     public IEnumerable<WeatherForecast> Get()
     {
         var timerManager = new TimerManager(() => _hub.Clients.All.SendAsync("servermessage", DateTime.Now.Ticks.ToString()));


Modified fetch-data.component.ts

 constructor(http: HttpClient, @Inject('BASE_URL') baseUrl: string) {
   http.get<WeatherForecast[]>(baseUrl + 'weatherforecast').subscribe(result => {

   this.forecasts = result;

   this.hub = new HubConnectionBuilder()
     .withUrl("/newshub")
     .build();

   this.hub.on("servermessage", (m: string) => { console.log(m); });

   this.hub.start()
     .then(() => console.log('MessageHub Connected'))
     .catch(err => console.log('MessageHub Connection Error: ' + err.toString()));

 }, error => console.error(error));

}

Authorization of SignalR hub fails. Output window:

IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.UserInfoEndpoint for /connect/userinfo
IdentityServer4.ResponseHandling.UserInfoResponseGenerator: Information: Profile service returned the following claim types: sub preferred_username name
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.CheckSessionEndpoint for /connect/checksession
[2021-08-01T15:43:11.337Z] Information: Normalizing '/newshub' to 'https://localhost:44306/newshub'.
Failed to load resource: the server responded with a status of 401 () [https://localhost:44306/newshub/negotiate?negotiateVersion=1]
[2021-08-01T15:43:11.347Z] Error: Failed to complete negotiation with the server: Error
[2021-08-01T15:43:11.347Z] Error: Failed to start the connection: Error
MessageHub Connection Error: Error

If I remove [Authorize] attribute - it works fine




dotnet-aspnet-core-generaldotnet-aspnet-general
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

FeiXue-MSFT avatar image
1 Vote"
FeiXue-MSFT answered

@Maxima-5691, Once users login, the application will store the data in the session storage, you can get the access token from session storage directly. Here is the code and figure for your reference this structure.

       this.hub = new HubConnectionBuilder()
         .withUrl("/newshub", {
           accessTokenFactory: () => JSON.parse(sessionStorage.getItem("oidc.user:https://localhost:44388:Source")).access_token})
         .build();

121480-untitled.png



untitled.png (64.4 KiB)
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

BrandoZhang-MSFT avatar image
0 Votes"
BrandoZhang-MSFT answered Maxima-5691 commented

Hi Maxima-5691,

As far as I know, if you want to use signlar client to pass the token to the backend hub, you should provide an access token instead of using a cookie. The server validates the token and uses it to identify the user. This validation is done only when the connection is established. During the life of the connection, the server doesn't automatically revalidate to check for token revocation.

In the JavaScript client, the token can be provided using the accessTokenFactory option.


Codes like below:

     public loginToken: string;
     public error: string;
     public connection: signalR.HubConnection;
     public connectionStarted: boolean;
    
     // TODO: Bind this method to a UI event that is triggered when the user is logging in
     // For example, if you add UI with a username/password box and a login button, this
     // method should be triggered when the login button is clicked.
     public async connect(evt: Event) {
         try {
             evt.preventDefault();
    
             // TODO: Use the user input to acquire a JWT token from your authentication provider.
             // Or, trigger an OIDC/OAuth login flow to acquire a token.
             throw new Error("TODO: Add code to acquire the token.")
    
             this.loginToken = "BOGUS TOKEN. Replace this with the real token.";
    
             // Update rendering while we connect
             this.render();
    
             // Connect, using the token we got.
             this.connection = new signalR.HubConnectionBuilder()
                 .withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
                 .build();
    
             this.connection.on("ReceiveSystemMessage", (message) => this.receiveMessage(message, "green"));
             this.connection.on("ReceiveDirectMessage", (message) => this.receiveMessage(message, "blue"));
             this.connection.on("ReceiveChatMessage", (message) => this.receiveMessage(message));
             await this.connection.start();
             this.connectionStarted = true;
    
         } catch (e) {
             this.error = `Error connecting: ${e}`;
         } finally {
             // Update rendering with any final state.
             this.render();
         }
     }



Then in the server-side, you should add a PostConfigureOptions<TOptions> service to the identity serve project to allow querystring access token. Details, you could refer to this article.

For the whole signalr auth example, I suggest you could refer to this link.

Best Regards,
Brando


· 3
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Thank you. Your answer mostly based on the article : https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0
I read it multiple times but I still dont understand how to authorize signalr.

First - I can see both - cookie and bearer token in the Fiddler request to the angular component where signalr client is (see the picture)., But I dont know how do I get the token to pass it into accessTokenFactory. All the examples say - "YOUR TOKEN GOES HERE" or similar..

Second - since I provide auth cookie (as you said and as Fiddler shows) - why doesnt it work anyway? The same article says (2nd paragraph):

In a browser-based app, cookie authentication allows your existing user credentials to automatically flow to SignalR connections. When using the browser client, no additional configuration is needed. If the user is logged in to your app, the SignalR connection automatically inherits this authentication.


119877-image.png









0 Votes 0 ·
image.png (123.4 KiB)

You are using AddIdentityServerJwt jwt authentication, not the cookie authentication. They are different, so I suggest you could follow my suggestion to send the access token to the server-side when connect to the signalr hub.

0 Votes 0 ·

Yes I know its IdentityServerJwt. I know I can send you token through accessTokenFactory. What I dont know is how to extract the access token (given by Identity Server) in Angular..

0 Votes 0 ·