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.
Related
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 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
}
};
}
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
I am in the process of creating my own webapi in asp.net using oauth as authorization provider.
The api wil basically serve as a provider for different modules as i call them. One could be a image gallery, the other could just be a user login module with different types of users.
I have the oauth part working fine. Api users can register and then ask for a Token by calling the /Token endpoint with the login credentials.
However i now want to create another seperate user module in the api that is only accessible by apiusers that registered . I want this module to have another register and login function and have their own endpoint to login (/UserModuleToken or something like that). The users coming from the user module are different users than the Api users. So the apiusers are the actual developers that want to call specific modules in my api, and the users from the user module are users that register on the site where that module is implemented.
All of my apicontrollers wil have the [Authorize] attribute for the api user, and i want specific ones, for example some function in the user module, to be decorated with [UserModuleAuthorize] attribute.
Below you can see my api user entity model:
public class ApiUserEntity : BaseEntity
{
public string Username { get; set; }
public string Password { get; set; }
public string Email { get; set; }
public string Salt { get; set; }
public ApiUserLevel Level { get; set; }
}
The userservice function that can validate an api user:
public UserLoginResult LoginUser(ApiUserEntityLoginForm userForm)
{
// retrieve user from database
var user = _userRepository.GetUser(userForm.UserName);
if(user == null)
return _modelStateWrapper.AddError(UserLoginResult.UserNotFound, "User does not exist");
var passwordHash = PasswordHash.HashPassword(user.Salt, userForm.Password);
// check if password matches with database.
if (passwordHash != user.Password)
return _modelStateWrapper.AddError(UserLoginResult.IncorrectPassword, "Incorrect password");
return UserLoginResult.Success;
}
And calling the /Token endpoint in my webapi will call the following function of the token provider:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
// create a userloginform object :
var loginForm = new ApiUserEntityLoginForm {UserName = context.UserName, Password = context.Password};
// pass it into the login validation function of the userservice:
var loginResult = _userService.LoginUser(loginForm);
// if login result was not sucesful, return an error.
if (loginResult != UserLoginResult.Success)
{
var jsonSerialiser = new JavaScriptSerializer();
var json = jsonSerialiser.Serialize(_userService.Errors());
context.SetError("invalid_grant", json);
return;
}
// result was succesful, grant the token.
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "user"));
context.Validated(identity);
}
i configure my oauth provider and define the /Token endpoint with the following function:
public static void ConfigureOAuth(IAppBuilder app, IUnityContainer container)
{
var simpleAuthorizationServerProvider = container.Resolve<SimpleAuthorizationServerProvider>();
var OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = simpleAuthorizationServerProvider
};
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
Now my question is if it is somehow possible to have multiple token endpoints so i can have a token for apiusers and then another one for a user that is using the custom user module and protect certain functionality based on those 2 users.
I couldnt find any information about this after an extensive amount of searching the internet. So im beginning to believe this is not good practice or not possible. If anyone would be able to point me in the right direction that would be great!
Well I believe you need to configure users authorization based on Roles, what you are trying to do is just complicating your solution.
What you can do is the following: inside method GrantResourceOwnerCredentials you need to obtain the correct role(s) for the authenticated user from the DB store i.e "Admin" and then add them as claims with type "Role" as the code below:
identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
identity.AddClaim(new Claim(ClaimTypes.Role, "Supervisor"));
Now on your controllers that you want just user with role "Admin" to access; you need to attribute with [Authorize(Roles="Admin")] or maybe multiple roles [Authorize(Roles="Admin,User")]
This is the straightest way to achive your goal.
Btw this code from http://bitoftech.net, right? Glad to see my code samples used :)
Let me know if you need further clarifications.
I created a custom principal class
public class FacebookPrincipal : ClaimsPrincipal
{
public JObject Data { get; set; }
}
And I want to use it. When the user logs in, I tried to set
var fbP = new FacebookPrincipal { Data = user.Data };
Thread.CurrentPrincipal = fbP;
AuthenticationManager.User = fbP;
HttpContext.User = fbP;
It works right after I set it, but when I go ho home/index the value is lost
var user = HttpContext.GetOwinContext().Authentication.User;
var bbbb = this.User;
var cccc = ClaimsPrincipal.Current;
All the above methods return a Principal of type ClaimsPrincipal and casting to FacebookPrincipal returns null.
How do I set a custom principal?
ASP.NET Identity uses default ClaimsIdentityFactory to create before assigning ClaimsIdentity to User and Thread. You should create your own ClaimsIdentityFactory where you can add or manage additional information.
UserManager<IdentityUser> userManager
= new UserManager<IdentityUser>(new UserStore<IdentityUser>());
userManager.ClaimsIdentityFactory
= new MyClaimsIdentityFactory<IdentityUser>();
And the following code to create your implementation for ClaimsIdentity or its subclass.
public class MyClaimsIdentityFactory<IUser> : ClaimsIdentityFactory<IUser> where IUser : IdentityUser
{
public MyClaimsIdentityFactory(): base()
{
}
public override System.Threading.Tasks.Task<System.Security.Claims.ClaimsIdentity> CreateAsync(UserManager<IUser> manager, IUser user, string authenticationType)
{
// Override Creation of ClaimsIdentity and return it.
}
}
Make sure you absolutely need to subclass ClaimsIdentity. You can add additional info as Claims.
You shall use base.CreateAsync and merge the Claims to your created ClaimsIdentity.
•Make sure you absolutely need to subclass ClaimsIdentity. You can add additional info as Claims.
You should be careful about adding additional claims for supplementary information as a side effect can be a change to how the authorization policy will make decisions.
As suggested by #marisks, you can use IUserClaimsStore to store claims issued from third-party for your user. only if the custom claims access is the problem.
Moreover, to persist the identity between two requests, use following code.
// One can create one's own Identity here.
var identity = await
UserManager.CreateIdentityAsync(user,
DefaultAuthenticationTypes.ApplicationCookie);
// Or add custom claims. Claims Stored in IUserClaimStore
// are already populated by above creation.
identity.AddClaim(new Claim("ProfileDATA", "VALUE"));
AuthenticationManager.SignIn(new AuthenticationProperties()
{ IsPersistent = isPersistent }, identity);