SignalR: Receive Custom Id on Every Call Including OnConnected - c#

I am using SignalR to relay messages from a WebAPI server back-end to a JavaScript web page. These messages are only relayed to certain users so I need to map the SignalR ConnectionId with the custom id of the user of the webpage.
Currently the WebAPI uses FormsAuthentication and the custom id I need is in the cookie.
Initially I inherited the IUserIdProvider to pull the value off of the cookie:
public class CustomIdProvider : IUserIdProvider
{
public string GetUserId(IRequest request)
{
Cookie formsAuthCookie = request.Cookies[FormsAuthentication.FormsCookieName];
try
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(formsAuthCookie.Value);
var obj = JsonConvert.DeserializeObject(ticket.Name) as JObject;
return (string)obj.GetValue("UserId");
}
catch (Exception)
{
return null;
}
}
}
which worked as far as getting the custom id correctly. But that value was never set on the identity as far as I could tell. I also was unable to edit any of the Identity values due to Context.User.Identity.Name all being readonly.
Edit: Trying the CustomIdProvider again, I correctly get the value out of the cookie but on returning from the GetUserId method, OnConnected is never called.
My next approach was based off of Shaun Xu's blogpost
Set Context User Principal For Customized Authentication In SignalR.
Here is my implementation:
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
private const string CUSTOM_IDENTITY_KEY = "server.User";
public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
{
string customId;
// This gets the custom id from the cookie.
TryGetCustomId(request, out customId);
// The CustomIdentity class just sets the customId to the name.
request.Environment.Add(CUSTOM_IDENTITY_KEY, new ClaimsPrincipal(new CustomIdentity(customId, true)));
return true;
}
public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
{
string connectionId = hubIncomingInvokerContext.Hub.Context.ConnectionId;
IDictionary<string, object> environment = hubIncomingInvokerContext.Hub.Context.Request.Environment;
object obj;
environment.TryGetValue(CUSTOM_IDENTITY_KEY, out obj);
var principal = obj as ClaimsPrincipal;
if (principal?.Identity == null || !principal.Identity.IsAuthenticated)
{
return false;
}
hubIncomingInvokerContext.Hub.Context = new HubCallerContext(new ServerRequest(environment), connectionId);
return true;
}
and this actually works to set the Context.User.Identity correctly when both methods are invoked.
However, my problem is that when the user first connects and OnConnected is called, it does not call the AuthorizeHubMethodInvocation and therefore the Context.User.Identity is not available in OnConnected.
I want to be able to access the correct Identity containing my custom id at all stages of the SignalR hub: OnConnected, OnDisconnected, and invoked methods.
Does anyone have a good solution for this?

I have some comments:
1) You should not try to establish your custom identity in authorization stage. It should be the concern of the authentication stage.
2) Implementing custom IUserIdProvider and establishing custom identity id of your Context.User.Identity are separate concerns. The custom IUserIdProvider is just to map any of the properties of your Context.User.Identity as the identifier of user in signalR.
So, to fix your problem. Try establishing your custom identity id at authentication stage. There are many ways to do it depending on how you setup your application. For example:
1) Establishing your custom identity id in Application_PostAuthenticateRequest:
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
//Provide custom identity id to your HttpContext.Current.User
//In your case, you may extract that information from your authentication ticket.
}
You could look at this post if you need detailed information: ASP.NET MVC - Set custom IIdentity or IPrincipal
2) Using claims identity, you can return the custom id as a claim, everytime when the browser sends a request to your server, your claims identity is re-established. In the example below, I use owin cookie authentication.
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, user.Id.ToString()));
var id = new ClaimsIdentity(claims, "Cookies");
var ctx = Request.GetOwinContext();
var authenticationManager = ctx.Authentication;
authenticationManager.SignIn(id);
Then in your IUserIdProvider implementation, you can extract the corresponding information in your identity (Name property, claim,...) to use as user identifier in your signalR application.

Related

IdentityServer4 and UserInfo endpoint customization

I have created an IdentityServer4 application, if I login inside that application the user claims are all good. If I login from another client application (MVC) the UserInfo endpoint doesn't return the same claims.
The IdentityServer is configured with ASP.NET Identity, so the UserProfile is already configured to return all UserClaims, like the one I created.
I don't understand why it's not showed on consent view or it's not included in UserInfo endpoint result
Please check for the below points if they can solve your issue
1.) Your Identity resource and API resource should have the required UserClaims.
2.) Check if there is some custom logic to issue requested claims for userinfo endpoint in your profile service.
public class ProfileService : IProfileService
{
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
if (context.Caller == IdentityServerConstants.ProfileDataCallers.UserInfoEndpoint)
{
//custom logic to add requested claims
context.AddRequestedClaims(claims);
}
}
}
3.) Try to make the property 'GetClaimsFromUserInfoEndpoint=true' in your MVC client AddOpenIdConnect configuration.
have you configured your IdentityResources?
Something like:
services.AddIdentityServer()
.AddInMemoryIdentityResources(GetIdentityResources())
//where
public static List<IdentityResource> GetIdentityResources()
{
// Claims automatically included in OpenId scope
var openIdScope = new IdentityResources.OpenId();
openIdScope.UserClaims.Add(JwtClaimTypes.Locale);
// Available scopes
return new List<IdentityResource>
{
openIdScope,
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource(Constants.RolesScopeType, Constants.RolesScopeType,
new List<string> {JwtClaimTypes.Role, Constants.TenantIdClaimType})
{
//when false (default), the user can deselect the scope on consent screen
Required = true
}
};
}

IdentityServer4 claims are not part of the token on hybrid flow

I'm trying to build a hybrid flow, and have claims on the returned access token with IdentityServer4. I'm using the QuickStart UI controlles.
In my AccountController after the user was authenticated successfully, I have the following code which signs him in:
await HttpContext.SignInAsync("anon#nymous.com", "anon#nymous.com", null, new Claim("MyName", "Ophir"));
In the MVC website that is causing this flow, on the page I want to "protect" I have the following code:
[Authorize]
public IActionResult RestrictedMvcResource()
{
var token = _httpContext.HttpContext.GetTokenAsync("access_token").Result;
var identity = User.Identity;
return View();
}
After a successful login, the debugger hits this code fine and I'm getting the access token.
The problem is that if I decode my access token (I'm using https://jwt.io/) I see the name and subject, but I do not see the MyName claim that I have defined.
(I have another flow in my system for client_credentials which does return the claims on the token - but it uses a different code flow).
How do I return the claims on the token for the hybrid flow?
EDIT:
Solving this problem was a combination of 2 things:
Implementing IProfileService as suggested in (selected) answer. here's my implementation:
public class ProfileService : IProfileService
{
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.AddRequestedClaims(context.Subject.Claims);
foreach (Claim claim in context.Subject.Claims)
{
if (context.IssuedClaims.Contains(claim))
continue;
context.IssuedClaims.Add(claim);
}
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
return Task.FromResult(0);
}
}
This will add any claim that isn't already on the token.
When calling HttpContext.SignInAsync you must pass the list of claims, otherwise no additional claims will be in the context.Subject.Claims collection.
You can implement custom IProfileService if you want to add custom claims to the token.
You can find more info in Identity Server 4 docs.
An example of simple custom profile service would be:
public class CustomProfileService : IProfileService
{
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.AddRequestedClaims(context.Subject.Claims);
context.IssuedClaims.Add(new Claim("MyName", "Ophir"));
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
return Task.FromResult(0);
}
}
Once you have this, just register it to the DI:
services.AddTransient<IProfileService, CustomProfileService>();
It will get called whenever an access_token or id_token is requested. You would need to check context.Caller as per Ruard's comment if you only wanted the extra claims in certain type of token.
EDIT:
Also alternatively, you can add the claims directly to the user configuration as per example in one of the Identity Server 4 quickstarts:
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password",
Claims = new []
{
new Claim("MyName", "Ophir")
}
},
If you end up not implementing custom IProfileService and keep using DefaultProfileService, then you would also need add a custom IdentityResource in your configuration:
return new List<IdentityResource>
{
//..Your other configured identity resources
new IdentityResource(
name: "custom.name",
displayName: "Custom Name",
claimTypes: new[] { "MyName" });
};
Any clients wanting to have this claim added in the token would need request for custom.name scope.
AspNet.Security.OpenIdConnect.Server does not serialize claims that do not have destinations set. I ran into this when using OpenIdDict.
try this:
var claim = new Claim("MyName", "Ophir");
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken);
await HttpContext.SignInAsync("anon#nymous.com", "anon#nymous.com", null, claim);
You will probably need to add these namespaces:
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
There are two steps in which I add claims to tokens for our identityserver.
Through a custom profile service like one of the other answers shows, you can define your own claims for an user.
Those claims can then be requested through the userinfo endpoint.
Or you create an Api ( resource) called for example IncludeNameInAccessToken that adds the name claim by default to the access token if you request that Api as a scope.

What is 'Xsrf check when linking' in ASP.Net Identity/OWIN [duplicate]

In my MVC 5 web app I have this (in AccountController.cs):
// Used for XSRF protection when adding external sign ins
private const string XsrfKey = "XsrfId";
and
public string SocialAccountProvider { get; set; }
public string RedirectUri { get; set; }
public string UserId { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
if (UserId != null)
{
properties.Dictionary[XsrfKey] = UserId;
}
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, SocialAccountProvider);
}
How exactly is it being used for protection?
Should I set the value of XsrfKey to something more random?
Take a look at ManageController methods LinkLogin and LinkLoginCallback:
//
// POST: /Manage/LinkLogin
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LinkLogin(string provider)
{
// Request a redirect to the external login provider to link a login for the current user
return new AccountController.ChallengeResult(provider, Url.Action("LinkLoginCallback", "Manage"), User.Identity.GetUserId());
}
//
// GET: /Manage/LinkLoginCallback
public async Task<ActionResult> LinkLoginCallback()
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, User.Identity.GetUserId());
if (loginInfo == null)
{
return RedirectToAction("ManageLogins", new { Message = ManageMessageId.Error });
}
var result = await UserManager.AddLoginAsync(User.Identity.GetUserId(), loginInfo.Login);
return result.Succeeded ? RedirectToAction("ManageLogins") : RedirectToAction("ManageLogins", new { Message = ManageMessageId.Error });
}
These are the methods that handle linking of external accounts (i.e. Google, Facebook, etc.). The flow goes like this:
User clicks "Link Account" button, which calls a POST to LinkLogin method.
LinkLogin returns ChallengeResult object, with callback url set to LinkLoginCallback method.
ChallengeResult.ExecuteResult is called by MVC framework, calls IAuthenticationManager.Challenge, which causes a redirect to the specific external login provider (let's say: google).
User authenticates with google, then google redirects to callback url.
The callback is handled with LinkLoginCallback. Here, we want to prevent XSRF and verify that the call was initiated by a user, from a page served by our server (and not by some malicious site).
Normally, if it was a simple GET-POST sequence, you would add a hidden <input> field with an anti-forgery token and compare it with a corresponding cookie value (that's how Asp.Net Anti-Forgery Tokens work).
Here, the request comes from external auth provider (google in our example). So we need to give the anti-forgery token to google and google should include it in the callback request. That's exactly what state parameter in OAuth2 was designed for.
Back to our XsrfKey: everything you put in AuthenticationProperties.Dictionary will be serialized and included in the state parameter of OAuth2 request - and consequentially, OAuth2 callback. Now, GetExternalLoginInfoAsync(this IAuthenticationManager manager, string xsrfKey, string expectedValue) will look for the XsrfKey in the received state Dictionary and compare it to the expectedValue. It will return an ExternalLoginInfo only if the values are equal.
So, answering your original question: you can set XsrfKey to anything you want, as long as the same key is used when setting and reading it. It doesn't make much sense to set it to anything random - the state parameter is encrypted, so no one expect you will be able to read it anyway.
Just leave it as is:
As the name of the member states it is a key:
private const string XsrfKey = "XsrfId";
It is defined in this manner to avoid "magic numbers" and then is used a little down in the scaffold code:
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
if (UserId != null)
{
properties.Dictionary[XsrfKey] = UserId;
}
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
}
The value of the dictionary item is then set to the UserId property in the above code by using the XsrfKey member as the key.
IOW the code is already setting the XSRF dictionary item to the value of the user ID in the snippet. If you change the XsrfKey members value to anything else you will cause problems down the line, since the expected key "XsrfId" will have no value set.
If by changing it to something more random you are implying to change the value and not they key of the dictionary, or in other words, not set it to the user id then please see the following for an explanation of the anti forgery token inner workings.
http://www.asp.net/mvc/overview/security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages

Possible to bypass servicestack's authentication mechanisms for non standard authentication

My authentication mechanism is in a way different it cannot fit into 1 of ServiceStack's current authentication methods (even overriding method 'TryAuthenticate' does not provide a solution). So would it be possible to authenticate from some arbitrary ServiceStack service?
To give an example:
I open a plain old HTML login page (I am using Angular for the record).
I login in and call my custom ServiceStack service in order to send the non-standard credentials to the server (of course using Angular's http directive).
I validate the credentials myself. If correct I like to hook up into servicestack authentication mechanism and probably have to send back a ServiceStack authentication cookie to the browser. Am I correct?
If someone can make 3 work I can call ServiceStack services which have the authenticate attribute
To be allowed through the [Authenticate] attribute, it needs any one of the registered AuthProviders IsAuthorized() to return true, i.e:
public class CustomAuthProvider : AuthProvider
{
public CustomAuthProvider()
{
this.Provider = "custom";
}
public override bool IsAuthorized(
IAuthSession session, IAuthTokens tokens, Authenticate request=null)
{
return true; //custom logic to verify if this session is authenticated
}
public override object Authenticate(
IServiceBase authService, IAuthSession session, Authenticate request)
{
throw new NotImplementedException();
}
}
Plugins.Add(new AuthFeature(() => new CustomUserSession(),
new IAuthProvider[] {
new CustomAuthProvider()
}));
In your Custom Authentication Service you should also save the Users Session with IsAuthenticated=true, e.g:
public object Any(CustomAuth request)
{
//Authenticate User
var session = base.SessionAs<CustomUserSession>();
session.IsAuthenticated = true;
this.SaveSession(session);
}

Web Api 2 Stateless with Users

I believe I understand the basics of sessionless/stateless REST but I am having problems with implementation in Asp.Net Web Api 2 because I haven't used it before. I have set up ApiControllers that use a custom System.Web.Http.AuthorizeAttribute like this.
public class ApiAuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
if (actionContext.Request.Headers.Authorization != null)
{
//Set identity??
return;
}
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
}
}
I have a database that contains users and I need to use them for getting privileges to get/post/put/delete things but dont want to use a session. I have never worked with the asp.net Identity so I am not familiar with its features and capabilities.
My idea for implementation is to use user credentials or api secret signing to authenticate and get privileges for a user for every request. The question is, by using a AuthorizeAttribute or something similar, how do i give the controller(s) during that one request the user information if their credentials were correct?
UPDATE:
Is using this.User (ApiController.User) session based or can it be used for that single request. If so, how does one set it
You can use HttpContext.Items to hold user data for current requests.
After setting identity based on Auth header , you can have
System.Web.HttpContext.Current.Items["userdata"]=userDataObject,
Another approach would be write your own action filter for authentication (but take utmost care)and pass data to controller.
public class MyAuthAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//do authorization here
base.OnActionExecuting(filterContext);
// Create object parameter.
filterContext.ActionParameters["userdata"] = new User("John", "Smith");
}
}
then in controller
[MyAuthAttribute]
ActionResult SomeAction(User userdata) //this will have user data
{
}
It looks like that using IPrincipal and setting HttpContext.Current.User will allow the ApiControllers to access that user through using
this.User
with web api not having access to the session
public override void OnAuthorization(HttpActionContext actionContext)
{
if (actionContext.Request.Headers.Authorization != null)
{
//Check user credentials/token here
//Get internal user
IPrincipal principal = new MyOwnCustomPrincipal(internalUser);
HttpContext.Current.User = principal;
return;
}
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
}

Categories

Resources