Asp.Net Core 2 oidc middleware does not challenge after proxy request returns 401 - authentication

I'm trying to build a centralised proxy that will intercept all requests and handle authentication with openidconnect.
Currently the proxied request simply returns 401, so the middleware suppose to challenge and redirect me to the login page. The issue is using .Net Core 1.1's implemtation it work, but it doesn't seem to work in .Net Core 2.
I've simplified the code but this works, I get redirected to google's signin page.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication();
services.AddProxy();
}
public void Configure(IApplicationBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
});
app.UseGoogleAuthentication(new GoogleOptions
{
AutomaticChallenge = true,
SignInScheme = "oidc",
ClientId = "clientId",
ClientSecret = "clientSecret",
});
app.MapWhen(
context => context.RequestStartsWith("http://www.web1.com"),
builder => builder.RunProxy(baseUri: new Uri("http://www.proxy1.com"))
);
}
}
And this doesn't work with .Net Core 2.0's implementation, I'm getting a 401 exception page
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogle(options =>
{
options.ClientId = "clientId";
options.ClientSecret = "clientSecret";
});
services.AddProxy();
}
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
app.MapWhen(
context => context.RequestStartsWith("http://www.web1.com"),
builder => builder.RunProxy(baseUri: new Uri("http://www.proxy1.com"))
);
}
}
Any ideas?

After looking through the source code, it turns out that Authentication middleware in Asp.Net Core 2 does not challenge after response returns 401 status code, so simply return HttpUnauthorizedResult does not work anymore. The reason Authorize attribute works is it returns a ChallengeResult, which it will eventually will call ChallengeAsync.
The work around is, I've created my own middleware which handles 401 Status Code
public class ChallengeMiddleware
{
private readonly RequestDelegate _next;
private readonly IAuthenticationSchemeProvider _schemes;
public ChallengeMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
{
_next = next;
_schemes = schemes;
}
public async Task Invoke(HttpContext context)
{
context.Response.OnStarting(async () =>
{
if (context.Response.StatusCode == 401)
{
var defaultChallenge = await _schemes.GetDefaultChallengeSchemeAsync();
if (defaultChallenge != null)
{
await context.ChallengeAsync(defaultChallenge.Name);
}
}
await Task.CompletedTask;
});
await _next(context);
}
}

Related

Multiple authentication schemes in ASP.NET Core 5.0 WebAPI

I have a full set of (ASP.NET Core) web APIs developed in .NET 5.0 and implemented Cookies & OpenIdConnect authentication schemes.
After successful authentication (user id and password) with Azure AD, cookie is generated and stores user permissions etc.
Now, I would like to expose the same set of APIs to a third party consumer using API Key based authentication (via api-key in the request headers).
I have developed a custom authentication handler as below.
using Microsoft.AspNetCore.Authentication;
namespace Management.Deployment.Securities.Authentication
{
public class ApiKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
}
namespace Management.Deployment.Securities.Authentication
{
public static class ApiKeyAuthenticationDefaults
{
public static readonly string AuthenticationScheme = "ApiKey";
public static readonly string DisplayName = "ApiKey Authentication Scheme";
}
}
ApiKeyAuthenticationHandler is defined as below, straight forward, if the request headers contain the valid api key then add permissions claim (assigned to the api key) and mark the authentication as success else fail.
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Management.Securities.Authorization;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace Management.Deployment.Securities.Authentication
{
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>
{
private const string APIKEY_NAME = "x-api-key";
private const string APIKEY_VALUE = "sdflasuowerposaddfsadf1121234kjdsflkj";
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
string extractedApiKey = Request.Headers[APIKEY_NAME];
if (!APIKEY_VALUE.Equals(extractedApiKey))
{
return Task.FromResult(AuthenticateResult.Fail("Unauthorized client."));
}
var claims = new[]
{
new Claim("Permissions", "23")
};
var claimsIdentity = new ClaimsIdentity(claims, nameof(ApiKeyAuthenticationHandler));
var authenticationTicket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(authenticationTicket));
}
}
}
I have also defined ApiKeyAuthenticationExtensions as below.
using Microsoft.AspNetCore.Authentication;
using Management.Deployment.Securities.Authentication;
using System;
namespace Microsoft.Extensions.DependencyInjection
{
public static class ApiKeyAuthenticationExtensions
{
public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder)
{
return builder.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, ApiKeyAuthenticationDefaults.DisplayName, x => { });
}
public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, Action<ApiKeyAuthenticationSchemeOptions> configureOptions)
{
return builder.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, ApiKeyAuthenticationDefaults.DisplayName, configureOptions);
}
}
}
Skimmed version of ConfigureServices() in Startup.cs is here. Please note I have used ForwardDefaultSelector.
public void ConfigureServices(IServiceCollection services)
{
IAuthCookieValidate cookieEvent = new AuthCookieValidate();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = ".Mgnt.AspNetCore.Cookies";
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.Events = new CookieAuthenticationEvents
{
OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = 403;
return Task.FromResult(0);
},
OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.FromResult(0);
},
OnValidatePrincipal = cookieEvent.ValidateAsync
};
options.ForwardDefaultSelector = context =>
{
return context.Request.Headers.ContainsKey(ApiConstants.APIKEY_NAME) ? ApiKeyAuthenticationDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme;
};
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => Configuration.Bind(OpenIdConnectDefaults.AuthenticationScheme, options))
.AddApiKey();
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
});
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
}
The Configure method is as below.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHsts();
app.Use((context, next) =>
{
context.Request.Scheme = "https";
return next();
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
When I send the correct apikey in the request headers, the custom handler is returning success as authentication result and the request is processed further.
But if an incorrect api key is passed, it is not returning the authentication failure message - "Unauthorized client.". Rather, the request is processed further and sending the attached response.
What changes to be made to resolve this issue so the api returns the authentication failure message - "Unauthorized client." and stops further processing of the request?
if you plan to use apikeys, then you are on your own and there is (as far as I know) no built in direct support for API-keys. There is however built in support for JWT based access tokens and I would recommend that you use that as well for external third parties who wants to access your api. Perhaps using client credentials flow.
For some help, see http://codingsonata.com/secure-asp-net-core-web-api-using-api-key-authentication/
I also think you should configure and let the authorization handler be responsible for deciding who can access the services.
see Policy-based authorization in ASP.NET Core

Blazor webassembly - IdentityServer EventSink and HttpContext

I'm working on a Blazor webassembly application, in ASP.NET Core 3.1 with IdentityServer.
As IdentityServer handle all login, logout, register, ... events, I'm trying to catch theses events in order to obtain some informations about users.
To be clear, I'm trying to memorize the login Date, and to catch new users registration to give them some role automatically.
Here all the services I use in my startup class :
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.Configure<IdentityOptions>(options =>
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
options.IdentityResources["openid"].UserClaims.Add("name");
options.ApiResources.Single().UserClaims.Add("name");
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});
// Need to do this as it maps "role" to ClaimTypes.Role and causes issues
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
services.AddAuthentication()
.AddIdentityServerJwt();
I've implemented the IEventSink pattern (http://docs.identityserver.io/en/stable/topics/events.html) :
public class IdentityServerEventSink : IEventSink
{
private readonly UserManager<ApplicationUser> userManager;
private readonly IHttpContextAccessor httpContextAccessor;
public IdentityServerEventSink(UserManager<ApplicationUser> userManager, IHttpContextAccessor httpContextAccessor)
{
this.userManager = userManager;
this.httpContextAccessor = httpContextAccessor;
}
public async Task PersistAsync(Event #event)
{
if (#event.Id.Equals(EventIds.ClientAuthenticationSuccess))
{
var identity = httpContextAccessor.HttpContext.User;
var id = httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await userManager.Users.FirstOrDefaultAsync(u => u.Id == id);
}
}
}
And in the startup.cs :
services.AddHttpContextAccessor();
services.AddTransient<IEventSink, IdentityServerEventSink>();
But when I get in the ClientAuthenticationSuccess event, the identity is always anonymous.
I tried also in the middleware but I have the same issue :
app.Use(async (context, next) =>
{
await next.Invoke();
//handle response
//you may also need to check the request path to check whether it requests image
if (context.User.Identity.IsAuthenticated)
{
var userName = context.User.Identity.Name;
//retrieve uer by userName
using (var dbContext = context.RequestServices.GetRequiredService<ApplicationDbContext>())
{
var user = dbContext.Users.Where(u => u.UserName == userName).FirstOrDefault();
user.LastLogon = System.DateTime.Now;
dbContext.Update(user);
dbContext.SaveChanges();
}
}
});
Do you have any ideas ?
I heard HttpContextAccessor is a bad things in Blazor.
Ok so after an update the middleware delegate works !
app.Use(async (context, next) =>
{
await next.Invoke();
if (context.User.Identity.IsAuthenticated)
{
var userName = context.User.Identity.Name;
using (var dbContext = context.RequestServices.GetRequiredService<ApplicationDbContext>())
{
var user = dbContext.Users.Where(u => u.UserName == userName).FirstOrDefault();
if (user != null)
{
user.LastLogon = System.DateTime.Now;
user.LastIpAddress = context.Connection?.RemoteIpAddress?.ToString();
dbContext.Update(user);
dbContext.SaveChanges();
}
}
}
});
Just be carefull to the order of app.use... in the Configure method ! You need to add this AFTER app.UseIdentityServer(); app.UseAuthentication(); app.UseAuthorization();
But badly the EventSink from IdentityServer still doesn't work, the httpContextAccessor.HttpContext.User.Identity is always anonymous.

blazor webassembly System.Net.Http.HttpRequestException: Response status code does not indicate success: 400 (Bad Request)

I have a blazor project that is dotnet core hosted. I am working on blazor authentication following this tutorial. Everything is ok on the server side because I was able to use Postman to create users successfully. I have tried different suggestions online but non work for me. Some suggested CORS which I fixed, some route which I amended but problem still persist.
I have been having issue debugging as I cant get break point in the browser console. I have debugged some blazor project I could set break points and view local variables but I couldnt with the current project. I dont know if its a bug.
server startup.cs
namespace EssentialShopCoreBlazor.Server
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration _configuration)
{
Configuration = _configuration;
}
readonly string AllowSpecificOrigins = "allowSpecificOrigins";
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUsersModel, IdentityRole>()
.AddEntityFrameworkStores<DbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
};
});
services.AddCors(options =>
{
options.AddPolicy(AllowSpecificOrigins,
builder =>
{
builder.WithOrigins("https://localhost:44365", "https://localhost:44398")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
services.AddScoped<AccountAuthController>();
services.AddMvc().AddNewtonsoftJson();
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBlazorDebugging();
}
app.UseCors(AllowSpecificOrigins);
app.UseStaticFiles();
app.UseClientSideBlazorFiles<Client.Startup>();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapFallbackToClientSideBlazor<Client.Startup>("index.html");
});
}
}
}
controller
namespace EssentialShopCoreBlazor.Server.Controllers
{
[Route("essential/users/")]
public class AccountAuthController : ControllerBase
{
private static UserModel LoggedOutUser = new UserModel { IsAuthenticated = false };
private readonly UserManager<IdentityUsersModel> userManager;
private readonly IConfiguration configuration;
private readonly SignInManager<IdentityUsersModel> signInManager;
public AccountAuthController(UserManager<IdentityUsersModel> _userManager, SignInManager<IdentityUsersModel> _signInManager, IConfiguration _configuration)
{
userManager = _userManager;
signInManager = _signInManager;
configuration = _configuration;
}
[HttpPost]
[Route("create")]
public async Task<ActionResult<ApplicationUserModel>> CreateUser([FromBody] ApplicationUserModel model)
{
var NewUser = new IdentityUsersModel
{
UserName = model.UserName,
BusinessName = model.BusinessName,
Email = model.Email,
PhoneNumber = model.PhoneNumber
};
var result = await userManager.CreateAsync(NewUser, model.Password);
if (!result.Succeeded)
{
var errors = result.Errors.Select(x => x.Description);
return BadRequest(new RegistrationResult { Successful = false, Errors = errors });
}
return Ok(new RegistrationResult { Successful = true });
}
}}
client service
public class AuthService : IAuthService
{
private readonly HttpClient _httpClient;
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly ILocalStorageService _localStorage;
string BaseUrl = "https://localhost:44398/essential/users/";
public AuthService(HttpClient httpClient,
AuthenticationStateProvider authenticationStateProvider,
ILocalStorageService localStorage)
{
_httpClient = httpClient;
_authenticationStateProvider = authenticationStateProvider;
_localStorage = localStorage;
}
public async Task<RegistrationResult> Register(ApplicationUserModel registerModel)
{
var result = await _httpClient.PostJsonAsync<RegistrationResult>(BaseUrl + "create", registerModel);
return result;
}
}

Authorize doesn't work in Signalr of ASP.NET Core 2.1

I've upgraded my project from ASP.Net Core 2.0 to ASP.NET Core 2.1 by following this tutorial.
Everything was fine until I applied Signar Core 2.1 to my project.
This is my Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IAuthorizationHandler, SolidAccountRequirementHandler>();
services.AddCors(
options => options.AddPolicy("AllowCors",
builder =>
{
builder
.AllowAnyOrigin()
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod();
})
);
services.AddAuthorization(x =>
{
x.AddPolicy("MainPolicy", builder =>
{
builder.Requirements.Add(new SolidAccountRequirement());
});
});
services.AddSignalR();
#region Mvc builder
var authenticationBuilder = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
authenticationBuilder.AddJwtBearer(o =>
{
// You also need to update /wwwroot/app/scripts/app.js
o.SecurityTokenValidators.Clear();
// Initialize token validation parameters.
var tokenValidationParameters = new TokenValidationParameters();
tokenValidationParameters.ValidAudience = "audience";
tokenValidationParameters.ValidIssuer = "issuer";
tokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SigningKey"));
tokenValidationParameters.ValidateLifetime = false;
o.TokenValidationParameters = tokenValidationParameters;
});
// Construct mvc options.
services.AddMvc(mvcOptions =>
{
////only allow authenticated users
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.AddRequirements(new SolidAccountRequirement())
.Build();
mvcOptions.Filters.Add(new AuthorizeFilter(policy));
})
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1); ;
#endregion
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
//app.UseHttpsRedirection();
app.UseCors("AllowCors");
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chathub");
});
app.UseMvc();
}
}
This is my SolidRequirementHandler
public class SolidAccountRequirementHandler : AuthorizationHandler<SolidAccountRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SolidAccountRequirement requirement)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
}
This is my ChatHub.cs:
public class ChatHub : Hub
{
[Authorize(Policy = "MainPolicy")]
public override Task OnConnectedAsync()
{
return base.OnConnectedAsync();
}
}
What I expected was MainPolicy would be called when I used my AngularJS app to connect to ChatHub. However, OnConnectedAsync() function was called without checking request identity.
The policy of MVC Controller was applied successfully, but Signalr's doesn't.
Can anyone help me please ?
Thank you,
I posted this question onto Signalr github issue page.
Here is the answer they gave me .
I tried and it worked successfully:
The solution is to put [Authorize] attribute onto ChatHub
[Authorize(Policy = "MainPolicy")]
public class ChatHub : Hub
{
public override Task OnConnectedAsync()
{
return base.OnConnectedAsync();
}
}
Just share to who doesn't know :)
I have the same problem, there are four key things:
1- In your Startup.cs be aware of this Order inside Configure(IApplicationBuilder app)
app.UseRouting();
app.UseAuthorization( );
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<myChat>("/chat");
});
the app.UseAuthorization( ); should always be between app.UseRouting(); and app.UseEndpoints().
2- SignalR doesn't send Tokens in Header but it sends them in Query. In your startup.cs inside ConfigureServices(IServiceCollection services) You have to configure your app in a way to read Tokens from the query and put them in the header. You can Configure your JWT in this way:
services.AddAuthentication()
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidIssuer = [Issuer Site],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes([YOUR SECRET KEY STRING]))
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var path = context.Request.Path;
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/chat"))
{
context.Request.Headers.Add("Authorization", new[] { $"Bearer {accessToken}" });
}
return Task.CompletedTask;
}
};
});
3- Your Client should send Token when it wants to establish a connection. You can add token to Query when building the connection.
var connection = new signalR.HubConnectionBuilder().withUrl(
"http://localhost:5000/chat", {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
accessTokenFactory: () => "My Token Is Here"}).build();
4- I didn't add a default Athuentication scheme inside services.AddAuthentication()
So every time I have to specify my authorization scheme like this. [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
And Finally, You can Protect your Chat Class Like this
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class myChat : Hub
{
///Some functions
}
It seems that Using statements is important, So make sure using the right ones.
SignalR hub Authorize attribute doesn't work
Note: I have a problem with Authorizing only a single method in the myChat class. I don't know why.

SignalR core not working with cookie Authentication

I cant seem to get SignalR core to work with cookie authentication. I have set up a test project that can successfully authenticate and make subsequent calls to a controller that requires authorization. So the regular authentication seems to be working.
But afterwards, when I try and connect to a hub and then trigger methods on the hub marked with Authorize the call will fail with this message: Authorization failed for user: (null)
I inserted a dummy middleware to inspect the requests as they come in. When calling connection.StartAsync() from my client (xamarin mobile app), I receive an OPTIONS request with context.User.Identity.IsAuthenticated being equal to true. Directly after that OnConnectedAsync on my hub gets called. At this point _contextAccessor.HttpContext.User.Identity.IsAuthenticated is false. What is responsible to de-authenticating my request. From the time it leaves my middleware, to the time OnConnectedAsync is called, something removes the authentication.
Any Ideas?
Sample Code:
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
await this._next(context);
//At this point context.User.Identity.IsAuthenticated == true
}
}
public class TestHub: Hub
{
private readonly IHttpContextAccessor _contextAccessor;
public TestHub(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
public override async Task OnConnectedAsync()
{
//At this point _contextAccessor.HttpContext.User.Identity.IsAuthenticated is false
await Task.FromResult(1);
}
public Task Send(string message)
{
return Clients.All.InvokeAsync("Send", message);
}
[Authorize]
public Task SendAuth(string message)
{
return Clients.All.InvokeAsync("SendAuth", message + " Authed");
}
}
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyContext>(options => options.UseInMemoryDatabase(databaseName: "MyDataBase1"));
services.AddIdentity<Auth, MyRole>().AddEntityFrameworkStores<MyContext>().AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options => {
options.Password.RequireDigit = false;
options.Password.RequiredLength = 3;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.User.RequireUniqueEmail = true;
});
services.AddSignalR();
services.AddTransient<TestHub>();
services.AddTransient<MyMiddleware>();
services.AddAuthentication();
services.AddAuthorization();
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware<MyMiddleware>();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseSignalR(routes =>
{
routes.MapHub<TestHub>("TestHub");
});
app.UseMvc(routes =>
{
routes.MapRoute(name: "default", template: "{controller=App}/{action=Index}/{id?}");
});
}
}
And this is the client code:
public async Task Test()
{
var cookieJar = new CookieContainer();
var handler = new HttpClientHandler
{
CookieContainer = cookieJar,
UseCookies = true,
UseDefaultCredentials = false
};
var client = new HttpClient(handler);
var json = JsonConvert.SerializeObject((new Auth { Name = "craig", Password = "12345" }));
var content = new StringContent(json, Encoding.UTF8, "application/json");
var result1 = await client.PostAsync("http://localhost:5000/api/My", content); //cookie created
var result2 = await client.PostAsync("http://localhost:5000/api/My/authtest", content); //cookie tested and works
var connection = new HubConnectionBuilder()
.WithUrl("http://localhost:5000/TestHub")
.WithConsoleLogger()
.WithMessageHandler(handler)
.Build();
connection.On<string>("Send", data =>
{
Console.WriteLine($"Received: {data}");
});
connection.On<string>("SendAuth", data =>
{
Console.WriteLine($"Received: {data}");
});
await connection.StartAsync();
await connection.InvokeAsync("Send", "Hello"); //Succeeds, no auth required
await connection.InvokeAsync("SendAuth", "Hello NEEDSAUTH"); //Fails, auth required
}
If you are using Core 2 try changing the order of UseAuthentication, place it before the UseSignalR method.
app.UseAuthentication();
app.UseSignalR...
Then inside the hub the Identity property shouldn't be null.
Context.User.Identity.Name
It looks like this is an issue in the WebSocketsTransport where we don't copy Cookies into the websocket options. We currently copy headers only. I'll file an issue to get it looked at.