In my project, I am using token-based authentication and after a successful login, I store some user-specific values in user's token and to do this I have used Claims.
Below is the code I am using to store claims after login:
User user = new UserManager().GetUser(UserName, Password);
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, user.FullName),
new Claim(ClaimTypes.Email, user.Email),
new Claim("IsLocked", Convert.ToString(user.IsLocked))
};
AuthenticationProperties properties = CreateProperties(context.UserName);
ClaimsIdentity oAuthIdentity = new ClaimsIdentity(claims, Startup.OAuthOptions.AuthenticationType);
var ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
As you can see in the above code that I have a claim to store IsLocked value of the user. As per the requirement, I need to prevent access of each API action from users whose account is locked. To do so, I have created a custom action filter and inside that, I use the value of IsLocked claim and thus prevent actions from being executed if user's claim value says that the user account is locked.
Below is the code of my custom action filter:
public class AllowActiveUsersAttribute : ActionFilterAttribute
{
public AllowActiveUsersAttribute()
{
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
var identity = (ClaimsPrincipal)Thread.CurrentPrincipal;
if (Convert.ToBoolean(identity.Claims.Where(c => c.Type == "IsLocked").Select(c => c.Value).SingleOrDefault()))
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
}
}
}
And then I use this custom Attribute on all Web API actions, like:
[AllowActiveUsers]
public async Task<IHttpActionResult> GetAccountDetails()
This code works perfectly and I get Unauthorized error when I log in with a locked account and then try to use any API endpoint.
In our system, we have some rules and breaking those rules can lock the users' account. When the account gets locked then the user shouldn't be able to access any API endpoint. So after the successful login (with an account that is not locked), if a user breaks any rule then his/her account should get locked immediately and after that he/she must not be able to use any API endpoints.
To do this, I added code to update the value of IsLocked claim and it successfully updates the claim value. But when I try to get the value of IsLocked claim in the custom action then I get the same old value instead of the new return value. Below is the code that I am using to update the claim value.
// check for the existing claim and remove it
var user = User as ClaimsPrincipal;
var identity = user.Identity as ClaimsIdentity;
var claim = (from c in user.Claims where c.Type == "IsLocked" select c).FirstOrDefault();
if (claim != null)
identity.RemoveClaim(claim);
// add new claim
identity.AddClaim(new Claim("IsLocked", Convert.ToString(true)));
var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identity), new AuthenticationProperties() { IsPersistent = true });
Can you please suggest how I can get the new value in the custom attribute so that if a user's account gets locked then from next requests, none of the API requests should be entertained?
That's cause you are still using the old/existing token which still don't have the updated value of IsLocked and thus the scenario. To resolve this, either the user has to start afresh by means of generating a new token which would have the updated value. Essentially a new access token has to be generated.
See this issue link for more understanding https://github.com/IdentityServer/IdentityServer3/issues/2783
Related
Tl;dr: How can I prevent custom claims from appearing in the access token and only be returned when you call the userinfo endpoint?
I have an IdentityServer4 (using authorization_code grant type) where, on successful 3rd party authentication, I get some additional information on the user. I store these as claims, sign in the user to the IS4 service and adding those claims to the user. The claims are hooked up to an identity resource scope called "custom"
var myUser = some3rdParty.GetUserData();
Claim[] claims = {
new Claim("custom.value1", myUser.Custom1),
new Claim("custom.value2", myUser.Custom2)
};
await _events.RaiseAsync(new UserLoginSuccessEvent("myOidcProviderIS4", myUser.Id, myUser.Id, myUser.Fullname));
await _httpContextAccessor.HttpContext.SignInAsync(
myUser.Id,
myUser.Fullname,
new AuthenticationProperties(),
claims);
Now, when I retrieve an access token, it does not contain these custom claims, but it contains the "custom" scope. The next step would be to call the userinfo endpoint to get these custom claims. However, by default, calls to the userinfo endpoint, does not include these custom claims.
I therefore created an IProfileService to copy the claims into IssuedClaims:
public class ProfileService : IProfileService
{
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
List<Claim> claims = new List<Claim>();
foreach (IdentityResource identityResource in context.RequestedResources.IdentityResources)
{
foreach (string userClaim in identityResource.UserClaims)
{
var claim = context.Subject.Claims.FirstOrDefault(c => c.Type == userClaim);
if (claim != null)
{
claims.Add(claim);
}
}
}
if (claims.Any())
{
context.IssuedClaims.AddRange(claims);
}
return Task.CompletedTask;
}
}
Now when I call the userinfo endpoint, my custom claims are visible.
But after inspecting the access token I could see that my custom claims are visible there as well. I feel that this is a bit redundant as you (in this case) don't really have to make a call to userinfo.
I made the following observations if I do not have my custom IProfileService implementation (The class is still there, but with just an empty method):
context.Subject.Claims contains the custom claims when calling the token endpoint, but it does not contain the custom claims when called from userinfo.
The PersistedGrants table in the database has a Data column which also contains the custom tokens.
So the question still is: How can I prevent my custom claims from appearing in the access token and only be returned when you call the userinfo endpoint?
I think this is probably a configuration issue.
Client has to be defined with 2 allowed scopes:
IdentityServerConstants.StandardScopes.OpenId
"custom"
new Client
{
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
"custom"
},
// etc...
And then on the client configure ResponseType = OpenIdConnectResponseType.IdTokenToken;:
.AddOpenIdConnect(o =>
{
o.Scope.Clear();
o.Scope.Add("openid");
o.Scope.Add("custom");
o.ResponseType = OpenIdConnectResponseType.IdTokenToken;
// etc...
You can check if the caller is for /userinfo (I assume this is what the original intention was behind including that property):
if (context.Caller == IdentityServerConstants.ProfileDataCallers.UserInfoEndpoint)
{
//custom logic to add requested claims
context.AddRequestedClaims(claims);
}
I think you should check ApiResource configuration. Whatever claims you add in UserClaims property of ApiResource configuration, those claims will appear in access token. e.g
public IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1")
{
UserClaims = new[] { "CustomClaim1", "CustomClaim2"},
},
}
}
In above code, access code will contain CustomClaim1 and CustomClaim2.
Hence if you don't mention them, they won't appear in access token.
I have a default ASP.NET Core website created within Visual Studio 2017. I have chosen to authenticate using an Azure Active Directory.
I run the site and can successfully login using an account in the Active Directory.
I can retrieve Claim information provided by Active Directory, e.g. by calling the following line I get the 'name'.
User.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
I want to add a custom claim - CompanyId = 123456 for the logged in user.
I'm able to add a custom claim however it is only available on the page where the claim is set.
Claim claim = new Claim("CompanyId", "123456", ClaimValueTypes.String);
((ClaimsIdentity)User.Identity).AddClaim(claim);
My understanding is that I somehow need to update the token that has been issued by Active Directory or set the claim before the token is issued. I'm unsure how to do this.
I suspect this needs to be done in the AccountController at SignIn()
// GET: /Account/SignIn
[HttpGet]
public IActionResult SignIn()
{
return Challenge(
new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme);
}
I've read numerous articles and samples about this scenario (including https://github.com/ahelland/AADGuide-CodeSamples/tree/master/ClaimsWebApp) however have not managed to solve how to persist the Claim across requests.
I have successfully managed to persist custom Claims using ASP.NET Identity as the Authentication Provider, but this appears to be because the custom Claim is saved to the database..
OnTokenValidated offers you the chance to modify the ClaimsIdentity obtained from the incoming token , code below is for your reference :
private Task TokenValidated(TokenValidatedContext context)
{
Claim claim = new Claim("CompanyId", "123456", ClaimValueTypes.String);
(context.Ticket.Principal.Identity as ClaimsIdentity).AddClaim(claim);
return Task.FromResult(0);
}
Setting the OpenIdConnectEvents:
Events = new OpenIdConnectEvents
{
OnRemoteFailure = OnAuthenticationFailed,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
OnTokenValidated = TokenValidated
}
Then in controller using :
var companyId= User.Claims.FirstOrDefault(c => c.Type == "CompanyId")?.Value;
For those who would like more detail, the code provided is placed in Startup.cs
In the Configure method add/edit:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["Authentication:AzureAd:ClientId"],
Authority = Configuration["Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"],
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
Events = new OpenIdConnectEvents
{
OnTokenValidated = TokenValidated
}
});
The private Task TokenValidated method is in the body of Startup.cs
The following sample is a good reference.
https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore-v2/blob/master/WebApp-OpenIDConnect-DotNet/Startup.cs
In my ASP.NET MVC 5 application I need to use custom Authentication. Basically a custom library on which I call a method and which returns an object that contains information about the user.
I've created a new MVC 5 application and selected the "No Authentication" option. Then I've added an Http Module which currently looks like this:
private void Context_AuthenticateRequest(object sender, EventArgs e)
{
// Make the call to authenticate.
// This returns an object with user information.
AuthResult result = new AuthLib().SignOn();
// Inspect the returned object and create a list claims.
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, result.Username),
new Claim(ClaimTypes.GivenName, result.Name)
}
claims.AddRange(result.Groups.Select(g => new Claim(ClaimType.Role, g));
// Create principal and attach to context
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "Sso");
HttpContext.Current.User = principal;
Thread.CurrentPrincipal = principal;
}
private void Context_PostAuthenticateRequest(object sender, EventArgs e)
{
var principal = ClaimsPrincipal.Current;
ClaimsAuthenticationManager transformer = FederatedAuthentication.SessionAuthenticationModule.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager;
transformer.Authenticate(string.Empty, principal);
}
My claimstransformer looks like this:
public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
{
if (!incomingPrincipal.Identity.IsAuthenticated)
{
return base.Authenticate(resourceName, incomingPrincipal);
}
ClaimsPrincipal newPrincipal = CreateApplicationPrincipal(incomingPrincipal);
EstablishSession(newPrincipal);
return newPrincipal;
}
private void EstablishSession(ClaimsPrincipal newPrincipal)
{
var sessionToken = new SessionSecurityToken(newPrincipal, TimeSpan.FromHours(8));
FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(sessionToken);
}
private ClaimsPrincipal CreateApplicationPrincipal(ClaimsPrincipal incomingPrincipal)
{
// Convert AD group to known role in our application.
string group = incomingPrincipal.FindFirst(ClaimTypes.Role).Value;
string role = new ADGroupToRoleConverter().ConvertADGroupToRole(group);
// Add claims for group.
// These would be loaded from a db.
List<Claim> claims = new ClaimDb().GetClaimsForRole(role);
// Just copy the claims for id and given name.
claims.Add(incomingPrincipal.FindFirst(ClaimTypes.NameIdentifier));
claims.Add(incomingPrincipal.FindFirst(ClaimTypes.GivenName));
return new ClaimsPrincipal(new ClaimsIdentity(claims, "MyApp"));
}
The main issue that I'm facing is that the authentication step is called for every request even though a session exists. How can I detect that a session exists and just load the session instead of going through the entire authentication process.
Another issue is that the call to the authentication library might take a while. I guess ideally it should also be moved to the claims transformer?
Any ideas to improve this code further are also very much appreciated.
Please let me know if something is not clear or if I need to provide more detailed information.
It seems to me that you do not provide authentication information with each request after the authentication. Can you verify that you have some session cookie or authentication header sent with each request after the authentication happens?
I have a custom attribute where I manually wanna check if a claims token is valid. How do I do that?
public class AuthorizeClaimsAttribute : AuthorizeAttribute {
protected override bool UserAuthorized(IPrincipal user) {
var cookie = HttpContext.Current.Request.Cookies.Get("bearerToken");
if (cookie != null) {
//Check if token is valid, how?
}
return false;
}
}
The token is created as follow:
var identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);
identity.AddClaim(new Claim("Username", model.Username));
identity.AddClaim(new Claim("IsAdmin", isAdmin.ToString()));
var properties = new AuthenticationProperties() {
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.Add(Startup.OAuthOptions.AccessTokenExpireTimeSpan)
};
var ticket = new AuthenticationTicket(identity, properties);
var accessToken = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);
Note: I cannot use the existing Authorize attribute. That´s why I need to check it manually.
One method is to store the token alongside its username somewhere in a persistant data structure
For example, when you create the Identity store the model.UserName & the accessToken in a a database of your choice.
Then, when you want to check your cookie you can re-open your database and query for it and take the appropriate action.
Also, adding that date in the database will also help you keep the size of it down resulting in faster searches, i.e. if your token only lasts for 3 months, delete the old ones as part of maintenance
How to use claims? For example, I want to set access to each page (resource) for each user. I understand, I can do it using roles, but as I understand, claim-based is more effectively. But when I try to create a claim, I see the following method:
userIdentity.AddClaim(new Claim(ClaimTypes.Role, "test role"));
first parameter of constructor of Claim class get ClaimTypes enum, which has many "strange" members like Email, Phone etc. I want to set that this claim and then check this claim to have access to certain resource. I'm on wrong way? How to do it?
From the code above, I am assuming you have already added the claim in startup class on authenticated of your provider as below.
context.Identity.AddClaim(new Claim("urn:google:name", context.Identity.FindFirstValue(ClaimTypes.Name))); // added claim for reading google name
context.Identity.AddClaim(new Claim("urn:google:email", context.Identity.FindFirstValue(ClaimTypes.Email))); // and email too
Once you have added the claims in startup, when the request is actually processed check if its a callback and if yes, read the claims as below(in IHttpHandler).
public void ProcessRequest(HttpContext context)
{
IAuthenticationManager authManager = context.GetOwinContext().Authentication;
if (string.IsNullOrEmpty(context.Request.QueryString[CallBackKey]))
{
string providerName = context.Request.QueryString["provider"] ?? "Google";//I have multiple providers so checking if its google
RedirectToProvider(context, authManager, providerName);
}
else
{
ExternalLoginCallback(context, authManager);
}
}
If its 1st call redirect to provider
private static void RedirectToProvider(HttpContext context, IAuthenticationManager authManager, string providerName)
{
var loginProviders = authManager.GetExternalAuthenticationTypes();
var LoginProvider = loginProviders.Single(x => x.Caption == providerName);
var properties = new AuthenticationProperties()
{
RedirectUri = String.Format("{0}&{1}=true", context.Request.Url, CallBackKey)
};
//string[] authTypes = { LoginProvider.AuthenticationType, DefaultAuthenticationTypes.ExternalCookie };
authManager.Challenge(properties, LoginProvider.AuthenticationType);
//without this it redirect to forms login page
context.Response.SuppressFormsAuthenticationRedirect = true;
}
And finally read the claims you get back
public void ExternalLoginCallback(HttpContext context, IAuthenticationManager authManager)
{
var loginInfo = authManager.GetExternalLoginInfo();
if (loginInfo == null)
{
throw new System.Security.SecurityException("Failed to login");
}
var LoginProvider = loginInfo.Login.LoginProvider;
var ExternalLoginConfirmation = loginInfo.DefaultUserName;
var externalIdentity = authManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
var emailClaim = externalIdentity.Result.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email);
var email = emailClaim.Value;
var pictureClaim = externalIdentity.Result.Claims.FirstOrDefault(c => c.Type.Equals("picture"));
var pictureUrl = pictureClaim.Value;
LogInByEmail(context, email, LoginProvider); //redirects to my method of adding claimed user as logged in, you will use yours.
}
Claim doesn't set permission. It's used to verify you that "you are who you claim to be you are". These claims are identified by issuer, usually a 3rd party. See for example this article for description.
So, you should define which claims are necessary (who user should be) in order to access a certain page. Otherwise, using claim-based authorization will be same as using identity based or role based.