I have an authentication setup where I store a session ID and an unverified user ID in my claims. Then, in a normal controller, I would look up the session in my DB to verify that it matches the userID and consider the user logged in.
I am trying to use Azure SignalR. I want to be able to send messages to connected users by userID, and I need to implement IUserIdProvider. The GetUserId method on it isn't async, but what I need to do is perform the same logic, which verifies the session ID and userID in the claims against the database before it considers the user valid. This code would be async, but the GetUserId method isn't async.
What options do I have?
Thanks!
I believe the reasoning behind IUserIdProvider.GetUserId(HubConnectionContext) not being async is that calls to the DB or other external resource would occur earlier in the request pipeline. This could include operations like mapping an external user id or session id to an internal user id.
The way I solved a similar problem was to place my DB lookups in an IClaimsTransformation implementation which stored the values I looked up in Claims on the User that flows through the request.
public static class ApplicationClaimTypes
{
public static string UserId => "user-id";
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Identity.Web;
class MsalClaimsTransformation : IClaimsTransformation
{
// IMapExternalUsers represents the actions you must take to map an external id to an internal user id
private readonly IMapExternalUsers _externalUsers;
private ClaimsPrincipal _claimsPrincipal;
public MsalClaimsTransformation(IMapExternalUsers externalUsers)
{
_externalUsers = externalUsers;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal claimsPrincipal)
{
_claimsPrincipal = claimsPrincipal;
// This check is important because the IClaimsTransformation may run multiple times in a single request
if (!claimsPrincipal.HasClaim(claim => claim.Type == ApplicationClaimTypes.UserId))
{
var claimsIdentity = await MapClaims();
claimsPrincipal.AddIdentity(claimsIdentity);
}
return claimsPrincipal;
}
private async Task<ClaimsIdentity> MapClaims()
{
var externalIds = new[]
{
// Extensions from Microsoft.Identity.Web
_claimsPrincipal.GetHomeObjectId(),
_claimsPrincipal.GetObjectId()
}
.Distinct()
.Where(id => !string.IsNullOrWhiteSpace(id))
.ToArray();
// Replace with implementation specific to your use case for mapping session/external
// id to authenticated internal user id.
var userId = await _externalUsers.MapExternalIdAsync(externalIds);
var claimsIdentity = new ClaimsIdentity();
AddUserIdClaim(claimsIdentity, userId);
// Add other claims as needed
return claimsIdentity;
}
private void AddUserIdClaim(ClaimsIdentity claimsIdentity, Guid? userId)
{
claimsIdentity.AddClaim(new Claim(ApplicationClaimTypes.UserId, userId.ToString()));
}
}
Once you have your internal user id stored in the User object as a claim it is accessible to SignalR's GetUserId() method without the need for async code.
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
internal class SignalrUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
var httpContext = connection.GetHttpContext();
// If you have a multi-tenant application, you may have an
// extension method like this to get the current tenant the user
// is in. Otherwise just remove it.
var tenantIdentifier = httpContext.GetTenantIdentifier();
var userId = connection.User.FindFirstValue(ApplicationClaimTypes.UserId);
if (string.IsNullOrWhiteSpace(userId))
{
return string.Empty;
}
return $"{tenantIdentifier}-{userId}";
}
}
Related
I'm using windows authentication and IClaimsTransformation in my Blazor ServerApp (.NET 6.0)
I don't know why the ClaimsTransformerService runs 4 times per user login or per page refresh. Is there any way to prevent this?
The following code basically tries to check if a username is available in a database and if it is, record the login event in another table.
namespace MyServerApp.Services
{
public class ClaimsTransformerService : IClaimsTransformation
{
private IUserService _userService;
private ILoginRecordService _loginRecordService;
public ClaimsTransformerService(IUserService userService, ILoginRecordService loginRecordService)
{
_userService = userService;
_loginRecordService = loginRecordService;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var user = await _userService.GetUser(principal.Identity.Name.ToLower());
((ClaimsIdentity)principal.Identity).AddClaim(new Claim(ClaimTypes.Role, "MyUser"));
return principal;
}
}
}
I've read this thread and it didn't help.
The Microsoft documentation states that the TransformAsync() method may get called multiple times. From the docs:
The IClaimsTransformation interface can be used to add extra claims to the ClaimsPrincipal class. The interface requires a single method TransformAsync. This method might get called multiple times. Only add a new claim if it does not already exist in the ClaimsPrincipal. A ClaimsIdentity is created to add the new claims and this can be added to the ClaimsPrincipal.
So you should write your AsyncMethod defensively and only add claims if they do not already exist:
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if (!principal.HasClaim(claim => claim.Type == ClaimTypes.Role && claim.Value == "MyUser"))
{
var user = await _userService.GetUser(principal.Identity.Name.ToLower());
((ClaimsIdentity)principal.Identity).AddClaim(new Claim(ClaimTypes.Role, "MyUser"));
}
return principal;
}
Using IdentityServer 4 (4.1.2), I've added a class implementing the IProfileService interface. The method GetProfileDataAsync is supposed to be called several times (for each token), but my method is only called for the Access Token (ClaimsProviderAccessToken).
public class LocalUserProfileService : IProfileService
{
private readonly IIdentityProviderUserService _identityProviderUserService;
public LocalUserProfileService(IIdentityProviderUserService identityProviderUserService)
{
_identityProviderUserService = identityProviderUserService ??
throw new ArgumentNullException(nameof(identityProviderUserService));
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var subjectId = context.Subject.GetSubjectId();
var claims = (await _identityProviderUserService.GetUserClaimsBySubjectAsync(subjectId)).ToList();
Debug.WriteLine($"Adding claims to {context.Caller}");
context.IssuedClaims.AddRange(claims);
}
public async Task IsActiveAsync(IsActiveContext context)
{
var subjectId = context.Subject.GetSubjectId();
context.IsActive = await _identityProviderUserService.IsUserActiveAsync(subjectId);
}
}
I could manage to only use my access token to get the custom claims, but I would like to know why the code is not called for the identity_token as I prefer to have the claims in both tokens.
Have you set the AlwaysIncludeUserClaimsInIdToken flag to true? Otherwise the claims for the ID-token will be provided through the UserInfo endpoint instead.
A common way to reduce the size of the ID-token is to not include all the claims in the ID-token. Large tokens will also result in large session cookies. By default, the tokens are stored inside the session cookie.
I want to access the token informations like claims of stored token in a service. I have tried injecting AuthorizationHandlerContext in my service. But the api can't to resolve the AuthorizationHandlerContext and throws exception. Is there any other way to access the token information inside a service?
You can inject IHttpContextAccessor to your service then get all user claims or user information from JWT like this.
Register IHttpContextAccessor into DI
services.AddHttpContextAccessor();
private readonly IHttpContextAccessor _httpContextAccessor;
public YourService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
var username =_httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
Try this code on OnAuthorization(IAuthorizationFilter) action:
string email=context.HttpContext.User.Claims.FirstOrDefault(c => c.Type == "email").Value;
Since you want to access the JWT token informations, A more structured approach will be,
Step 1. Create a Model
public class AuthorizedUser
{
public ClaimsPrincipal Claims { get; set; }
}
Step 2. Use this model inside your authorized Controller method. like:
[Authorized]
[HttpPost]
public async Task<IActionResult> Modify([FromForm] RequestDto dto)
{
var user = new AuthorizedUser()
{
Claims = User
};
return Ok(await _service.MakeChanges(user, dto));
}
Step 3. Create an extension function for ease of access
public static string GetUserId(this ClaimsPrincipal claims)
{
return claims.Claims.Where(c => c.Type == "sub")
.Select(c => c.Value).SingleOrDefault();
}
Step 4. Simply access that users claims and other JWT properties like,
public Task<object> MakeChanges(AuthorizedUser user, RequestDto dto){
var userId = user.Claims.GetUserId();
}
That's it.
I have created multiple claims that sit in the AspNetUserClaims table for identity and have assigned them to my user id.
I am currently trying to get these to pull through in the list of claims I receive in my client application.
I have managed to pull through all the roles from the AspNetUserRoles table by adding the 'roles' scope to my client identity settings and then also in identity configuration (using the EF database format a.k.a ConfigurationDbContext) created a record in the IdentityResources table which links to an identity claim called 'role'.
This is working as expected. However, I am not getting any of my UserClaims I have created through, do I need to create another specific scope?
Here is my client configuration:
services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("cookie")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:44335/";
options.ClientId = "openIdConnectClient";
options.SignInScheme = "cookie";
options.ResponseType = "id_token";
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid profile roles all_claims");
});
services.AddAuthorization();
this is how I'm checking what claims the user has:
var claims = ((ClaimsIdentity)User.Identity).Claims;
and it returns all roles and profile claims (e.g. preferred_username) just not those specified within the AspNetUserClaims table.
For my client I have also set the property [AlwaysIncludeUserClaimsInIdToken] to true with no luck.
Does anyone know what I'm missing to pass through the user claims?
Do you have a IProfileService implementation to populate your custom claims?
You should implemet IProfileService as indicated in this answer.
Try other response_type than id_token since your application does not have an access token to call User Info endpoint. Maybe with id_token token to maintain the implicit flow grant of your client.
you can get the user claims like this:
var claims = User.Claims.Select(c => new { c.Type, c.Value });
you can implement this as an endpoint in your api which you stated as scope in your identity server:
using IdentityServer4;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
namespace IdentityServer4Demo.Api
{
[Route("/api/test")]
[Authorize]
public class TestController : ControllerBase
{
public IActionResult Get()
{
var claims = User.Claims.Select(c => new { c.Type, c.Value });
return new JsonResult(claims);
}
}
}
If you want to add more claim you need to add property to the class which implements IdentityUser and use it in your custom profile service
using Microsoft.AspNetCore.Identity;
namespace AuthServer.Infrastructure.Data.Identity
{
public class AppUser : IdentityUser
{
// Add additional profile data for application users by adding properties to this class
public string Name { get; set; }
}
}
your custom profile service:
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AuthServer.Infrastructure.Constants;
using AuthServer.Infrastructure.Data.Identity;
using IdentityModel;
using IdentityServer4;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Identity;
namespace AuthServer.Infrastructure.Services
{
public class IdentityClaimsProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<AppUser> _claimsFactory;
private readonly UserManager<AppUser> _userManager;
public IdentityClaimsProfileService(UserManager<AppUser> userManager, IUserClaimsPrincipalFactory<AppUser> claimsFactory)
{
_userManager = userManager;
_claimsFactory = claimsFactory;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
var principal = await _claimsFactory.CreateAsync(user);
var claims = principal.Claims.ToList();
claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
claims.Add(new Claim(JwtClaimTypes.GivenName, user.Name));
claims.Add(new Claim(IdentityServerConstants.StandardScopes.Email, user.Email));
// note: to dynamically add roles (ie. for users other than consumers - simply look them up by sub id
claims.Add(new Claim(ClaimTypes.Role, Roles.Consumer)); // need this for role-based authorization - https://stackoverflow.com/questions/40844310/role-based-authorization-with-identityserver4
context.IssuedClaims = claims;
}
}
I am developing an intranet asp.net core web api application. The requirements for authentications are:
REQ1 - when user which is trying to access the website is not in Active Directory's special group (let's name it "commonUsers") it is simply not authorized
REQ2 - when user which is trying to access the website is in Active Directory's group "commonUsers" is is authorized and a web resource is returned
REQ3 - when user which is trying to access the website is in Active Directory's group "superUser", it need to be prompted for his domain password once again (because it tries to access some very restricted resources)
Now, what I have so far:
My service is hosted using http.sys server in order to support windows authentication.
I am using claims transformer middlewere in order to check the user's Active Directory group, let's say something like this:
public class ClaimsTransformer : IClaimsTransformation {
private readonly IAuthorizationService _authorizationService;
public ClaimsTransformer(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
_authorizationService.Authorize(principal as IHmiClaimsPrincipal);
return Task.FromResult(principal);
}}
I have specified a special policies also in my service configuration, for instance something like that:
services.AddAuthorization(options =>
{
options.AddPolicy("TestPolicy", policy =>
policy.RequireClaim(ClaimTypes.Role, "TestUser"));
options.AddPolicy("TestPolicy2", policy =>
policy.RequireClaim(ClaimTypes.Role, "SuperUser"));
});
I am using [Authorize] attribute with specific policy in order to restrict access to specific resources based on policies
Now the question is, how should I satisfy REQ3?
I think I would try to use MVC Filters : https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#authorization-filters
Filters run after all Middleware, but before the Action. This will allow you to control the redirect to credentials page just for specific actions or controllers. Whilst normally this is not the recommended method for authorization, I think it fits your requirements for a hybrid secondary authentication.
public class SuperUserFilter : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.Request.Cookies.TryGetValue("SuperUserCookie", out string cookieVal))
{
if (!IsValidCookie(cookieVal))
context.Result = LoginPage(context);
}
else
{
context.Result = LoginPage(context);
}
}
private bool IsValidCookie(string cookieVal)
{
//validate cookie value somehow
// crytpographic hash, store value in session, whatever
return true;
}
private ActionResult LoginPage(AuthorizationFilterContext context)
{
return new RedirectToActionResult("SuperUser", "Login",
new {redirectUrl = context.HttpContext.Request.GetEncodedUrl()});
}
}
Then you create a Login Controller
public class LoginController : Controller
{
[HttpGet]
public IActionResult SuperUser(string redirectUrl)
{
// return a page to enter credentials
// Include redirectUrl as field
}
[HttpPost]
public IActionResult SuperUser(LoginData loginData)
{
// Validate User & Password
Response.Cookies.Append("SuperUserCookie", "SomeValue");
return Redirect(loginData.RedirectUrl);
}
}
Then you can decorate specific actions (or controllers) as required:
public class MyController : Controller
{
[HttpGet]
[SuperUserFilter]
public IActionResult MySensitiveAction()
{
// Do something sensitive
}
}
I'm guessing you are try to implement two step authentication for some of your resource.
To do that you must use multiple authentication scheme and Authorize policies,
but it's difficult because windows authentication is not controllable. we need to use some trick to know this is your second login.
authentication
The Default Authenticaiton Scheme : Windows, it's the basic scheme for authenticate a windows user.
Second Cookies base Authentication scheme : SuperUserTwoStep. we need this to goto our custom login logic.
Authorize
the Authorize policies for specified scheme.
a login page for login to SuperUserTwoStep scheme.
//startup
services.AddAuthentication(HttpSysDefaults.AuthenticationScheme)
.AddCookie("SuperUserTwoStep",op=>op.LoginPath = "/account/superuser2steplogin");
services.AddAuthorization(op =>
{
op.AddPolicy("SuperUser", b => b.AddAuthenticationSchemes("SuperUserTwoStep")
.RequireAuthenticatedUser()
.RequireClaim(ClaimTypes.Role, "SuperUser"));
});
// login
public static IDictionary<string, string> States { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
[Route("/account/superuser2steplogin")]
public async Task<IActionResult> LoginTwoStepConfirm(string returnUrl, [FromServices]IAuthorizationService authorizationService,
[FromServices]IAuthorizationPolicyProvider policyProvider)
{
var winresult = await HttpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme);
if (winresult.Succeeded)
{
if (States.TryGetValue(winresult.Principal.Identity.Name, out _))
{
States.Remove(winresult.Principal.Identity.Name);
var principal = new System.Security.Claims.ClaimsPrincipal(new System.Security.Claims.ClaimsIdentity(winresult.Principal.Claims,"twostepcookie"));
await HttpContext.SignInAsync("SuperUserTwoStep", principal);
return Redirect(returnUrl);
}
else
{
States[winresult.Principal.Identity.Name] = "1";
return Challenge(IISDefaults.AuthenticationScheme);
}
}
else
{
return Challenge(IISDefaults.AuthenticationScheme);
}
}
[Authorize("SuperUser")]
public IActionResult YourSecurePage()
{
return Content("hello world");
}
the most difficult thing is to track that this is the second time to login, I try to use cookie , but it doen't work, so I crate a static IDitionary<string,string> to track ,maybe use distributed cache is better
I think in my opinion you should consider using: Policy-based authorization with Requirements, basically you have different authorization requirements that you want to treat them on and AND basis
REQ1 and REQ2 and REQ3
Here you have the link to the documentation: Requirements
But you need to understand that identity != permissions, the guys that introduce this concept of policies to Microsoft created a project named: PolicyServer and it is opensource: PolicyServer Git and they created a pattern there of how you should use your policies. Basically, you have external and internal users that are authenticated against your AD, all internal users should have permissions assigned to a role. And you only decorate your controller action with the permission rule you created for that policy
[Authorize("PerformSurgery")]
public async Task<IActionResult> PerformSurgery()
{
// omitted
}
To understand the code and how they evaluate a policy, I think you should see the video they have online on the website: Policy Server
Hope this helps