City and Country from Microsoft Graph equals null - c#

I am using microsoft graph to get user data from active directory. When I use sdk I got only basic informations about the users eg. "displayName", "mail", "userPrincipalName", "id". Everything else have null value. My app in azure has permissions to see informations. When I turned on all permissions on azure the result was the same. How can I get city and country informations?

This is expected behavior since Microsoft Graph API endpoint returns a default set of properties for a User resource.
To return an additional properties, they need to be explicitly requested via the $select query option. In case of msgraph-sdk-dotnet an additional properties could be specified like this:
var users = await graphClient.Users.Request().Select("companyName,city,country,contacts,contactFolders").GetAsync();
Another option would be to target Microsoft Graph beta endpoint. In case of endpoint https://graph.microsoft.com/beta/users, along with default properties, at least companyName,city,country will be included in the result.
The below snippet shows how to initializes msgraph-sdk-dotnet to target API beta version:
_graphClient = new GraphServiceClient("https://graph.microsoft.com/beta",
new DelegateAuthenticationProvider(async (requestMessage) =>
{
// Passing tenant ID to the sample auth provider to use as a cache key
string accessToken = await _authProvider.GetUserAccessTokenAsync(userId);
// Append the access token to the request
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}));

Related

Why doesn’t the "https://login.microsoftonline.com/common" AAD endpoint work, while the "https://login.microsoftonline.com/[tenant ID]" does?

I’m developing a UWP application that calls an API. The API is made of an Azure Function triggered by HTTP requests. I want the Azure Function to be secured through Azure Active Directory. To do so, I created two app registrations in AAD, one for the UWP and one for the API. Both support accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g., Skype, Xbox). The API app registration provides scope, and the UWP app registration uses that scope. The code I use on my UWP is:
var HttpClient _httpClient = new HttpClient();
const string clientId = "[UWP app registration’s client ID]";
const string authority = "https://login.microsoftonline.com/[Tenant ID of the UWP app registration]";
string[] scopes = { "api://[API app registration’s client ID]/[scope]" };
var app = PublicClientApplicationBuilder
.Create(clientId)
.WithAuthority(authority)
.WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient")
.Build();
AuthenticationResult result;
var accounts = await app.GetAccountsAsync();
try {
result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync();
}
catch (MsalUiRequiredException) {
try {
result = await app.AcquireTokenInteractive(scopes).ExecuteAsync();
}
catch (Exception exception) {
Console.WriteLine(exception);
throw;
}
}
if (result == null) return;
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
var response = _httpClient.GetAsync("[API URL]").Result;
This code works, but if I replace the authority with https://login.microsoftonline.com/common (as specified here), being my app registrations multi-tenant, I get a 401 response when calling the API _httpClient.GetAsync("[API URL]").Result. The docs say the code must be updated somehow when using the /common endpoint, but I don’t understand how I should edit it. I also tried to follow these tips, but without success, while these seem not to be related to my case since I’m not building an IWA. If I run the working version of the code, result is populated with an object whose TenantId property gets the right value of the tenant that owns the app registrations while using the not-working version of the code, result is populated with an object whose TenantId property gets a value I don’t know where it’s coming from.
Can anyone help me, please?
Here's my understanding of AAD multitenancy flow :
The common authority can't be used to get a token. It's used as a common endpoint to get the templated server metadata :
v1 : https://login.microsoftonline.com/common/.well-known/openid-configuration
v2 : https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
A token should be requested from the issuer where the client is defined.
But the common authority can be used in a multitenant API (eg your Azure Functions API) to verify that a client has a valid AAD token. From the documentation :
Because the /common endpoint doesn’t correspond to a tenant and isn’t an issuer, when you examine the issuer value in the metadata for /common it has a templated URL instead of an actual value : https://sts.windows.net/{tenantid}/
Therefore, a multi-tenant application can’t validate tokens just by matching the issuer value in the metadata with the issuer value in the token. A multi-tenant application needs logic to decide which issuer values are valid and which are not based on the tenant ID portion of the issuer value.

How to set custom claims to aad token using C# code

I have a webapi which generates aad token and I have written token generation logic in Get() method in webapi.
I'm able generate aad jwt token from webapi get() method but, now I want to include some custom claims into the token.
How can I set custom claims to aad token using c#.
I have used below code for generating aad token.
var authenticationContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext("https://login.windows.net/" + ConfigurationManager.AppSettings["TenantID"].ToString());
var credential = new ClientCredential(clientId: ConfigurationManager.AppSettings["ClientID"].ToString(), clientSecret: secret);
var result = await authenticationContext.AcquireTokenAsync(
ConfigurationManager.AppSettings["Resource"].ToString(),
credential
).ConfigureAwait(false);
Kindly share any sample c# code to set custom claims to aad token generated from above code .
Note: I want to set a new custom claim for aad token where custom claim value obtained from external logic.
Update 1:
Looks like below post may be useful.
https://www.rahulpnath.com/blog/azure-ad-custom-attributes-and-optional-claims-from-an-asp-dot-net-application/
I tried below following above post.
Generated jwt token to call Graph API. But I got blocked at below code.
var dictionary = new Dictionary<string, object>();
dictionary.Add(employeeCodePropertyName, employee.Code);
//Here I can't use graphApiClient.Users because, I don't have any user info on my jwt token. It will be just Access token which as details related to aad application.I want to update extension attribute which is present in OptionalClaims -> Access Token of AAD Application Manifest.
await graphApiClient.Users[employee.EmailAddress]
.Request()
.UpdateAsync(new User()
{
AdditionalData = dictionary
});
How to update extension claim attribute present in access token of optional claims . I want to update through c# code.
How to do that. Kindly suggest.
Update 2:
I want to use code similar to below for updating custom extension claim attribute present in optional claims of azure ad app but UpdateAsync is not working.
await graphApiClient.Application[clientid]
.Request()
.UpdateAsync(new Application()
{
AdditionalData = dictionary
});
At UpdateAsync(), I'm getting issue as below
Specified HTTP method is not allowed for the request
Kindly let me know the solution to add custom user defined claim to azure ad access token.
Note: Since, I want to update access token ,there will be not be any user info on access token. So, graphApiClient.Users[employee.EmailAddress] won't work, as access token will not have any user info like EmailAddress
I have created extension claim attribute in azure ad ap manifest as below
I want to update extension claim attribute extension_clientid_moviename value dynamically with value obtained from external source through code. How to do that. Kindly suggest.
Update 3:
I have tried below code to update extension claim attribute present in Optional claims ID Token.
await graphApiServiceClient.Users["abcd#hotmail.com"]
.Request()
.UpdateAsync(new User()
{
AdditionalData = dictionary
});
I am getting error as below
Code: Request_ResourceNotFound\r\nMessage: Resource '' does not exist or one of its queried reference-property objects are not present.
That is because you are using OAuth 2.0 client credentials flow to get access token :
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
That flow is commonly used for server-to-server interactions without immediate interaction with a user. So you can't find user information in access token .
The document you provided is adding User's extension property , update that value via Microsoft Graph :
await graphApiClient.Users[employee.EmailAddress]
.Request()
.UpdateAsync(new User()
{
AdditionalData = dictionary
});
That email is not from access token , you should manually provide the user's email who the api wants to update the properties value .
But since the property is a User's extension property , you can get the value from claims in ID token when user login with Azure AD in your client app . That claim won't include in access token which issued using client credential flow .
Regarding the issue in Update 3, we can not use the guest user email here. We should use ObjectId instead. That's why you got Request_ResourceNotFound error.
await graphApiServiceClient.Users["ObjectId"]
.Request()
.UpdateAsync(new User()
{
AdditionalData = dictionary
});

MS Graph - 401 Unauthorized seemingly with proper token and access

I am working with an ASP.NET Core 2.0 application hosted on Azure and authenticates users through Microsoft using MSAL. I am getting the basic information through the authentication process like name, username and group claims. However, I want to access some additional information through MS Graph, like the users profile photo. Initial authentication and token acquisition runs smoothly, and sending a request to https://graph.microsoft.com/beta/me returns 200 OK. When trying to call https://graph.microsoft.com/beta/me/photo/$value, however, I get a 401 - Unauthorized in return.
I have seen several other posts on this issue, but most of them concludes that the developer have either forgotten to ask for the proper consents, gotten tokens from the wrong endpoints, or similar issues. All of which I have confirmed not to be the case.
I have confirmed that the proper scopes are included in the token using https://jwt.ms/. I also tried asking for greater scopes than necessary. Currently I am using the following scopes: openid profile User.ReadBasic.All User.Read.All User.ReadWrite Files.ReadWrite.All. According to the beta reference for get user the least required permission is User.Read and according to the reference for get photo the least required permission is also User.Read. Using the Graph Explorer I have also confirmed that I should have had access to the photo using the permissions that I do, although, I have not set any pictures on my profile so it gives me a 404 response.
I am at a loss as to why I cannot get access to the profile photo so any suggestions are much appreciated. If you need more information or details, please let me know. If relevant, I have a custom middleware that handles the post-authentication process of reading the user information which also makes the additional call to MS Graph for the photo.
Edit:
I also tried https://graph.microsoft.com/beta/users/{my-user-id}/photo/$value which yielded the same results - 404 in Graph Explorer and 401 through my code
Edit 2: Code
Here is the code that I am using. This first snippet is in a middleware that puts the claims from the authenticated user in a specific format. I have just been putting a break point on the return and inspected the response object.
public async Task GetUserPhotoAsync(string userid, HttpContext context)
{
HttpClient client = new HttpClient();
//client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var result = await new TokenHelper(_settings).GetAuthenticationAsync(userid, context, new string[] { "User.ReadBasic.All", "User.Read.All", "User.ReadWrite", "Files.ReadWrite.All" });
var url = "https://graph.microsoft.com/beta/me/photo/$value";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
return;
}
Here is the function that gets the token from the cache. MSALSessionCache is some code I have borrowed from here with some tweaks to fit .net core.
public async Task<AuthenticationResult> GetAuthenticationAsync(string signedInUserId, HttpContext context, string[] scopes)
{
TokenCache userTokenCache = new MSALSessionCache(signedInUserId, context).GetMsalCacheInstance();
ConfidentialClientApplication cca =
new ConfidentialClientApplication(_settings.ClientId, $"{_settings.Domain}/{_settings.AADInstance}/v2.0", "http://localhost:5000", new ClientCredential(_settings.ClientSecret), userTokenCache, null);
if (cca.Users.Count() > 0)
{
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes, cca.Users.First());
return result;
}
else
{
throw new Exception();
}
}
Initial token acquisition
options.Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async context =>
{
string signedInUserId = context.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
TokenCache userTokenCache = new MSALSessionCache(signedInUserId, context.HttpContext).GetMsalCacheInstance();
ConfidentialClientApplication cca =
new ConfidentialClientApplication(aadOptions.ClientId, aadOptions.RedirectUri, new ClientCredential(aadOptions.ClientSecret), userTokenCache, null);
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(context.ProtocolMessage.Code, new string[] { "User.ReadBasic.All", "User.Read.All", "User.ReadWrite", "Files.ReadWrite.All" });
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
};
Edit 3: Using the /v1.0 endpoint
As per Marc LaFleur's request I have tried the v1.0 endpoint with the same result. https://graph.microsoft.com/v1.0/me gives a 200 OK response code while https://graph.microsoft.com/v1.0/me/photo/$value returns 401 Unauthorized
I had the same problem with the Microsoft Graph giving a 401 Unauthorized exception when I was trying to query a user's photo or the photo's metadata on both the /V1.0 and /beta API endpoints. Like you, I verified I had the right tokens and was able to successfully access the user profile API.
In my case, I found it was because the photo for the user I was testing with hadn't been set. Once I assigned a photo I was able to successfully call both the photo value and photo metadata beta endpoints.
The v1.0 endpoints still gave me a 401 Unauthorized exception, but in my application I only use AzureAD, not Exchange. Based on #MarcLaFleur comments and the API documentation, this sounds like "expected" behaviour.
Why it returns a 401 Unauthorized instead of something like a 404 Not Found, or returning null values, I don't know.

How to get Google+ profile picture on login in ASP.NET MVC

I'm working on a ASP.NET MVC 5 with Entity Framework (version 6.0) application.
I have added the simple google login, that saves the google email with the user on registration. How do I also get the profile picture of the Google+ user when they login and cast it in a view?
Google Plus API for developers allows you to fetch public data from Google+.
Followed by detail tutorial of all the necessary steps one need to perform to successfully fetch public data from Google+.
Google implies a limit to the usage of Google+ API - Each developer has a quota. We will see about that when we will discuss Google API console.
Google uses OAuth2.0 protocol to authorize your application when it tries to access user data.
It mostly uses standard HTTP method by means of RESTful API design to fetch and manipulate user data.
Google uses JSON Data Format to represent the resources in the API.
Step1: Generate an API key through Google API Console.
Step2: used GoogleOAuth2AuthenticationOptions which means you'll need to set
up a project at https://console.developers.google.com/project first to get a ClientId and ClientSecret.
At that link (https://console.developers.google.com/project), create a project and then select it.
Then on the left side menu, click on "APIs & auth".
Under "APIs", ensure you have "Google+ API" set to "On".
Then click on "Credentials" (in the left side menu).
Then click on the button "Create new Client ID".
Follow the instructions and you will then be provided with a ClientId and ClientSecret, take note of both.
var googleOptions = new GoogleOAuth2AuthenticationOptions()
{
ClientId = [INSERT CLIENT ID HERE],
ClientSecret = [INSERT CLIENT SECRET HERE],
Provider = new GoogleOAuth2AuthenticationProvider()
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new Claim("urn:google:name", context.Identity.FindFirstValue(ClaimTypes.Name)));
context.Identity.AddClaim(new Claim("urn:google:email", context.Identity.FindFirstValue(ClaimTypes.Email)));
//This following line is need to retrieve the profile image
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:google:accesstoken", context.AccessToken, ClaimValueTypes.String, "Google"));
return Task.FromResult(0);
}
}
};
app.UseGoogleAuthentication(googleOptions);
//get access token to use in profile image request
var accessToken = loginInfo.ExternalIdentity.Claims.Where(c => c.Type.Equals("urn:google:accesstoken")).Select(c => c.Value).FirstOrDefault();
Uri apiRequestUri = new Uri("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + accessToken);
//request profile image
using (var webClient = new System.Net.WebClient())
{
var json = webClient.DownloadString(apiRequestUri);
dynamic result = JsonConvert.DeserializeObject(json);
userPicture = result.picture;
}
OR
var info = await signInManager.GetExternalLoginInfoAsync();
var picture = info.ExternalPrincipal.FindFirstValue("pictureUrl");
ExternalLoginCallback method I check for which login provider is being used and handle the data for Google login.
Go through the link to get more information.
https://developers.google.com/identity/protocols/OAuth2
I have tried it its working.

How to get additional fields using the Facebook provider in ASP.NET Core RC1?

I'm using ASP.NET core RC1 (and can't upgrade to the not yet release RC2 nightly builds because of the lack of VS support in RC2).
I'm trying to get additional fields from Facebook (first_name, last_name, email and significant_other).
I used the code suggested on Github:
app.UseFacebookAuthentication(options =>
{
options.AppId = Configuration["Authentication:Facebook:AppId"];
options.AppSecret = Configuration["Authentication:Facebook:AppSecret"];
options.Scope.Add("email");
options.Scope.Add("user_relationships");
options.BackchannelHttpHandler = new HttpClientHandler();
options.UserInformationEndpoint =
"https://graph.facebook.com/v2.5/me?fields=id,email,first_name,last_name,significant_other";
This solution indeed returns the email of the user, but fails with first_name, last_name and significant_other (and any other field I tried besides name, id and email).
Also, is it possible getting the FB access token? We might need it for future querying of other edges, or use it to manually query Facebook because ASP.NET Core has a bug (at least in RC1).
I need a way, even if not the cleanest.
I'm trying to get additional fields from Facebook (first_name, last_name, email and significant_other). This solution indeed returns the email of the user, but fails with first_name, last_name and significant_other (and any other field I tried besides name, id and email).
In RC1, the Facebook middleware automatically stores the email as a claim, but not the first name or last name so you need to manually extract them using the event model if you want to be able to retrieve from application code:
app.UseFacebookAuthentication(options => {
options.Events = new OAuthEvents {
OnCreatingTicket = context => {
var surname = context.User.Value<string>("last_name");
context.Identity.AddClaim(new Claim(ClaimTypes.Surname, surname));
return Task.FromResult(0);
}
};
});
In RC2, custom code won't be necessary, as the first name/last name are now included by default: https://github.com/aspnet/Security/issues/688.
Also, is it possible getting the FB access token? We might need it for future querying of other edges, or use it to manually query Facebook because ASP.NET Core has a bug (at least in RC1).
You can use the SaveTokensAsClaims option to store the access/refresh tokens as claims (enabled by default in RC1). If you need more information about this feature, you can take a look the PR that introduced it: https://github.com/aspnet/Security/pull/257.
app.UseFacebookAuthentication(options => {
options.SaveTokensAsClaims = true;
});
You can retrieve it like any other claim:
var token = User.FindFirst("access_token")?.Value
Note: in RC2, this feature was revamped and tokens won't be stored in claims but in authentication properties: https://github.com/aspnet/Security/pull/698.
Update to #Pinpoint answer: current version doesn't have SaveTokensAsClaims option anymore. Instead, now there is a SaveTokens option:
Defines whether access and refresh tokens should be stored in the
Http.Authentication.AuthenticationProperties after a successful authorization. This property is set to false by default to reduce the size of the final authentication cookie.
Note, that by default it is false and don't store in Claims. AuthenticationTokenExtensions class has been added
To get those tokens then, you may use one of the GetToken extension method defined in AuthenticationTokenExtensions class. For example in controller action method the following code should work:
var token = await HttpContext.Authentication.GetTokenAsync("access_token");
Related links from github:
What happened to SaveTokensAsClaims property?
Save tokens in auth properties instead of claims

Categories

Resources