Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core SignalR returns 401 Unauthorized using Azure AD

Tags:

I have a SPA (angular 7) and an API (.Net Core) which I authenticate with Azure AD. I'm using adal-angular4 to integrate my angular application with AAD.

Everything works great, but I'm also using SignalR with the API as server and when I try to connect from my SPA I get 401 Unauthorized on the negotiate "request" and I get this back in the Response Headers:

Response Header

The request contains my Bearer token in the Authorization header, and when I run the token through jwt.io, I can see that the "aud" value is the Azure AD ClientId for my SPA.

All regular request to the API contains the same token and I have no issues with those. I have [Authorize] on all my Controllers and on my Hub, but it's only the SignalR Hub that causes this issue.

My server Startup:

public Startup(IConfiguration configuration, IHostingEnvironment env)
{
    Configuration = configuration;
    _env = env;
}

public IConfiguration Configuration { get; }
private IHostingEnvironment _env;
public void ConfigureServices(IServiceCollection services)
{

    StartupHandler.SetupDbContext(services, Configuration.GetConnectionString("DevDb"));


    // Setup Authentication
    services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
        .AddAzureADBearer(options =>
        {
            Configuration.Bind("AzureAD", options);


        });

    services.AddMvc()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    // Add functionality to inject IOptions<T>
    services.AddOptions();

    // Add AzureAD object so it can be injected
    services.Configure<AzureAdConfig>(Configuration.GetSection("AzureAd"));

    services.AddSignalR(options =>
    {
        options.EnableDetailedErrors = true;
        options.KeepAliveInterval = TimeSpan.FromSeconds(10);
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseDeveloperExceptionPage();
        app.UseHsts();
    }

    app.UseCookiePolicy();

    app.UseHttpsRedirection();

    //app.UseCors("AllowAllOrigins");
    app.UseCors(builder =>
    {
        builder.AllowAnyOrigin();
        builder.AllowAnyMethod().AllowAnyHeader();
        builder.AllowCredentials();
    });


    app.UseAuthentication();

    app.UseSignalR(routes => routes.MapHub<MainHub>("/mainhub"));

    app.UseStaticFiles(new StaticFileOptions()
    {
        FileProvider = new PhysicalFileProvider(Path.Combine(_env.ContentRootPath, "Files")),
        RequestPath = new PathString("/Files")
    });

    app.UseMvc();
}

My SignalR Hub:

[Authorize]
public class MainHub : Hub
{
    private readonly IEntityDbContext _ctx;

    public MainHub(IEntityDbContext ctx)
    {
        _ctx = ctx;
        _signalRService = signalRService;
    }

    public override Task OnConnectedAsync()
    {
        return base.OnConnectedAsync();
    }

    public override Task OnDisconnectedAsync(Exception exception)
    {
        return base.OnDisconnectedAsync(exception);
    }
}

And this is my SignalRService on my angular client. I'm running startConnection() in the constructor of app.component.ts.

export class SignalRService {
    private hubConnection: signalR.HubConnection;

    constructor(private adal: AdalService) {}

    startConnection(): void {
        this.hubConnection = new signalR.HubConnectionBuilder()
            .withUrl(AppConstants.SignalRUrl, { accessTokenFactory: () => this.adal.userInfo.token})
            .build();

        this.hubConnection.serverTimeoutInMilliseconds = 60000;

        this.hubConnection.on('userConnected', (user) => 
        {
            console.log(user);
        });

        this.hubConnection.start()
            .then(() => console.log('Connection started'))
            .catch(err => 
            {
                console.log('Error while starting connection: ' + err);
            });
    }
}

I have tried this solution, but I can't get that to work either.

Edit

When I've implemented the solution from the official docs, the API stops working on regular requests as well and I get back:

Signature key was not found

I've populate the IssuerSigningKey property in TokenValidationParameters with new SymmetricSecurityKey(Guid.NewGuid().ToByteArray());. Am I doing anything wrong here?

/EDIT

Why won't SignalR accept my accesstoken when the API otherwise accept it?

like image 874
Martin Johansson Avatar asked Aug 28 '19 08:08

Martin Johansson


People also ask

How do I connect to Azure SignalR?

Sign in to the Azure portal. In the upper-left side of the page, select + Create a resource. On the Create a resource page, in the Search services and marketplace text box, enter signalr and then select SignalR Service from the list.

Does SignalR need SSL?

If your SignalR application transmits sensitive information between the client and server, use SSL for the transport.

Why does SignalR disconnect?

A SignalR connection can end in any of the following ways: If the client calls the Stop method, a stop message is sent to the server, and both client and server end the SignalR connection immediately.

How do I protect my SignalR?

If you want to restrict access to it you need to authenticate users and authorize their actions. You authenticate using standard web auth methods (forms auth, cookies, Windows auth, etc.) and you can authorize in code using SignalR constructs (like the Authorize attribute you point out) or with your own code.


2 Answers

Just have a look at the official docs. You need a special handling for the JWT Bearer events so your authentication is working. The token needs to be forwarded to the hubs. Have a look at the part where I said

THAT PART IS MISSING

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    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 =>
        {
            // Configure JWT Bearer Auth to expect our security key
            options.TokenValidationParameters =
                new TokenValidationParameters
                {
                    LifetimeValidator = (before, expires, token, param) =>
                    {
                        return expires > DateTime.UtcNow;
                    },
                    ValidateAudience = false,
                    ValidateIssuer = false,
                    ValidateActor = false,
                    ValidateLifetime = true,
                    IssuerSigningKey = SecurityKey
                };

            //THAT IS THE PART WHICH IS MISSING IN YOUR CONFIG !
            // 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.
            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/chat")))
                    {
                        // Read the token out of the query string
                        context.Token = accessToken;
                    }
                    return Task.CompletedTask;
                }
            };
        });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddSignalR();

    // Change to use Name as the user identifier for SignalR
    // WARNING: This requires that the source of your JWT token 
    // ensures that the Name claim is unique!
    // If the Name claim isn't unique, users could receive messages 
    // intended for a different user!
    services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

    // Change to use email as the user identifier for SignalR
    // services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

    // WARNING: use *either* the NameUserIdProvider *or* the 
    // EmailBasedUserIdProvider, but do not use both. 
}
like image 147
Coder949 Avatar answered Oct 12 '22 23:10

Coder949


When validating the signature of access token , you should get the public key since Azure AD may sign token using any one of a certain set of public-private key pairs , the keys could be found at :

https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration 

Within the JSON response, you’ll see a property jwks_uri which is the URI that contains the JSON Web Key Set for Azure AD. Matching the kid claim in jwt token , you can find the key which AAD used to sign the token with asymmetric encryption algorithms, such as RSA 256 by default .

In asp.net core apis , when validating the access token which issued by Azure AD , you can use AddJwtBearer extension and provide the correct Authority , so that middleware will correctly get the keys from Azure AD OpenID configuration endpoint :

options.Authority = "https://login.microsoftonline.com/yourtenant.onmicrosoft.com/"

Another choice is to use AddAzureADBearer extension from Microsoft.AspNetCore.Authentication.AzureAD.UI library . You should also set correct authority(instance + domain) , middleware will help validating the signature and claims based on your configuration .

like image 22
Nan Yu Avatar answered Oct 12 '22 23:10

Nan Yu