I created two projects from templates.
The first contains Membership and MVC4. And the second uses MVC5 and Asp.Net Identity.
Then I added signalR to both projects using same code.
In Membership project I can access HttpContext.User both in controllers and SignalR connection class(OnConnected method). But in Identity project I have proper value of HttpContext.User only in controllers. In OnConnected method HttpContext.User returns null.
Code of signalR is same in both projects:
1) SynchronizationConnection.cs
public class SynchronizationConnection : PersistentConnection
{
public SynchronizationConnection()
{
}
protected override Task OnReceived(IRequest request, string connectionId, string data)
{
Debugger.Break();
return base.OnReceived(request, connectionId, data);
}
protected override Task OnConnected(IRequest request, string connectionId)
{
Debugger.Break(); //HttpContext.Current.User == null
return base.OnConnected(request, connectionId);
}
protected override Task OnDisconnected(IRequest request, string connectionId)
{
Debugger.Break();
return base.OnDisconnected(request, connectionId);
}
}
2) Startup.cs
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
ConfigureAuth(app);
}
}
3) Startup.Auth.cs
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.MapSignalR<Services.Realtime.SynchronizationConnection>("/test");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}
}
4) Client javascript
var connection = $.connection('/test');
connection.logging = true;
console.log('Receiving connection');
connection.received(function (data) {
console.log('received');
});
connection.disconnected(function () {
console.log('disconnected');
});
connection.error(function (data) {
console.log('error');
});
connection.start().done(function () {
console.log('Connection started');
});
I've seen questions about null User.Identity.Name because of missing [Authorize] attribute. In my case I cannot access even User.Identity. Also I have [Authorize] attribute on my action, that contains client javascript.
Inside of the OnConnected method, you can use:
request.User
That will carry the IPrincipal that was in the HttpContext.Current.User at the time of connection, and it is the one you should mind.
If you need to alter that IPrincipal (like removing the default one generated by Forms authentication and set your own), I recommend you to use an IHttpModule. All the transports in SignalR at least start as an HTTP (even WebSockets), so all transports are going to hit the module at least during connection.
Cheers.
I had the same problem: request.User was null in the PersistentConnection's OnConnected() method. Changing the order in which I initialized SignalR and my authentication provider fixed it. I changed it so that my SignalR endpoints were mapped after my authentication was configured:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
ExpireTimeSpan = new TimeSpan(999, 0, 0, 0),
SlidingExpiration = true
});
GlobalConfiguration.Configuration.UseSqlServerStorage("MyDatabase", new SqlServerStorageOptions() { PrepareSchemaIfNecessary = false });
app.MapSignalR<MyConnection>("/MyConnection");
Related
In our ASP .NET Core 2.0, Web API, when the user logs in, we generate a GUID and return that to the user after storing it in database. What is the best practice to validate this token when the user submits a request to a controller having Authorize attribute on it.
Should I override AuthorizeAttribute.OnAuthorization and put my custom logic in there ? or is there any other place where I should place my custom logic ?
Thanks in advance.
In ASP .NET Core 2.0 you can write you own Middleware to validate token. You can see this video as exapmle - https://www.youtube.com/watch?v=n0llyujNGw8.
Summarily:
1. Create TokenMiddleware:
public class TokenMiddleware
{
// always should be RequestDelegate in constructor
private readonly RequestDelegate _next;
public TokenMiddleware(RequestDelegate next)
{
_next = next;
}
// always should be defiened Invoke or InvokeAsync with HttpContext and returned Task (You can also inject you services here - for example DataContext)
public async Task InvokeAsync(HttpContext context, DataContext dataContext)
{
var validKey = true;
// than you logic to validate token
if (!validKey)
{
context.Response.StatusCode = (int) HttpStatusCode.Forbidden;
await context.Response.WriteAsync("Invalid Token");
}
// if validm than next middleware Invoke
else
{
await _next.Invoke(context);
}
}
}
// Extension to IApplicationBuilder (to register you Middleware)
public static class TokenExtensions
{
public static IApplicationBuilder UseTokenAuth(this IApplicationBuilder builder)
{
return builder.UseMiddleware<TokenMiddleware>();
}
}
Registred you Middleware in Startup:
app.UseTokenAuth();
Question was made long time ago, but for people that might stumble upon it, here is the way I did it, taking advantage of authentication and authorization middlewares. The question doesn't have details about the way the token is passed in the request but I am assuming a standard Authorization header.
Create a custom AuthenticationHandler
MyCustomTokenHandler.cs
public class MyCustomTokenHandler: AuthenticationHandler<AuthenticationSchemeOptions>
{
public MyCustomTokenHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("Authorization"))
{
return AuthenticateResult.NoResult();
}
if (!AuthenticationHeaderValue.TryParse(Request.Headers["Authorization"], out AuthenticationHeaderValue? headerValue))
{
return AuthenticateResult.NoResult();
}
if (!Scheme.Name.Equals(headerValue.Scheme, StringComparison.OrdinalIgnoreCase))
{
return AuthenticateResult.NoResult();
}
if (headerValue.Parameter == null)
{
return AuthenticateResult.NoResult();
}
//The token value is in headerValue.Parameter, call your db to verify it and get the user's data
var claims = new[] { new Claim(ClaimTypes.Name, "username found in db") };
//set more claims if you want
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
Register the handler and enable authorization
Program.cs
builder.Services.AddAuthentication("Bearer").AddScheme<AuthenticationSchemeOptions, MyCustomTokenHandler>("Bearer", null);
//...
var app = builder. Build();
app.UseAuthentication();
app.UseAuthorization();
Most of the code is inspired by this blog post: https://joonasw.net/view/creating-auth-scheme-in-aspnet-core-2
In my Startup.Auth.cs:
private static void ConfigSignalR(IAppBuilder appBuilder)
{
appBuilder.MapSignalR();
var idProvider = new PrincipalUserIdProvider();
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider);
}
My UserHub.cs:
public class UserHub : Hub
{
}
On the server-side, in one of my API Controller action (a Put related to a Grid Update):
[...]
var userHub = GlobalHost.ConnectionManager.GetHubContext<UserHub>();
// Line below does not work
// userHub.Clients.User(userId).send("Hi");
// But this line below works when sending the message to everybody
userHub.Clients.All.send("Hi");
return Request.CreateResponse(HttpStatusCode.OK);
On the JS View client-side:
#Request.IsAuthenticated
{
<script>
$(function() {
var userHub = $.connection.userHub;
console.log(userHub.client);
userHub.client.send = function(message) {
alert('received: ' + message);
};
$.connection.hub.start().done(function() {
});
});
</script>
}
Why when passing the userId my client receives nothing?
(also tried passing the userName, with the same outcome).
[EDIT]
Technically the right way to achieve that is to leverage the implementation of the IUserIdProvider:
https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/mapping-users-to-connections#IUserIdProvider
SignalR - Sending a message to a specific user using (IUserIdProvider) *NEW 2.0.0*
However, I've noticed that in my case the User property of the IRequest object passed to the GetUserId method is always set to null...
The solution was actually already given for another issue, right here: https://stackoverflow.com/a/22028296/4636721
The problem was all about the initialization order in the Startup.Auth.cs:
SignalR must be initialized after the cookies and the OwinContext initialization, such as that IUserIdProvider passed to GlobalHost.DependencyResolver.Register receives a IRequest containing a non-null User for its GetUserId method:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder appBuilder)
{
// Order matters here...
// Otherwise SignalR won't get Identity User information passed to Id Provider...
ConfigOwinContext(appBuilder);
ConfigCookies(appBuilder);
ConfigSignalR(appBuilder);
}
private static void ConfigOwinContext(IAppBuilder appBuilder)
{
appBuilder.CreatePerOwinContext(ApplicationDbContext.Create);
appBuilder.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
appBuilder.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
appBuilder.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
appBuilder.CreatePerOwinContext(LdapAdEmailAuthenticator.Create);
}
private static void ConfigCookies(IAppBuilder appBuilder)
{
appBuilder.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>
(
TimeSpan.FromHours(4),
(manager, user) => user.GenerateUserIdentityAsync(manager)
)
}
});
appBuilder.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
appBuilder.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
appBuilder.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
}
private static void ConfigSignalR(IAppBuilder appBuilder)
{
appBuilder.MapSignalR();
var idProvider = new HubIdentityUserIdProvider();
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider);
}
}
Using the IUserIdProvider below, I explicit declared that I want to use the UserId and not the UserName as given by the default implementation of the IUserIdProvider, aka PrincipalUserIdProvider:
public class HubIdentityUserIdProvider : IUserIdProvider
{
public string GetUserId(IRequest request)
{
return request == null
? throw new ArgumentNullException(nameof(request))
: request.User?.Identity?.GetUserId();
}
}
I am trying to use the new User Id provider specified in signalr 2 to send messages to a specific user. When I call the Clients.All method, I see this working as my javascript code gets called from the server and the ui produces some expected text for my test case. However, when I switch to Clients.User the client side code is never called from the server. I followed the code outlined in this example: SignalR - Sending a message to a specific user using (IUserIdProvider) *NEW 2.0.0*.
NotificationHub.cs:
public class NotificationHub : Hub
{
[Authorize]
public void NotifyUser(string userId, int message)
{
Clients.User(userId).DispatchMessage(message);
}
public override Task OnConnected()
{
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
return base.OnDisconnected(stopCalled);
}
public override Task OnReconnected()
{
return base.OnReconnected();
}
}
IUserIdProvider.cs:
public class UserIdProvider : IUserIdProvider
{
MemberService _memberService;
public UserIdProvider()
{
}
public string GetUserId(IRequest request)
{
long UserId = 0;
if (request.User != null && request.User.Identity != null &&
request.User.Identity.Name != null)
{
var currenUser = Task.Run(() => _memberService.FindByUserName(request.User.Identity.Name)).Result;
UserId = currenUser.UserId;
}
return UserId.ToString();
}
}
Startup.cs
HttpConfiguration config = GlobalConfiguration.Configuration;
config.Routes.MapHttpRoute(
"Default2",
"api/{controller}/{action}/{id}",
new { id = RouteParameter.Optional });
config.Routes.MapHttpRoute(
"DefaultApi2",
"api/{controller}/{id}",
new { id = RouteParameter.Optional });
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
var idProvider = new UserIdProvider();
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider);
map.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
Provider = new QueryStringOAuthBearerAuthenticationProvider()
});
var hubConfiguration = new HubConfiguration
{
};
map.RunSignalR(hubConfiguration);
});
app.MapSignalR();
QuerstringOAuthBearerAuthenticationProvider:
public class QueryStringOAuthBearerAuthenticationProvider
: OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
if (context == null) throw new ArgumentNullException("context");
// try to find bearer token in a cookie
// (by default OAuthBearerAuthenticationHandler
// only checks Authorization header)
var tokenCookie = context.OwinContext.Request.Cookies["BearerToken"];
if (!string.IsNullOrEmpty(tokenCookie))
context.Token = tokenCookie;
return Task.FromResult<object>(null);
}
}
Do I need to map the user to the connections myself using the IUserIdProvider through the OnConnected, OnDisconnected, etc. or does this happen automatically behind the scenes? Is there someone wrong in my posted code that could be a problem as well? I am running signalr from the same environment as my web api rest services, don't know if this makes a difference and using the default bearer token setup web api is using.
It would be far easier for you to create a group based on the connectionid of the connecting client, in the onConnected event and broadcast to the group that matches the connected id, that way if the client disconnects, when they reconnect they would simply belong to a new group the themselves. Unless of course you are required to have an authenticated user.
I'm working on writing a simple login page + SignalR chat room for my website with vNext beta8. Unfortunately, I'm having a very difficult time understanding how claims and authentication work.
All I am trying to do is authenticate a user and set Context.User to their identity, so it can be accessed in SignalR. With the way I have it now, though, Context.User is null everywhere, so SignalR isn't even the meat of this question.
Here's the relevant bits of code for this issue:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSession();
services.AddCaching();
services.AddSignalR();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseStaticFiles();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
});
app.UseSignalR();
}
Login Web API controller method with default values provided, skipping password/authentication checks, and setting the session, which works. LoginUserInfo is just a small class to receive a username/password for logging in.
[HttpPost]
public string Login(LoginUserInfo info)
{
kUser user = new kUser();
user.Name = "Test user";
//This is where I am completely missing the point of something
Context.User = new ClaimsPrincipal(user);
}
kUser - this class is incomplete, I'm assuming that AuthenticationType and IsAuthenticated don't come into affect here, since the exception is never thrown.
[Serializable]
public class kUser : IIdentity
{
public ObjectId Id { get; set; }
public string Name { get; set; }
public string AuthenticationType
{
get { return "MongoDB"; }
}
public bool IsAuthenticated
{
get
{
//If ID exists in database
throw new NotImplementedException();
}
}
}
Research leads me to many different ways to accomplish setting Context.User and having it available across the entire application. Some guides point to FormsAuthentication, which doesn't seem to exist in ASP.NET 5, some talk about setting user IDs to each thread, and some detail how to write an entire service provider for authentication.
All I want is an extremely basic login procedure - no Remember Me style cookies, nothing fancy. Just type in username/password, and the server remembers you for your session. Where am I going wrong, and what am I missing?
You need to do something like this
var claims = new[] { new Claim("name", authUser.Username), new Claim(ClaimTypes.Role, "Admin") };
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
Context.Authentication.SignIn(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
More details - https://github.com/anuraj/CookieAuthMVCSample
I didn't verified the code with latest runtime.
How can I set the CookieDOmain in the CookieAuthenticationOptions at runtime if i want to pull this value from the Request.Url or from some settings stored in my database?
I want to support sub-domains, but also support multi-tenants too which each have different domains.
At the moment this is configured I don't have access to either of these.
Paul
You can assign your own cookie provider:
CookieAuthProvider myProvider = new CookieAuthProvider();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = myProvider
});
Either implement your own, or simply inherit from the existing provider:
public class CookieAuthProvider : CookieAuthenticationProvider
{
public override void ResponseSignIn(CookieResponseSignInContext context)
{
//Alter you cookie options
//context.CookieOptions.Domain = "www...";
base.ResponseSignIn(context);
}
}
And implement ResponseSignIn, it is called when an endpoint has provided sign in information before it is converted into a cookie. By implementing this method the claims and extra information that go into the ticket may be altered.
You'll be passed a CookieResponseSignInContext, which exposes CookieOptions property that can be replaced or altered during the ResponseSignIn call.
Code references from Katana project:
ICookieAuthenticationProvider
CookieResponseSignInContext
CookieAuthenticationHandler
Do you already try this:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Application",
LoginPath = "/Account/Login",
CookieDomain = ".myDomain.com"
});
It looks like MK. answer does not allow proper handling of token renewal when using SlidingExpiration option.
As a workaround, instead of supplying a custom cookie provider, it appears you can supply a custom cookie manager, and define your own methods for adding/removing the cookie.
To keep it simple in my case, I reuse the default cookie manager under the hood. (I can not extend it, its methods are not overridable.)
Here is the code I have ended up with:
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Infrastructure;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataProtection;
using Owin;
public class Startup
{
public void Configuration(IAppBuilder app)
{
var options = new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
SlidingExpiration = true,
CookieManager = new CustomCookieManager()
};
app.UseCookieAuthentication(options);
}
}
public class CustomCookieManager : ICookieManager
{
private readonly ICookieManager ConcreteManager;
public CustomCookieManager()
{
ConcreteManager = new ChunkingCookieManager();
}
string ICookieManager.GetRequestCookie(IOwinContext context, string key)
{
return ConcreteManager.GetRequestCookie(context, key);
}
void ICookieManager.AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
{
SetupDomain(context, options);
ConcreteManager.AppendResponseCookie(context, key, value, options);
}
void ICookieManager.DeleteCookie(IOwinContext context, string key, CookieOptions options)
{
SetupDomain(context, options);
ConcreteManager.DeleteCookie(context, key, options);
}
private void SetupDomain(IOwinContext context, CookieOptions options)
{
// custom logic for assigning something to options.Domain
}
}