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.
Related
I have an ASP.Net Core API project. I want to be able to write a custom routing logic to be able to choose different controller actions based on HTTP Body parameters. To illustrate my problem, this is my Controller class:
[ApiController]
[Route("api/[controller]")]
public class TestController
{
// should be called when HTTP Body contains json: '{ method: "SetValue1" }'
public void SetValue1()
{
// some logic
}
// should be called when HTTP Body contains json: '{ method: "SetValue2" }'
public void SetValue2()
{
// some logic
}
}
As you can see from my comments I want to choose different action methods based on HTTP body.
Here is my Startup class:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// I assume instead of this built in routing middleware,
// I will have to implement custom middleware to choose the correct endpoints, from HTTP body,
// any ideas how I can go about this?
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
One of the option I could use is having one entry Action method that will call differnt methods based on the HTTP Body content, but I would like to avoid that and encapsulate this logic somewhere in a custom routing.
In the old APS.Net Web API there was a handy class ApiControllerActionSelector that I could extend, and define my custom logic of selecting Action methods, however this is not supported in new ASP.Net Core. I think I will have to implement my own version of app.UseRouting middleware. Any ideas on how I can do it?
In the old asp.net core (before 3.0), we can implement a custom IActionSelector and it's especially convenient when the ActionSelector is still made public. But with the new endpoint routing, it's changed to the so-called EndpointSelector. The implementation is fairly the same, the point is how we extract the ActionDescriptor which is put in the Endpoint as metadata. The following implementation requires a default EndpointSelector (which is DefaultEndpointSelector) but that's unfortunately made internal. So we need to use a trick to get an instance of that default implementation to use in our custom implementation.
public class RequestBodyEndpointSelector : EndpointSelector
{
readonly IEnumerable<Endpoint> _controllerEndPoints;
readonly EndpointSelector _defaultSelector;
public RequestBodyEndpointSelector(EndpointSelector defaultSelector, EndpointDataSource endpointDataSource)
{
_defaultSelector = defaultSelector;
_controllerEndPoints = endpointDataSource.Endpoints
.Where(e => e.Metadata.GetMetadata<ControllerActionDescriptor>() != null).ToList();
}
public override async Task SelectAsync(HttpContext httpContext, CandidateSet candidates)
{
var request = httpContext.Request;
request.EnableBuffering();
//don't use "using" here, otherwise the request.Body will be disposed and cannot be used later in the pipeline (an exception will be thrown).
var sr = new StreamReader(request.Body);
try
{
var body = sr.ReadToEnd();
if (!string.IsNullOrEmpty(body))
{
try
{
var actionInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<ActionInfo>(body);
var controllerActions = new HashSet<(MethodInfo method, Endpoint endpoint, RouteValueDictionary routeValues, int score)>();
var constrainedControllerTypes = new HashSet<Type>();
var routeValues = new List<RouteValueDictionary>();
var validIndices = new HashSet<int>();
for (var i = 0; i < candidates.Count; i++)
{
var candidate = candidates[i];
var endpoint = candidates[i].Endpoint;
var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
if (actionDescriptor == null) continue;
routeValues.Add(candidate.Values);
constrainedControllerTypes.Add(actionDescriptor.MethodInfo.DeclaringType);
if (!string.Equals(actionInfo.MethodName, actionDescriptor.MethodInfo.Name,
StringComparison.OrdinalIgnoreCase)) continue;
if (!controllerActions.Add((actionDescriptor.MethodInfo, endpoint, candidate.Values, candidate.Score))) continue;
validIndices.Add(i);
}
if (controllerActions.Count == 0)
{
var bestCandidates = _controllerEndPoints.Where(e => string.Equals(actionInfo.MethodName,
e.Metadata.GetMetadata<ControllerActionDescriptor>().MethodInfo.Name,
StringComparison.OrdinalIgnoreCase)).ToArray();
var routeValuesArray = request.RouteValues == null ? routeValues.ToArray() : new[] { request.RouteValues };
candidates = new CandidateSet(bestCandidates, routeValuesArray, new[] { 0 });
}
else
{
for(var i = 0; i < candidates.Count; i++)
{
candidates.SetValidity(i, validIndices.Contains(i));
}
}
//call the default selector after narrowing down the candidates
await _defaultSelector.SelectAsync(httpContext, candidates);
//if some endpoint found
var selectedEndpoint = httpContext.GetEndpoint();
if (selectedEndpoint != null)
{
//update the action in the RouteData to found endpoint
request.RouteValues["action"] = selectedEndpoint.Metadata.GetMetadata<ControllerActionDescriptor>().ActionName;
}
return;
}
catch { }
}
}
finally
{
request.Body.Position = 0;
}
await _defaultSelector.SelectAsync(httpContext, candidates);
}
}
The registration code is a bit tricky like this:
//define an extension method for registering conveniently
public static class EndpointSelectorServiceCollectionExtensions
{
public static IServiceCollection AddRequestBodyEndpointSelector(this IServiceCollection services)
{
//build a dummy service container to get an instance of
//the DefaultEndpointSelector
var sc = new ServiceCollection();
sc.AddMvc();
var defaultEndpointSelector = sc.BuildServiceProvider().GetRequiredService<EndpointSelector>();
return services.Replace(new ServiceDescriptor(typeof(EndpointSelector),
sp => new RequestBodyEndpointSelector(defaultEndpointSelector,
sp.GetRequiredService<EndpointDataSource>()),
ServiceLifetime.Singleton));
}
}
//inside the Startup.ConfigureServices
services.AddRequestBodyEndpointSelector();
The old solution for the old conventional routing used in asp.net core 2.2
Your requirement is a bit weird and you may have to accept some trade-off for that. First that requirement may require you to read the Request.Body twice (when the selected action method has some arguments to model-bind). Even when the framework supports the so-called EnableBuffering on the HttpRequest, it's still a bit trade-off to accept. Secondly in the method to select the best action (defined on IActionSelector), we cannot use async so reading the request body of course cannot be done with async.
For high performance web apps, that definitely should be avoided. But if you can accept that kinds of trade-off, we have a solution here by implementing a custom IActionSelector, at best let it inherit from the default ActionSelector. The method we can override is ActionSelector.SelectBestActions. However that method does not provide the RouteContext (we need access to that to update the RouteData), so we'll re-implement another method of IActionSelector named IActionSelector.SelectBestCandidate which provides a RouteContext.
Here's the detailed code:
//first we define a base model for parsing the request body
public class ActionInfo
{
[JsonProperty("method")]
public string MethodName { get; set; }
}
//here's our custom ActionSelector
public class RequestBodyActionSelector : ActionSelector, IActionSelector
{
readonly IEnumerable<ActionDescriptor> _actions;
public RequestBodyActionSelector(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
ActionConstraintCache actionConstraintCache, ILoggerFactory loggerFactory)
: base(actionDescriptorCollectionProvider, actionConstraintCache, loggerFactory)
{
_actions = actionDescriptorCollectionProvider.ActionDescriptors.Items;
}
ActionDescriptor IActionSelector.SelectBestCandidate(RouteContext context, IReadOnlyList<ActionDescriptor> candidates)
{
var request = context.HttpContext.Request;
//supports reading the request body multiple times
request.EnableBuffering();
var sr = new StreamReader(request.Body);
try
{
var body = sr.ReadToEnd();
if (!string.IsNullOrEmpty(body))
{
try
{
//here I use the old Newtonsoft.Json
var actionInfo = JsonConvert.DeserializeObject<ActionInfo>(body);
//the best actions should be on these controller types.
var controllerTypes = new HashSet<TypeInfo>(candidates.OfType<ControllerActionDescriptor>().Select(e => e.ControllerTypeInfo));
//filter for the best by matching the controller types and
//the method name from the request body
var bestActions = _actions.Where(e => e is ControllerActionDescriptor ca &&
controllerTypes.Contains(ca.ControllerTypeInfo) &&
string.Equals(actionInfo.MethodName, ca.MethodInfo.Name, StringComparison.OrdinalIgnoreCase)).ToList();
//only override the default if any method name matched
if (bestActions.Count > 0)
{
//before reaching here,
//the RouteData has already been populated with
//what from the request's URL
//By overriding this way, that RouteData's action
//may be changed, so we need to update it here.
var newActionName = (bestActions[0] as ControllerActionDescriptor).ActionName;
context.RouteData.PushState(null, new RouteValueDictionary(new { action = newActionName }), null);
return SelectBestCandidate(context, bestActions);
}
}
catch { }
}
}
finally
{
request.Body.Position = 0;
}
return SelectBestCandidate(context, candidates);
}
}
To register the custom IActionSelector in the Startup.ConfigureServices:
services.AddSingleton<IActionSelector, RequestBodyActionSelector>();
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();
}
}
System.InvalidOperationException: The OpenID Connect request cannot be
retrieved from the ASP.NET context. Make sure that
'app.UseOpenIddict()' is called before 'app.UseMvc()' and that the
action route corresponds to the endpoint path registered via
'services.AddOpenIddict().Enable[...]Endpoint(...)'. at
OpenIddict.Mvc.OpenIddictModelBinder.BindModelAsync(ModelBindingContext
context)
MyStartup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry(Configuration);
services.AddDbContext<ApplicationUserDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationUserDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
.AddMvcBinders()
.EnableAuthorizationEndpoint("/connect/authorize")
.EnableLogoutEndpoint("/connect/logout")
.EnableTokenEndpoint("/connect/token")
.EnableUserinfoEndpoint("/Account/Userinfo")
.AllowAuthorizationCodeFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow()
.RequireClientIdentification()
// During development, you can disable the HTTPS requirement.
.DisableHttpsRequirement()
.AddEphemeralSigningKey();
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseApplicationInsightsRequestTelemetry();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseApplicationInsightsExceptionTelemetry();
app.UseStaticFiles();
app.UseCsp(options => options.DefaultSources(directive => directive.Self())
.ImageSources(directive => directive.Self()
.CustomSources("*"))
.ScriptSources(directive => directive.Self()
.UnsafeInline())
.StyleSources(directive => directive.Self()
.UnsafeInline()));
app.UseXContentTypeOptions();
app.UseXfo(options => options.Deny());
app.UseXXssProtection(options => options.EnabledWithBlockMode());
app.UseIdentity();
// Add a middleware used to validate access
// tokens and protect the API endpoints.
app.UseOAuthValidation();
app.UseGoogleAuthentication(new GoogleOptions
{
});
app.UseStatusCodePagesWithReExecute("/error");
app.UseOpenIddict();
app.UseMvcWithDefaultRoute();
}
Update
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Mvc.Server.Models;
using Mvc.Server.ViewModels.Authorization;
using Mvc.Server.ViewModels.Shared;
using OpenIddict;
public class AuthorizationController : Controller {
private readonly OpenIddictApplicationManager<OpenIddictApplication> _applicationManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public AuthorizationController(
OpenIddictApplicationManager<OpenIddictApplication> applicationManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager) {
_applicationManager = applicationManager;
_signInManager = signInManager;
_userManager = userManager;
}
// Note: to support interactive flows like the code flow,
// you must provide your own authorization endpoint action:
[Authorize, HttpGet, Route("~/connect/authorize")]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request) {
// Retrieve the application details from the database.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null) {
return View("Error", new ErrorViewModel {
Error = OpenIdConnectConstants.Errors.InvalidClient,
ErrorDescription = "Details concerning the calling client application cannot be found in the database"
});
}
// Flow the request_id to allow OpenIddict to restore
// the original authorization request from the cache.
return View(new AuthorizeViewModel {
ApplicationName = application.DisplayName,
RequestId = request.RequestId,
Scope = request.Scope
});
}
[Authorize, HttpPost("~/connect/authorize/accept"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept(OpenIdConnectRequest request) {
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(User);
if (user == null) {
return View("Error", new ErrorViewModel {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
[Authorize, HttpPost("~/connect/authorize/deny"), ValidateAntiForgeryToken]
public IActionResult Deny() {
// Notify OpenIddict that the authorization grant has been denied by the resource owner
// to redirect the user agent to the client application using the appropriate response_mode.
return Forbid(OpenIdConnectServerDefaults.AuthenticationScheme);
}
// Note: the logout action is only useful when implementing interactive
// flows like the authorization code flow or the implicit flow.
[HttpGet("~/connect/logout")]
public IActionResult Logout(OpenIdConnectRequest request) {
// Flow the request_id to allow OpenIddict to restore
// the original logout request from the distributed cache.
return View(new LogoutViewModel {
RequestId = request.RequestId
});
}
[HttpPost("~/connect/logout"), ValidateAntiForgeryToken]
public async Task<IActionResult> Logout() {
// Ask ASP.NET Core Identity to delete the local and external cookies created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
await _signInManager.SignOutAsync();
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application.
return SignOut(OpenIdConnectServerDefaults.AuthenticationScheme);
}
// Note: to support non-interactive flows like password,
// you must provide your own token endpoint action:
[HttpPost("~/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request) {
if (request.IsPasswordGrantType()) {
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the user is allowed to sign in.
if (!await _signInManager.CanSignInAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Reject the token request if two-factor authentication has been enabled by the user.
if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Ensure the user is not already locked out.
if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password)) {
if (_userManager.SupportsUserLockout) {
await _userManager.AccessFailedAsync(user);
}
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
if (_userManager.SupportsUserLockout) {
await _userManager.ResetAccessFailedCountAsync(user);
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
private async Task<AuthenticationTicket> CreateTicketAsync(OpenIdConnectRequest request, ApplicationUser user) {
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in principal.Claims) {
// In this sample, every claim is serialized in both the access and the identity tokens.
// In a real world application, you'd probably want to exclude confidential claims
// or apply a claims policy based on the scopes requested by the client application.
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(
principal, new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
ticket.SetScopes(new[] {
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess,
OpenIddictConstants.Scopes.Roles
}.Intersect(request.GetScopes()));
return ticket;
}
}
Changed:
[Authorize, HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept(OpenIdConnectRequest request) {
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(User);
if (user == null) {
return View("Error", new ErrorViewModel {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
OpenIddict used to allow "subroutes" like /connect/authorize/accept or /connect/authorize/deny to be recognized as valid authorization endpoint paths when /connect/authorize was specified, but this feature was removed recently.
With the latest OpenIddict bits, you're encouraged to use the same route template for all your authorization endpoint actions.
[Authorize, HttpGet("~/connect/authorize")]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
{
// ...
}
[Authorize, FormValueRequired("submit.Accept")]
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept(OpenIdConnectRequest request)
{
// ...
}
[Authorize, FormValueRequired("submit.Deny")]
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
public IActionResult Deny()
{
// ...
}
You can use Orchard's [FormValueRequired] approach to discriminate your actions:
public sealed class FormValueRequiredAttribute : ActionMethodSelectorAttribute
{
private readonly string _name;
public FormValueRequiredAttribute(string name)
{
_name = name;
}
public override bool IsValidForRequest(RouteContext context, ActionDescriptor action)
{
if (string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.HttpContext.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.HttpContext.Request.Method, "DELETE", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.HttpContext.Request.Method, "TRACE", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (string.IsNullOrEmpty(context.HttpContext.Request.ContentType))
{
return false;
}
if (!context.HttpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return !string.IsNullOrEmpty(context.HttpContext.Request.Form[_name]);
}
}
Don't forget to also update your submit buttons:
<input class="btn btn-lg btn-success" name="submit.Accept" type="submit" value="Yes" />
<input class="btn btn-lg btn-danger" name="submit.Deny" type="submit" value="No" />
I'm hosting my signalr hub on a separate domain and making cross domain connection to hub from my main application. When a user logs into the main application, signalr connection is established. Now, the problem I'm having is how to identify the connected user inside the hub.
If my Hub was within the main application then I could use the Context.User of the logged in user and maintain a dictionary and update them on Connect and Disconnect events.
But being a cross-domain connection, I don't have the Context.User and no way for me to know to whom that connection ID belongs to. I'm lost here.
What am I missing here?
You should keep users credentials and connections ids yourself. You should define List<ClientsEntity> or something like that. Then override onConnected and onDisconnected methods. Client has to send querystring for connecting to your Hub as Lars said.
for example clients send to you like this
$.connection.hub.qs = { 'token' : 'id' };
In the Hub Class:
public class ChatHub : Hub
{
static List<ClientsEntity> clientsList = new List<ClientsEntity>();
public override Task OnConnected()
{
string connectionID = Context.ConnectionId;
string token = Context.QueryString["token"];
ClientsEntity clientItem = new ClientsEntity();
clientItem.connectionId = connectionID;
clientItem.token = token;
clientItem.connectionTime = DateTime.Now;
clientsList.Add(clientItem);
return base.OnConnected();
}
public override Task OnDisconnected()
{
ClientsEntity item = clientsList.FirstOrDefault(c => c.connectionId == Context.ConnectionId);
if (item != null) {
clientsList.Remove(item);
}
return base.OnDisconnected();
}
public override Task OnReconnected()
{
return base.OnReconnected();
}
public void Send(string token, string message)
{
ClientsEntity user = clientsList.FirstOrDefault(c => c.token == token);
if (user != null)
Clients.Client(user.connectionId).sendMessage(token, message);
}
public void GetConnectedClients(string token) {
ClientsEntity user = clientsList.FirstOrDefault(c => c.token == token);
if(token.Equals("-1") && user != null)
Clients.Client(user.connectionId).getConnClients(clientsList);
}
}
You could assign a unique connection token to the user once they log in; then make the client send that in the query string:
$.connection.hub.qs = { 'token' : id };