I have all my Active Directory users in a list and am iterating through the users to get their manager, using the below code.
foreach (User u in userResult)
{
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
var directoryObject = await graphClient2.Users[u.UserPrincipalName].Manager
.Request()
.GetAsync();
}
This works fine if the user has a manger, but if not, the code fails.
The only way i can currently see around this is to use a try catch, but that feels crude, and when iterating over a lot of users, its not very efficient.
is there a better way of doing this?
You can use OData query expand parameter here to query manager alone with users.
Query url looks like this:
https://graph.microsoft.com/v1.0/users?$expand=manager($select=id,displayName)
And when using Graph SDK in asp.net core, since you need to query all users, you should use client credential flow, and it should look like this, please note, if the use doesn't have an manager, the property Manager will be null:
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "your_tenant_name.onmicrosoft.com";
var clientId = "azure_ad_clientid";
var clientSecret = "client_secret";
var clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
var users = await graphClient.Users.Request().Expand("manager($select=id,displayName)").GetAsync();
Get the user, not the manager. The user has a manager field which can be checked for null.
You still need to do try/catch in the case that the user is not a part of the org anymore or if the server has an internal error. 4xx and 5xx error responses throw exceptions in .NET code.
Related
I am trying to implement policy-based authorization in my Web API. What I am trying to figure out is how to determine which claims should be added to the token when generating it for the user on his/her log-in operation. Should I store information about claims for each user in the database, or I am misunderstanding some concepts?
Here is the method I use to generate JWT/refresh-token pair:
public async Task<AuthenticationResponse> GenerateTokenPairForUserAsync(User user)
{
var jwtTokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_jwtConfig.Secret);
var guid = Guid.NewGuid().ToString();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(ClaimTypes.Sid, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(ClaimTypes.Role, user.RoleId.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, guid)
}),
Expires = DateTime.UtcNow.Add(_jwtConfig.TokenLifetime),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature)
};
var token = jwtTokenHandler.CreateToken(tokenDescriptor);
var jwtToken = jwtTokenHandler.WriteToken(token);
var refreshToken = new RefreshToken
{
JwtId = token.Id,
IsUsed = false,
IsRevoked = false,
UserId = user.Id,
CreationDate = DateTime.UtcNow,
ExpiryDate = DateTime.UtcNow.Add(_refreshTokenConfig.TokenLifetime),
Token = RandomString(25) + Guid.NewGuid()
};
await _refreshTokenRepository.CreateAsync(refreshToken);
return new AuthenticationResponse
{
Token = jwtToken,
Success = true,
RefreshToken = refreshToken.Token
};
}
You need a few things to achieve that:
You should have a mapping of which clients can receive which claims. Clients, meaning the apps which call your authorization endpoint. If you only have one, then this is not a problem, but if you have many, you should keep somewhere in a database a list of all the claims that the given client should receive in the token.
It's convenient to have a mapping of scopes to claims. In an authorization request you can then request scopes, which are essentially groups of claims. You will need that if your clients (or client) can actually request tokens with different scopes. E.g. you might want to request a token which can be used to perform some more sensitive operations, maybe change user's email. Then you can ask the server to issue the token maybe with a scope "admin", which translates to, among others, a claim can_change_email: true. This claim can then be used to perform authorization decisions.
Finally you need to know, for every claim, what is the source of data. So, once you know that your tokens must contain claims claim1, claim2 and claim3, then you must know where to take the data from. This can be hardcoded - e.g. you implement a getValueForClaim2() method which knows that it should read data from a database (e.g. it's a user's phone number). Or you can create some more sophisticated solutions, where you keep some mappings to claimProviders, then implement those providers. In the end, where do you get the data from it's totally up to you - this can be a database, a file, maybe an API call, or the value is calculated based on some input.
Have a look at these resources about claims that we wrote at Curity: https://curity.io/resources/claims/ if you want to learn more about this topic.
var authenticateResult = await HttpContext.AuthenticateAsync("External");
if (!authenticateResult.Succeeded) return BadRequest();
var email = authenticateResult.Principal.FindFirst(ClaimTypes.Email);
var id = _usersService.Create(email.Value);
var claimsIdentity = new ClaimsIdentity("Application");
claimsIdentity.AddClaim(new Claim("id", id.ToString()));
await HttpContext.SignInAsync("Application", new ClaimsPrincipal(claimsIdentity));
In controllers the following line throws Sequence contains no elements in controllers:
var idClaim = HttpContext.User?.Claims.Where(x => x.Type == "id").Single();
I call UseAuthentication and UseAuthorization in Startup and I thought that the first snippet would set cookies and then User would provide access to it on every client request but it doesn't work.
If I put a breakpoint right after SiginInAsync then HttpContext.User contains the expected claim but not in other calls.
HttpContext.Request.Cookies is empty. In browser I see several cookies among them .AspNetCore.External and .AspNetCore.Application plus some antiforgery cookies.
How do I achieve having claims in User for all requests after authentication was done?
Can it be the problem that my front-end runs on a separate port? Probably not.
You could follow the steps below to add updated claims to HttpContext.User:
https://stackoverflow.com/a/65319557/11398810
Then you can get claims in other request:
var idClaim = HttpContext.User?.Claims.Where(x => x.Type == "id").Single();
I'm building an app in ASP.NET Core 3.1 with Entity Framework Core 3.1 and am having some trouble debugging Authorization. Currently, I'm working on Claims-Based Authorization, as the code samples below will illustrate.
In the AdminController:
public async Task<IActionResult> EditUser(string id)
{
var user = await _userManager.FindByIdAsync(id);
if (user == null)
return RedirectToAction("UserManagement", _userManager.Users);
var claims = await _userManager.GetClaimsAsync(user);
var vm = new EditUserViewModel()
{
Id = user.Id,
UserClaims = claims.Select(c => c.Value).ToList(),
Email = user.Email,
UserName = user.UserName,
Birthdate = user.Birthdate,
City = user.City,
Country = user.Country
};
return View(vm);
}
This function is called when I click on the Edit button next to a user in the Admin section of the app. The problem I'm having lies with the claims. I have the ability to add claims to a user, but somewhere in the code it isn't working correctly. When debugging, everything seems to work, and the database table AspNetUserClaims does contain the correct data after adding the claim to the user, but the function above has a bug in it that I'll try to describe.
Basically, on the EditUser View page, there should be an HTML ul of all claims the current user has. Even after adding claims to the user, though, no ul of claims displays. When debugging, I found that the problem seems to be in the EditUser code above, because the var claims is set to Count=0 after calling GetClaimsAsync(user). This value of Count=0 is then passed to the EditUserViewModel and set to the List property UserClaims. But Count=0 isn't any legitimate value, and I'm not sure where GetClaimsAsync(user) is getting that Count=0 from. As I said, the database table seems to be storing the data correctly.
If you need any more code or further details, be sure to let me know so I can make things easier to understand.
I am trying to sign a user in, in a normal controller. I have something along the lines of:
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.Subject, "xxx-xxx-xx-xxx),
new Claim(JwtClaimTypes.PreferredUserName, "someusername"),
new Claim(JwtClaimTypes.Email, "foo#bar"),
new Claim(JwtClaimTypes.EmailVerified, "foo#bar:),
new Claim(JwtClaimTypes.IdentityProvider, "idsvr"),
};
var ci = new ClaimsIdentity(claims, "password", JwtClaimTypes.PreferredUserName, JwtClaimTypes.Role);
var cp = new ClaimsPrincipal(ci);
await HttpContext.Authentication.SignInAsync("myscheme", cp);
And I get an exception saying InvalidOperationException: name claim is missing
In the constructor of the ClaimsIdentity I specified my name claim to be JwtClaimTypes.PreferredUsername (which is "preferred_username")
If I do add a name claim to my claims list, then sign in works fine with the above code snippet. However we actually don't have name claims in our system. We want to use the "preferred_username" as the name claim in the abstractions.
I am working on adding WIF support to my WCF Data Services / ODATA server, and the first thing I'd like to do is create a nUnit test which passes some sort of identity to said server. I believe this falls under the category of an active client: there's no UI; I want to make a call out to a app.config established provider (Google, Yahoo, Windows Live, or some other provider) to get my identity token. Frankly, it doesn't matter what, just that it's more-or-less always accessable and has no administration to get the test running. (If there's some host app that I can include in my solution to act as an IP, I'd be perfectly happy with that.)
All of my existing tests use HttpRequest directly -- I am not using a generated client. While I'm creating my HttpRequest object, I check to see if I already have an authentication token to put in my headers. If not, I am trying something like this:
using (WSTrustChannelFactory factory = new WSTrustChannelFactory(
new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
new EndpointAddress(new Uri("https://dev.login.live.com/wstlogin.srf"))))
{
factory.Credentials.UserName.UserName = "MYUSERNAME";
factory.Credentials.UserName.Password = "MYPASSWORD";
factory.TrustVersion = TrustVersion.WSTrust13;
WSTrustChannel channel = null;
try
{
var rst = new RequestSecurityToken
{
RequestType = WSTrust13Constants.RequestTypes.Issue,
AppliesTo = new EndpointAddress("http://localhost:60711/Service"),
KeyType = WSTrust13Constants.KeyTypes.Bearer,
};
channel = (WSTrustChannel)factory.CreateChannel();
return channel.Issue(rst);
}
finally
{
if (null != channel)
{
channel.Abort();
}
factory.Abort();
}
}
So to start... I don't even know if I'm aiming at the right URI for the IP, but when I changed it, I got a 404, so I figure maybe I'm on the right track there. At the moment, the channel.Issue method returns a MessageSecurityException with an inner exception of type FaultException, noting "Invalid Request". The FaultException has a Code with Name=Sender and Namespace=http://www.w3.org/2003/05/soap-envelope, which then has a SubCode with Name=InvalidRequest and Namespace=http://schemas.xmlsoap.org/ws/2005/02/trust. I don't know what to do with this information. :)
My apologies if I'm asking a very basic question. I've been looking at authentication for only a couple of days, and don't know my way around yet. Thanks for any help!
EDIT -- SOLUTION
Eugenio is right -- I am doing something a little heavyweight, and it is more of integration testing stuff. I ditched the Google/Yahoo/Live stuff, and found a modified version of SelfSTS, which I cobbled into my project. I don't fully understand what's going on just yet, but I got back a SAML token. Here is final code:
var binding = new WS2007HttpBinding();
binding.Security.Mode = SecurityMode.Message;
binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
binding.Security.Message.EstablishSecurityContext = false;
binding.Security.Message.NegotiateServiceCredential = true;
using (var trustChannelFactory = new WSTrustChannelFactory(binding, new EndpointAddress(new Uri("http://localhost:8099/STS/Username"), new DnsEndpointIdentity("adventureWorks"), new AddressHeaderCollection())))
{
trustChannelFactory.Credentials.UserName.UserName = MYUSERNAME;
trustChannelFactory.Credentials.UserName.Password = MYPASSWORD;
trustChannelFactory.Credentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None;
trustChannelFactory.TrustVersion = TrustVersion.WSTrust13;
WSTrustChannel channel = null;
try
{
var rst = new RequestSecurityToken(WSTrust13Constants.RequestTypes.Issue)
{
AppliesTo = new EndpointAddress("http://localhost:60711/Service"),
KeyType = WSTrust13Constants.KeyTypes.Bearer
};
channel = (WSTrustChannel)trustChannelFactory.CreateChannel();
GenericXmlSecurityToken token = channel.Issue(rst) as GenericXmlSecurityToken;
((IChannel)channel).Close();
channel = null;
trustChannelFactory.Close();
string tokenString = token.TokenXml.OuterXml;
return tokenString;
}
finally
{
if (null != channel)
{
((IChannel)channel).Abort();
}
trustChannelFactory.Abort();
}
}
(This code is also lifted from the same place I got the modified SelfSTS -- thanks, Wade Wegner!)
I'm not sure about the use of "adventureWorks". The version of SelfSTS that I'm using names that as the issuername in the configuration, but I haven't checked to see if there's any correlation.
So... now to hack up my actual server so that it can figure out what to do with the SAML!
Not all of those IPs support active calls. Even if they do, the protocols might not be compatible.
For example, I'm not sure Google implements WS-Trust (what WIF is using under the hood). LiveID might have a WS-Trust endpoint somewhere, but not sure if it is officially supported/documented (for example, likely the error you are getting is because LiveID doesn't know about your RP: http:// localhost:60711/Service; and thus cannot issue a token for it).
For multiple IPs like these, apps typically embed a web browser and use it for all token negotiations (using WS-Federation for example). And often they rely on a specialized STS to deal with protocol transitions (e.g. Windows Azure Access Control Service)
In any case, what you are doing sounds a little bit heavyweight for a Unit test. It sounds more like an integration test you want to automate.
Maybe you could start with a custom (fake) STS of your own. In which you can control everything and simulate different outputs (e.g. different claims, etc.)
This chapter: http://msdn.microsoft.com/en-us/library/hh446528 (and the samples) can give you more information.