I'm trying to get the MS example application to work for the Graph Beta Webhooks API and it's currently crashing because I've had to to modify some of the example code to remove some obsolete code and I'm not sure what to replace it with.
This is the function:
public static GraphServiceClient GetAuthenticatedClient(string userId, string redirect)
{
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
async (request) =>
{
var tokenCache = new SampleTokenCache(userId);
// Obsolete code
//var cca = new ConfidentialClientApplication(Startup.ClientId, redirect, new ClientCredential(Startup.ClientSecret), tokenCache.GetMsalCacheInstance(), null);
// New code
var cca2 = ConfidentialClientApplicationBuilder.Create(Startup.ClientId).WithClientSecret(Startup.ClientSecret).WithRedirectUri(redirect).Build();
// Question - how do I pass the tokenCache in here as the userTokenCache ?
// ERROR - With the new code this returns zero accounts presuambly because I haven't passed in a userTokenCache
var accounts = await cca2.GetAccountsAsync();
// Obsolete code
//var authResult = await cca.AcquireTokenSilentAsync(Startup.Scopes, accounts.First());
// New code
var authResult2 = await cca2.AcquireTokenSilent(Startup.Scopes, accounts.First()).ExecuteAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult2.AccessToken);
}));
return graphClient;
}
If I use the ConfidentialClientApplicationBuilder then the GetAccountsAsync() returns an empty collection and I think it's because I haven't passed the tokenCache into the builder. Does anyone know how to fix this code or has anyone got the example App working ?
This is the link to the example App:
https://learn.microsoft.com/en-us/samples/microsoftgraph/aspnet-webhooks-rest-sample/microsoft-graph-aspnet-webhooks/
Thanks
Ed James
You need to login as user so that an account is added to the token cache which is available calling the tokenCache.GetMsalCacheInstance() method.
Related
My project is using GraphServiceClient to get the user Group Names using the below code.This is also using Microsoft.Identity.Web package so GraphServiceClient is injected through constructor.
var group = await graphClient.Me.TransitiveMemberOf
.Request()
.GetAsync();
The group variable is then used to get the DisplayName of the group.
I want to unit test the above code using NUnit and Moq.
var mockAuthProvider = new Mock<IAuthenticationProvider>();
var mockHttpProvider = new Mock<IHttpProvider>();
var mockGraphClient = new Mock<GraphServiceClient>(mockAuthProvider.Object, mockHttpProvider.Object);
mockGraphClient.Setup(c => c.Me.TransitiveMemberOf.Request().GetAsync(CancellationToken.None)).ReturnsAsync(???);
The ReturnAsync will return IUserTransitiveMemberOfCollectionWithReferencesPage, but how can I give a default value for it so I can test the rest of the method which actually gets the displayName
Thanks in advance.
Create a new instance of UserTransitiveMemberOfCollectionWithReferencesPage and add a new Group item to the current page.
UserTransitiveMemberOfCollectionWithReferencesPage page = new
UserTransitiveMemberOfCollectionWithReferencesPage
{
AdditionalData = new Dictionary<string, object>(),
};
page.Add(new Group { DisplayName = "MyName" });
Return page in ReturnsAsync method
mockGraphClient.Setup(c => c.Me.TransitiveMemberOf.Request().GetAsync(CancellationToken.None))
.ReturnsAsync(() => page);
I am using Microsoft Graph API to call some endpoints. I am using the SDK for C#.
When opening a fiddler trace, I found out that my _graphClientService is issuing an authentication to get a new token at every call. Why would that happen and how to prevent it?
It is also causing this error in some calls.
AADSTS50196: The server terminated an operation because it encountered a client request loop
It looks like this piece of code works. It generates a GraphServiceClient that reuses the same token at every call, instead of generating a new one.
public GraphServiceClient GenerateGraphUserClient()
{
string userToken = GetUserAccessToken();
GraphServiceClient client= new GraphServiceClient("https://graph.microsoft.com/v1.0", new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", userToken);
}));
return client;
}
public string GetUserAccessToken()
{
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
IPublicClientApplication publicClientApplication = PublicClientApplicationBuilder
.Create(_clientId)
.WithTenantId(_domain)
.Build();
var securePassword = new SecureString();
foreach (char c in _password)
securePassword.AppendChar(c);
var result = publicClientApplication.AcquireTokenByUsernamePassword(scopes, _userName, securePassword).ExecuteAsync().Result;
return result.AccessToken;
}
If you are using MSAL.NET you can cache a token. Public client applications (desktop/mobile apps) should try to get a token from the cache before acquiring a token by another method.
Acquisition methods on confidential client applications manage the cache themselves.
Resource:
Token cache serialization in MSAL.NET
I'm writing a utility in C# using the Microsoft.Graph SDK that connects and reads a user's OneDrive. I have created an App Registration, granted the application Files.Read.All permissions, and given Admin consent per the documentation.
I am able to connect to the Graph and the metadata for the OneDrive:
List<string> scopes = new List<string>();
scopes.Add("https://graph.microsoft.com/.default");
var authenticationProvider = new MsalAuthenticationProvider(cca, scopes.ToArray());
GraphServiceClient graphClient = new GraphServiceClient(authenticationProvider);
var drive = await graphClient.Users[userId].Drive
.Request()
.GetAsync();
It appears to correctly connect to the OneDrive, as evidenced by the properties that do return correct data like Quota, Owner, etc.
The issue is that the Items object is null, so I can't read any of the drive items:
I tried using the returned drive Id to access the drive directly, but received the same result:
var driveById = await graphClient.Drives[drive.Id]
.Request()
.GetAsync();
The few examples I found don't indicate any additional Request options or missing permissions. So how do I access the OneDrive Items?
The solution for this issue was presented in the comments, so I'm writing it up here for completeness.
ORIGINAL CODE:
var rootDrive = await GraphClient.Users[UserId].Drive.Request().GetAsync();
This returns the metadata of the user's OneDrive, but does not capture the Children. We will need this information later, however, so the final solution uses both this reference and the updated code.
UPDATED CODE:
To do that, you need to reference the drive's Root and its Children:
var driveItems = await GraphClient.Users[UserId].Drive
.Root
.Children
.Request()
.GetAsync();
Doing so returns an IDriveItemChildrenCollectionPage:
PROCESS THE CHILDREN:
For small samples, a standard foreach will work fine, but for larger collections you will need to implement a PageIterator (which I have not done yet). To get the children of this driveItem, you will need the drive Id of the root element as well as the current driveItem.Id:
var children = await GraphClient.Drives[rootDriveId].Items[item.Id].Children.Request().GetAsync()
Putting it altogether, it looks something like this:
public async Task ListOneDrive()
{
var rootDrive = await GraphClient.Users[UserId].Drive.Request().GetAsync();
var driveItems = await GraphClient.Users[UserId].Drive
.Root
.Children
.Request()
.GetAsync();
foreach (var item in driveItems)
{
await ListDriveItem(rootDrive.Id, item);
}
}
public async Task ListDriveItem(string rootDriveId, DriveItem item, int indentLevel = 1)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < indentLevel; i++)
{
sb.Append($" ");
}
if (item.Folder != null)
{
Console.WriteLine($"{sb.ToString()}> {item.Name}/");
var children = await GraphClient.Drives[rootDriveId].Items[item.Id].Children.Request().GetAsync();
foreach (var child in children)
{
await (ListDriveItem(rootDriveId, child, indentLevel + 1));
}
}
else if (item.File != null)
{
Console.WriteLine($"{sb.ToString()} {item.Name}");
}
}
This example is from a Console app that uses a recursive call to drill down through all the folders. Both methods really should have a PageIterator as mentioned above.
As per this documentation you should be able to create an appRoleAssignment via Microsoft Graph, however this doesn't work. In a GitHub issue I was instructed to create the issue here. We have migrated most of our code from Azure Graph API to Microsoft Graph and this is the last piece that is missing.
This finally worked for me!
There might be more optimized ways to post the JSON but I had to go to basics to make sure nothing is causing this to fail behind the scenes.
const string ROLE_ASSIGNMENT_FORMATTER = "https://graph.microsoft.com/beta/servicePrincipals/{0}/appRoleAssignments";
public static async Task AddApplicationUsers(string enterpriseAppId, string userId, string roleId)
{
HttpClient client = new HttpClient();
string url = string.Format(ROLE_ASSIGNMENT_FORMATTER, enterpriseAppId);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", await GetAccessToken());
var roleAssignment = new
{
appRoleId = roleId,
principalId = userId,
resourceId = enterpriseAppId
};
var content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(roleAssignment), Encoding.UTF8, "application/json");
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = await client.PostAsync(url, content);
if (response.IsSuccessStatusCode)
{
return ;
}
else
{
throw new HttpRequestException(response.ReasonPhrase);
}
}
I am attempting to add members to a group. I am able to list all groups in my org, get user by email, get all users and I can even remove a Member from a group but I cannot add one - The error returned is 400 Bad Request.
Here is the function which is the same function signature as those that work: (I do have the accesstoken, valid group id and a valid member id)
I have confirmed the body data looks correct at least as far as I can see from the example in the docs.
Not sure what else I can add to make things clearer, ask and I'll update
public async Task<string> AddGroupMember(string accessToken, string groupId, string memberId)
{
var status = string.Empty;
string endpoint = $"https://graph.microsoft.com/v1.0/groups/{groupId}/members/$ref";
string queryParameter = "";
// pass body data
var keyOdataId = "#odata.id";
var valueODataId = $"https://graph.microsoft.com/v1.0/directoryObjects/{memberId}";
var values = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(keyOdataId, valueODataId)
};
var body = new FormUrlEncodedContent(values);
try
{
using(var client = new HttpClient())
{
using(var request = new HttpRequestMessage(HttpMethod.Post, endpoint + queryParameter))
{
request.Content = body;
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
using(var response = await client.SendAsync(request))
{
if (response.StatusCode == HttpStatusCode.NoContent)
status = "Member added to Group";
else
status = $"Unable to add Member to Group: {response.StatusCode}";
}
}
}
}
catch (Exception ex)
{
status = $"Error adding Member to Group: {ex.Message}";
}
return status;
}
Thanks for any help that anyone can offer - this is the last call I have to make then home free
Found the issue for any who care to know for the future:
var body = new FormUrl... my code was incorrect, what's needed is a simple json string changed to this UPDATED:
var jsonData = $#"{{ ""{keyOdataId}"": ""{valueODataId}"" }}";
var body = new StringContent(jsonData, Encoding.UTF8, "application/json");
I would normally put the values in a class but this is for proof of concept and the json key needs to look exactly like this #odata.id
Clarifying what is happening here:
The request body for this call should be JSON encoded (application/json). The FormUrlEncodedContent method returns your dictionary as Form encoded (application/x-www-form-urlencoded).
You can write the JSON by hand (like you have so far) but a better solution would be to leverage Json.NET. This will let you encode the dictionary in much the same way you were with FormUrlEncodedContent:
var values = new Dictionary<string, string>
{
{ keyOdataId, valueODataId}
};
var body = JsonConvert.SerializeObject(values);
If you're going to be doing a lot of work with Microsoft Graph, I would highly recommend switching to the Microsoft Graph .NET SDK.
You're method here would be far simpler using the SDK:
public async Task<string> AddGroupMember(string groupId, string memberId)
{
GraphServiceClient graphClient = AuthenticationHelper.GetAuthenticatedClient();
User userToAdd = new User { Id = memberId };
await graphClient.Groups[groupId].Members.References.Request().AddAsync(userToAdd);
}