I want to moq HttpContext in .net core 1.0.0 for test case
Here is my code:
public async Task<string> Login(string email, string password)
{
var result = await _signInManager.PasswordSignInAsync(email, password, false, lockoutOnFailure: false);
if (result.Succeeded)
{
return HttpContext.User.Identity.Name;
}
else
{
return "";
}
}
Here is my test case
[Fact]
public async Task Login()
{
ApplicationUser user = new ApplicationUser() { UserName = "siddhartha#promactinfo.com", Email = "siddhartha#promactinfo.com", Name = "siddhartha" };
await _userManager.CreateAsync(user, "Something#123");
var userAdded = await _userManager.CreateAsync(user);
var result = await Login("siddhartha#promactinfo.com", "Something#123");
Assert.Equal("siddhartha", result);
}
It goes fail, gets error message:
HttpContext must not be null.
Here is my service - startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<PromactOauthDbContext>()
.AddDefaultTokenProviders();
services.AddMvc().AddMvcOptions(x => x.Filters.Add(new GlobalExceptionFilter(_loggerFactory)));
}
Without using controller. I have moq HttpContext in .net core. And used HttpContext in repository
Register HttpContext in test case project like this
public void ConfigureServices(IServiceCollection services)
{
var authenticationManagerMock = new Mock<AuthenticationManager>();
var httpContextMock = new Mock<HttpContext>();
httpContextAccessorMock.Setup(x => x.HttpContext.User.Identity.Name).Returns("Siddhartha");
httpContextMock.Setup(x => x.Authentication).Returns(authenticationManagerMock.Object);
var httpContextAccessorMock = new Mock<IHttpContextAccessor>();
httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContextMock.Object);
var httpContextMockObject = httpContextAccessorMock.Object;
services.AddScoped(x => httpContextAccessorMock);
services.AddScoped(x => httpContextMockObject);
serviceProvider = services.BuildServiceProvider();
}
Then you will get HttpContext.User.Identity.Name = Siddhartha
public async Task<string> Login(string email, string password)
{
var result = await _signInManager.PasswordSignInAsync(email, password, false, lockoutOnFailure: false);
if (result.Succeeded)
{
return HttpContext.User.Identity.Name;
}
else
{
return "";
}
}
I believe that error is from a null check on the SignInManager's constructor. I cant see how you built your SignInManager for your tests so I cannot be sure if you passed something in or not, but I suspect not.
If that is the case, create a mock of IHttpContextAccessor and setup the HttpContext property to return a new DefaultHttpContext() then pass that mocked object into the SignInManager.
Related
I am trying to get groups from AzureAD by calling graph in the hub in the OnConnectedAsync() to add the current user to groups.
When calling my graph service from a controller it works just fine and returns me the data I want, but when calling it from inside the hub I get an error:
IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. No account or login hint was passed to the AcquireTokenSilent call.
Any idea?
Here is my graph service :
public class GraphService : IGraphService
{
private readonly ITokenAcquisition _tokenAcquisition;
private readonly IHttpClientFactory _httpClientFactory;
public GraphService(ITokenAcquisition tokenAcquisition,
IHttpClientFactory clientFactory)
{
_httpClientFactory = clientFactory;
_tokenAcquisition = tokenAcquisition;
}
public async Task<IEnumerable<Group>> GetUserGroupsAsync()
{
var graphClient = await GetGraphClient();
var memberShipCollection = await graphClient
.Me
.MemberOf
.Request()
.GetAsync();
return memberShipCollection
.OfType<Group>();
}
private async Task<GraphServiceClient> GetGraphClient()
{
var token = await _tokenAcquisition
.GetAccessTokenForUserAsync(new string[] { "https://graph.microsoft.com/.default" });
var client = _httpClientFactory.CreateClient();
client.BaseAddress = new Uri("https://graph.microsoft.com/v1.0");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var graphClient = new GraphServiceClient(client)
{
AuthenticationProvider = new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
})
};
graphClient.BaseUrl = "https://graph.microsoft.com/v1.0";
return graphClient;
}
}
The hub :
[Authorize]
public class NotificationHub : Hub
{
ILogger<NotificationHub> _logger;
private readonly IGraphService _graphService;
public NotificationHub(ILogger<NotificationHub> logger,
IGraphService graphService)
{
_logger = logger;
_graphService = graphService;
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
var userDynamicGroups = await GetUserDynamicGroupsAsync();
//foreach (var group in userDynamicGroups)
// await Groups.AddToGroupAsync(Context.ConnectionId, group.DisplayName);
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
//var userDynamicGroups = await GetUserDynamicGroupsAsync();
//foreach (var group in userDynamicGroups)
// await Groups.RemoveFromGroupAsync(Context.ConnectionId, group.DisplayName);
}
private async Task<IEnumerable<Group>> GetUserDynamicGroupsAsync()
{
var AllUserGroups = await _graphService.GetUserGroupsAsync();
return AllUserGroups.Where(g => g.DisplayName.Contains("ONE_"));
}
}
The part related to auth in my startup:
public static IServiceCollection AddInternalAuthentification(this IServiceCollection services, IConfiguration configuration)
{
services.AddMicrosoftIdentityWebApiAuthentication(configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
Func<MessageReceivedContext, Task> existingOnMessageReceivedHandler = options.Events.OnMessageReceived;
options.Events.OnMessageReceived = async context =>
{
await existingOnMessageReceivedHandler(context);
StringValues accessToken = context.Request.Query["access_token"];
PathString path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/Notify"))
context.Token = accessToken;
};
});
return services;
}
[ApiController]
[Route("[controller]")]
public class AuthController : ControllerBase
{
private readonly SignInManager<IdentityUser> _signInManager;
public AuthController(SignInManager<IdentityUser> signInManager)
{
_signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
}
[HttpGet("token")]
public ChallengeResult Token()
{
var properties = new GoogleChallengeProperties
{
RedirectUri = "/auth/retrieve",
AllowRefresh = true,
};
return Challenge(properties, "Google");
}
[HttpGet("[action]")]
public async Task Retrieve()
{
var token = await HttpContext.GetTokenAsync("access_token");
var externalLoginInfoAsync = await _signInManager.GetExternalLoginInfoAsync();
var identityName = User?.Identity?.Name;
var authenticateResult = await HttpContext.AuthenticateAsync();
}
}
I direct the user to /auth/token, where he is redirected to the Google Oauth Page, if successful, he is redirected to /auth/retrieve, where I expect the user data, but token, externalLoginInfoAsync, identityName, authenticateResult is null
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("Default")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication()
.AddCookie()
.AddGoogle(options =>
{
options.Scope.Add("https://www.googleapis.com/auth/gmail.settings.basic");
options.AccessType = "offline";
options.SaveTokens = true;
options.SignInScheme = IdentityConstants.ExternalScheme;
options.Events.OnCreatingTicket = ctx =>
{
var identityName = ctx.Identity.Name;
return Task.CompletedTask;
};
options.ClientId = "SMTH_VALUE";
options.ClientSecret = "SMTH_VALUE";
});
services.AddControllers();
}
I debug the google provider and found the user values in the Events - identityName is not null.
How i can get this value in the controller?
You could refer the following code to configure Google authentication in Startup.ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication()
.AddGoogle(opt =>
{
opt.ClientId = "620831551062-rcvu44q4rhr5d8ossu3m0163jqbjdji0.apps.googleusercontent.com";
opt.ClientSecret = "GXFN0cHBbUlZ6nYLD7a7-cT8";
opt.SignInScheme = IdentityConstants.ExternalScheme;
});
services.AddControllersWithViews();
services.AddRazorPages();
}
Then, use the following sample to login using Google and get user information:
[Authorize]
public class AccountController : Controller
{
private UserManager<ApplicationUser> userManager;
private SignInManager<ApplicationUser> signInManager;
public AccountController(UserManager<ApplicationUser> userMgr, SignInManager<ApplicationUser> signinMgr)
{
userManager = userMgr;
signInManager = signinMgr;
}
// other methods
public IActionResult AccessDenied()
{
return View();
}
[AllowAnonymous]
public IActionResult GoogleLogin()
{
string redirectUrl = Url.Action("GoogleResponse", "Account");
var properties = signInManager.ConfigureExternalAuthenticationProperties("Google", redirectUrl);
return new ChallengeResult("Google", properties);
}
public IActionResult Login()
{
return View();
}
[AllowAnonymous]
public async Task<IActionResult> GoogleResponse()
{
ExternalLoginInfo info = await signInManager.GetExternalLoginInfoAsync();
if (info == null)
return RedirectToAction(nameof(Login));
var result = await signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, false);
string[] userInfo = { info.Principal.FindFirst(ClaimTypes.Name).Value, info.Principal.FindFirst(ClaimTypes.Email).Value };
if (result.Succeeded)
return View(userInfo);
else
{
ApplicationUser user = new ApplicationUser
{
Email = info.Principal.FindFirst(ClaimTypes.Email).Value,
UserName = info.Principal.FindFirst(ClaimTypes.Email).Value
};
IdentityResult identResult = await userManager.CreateAsync(user);
if (identResult.Succeeded)
{
identResult = await userManager.AddLoginAsync(user, info);
if (identResult.Succeeded)
{
await signInManager.SignInAsync(user, false);
return View(userInfo);
}
}
return AccessDenied();
}
}
}
The result like this:
More detail information, see How to integrate Google login feature in ASP.NET Core Identity and Google external login setup in ASP.NET Core
Below is my seed class..
public static class DataInitializer
{
public static async void SeedRolesAsync(RoleManager<Role> roleManager)
{
if (roleManager.RoleExistsAsync("Administrator").Result)
return;
var role = new Role
{
Name = "Administrator",
Description = "Perform all the operations."
};
await roleManager.CreateAsync(role);
}
public static async void SeedRoleClaimsAsync(RoleManager<Role> roleManager)
{
var role = await roleManager.FindByNameAsync("Administrator");
var roleClaims = await roleManager.GetClaimsAsync(role);
foreach (var claimString in AllClaims.GetList())
{
var newClaim = new Claim(claimString, "");
if (!roleClaims.Any(rc => rc.Type.ToString() == claimString))
{
await roleManager.AddClaimAsync(role, newClaim);
}
}
}
public static async void SeedUsersAsync(UserManager<User> userManager)
{
var user = new User
{
UserName = "admin#example.com",
Email = "admin#example.com",
FirstName = "Admin",
LastName = "User",
Enabled = true
};
var result = await userManager.CreateAsync(user, "Admin#123");
if (result.Succeeded)
{
await userManager.AddToRoleAsync(user, "Administrator");
}
}
public static void SeedData(UserManager<User> userManager, RoleManager<Role> roleManager)
{
SeedRolesAsync(roleManager);
SeedRoleClaimsAsync(roleManager);
SeedUsersAsync(userManager);
}
}
calling this method in startup class DataInitializer.SeedData(userManager, roleManager);
Am getting error below error while seeding.. i am using ef core 3 for postgresql.. Am getting
System.ObjectDisposedException: 'Cannot access a disposed object.
Object name: 'MyUserManager'.'
Try using tasks all the way - if you don't return anything to wait on chances are the code will continue with disposal even when something hasn't completed yet.
public static async Task SeedData(UserManager<User> userManager, RoleManager<Role> roleManager)
{
await SeedRolesAsync(roleManager);
await SeedRoleClaimsAsync(roleManager);
await SeedUsersAsync(userManager);
}
public static async Task SeedRolesAsync(RoleManager<Role> roleManager)
{
⋮
}
public static async Task SeedRoleClaimsAsync(RoleManager<Role> roleManager)
{
⋮
}
public static async Task SeedUsersAsync(UserManager<User> userManager)
{
⋮
}
You can also move this call into the Main method of your Program.cs because that method can be made async.
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
using (var userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>())
using (var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<Role>>())
{
await DataInitializer.SeedData(userManager, roleManager).ConfigureAwait(false);
}
await host.RunAsync().ConfigureAwait(false);
}
I implemented own solution to make login without page refresh. However can't figure out why I am losing logged-in state on application restart (new debug run).
Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DatabaseContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);
services.AddIdentity<User, Role>(options =>
{ // options...
}).AddEntityFrameworkStores<DatabaseContext>().AddDefaultTokenProviders();
services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo("/keys")).SetApplicationName("App").SetDefaultKeyLifetime(TimeSpan.FromDays(90));
services.AddRazorPages();
services.AddServerSideBlazor().AddCircuitOptions(options =>
{
options.DetailedErrors = true;
});
services.AddSession();
services.AddSignalR();
services.AddBlazoredLocalStorage();
services.AddHttpClient();
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
services.AddAuthorization(options =>
{ // options...
});
// Add application services.
services.AddScoped<AuthenticationStateProvider, IdentityAuthenticationStateProvider>();
// .. services
services.AddMvc(options =>
{
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
}).AddSessionStateTempDataProvider();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseSession();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
AuthorizeController
[HttpPost("Login")]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (ModelState.IsValid)
{
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
Microsoft.AspNetCore.Identity.SignInResult result = await signInMgr.PasswordSignInAsync(model.LEmail, model.LPassword, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
return Ok();
return BadRequest(string.Join(", ", ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage)));
}
return BadRequest();
}
[HttpGet("UserInfo")]
public UserInfo UserInfo()
{
return new UserInfo
{
IsAuthenticated = User.Identity.IsAuthenticated,
UserName = User.Identity.Name,
ExposedClaims = User.Claims.ToDictionary(c => c.Type, c => c.Value)
};
}
I believe problem is in my AuthenticationStateProvider implementation
public class IdentityAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
readonly AuthorizeService authorizeSvc;
readonly IServiceScopeFactory scopeFactory;
readonly IdentityOptions options;
UserInfo userInfoCache;
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
public IdentityAuthenticationStateProvider(AuthorizeService authorizeService, ILoggerFactory loggerFactory, IServiceScopeFactory serviceScopeFactory, IOptions<IdentityOptions> optionsAccessor) : base(loggerFactory)
{
authorizeSvc = authorizeService;
scopeFactory = serviceScopeFactory;
options = optionsAccessor.Value;
}
public async Task LoginAsync(LoginViewModel model)
{
await authorizeSvc.LoginAsync(model);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task RegisterAsync(RegisterViewModel register)
{
await authorizeSvc.RegisterAsync(register);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task LogoutAsync()
{
await authorizeSvc.LogoutAsync();
userInfoCache = null;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
async Task<UserInfo> GetUserInfoAsync()
{
if (userInfoCache != null && userInfoCache.IsAuthenticated)
return userInfoCache;
userInfoCache = await authorizeSvc.GetUserInfo();
return userInfoCache;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
ClaimsIdentity identity = new ClaimsIdentity();
try
{
UserInfo userInfo = await GetUserInfoAsync();
if (userInfo.IsAuthenticated)
{
IEnumerable<Claim> claims = new[] { new Claim(ClaimTypes.Name, userInfoCache.UserName) }.Concat(userInfoCache.ExposedClaims.Select(c => new Claim(c.Key, c.Value)));
identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
}
}
catch (HttpRequestException ex)
{
Console.WriteLine("Request failed:" + ex.ToString());
}
return new AuthenticationState(new ClaimsPrincipal(identity));
}
protected override async Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user manager from a new scope to ensure it fetches fresh data
IServiceScope scope = scopeFactory.CreateScope();
try
{
UserManager<User> userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
finally
{
if (scope is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync();
else
scope.Dispose();
}
}
async Task<bool> ValidateSecurityStampAsync(UserManager<User> userManager, ClaimsPrincipal principal)
{
User user = await userManager.GetUserAsync(principal);
if (user is null)
return false;
else if (!userManager.SupportsUserSecurityStamp)
return true;
string principalStamp = principal.FindFirstValue(options.ClaimsIdentity.SecurityStampClaimType);
string userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
AuthorizeService just calls httprequests like
public async Task<UserInfo> GetUserInfo()
{
HttpContext context = contextAccessor.HttpContext;
HttpClient client = clientFactory.CreateClient();
client.BaseAddress = new Uri($"{context.Request.Scheme}://{context.Request.Host}");
string json = await client.GetStringAsync("api/Authorize/UserInfo");
return Newtonsoft.Json.JsonConvert.DeserializeObject<UserInfo>(json);
}
I noticed in Chrome developer tools that cookies is unchanged after login. This is probably main issue. Any idea how to fix it?
Thanks
AuthorizeService has to pass cookies. For server-side on prerendering pass cookies from HttpContext, at runtime pass cookies from javascript via IJSRuntime injection.
Implemntation looks like for custom AuthenticationStateProvider
async Task<string> GetCookiesAsync()
{
try
{
return $".AspNetCore.Identity.Application={await jsRuntime.InvokeAsync<string>("getLoginCookies")}";
}
catch
{
return $".AspNetCore.Identity.Application={httpContextAccessor.HttpContext.Request.Cookies[".AspNetCore.Identity.Application"]}";
}
}
public async Task<UserInfo> GetUserInfoAsync()
{
if (userInfoCache != null && userInfoCache.IsAuthenticated)
return userInfoCache;
userInfoCache = await authorizeSvc.GetUserInfo(await GetCookiesAsync());
return userInfoCache;
}
AuthorizeService
public async Task<UserInfo> GetUserInfo(string cookie)
{
string json = await CreateClient(cookie).GetStringAsync("api/Authorize/UserInfo");
return Newtonsoft.Json.JsonConvert.DeserializeObject<UserInfo>(json);
}
HttpClient CreateClient(string cookie = null)
{
HttpContext context = contextAccessor.HttpContext;
HttpClient client = clientFactory.CreateClient();
client.BaseAddress = new Uri($"{context.Request.Scheme}://{context.Request.Host}");
if(!string.IsNullOrEmpty(cookie))
client.DefaultRequestHeaders.Add("Cookie", cookie);
return client;
}
There is also need to mention need to do following steps for following actions
Login/Register
SignIn
GetUserInfo (including cookies)
Write cookie via javascript
Logout
Remove cookie via javascript
SignOut
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.