Microsoft Graph API Update another user's photo? - c#

Using the Microsoft Graph API I was able to get a list of all users in our Azure Active Directory tenant and determine if they have a profile picture. I wanted to then take the list of users without a photo and upload one for them, but the API returns a 403 error even though the account I'm using has full access to all of the user accounts and the application is setup with full permissions to the Graph API.
using (HttpClient client = new HttpClient())
{
client.BaseAddress = new Uri("https://graph.microsoft.com/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("image/jpeg"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", oauthToken);
// HTTP GET
HttpResponseMessage response = await client.PatchAsync($"v1.0/users/{emailAddress}/photo/$value", byteContent);
if (!response.IsSuccessStatusCode)
{
throw new Exception("Error!");
}
}
403 FORBIDDEN
Is it not possible to do this using the Graph API or am I missing a permission somewhere?
The SCP value is:
Calendars.Read Calendars.ReadWrite Contacts.Read Contacts.ReadWrite Directory.AccessAsUser.All Directory.Read.All Directory.ReadWrite.All email Exchange.Manage Files.Read Files.Read.Selected Files.ReadWrite Files.ReadWrite.AppFolder Files.ReadWrite.Selected full_access_as_user Group.Read.All Group.ReadWrite.All Mail.Read Mail.ReadWrite Mail.Send MailboxSettings.ReadWrite Notes.Create Notes.Read Notes.Read.All Notes.ReadWrite Notes.ReadWrite.All Notes.ReadWrite.CreatedByApp offline_access openid People.Read People.ReadWrite profile Sites.Read.All Tasks.Read Tasks.ReadWrite User.Read User.Read.All User.ReadBasic.All User.ReadWrite User.ReadWrite.All

First of all, you should take a look at the new Microsoft Graph SDK, released during //Build 2016
Here is the Github of Microsoft Graph SDK : https://github.com/microsoftgraph
Here is a full sample I ve created, using it :
https://github.com/Mimetis/NextMeetingsForGraphSample
For your question, here are two methods I've written, which worked for me :
I assume, you have a method to get a valid access token.
using (HttpClient client = new HttpClient())
{
var authResult = await AuthenticationHelper.Current.GetAccessTokenAsync();
if (authResult.Status != AuthenticationStatus.Success)
return;
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + authResult.AccessToken);
Uri userPhotoEndpoint = new Uri(AuthenticationHelper.GraphEndpointId + "users/" + userIdentifier + "/Photo/$value");
StreamContent content = new StreamContent(image);
content.Headers.Add("Content-Type", "application/octet-stream");
using (HttpResponseMessage response = await client.PutAsync(userPhotoEndpoint, content))
{
response.EnsureSuccessStatusCode();
}
}
If you use the Microsoft Graph SDK, it will be really straightforward :)
GraphServiceClient graphService = new GraphServiceClient(AuthenticationHelper.Current);
var photoStream = await graphService.Users[userIdentifier].Photo.Content.Request().PutAsync(image); //users/{1}/photo/$value
Seb

The documentation is pretty clear about updating a photo of an other user.
To update the photo of any user in the organization, your app must have the User.ReadWrite.All application permission and call this API under its own identity, not on behalf of a user. To learn more, see get access without a signed-in user.
This means you'll need an access token for the application, and not the token you get if the user accesses your application. This page shows how to get an application access token. This also means you have to grant your application the application permissions, which is often forgotten.
Once you get the token you can check it on https://jwt.ms to see the claims in the token explained.
You're talking about the scp claim in the token. That is only there if you have a user token, not an application token.

Related

Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential

I have a console app written with C# on the top of .NET Core 2.2 framework.
I am trying to use my app to connect Google My Business API to create posts.
But every time I try to call the REST API I get the following error
Request had invalid authentication credentials. Expected OAuth 2
access token, login cookie or other valid authentication credential.
See
https://developers.google.com/identity/sign-in/web/devconsole-project.
The code worked previously, but for some odd reason, it stopped!
Hereis an example where I get authentication token and then call the API to get a list of Google accounts.
var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets
{
ClientId = "Client ID",
ClientSecret = "Client Secret",
}, new[] { "https://www.googleapis.com/auth/plus.business.manage" }, "google username", CancellationToken.None);
using (var client = new HttpClient())
{
//client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", credential.Token.AccessToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var c = await client.GetAsync("https://mybusiness.googleapis.com/v4/accounts");
var accountContentss = await c.Content.ReadAsStringAsync();
c.EnsureSuccessStatusCode();
var accountContent = await c.Content.ReadAsStringAsync();
}
I am able to authenticate and get the access token with no issue. However, the second call to the API fails for some reason.
How can I correctly call Google API? Beside the AccesToken, is there something else that should get passed in the header?
It turned out to be that GoogleWebAuthorizationBroker.AuthorizeAsync() was returning an expired token! It would have been very helpful if the API returned a message stating that the token is expired instead!
To fix the issue, instead of accessing the AccessToken manually (i.e, credential.Token.AccessToken), I used await Credential.GetAccessTokenForRequestAsync() method to get an access token.
The Credential.GetAccessTokenForRequestAsync() method returns an valid token any time it is called. In other words, if the token is expired, it uses the refresh-token to generate a new one. Otherwise, it return the existing non-expired token.
I change my Authorization header to this
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", await Credential.GetAccessTokenForRequestAsync());

Access token not containing SCP (roles) claims via Microsoft Graph

I'm using the Microsoft Graph SDK to get an access token for my application (not a user) in order to read from sharepoint. I've been following this document, as well as posted this SO question. The code in the linked SO is the same. I was able to add application permissions as well as grant them (by pressing the button) in azure portal. The problem is, the token that comes back to be used does not contain any roles / scp claims in it. Therefore when using the token, I get the "Either scp or roles claim need to be present in the token" message.
Just to be certain, the only value for my scope that I pass when getting the access token is: https://graph.microsoft.com/.default. I don't pass anything else like Sites.ReadWrite.All (I get an exception if I add that scope anyway). I'm not sure how to continue troubleshooting and any help would be appreciated.
Edit: added code using the graph SDK shown below:
var client = new ConfidentialClientApplication(id, uri, cred, null, new SessionTokenCache());
var authResult = await client.AcquireTokenForClientAsync(new[] {"https://graph.microsoft.com/.default"});
var token = authResult.AccessToken;
var graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider(async request => {request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token)}));
var drives = await graphServiceClient.Sites[<sharepoint_host>].SiteWithPath(<known_path>).Drives.Request().GetAsync();
Seems like doing the app initialization in a different way is the solution. Instead of this:
var client = new ConfidentialClientApplication(id, uri, cred, null, new SessionTokenCache());
do this:
var app = new ConfidentialClientApplication(ClientId, Authority, RedirectUri, credentials, null, new TokenCache());
The problem is, the token that comes back to be used does not contain
any roles / scp claims in it.
If you can not find any roles/scp claims in the decoded access token. You need to check the permission in Azure portal again.
The decoded access token should contain the roles you granted.
Login Azure portal->click Azure Active Directory->click App registrations(preview)->find your application.
Click your application->API permissions->check if you have grant admin consent for your application. If not, click 'Grant admin consent'.
The code for getting access token. You can find more details here.
//authority=https://login.microsoftonline.com/{tenant}/
ClientCredential clientCredentials;
clientCredentials = new ClientCredential("{clientSecret}");
var app = new ConfidentialClientApplication("{clientId}", "{authority}", "{redirecturl}",
clientCredentials, null, new TokenCache());
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
AuthenticationResult result = null;
result = app.AcquireTokenForClientAsync(scopes).Result;
Console.WriteLine(result.AccessToken);

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.

PATCH request on https://graph.microsoft.com/beta/applications returns 403

I am trying to add app roles to my app registration in Azure Active Directory programmatically, I am using the following Microsoft article as a reference: https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/api/application_update
Here is my code:
string bearer = "Bearer <token>";
string appId = "<guid>";
string appEndPoint = "https://graph.microsoft.com/beta/applications/{0}";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(string.Format(appEndPoint, appId));
request.Headers.Add("Authorization", bearer);
request.Method = "PATCH";
request.ContentType = "application/json";
string jsonBody = "{\"appRoles\":[{\"allowedMemberTypes\":[\"User\"],\"description\":\"This is a test role\",\"displayName\":\"Test Role\",\"id\":\"fb3d0a97-b19e-4132-bb62-4a0213b37178\",\"isEnabled\":true,\"origin\":\"Application\",\"value\":\"Test\"}]}";
request.ContentLength = Encoding.ASCII.GetBytes(jsonBody).Length;
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
{
streamWriter.Write(jsonBody);
streamWriter.Flush();
streamWriter.Close();
}
var responce = request.GetResponse(); // throws 403 Forbidden
var responseStr = new StreamReader(responce.GetResponseStream()).ReadToEnd();
This is how I am acquiring the bearer token:
string domain = "my.domain.com";
string appId = "<guid>";
string clientSecret = "<secret>";
AuthenticationContext authContext = new AuthenticationContext(string.Format("https://login.windows.net/{0}/oauth2/token", domain));
ClientCredential creds = new ClientCredential(appId, clientSecret);
AuthenticationResult result = await authContext.AcquireTokenAsync("https://graph.microsoft.com/", creds);
string bearer = result.AccessToken;
I have granted my app registration all the required permissions specified in the Microsoft article, but I keep getting a 403 response.
I have also tried granting my app registration all permissions available and still get 403, does anybody know what I am doing wrong here?
403 error means that the bearer token has insufficient privileges to complete the operation.
If we get the bearer token with Delegate permssion, we need (Directory.AccessAsUser.All), we could check it with https://jwt.io/
I also test your code on my side, it works correctly.
Note: Based on my test, if bearer token with Delegate permssion Directory.ReadWrite.All, then it has insufficient privileges
Update:
Based on my test, if I use the application permission(with AD v1 or v2), I also get the same result with you. You could give your feedback to Azure team.
APIs under the /beta version in Microsoft Graph are in preview and are subject to change. Use of these APIs in production applications is not supported.
"Directory.ReadWrite.All" is not required and is overkill. Some the service principle api's have not be migrated over to the new graph api. Try granting the below permissions, the one you are probably missing is the Azure Active Directory Graph permission
Azure Active Directory Graph - Note this takes a few minutes to apply...
Application.ReadWrite.All
Microsoft Graph
Application.ReadWrite.All

AdminSettings API using service account in a C# Console application

I'm trying to use the Google Admin Settings API with a Service Account with no success from a C# Console application.
From what I've understood, I first have to get an OAuth token. I've tried 2 methods successfully for this: using Google.Apis.Auth.OAuth2.ServiceAccountCredentials or by creating manually the JWT assertion.
But when I call an Admin Settings API with the OAuth token (maximumNumberOfUsers for instance), I always get a 403 error with " You are not authorized to perform operations on the domain xxx" message.
I downloaded GAM as the author calls this API too so that I can compose the same HTTP requests. Like explained in GAM wiki, I followed all the steps to create a new Service Account and a new OAuth Client ID so that I can be sure it's not a scope issue. I also activated the debug mode like proposed by Jay Lee in this thread. Like explained in the thread comments, it still doesn't work with my OAuth token but the call to the API succeeds with GAM OAuth token.
So it seems it's related to the OAuth token itself. An issue I get while creating the OAuth token is that I can't specify the "sub" property (or User for ServiceAccountCredentials). If I add it, I get a 403 Forbidden response with "Requested client not authorized." as error_description while generating the token i.e. before calling the API. So maybe it is the issue but I don't see how to fix it as I use an Admin email.
Another possibility is that this API needs the OAuth Client credentials as GAM requires 2 different types of credentials, Service Account and OAuth Client. As I only can use Service Account credentials in my project, I'm afraid I will be stuck if it is the case...
I don't see other options and I'm stuck with both, so any help appreciated. Thanks!
My code:
public static string GetEnterpriseUsersCount()
{
string domain = MYDOMAIN;
string certPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
certPath = certPath.Substring(0, certPath.LastIndexOf("\\") + 1) + "GAMCreds.p12";
var certData = File.ReadAllBytes(certPath);
X509Certificate2 privateCertificate = new X509Certificate2(certData, "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(SERVICE_ACCOUNT_EMAIL)
{
Scopes = new[] { "https://apps-apis.google.com/a/feeds/domain/" },
User = ADMIN_EMAIL
}.FromCertificate(privateCertificate));
Task<bool> oAuthRequest = credential.RequestAccessTokenAsync(new CancellationToken());
oAuthRequest.Wait();
string uri = string.Format("https://apps-apis.google.com/a/feeds/domain/2.0/{0}/general/maximumNumberOfUsers", domain);
HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest;
if (request != null)
{
request.Method = "GET";
request.Headers.Add("Authorization", string.Format("Bearer {0}", credential.Token.AccessToken));
// Return the response
using (WebResponse response = request.GetResponse())
{
using (StreamReader sr = new StreamReader(response.GetResponseStream()))
{
return sr.ReadToEnd();
}
}
}
return null;
}
Edit: I focused on scopes like advised by Jay Lee below and it appears that the missing scope was 'https://www.googleapis.com/auth/admin.directory.domain'. However, nowhere is this written in Admin Settings API documentation page. At least, I didn't find it. 'https://apps-apis.google.com/a/feeds/domain/' is necessary too but I already added it to the list of allowed scopes. Thanks Jay!
Edit 2: I also updated the source code so that it can help in the future.
You need to grant your service account's client ID access to the scopes for admins settings API. Follow the Drive domain wide delegation instructions except sub in the correct correct scope. Then you can set sub= without an error.

Categories

Resources