Cannot update users with GraphClientService - c#

While using newest version of Microsoft.Graph and Microsoft.Graph.Core libraries I am trying to execute following snippet to find user and then update him. User is found and retrieved but after executing Update method, program crashes. The application is .Net Framework 4.6.2. Not possible to upgrade due to other dependencies.
var builder = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(new ConfidentialClientApplicationOptions()
{
ClientId = "appId",
ClientSecret = "clientSecret",
TenantId = "tenantId"
});
var client = new GraphServiceClient(
new MicrosoftGraphAuthProvider(
builder.Build(),
new string[] {"https://graph.microsoft.com/.default" }));
var user = new Microsoft.Graph.User()
{
GivenName = "test",
Surname = "test",
Mail = "test#testmail.com"
};
var originalUser = await client.Users[upn]
.Request()
.Select("displayName")
.GetAsync();
await client.Users[originalUser.Id]
.Request()
.UpdateAsync(user);
Exception message
{"error":{"code":"Request_BadRequest","message":"Specified HTTP method is not allowed for the request target.","innerError":{"date":"2022-11-04T18:13:06","request-id":"","client-request-id":""}}}
First thoughts are to look at HTTP method used. But this is official distribution from Microsoft running against very slightly customized AzureAd tenant, so I would expect to work just like that. I am starting to be clueless. Gonna be helpful for ideas.

If you want to update user , you have to use
var result = await graphClient.Users[userId].Request().UpdateAsync(userUpdate);
doc - https://learn.microsoft.com/en-us/graph/sdks/create-requests?tabs=CS#updating-an-existing-entity-with-patch
Hope this helps ,
Thanks

Related

I just want to access some files from my .net core web application using Microsoft Graph and nothing is current

I have a .net core web application and I just want to access some files that are on one of our SharePoint sites, using Microsoft Graph. I've looked at courses on Pluralsight and the most current course has outdated material. I'm looking for a simple code example that gets me from a - z and I can't find any information that exists before mid 2022! On a similar question, I got an answer with code that didn't even work. Apparently I have to get an authorization code, in order to get an access token. BUT, the authorization code pretty much expires as soon as the user is logged into my application. Below is a modified version of the code I was given. I modified it in an effort to try to make it work. As you will see, I tried various version of "scopes" and I'm getting a token that I'm trying to use in the AuthorizationCode Credentials. I don't know if it's the right token to use. I've also seen some examples using PostMan. Getting things to work in postman is absolutely wonderful, but it's not C# code. I apologize if I seem a little rough, I'm just extremally frustrated. It should not be this difficult to find a working code sample. Any help would be appreciated. Here is the code I have that doesn't work:
//var scopes = new[] { "https://mysite.sharepoint.com/.default" };
//var scopes = new[] { "https://graph.microsoft.com/.default" };
var scopes = new[] { "https://graph.microsoft.com/User.ReadWrite.All" };
var tenantId = "tenant";
var clientId = "clientId";
var clientSecret = "shhItsASecret";
var client = new RestClient("https://login.microsoftonline.com/siteId/oauth2/v2.0/token");
var request = new RestRequest();
request.Method = Method.Post;
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("client_id", clientId);
request.AddParameter("client_secret", clientSecret);
request.AddParameter("scope", "https://graph.microsoft.com/User.ReadWrite.All");
request.AddParameter("response_type", "code");
request.AddParameter("grant_type", "client_credentials");
RestResponse response = client.Execute(request);
TokenModel tokenModel = new TokenModel();
JsonConvert.PopulateObject(response.Content, tokenModel);
var authorizationCode = tokenModel.access_token;
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// https://learn.microsoft.com/dotnet/api/azure.identity.authorizationcodecredential
var authCodeCredential = new AuthorizationCodeCredential(tenantId, clientId, clientSecret, authorizationCode, options);
Azure.Core.AccessToken accessToken = new Azure.Core.AccessToken();
try
{
accessToken = await authCodeCredential.GetTokenAsync(new Azure.Core.TokenRequestContext(scopes) { });
}
catch (System.Exception ex)
{
throw;
}
var tok = accessToken;
UPDATE:
I now know that I need to use delegated permissions and I need to use the auth code flow in order to do that. However, we use 2 factor authentication and it seems that by the time I can read anything from a variable, I can only see an access-token. If I understand correctly, the auth code is used to get an access-token and it expires. So, I can't seem to use that. Could I pass that access-token to my code that instantiates the graphService?
Someone else suggested I need to adjust my startup file and my appsettings file. I can't really do that. We have 5 other modules in our web application and this would be a big change to all of that. So, I'm not sure what I should be doing there. Bellow is what is in our startup, as it pertains to authentication:
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<UnitRateContractSystemContext>()
.AddDefaultTokenProviders()
.AddUserStore<UserStore<ApplicationUser, ApplicationRole, UnitRateContractSystemContext, Guid, ApplicationUserClaim<Guid>, ApplicationUserRole, IdentityUserLogin<Guid>, IdentityUserToken<Guid>, IdentityRoleClaim<Guid>>>()
.AddRoleStore<RoleStore<ApplicationRole, UnitRateContractSystemContext, Guid, ApplicationUserRole, IdentityRoleClaim<Guid>>>();
UPDATE 3:
I looked a little further down in my startup file and there was some openID connect information. Not sure why it was moved so far down, but I moved it up. Below is my entire authentication setup. The last 4 lines I added as a result of following one of the examples that someone provided. It builds just fine, but when I run it, I get an error in the Program.cs file: System.InvalidOperationException: 'Scheme already exists: Cookies'. If I go and comment out the "AddCookie()" line I get a similar error, but it says that OpenId Connect exists. So, at this point I'm stuck, but I feel if this can be solved, it might be the solution.
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<UnitRateContractSystemContext>()
.AddDefaultTokenProviders()
.AddUserStore<UserStore<ApplicationUser, ApplicationRole, UnitRateContractSystemContext, Guid, ApplicationUserClaim<Guid>, ApplicationUserRole, IdentityUserLogin<Guid>, IdentityUserToken<Guid>, IdentityRoleClaim<Guid>>>()
.AddRoleStore<RoleStore<ApplicationRole, UnitRateContractSystemContext, Guid, ApplicationUserRole, IdentityRoleClaim<Guid>>>();
#region Authentication
string[] initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
//auth
services.AddAuthentication(options =>
{
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = Configuration["Authentication:Microsoft:OAuth"];
options.RequireHttpsMetadata = true;
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.UsePkce = false;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("email");
options.SaveTokens = true;
options.CallbackPath = new PathString(Configuration["Authentication:Microsoft:Callback"]);
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
};
// MetadataAddress represents the Active Directory instance used to authenticate users.
options.MetadataAddress = Configuration["Authentication:Microsoft:Meta"];
options.ClientId = Configuration["Authentication:Microsoft:ApplicationId"];
options.ClientSecret = Configuration["Authentication:Microsoft:Password"];
})
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
You have an asp.net core web application, and you want to access some files that are on one of your SharePoint sites. So I think you may want to use this graph api with request /sites/{site-id}/drive/items/{item-id}. If you want to use other APIs, the steps are the same.
First, since the scenario for you is access files in different sites, so if you used delegated permission(require users sign in first and get access token on behalf the user), you may meet an issue that the user is not allowed to this site so that he can't access the site. I'm afraid this is what you want, so you can use application permissions. For this api, the permission is like below, please add api permissions in Azure AD first.
Then, since you have an asp.net core web application, then you can use Azure identity + graph SDK to do this. You can use code below:
using Microsoft.Graph;
using Azure.Identity;
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "tenant_name.onmicrosoft.com";
var clientId = "aad_app_id";
var clientSecret = "client_secret";
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
var file = await graphClient.Sites["site_id"].Drive.Items["item_id"].Request().GetAsync();
If you want to let user sign in and then list some files which is allowed to the signed in user, the easiest way is adding Microsoft identity platform into your application, which can created by a template, just need to choose the authentication option when creating application in visual studio, then update the configurations. And certainly, you need to give delegated API permission, which is different than above.
Finally here's the official sample, you can see what codes/packages/configurations are added based on a web application.

Insufficient privileges to add Azure AD user

I created a console application to create an Azure AD user as follows (doc referred: https://learn.microsoft.com/en-us/graph/api/user-post-users?view=graph-rest-1.0&tabs=http):
static async Task Main(string[] args)
{
var credential = new ClientCredential("<clientt-id>", "<client-seceret>");
var authProvider = new HttpRequestMessageAuthenticationProvider(
credential,
"https://login.windows.net/<tenant-id>",
"https://graph.microsoft.com/");
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
var user = new User
{
AccountEnabled = true,
DisplayName = "Test User",
MailNickname = "testuser",
UserPrincipalName = "testuser#M365xxxxxxx.onmicrosoft.com ",
PasswordProfile = "xxxxxxxxxxxx"
OnPremisesImmutableId = "id"
};
await graphClient.Users
.Request()
.AddAsync(user);
}
API permissions added to app are Group.ReadWrite.All and User.ReadWrite.All.
On running this code, I see the following error:
Code: Authorization_RequestDenied
Message: Insufficient privileges to complete the operation.
What am I missing?
For this problem, I summarize the points below which you need to check:
1. It seems your code use client_credentials as grant flow to do the job, so please check you have added the permissions of "Application" but not "Delegated". And don't forget grant admin consent.
2. If still show Authorization_RequestDenied message, please remove the permission Group.ReadWrite.All because this permission is unnecessary. And the Group permission may affect other permissions in my past tests.
3. It seems you develop the specific code in class HttpRequestMessageAuthenticationProvider, actually there is an off-the-shelf SDK avaiable for us to use. I provide my code below for your reference, the code works fine to create a user.
using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;
using System;
using System.Threading.Tasks;
namespace ConsoleApp23
{
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Hello World!");
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create("<client_id>")
.WithTenantId("<tenant_id>")
.WithClientSecret("<client_secret>")
.Build();
ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
var user = new User
{
AccountEnabled = true,
DisplayName = "huryAdd",
MailNickname = "huryAdd",
UserPrincipalName = "huryAdd#xxx.onmicrosoft.com",
PasswordProfile = new PasswordProfile
{
ForceChangePasswordNextSignIn = true,
Password = "Password0123"
},
OnPremisesImmutableId = "testOnPre"
};
await graphClient.Users.Request().AddAsync(user);
Console.WriteLine("====success====");
}
}
}
And also provide the packages installed in my project.
Install-Package Microsoft.Identity.Client -Version 4.16.1
Install-Package Microsoft.Graph
Install-Package Microsoft.Graph.Auth -IncludePrerelease
4. By the way, there is a blank space in the end of your UserPrincipalName. Please remove it, otherwise it will show invalid principal name.
Hope it helps~
I had the same issue and managed to solve it.
I used the Directory.ReadWrite.All permission but still experienced the problem with setting the OnPremisesImmutableId attribute for our users.
After a bit of investigation, it turned out that i had to assign the roles "Group Administrator" and "User Administrator" to my application in Azure AD Portal (Azure AD > Roles and administrators > Click each group > Add Assignments). After both these roles had been applied to my application, the problem disappeard.

Get user mail from o365 using graph sdk in a console app

I want to read a users mail from a .net console app without user interaction.
I would like to give the app access to read only selected user(s) mail and not as global admin that can read all users mail.
I would like to use the .net Microsoft.Graph library and not raw REST interface.
I think i need more or less step-by-step instructions i this i seven possible
I have created an new application registration and a client secret
If i give Application permission to mail it works but i cant get delegated permission to work.
The code is just one of many i have tried , but i cant really find any that do what i want to do.
var tenantId = "domain123.onmicrosoft.com";
var client_Id = "1234567789";
var client_Secret = "123243456777";
var scopes = new[] { "https://graph.microsoft.com/.default" };
// Configure app builder
var authority = $"https://login.microsoftonline.com/{tenantId}";
var app = ConfidentialClientApplicationBuilder
.Create(client_Id)
.WithClientSecret(client_Secret)
.WithAuthority(new Uri(authority))
.WithLogging(MyLoggingMethod, LogLevel.Verbose,
enablePiiLogging: true,
enableDefaultPlatformLogging: true)
.Build();
// Acquire tokens for Graph API
var authenticationResult = await app.AcquireTokenForClient(scopes).ExecuteAsync();
// Create GraphClient and attach auth header to all request (acquired on previous step)
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(requestMessage =>
{
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("bearer", authenticationResult.AccessToken);
return Task.FromResult(0);
}));
// Call Graph API
var user = await graphClient.Users["user123#domain123.onmicrosoft.com"].Messages.Request().GetAsync();
Code: NoPermissionsInAccessToken
Message: The token contains no permissions, or permissions can not be understood.
Inner error
Can I suggest rather than trying to roll your own DelegateAuthenticationProvider, that you use one of the provided ones? e.g.
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantID)
.WithClientSecret(clientSecret)
.Build();
ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authProvider);
Docs: https://learn.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS#ClientCredentialsProvider
Nuget: https://www.nuget.org/packages/Microsoft.Graph.Auth/1.0.0-preview.1
I did try this earlier with success but since the comment on UsernamePasswordProvider is NOT RECOMMENDED I was hoping for a better solution.
To get this to work I did following steps
Create new App registration with redirect as public client/native = myapp://auth
Go into Authentication and set "Treat application as public client" = Yes
To allow app to access user data, go to:
https://login.microsoftonline.com/[tenant]/oauth2/v2.0/authorize?client_id=[client_id]&response_type=code&redirect_uri=myapp://auth&response_mode=query&scope=user.read&state=12345
Login with user that you want to use in app , this page will hand but it ok.
Is this really the best way to do what i want?
static async Task Main(string[] args)
{
var GraphClient = CreateGraphClient();
User me = await GraphClient.Me.Request()
.WithUsernamePassword("user123#domain123.onmicrosoft.com", new NetworkCredential("", "MyPassword").SecurePassword)
.GetAsync();
Console.WriteLine("OK:" + me.DisplayName);
Console.ReadLine();
}
public static GraphServiceClient CreateGraphClient()
{
string clientId = "1234567-1234-1234-12345-1234567890";
string tenantID = "domain123.onmicrosoft.com";
IPublicClientApplication publicClientApplication = PublicClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantID)
.Build();
UsernamePasswordProvider authProvider = new UsernamePasswordProvider(publicClientApplication, null);
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
return graphClient;
}
Another solution could be to make an application permission and then set access policy using PowerShell new-applicationaccesspolicy
https://learn.microsoft.com/en-us/powershell/module/exchange/organization/new-applicationaccesspolicy?view=exchange-ps
I have not tried that one yet, anyone know if this could help?

How and where do I add a new Schema Extension?

I have been trying for weeks to add a new Schema Extension for my Microsoft Graph based MVC application, essentially to store some basic variables along with a mail Message.
I've followed this example from GitHub and after some very frustrating days of working out that "Boolean" & "Integer" weren't supported property types, I then ran into the fabled "Insufficient privileges to complete the operation"...
I have been pulling my hair out trying to work out how and where I'm supposed to add my new extension, as it stands I'm trying to add it with the below code as an authenticated user (who is an admin):
SchemaExtension extensionPayload = new SchemaExtension()
{
Description = "my extension example",
Id = $"myExtensionExample",
Properties = new List<ExtensionSchemaProperty>()
{
new ExtensionSchemaProperty() { Name = "prop1", Type = "String" },
new ExtensionSchemaProperty() { Name = "prop2", Type = "String" }
},
TargetTypes = new List<string>()
{
"Message"
}
};
SchemaExtension test = await client
.SchemaExtensions
.Request()
.AddAsync(extensionPayload);
My Graph Client is generated with the below code:
public static async Task<GraphServiceClient> GetClient(HttpContextBase context)
{
string token = await GetAccessToken(context);
GraphServiceClient client = new GraphServiceClient(
new DelegateAuthenticationProvider(
(requestMessage) =>
{
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
return Task.FromResult(0);
}
)
);
return client;
}
And my Oauth config requests the following permissions:
<add key="ida:AppScopes" value="User.Read Mail.ReadWrite Mail.Send Contacts.ReadWrite Directory.AccessAsUser.All" />
I've checked the Azure App Permissions of the account I'm testing with and they all appear to be correct? Is that where they're supposed to be??
ANY pointers would be greatly appreciated, as I've lost so much time trying to get what I thought was a very straight forward test app up and running.
According to the docs, using this call with Application permissions isn't supported.

Server-side task to query Office 365 account for new emails

I need a server-side task on my .NET 4.6.1/MVC 5 app that will periodically check a specific O365 email address for new emails and retrieve them if found. This seems like a stupidly simple task, but I cannot find documentation anywhere for creating a server-side process to accomplish this. The only documentation Microsoft seems to have is for OAuth2 and passing through credentials when users sign in. I don't want that. I want to check one specific account, that's it. How would I accomplish this?
These are the pages I've found. There are others, but all are along these lines.
Intro to the Outlook API - I don't see a way to use a service account with the v2 endpoint.
Get Started with the Outlook REST APIs - This is specific to logging users in with OAuth2, unhelpful for my purposes.
Intro to the Outlook API - I don't see a way to use a service account with the v2 endpoint.
The v2 endpoint doesn’t support client credential at present( refer to the limitation). You need to register/configure the app using Azure portal and use the original endpoint to authenticate the app. More detail about register the app please refer to here. And we need to ‘read mail in all mailbox’ to use the client credential to read the messages like figure below.
And here is the code that using client credential to read messages using the Microsoft Graph:
string clientId = "";
string clientsecret = "";
string tenant = "";
string resourceURL = "https://graph.microsoft.com";
string authority = "https://login.microsoftonline.com/" + tenant + "/oauth2/token";
string userMail = "";
var accessToken = new TokenHelper(authority).AcquireTokenAsync(clientId, clientsecret, resourceURL);
var graphserviceClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
(requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
return Task.FromResult(0);
}));
var items = await graphserviceClient.Users[user].Messages.Request().OrderBy("receivedDateTime desc").GetAsync();
foreach (var item in items)
{
Console.WriteLine(item.Subject);
}
class TokenHelper
{
AuthenticationContext authContext;
public TokenHelper(string authUri)
{
authContext = new AuthenticationContext(authUri);
}
public string AcquireTokenAsync(string clientId, string secret,string resrouceURL)
{
var credential = new ClientCredential(clientId: clientId, clientSecret: secret);
var result = authContext.AcquireTokenAsync(resrouceURL, credential).Result;
return result.AccessToken;
}
}
In addition, if we authenticate the app with code grant flow we can also create a subscription which notify the app when the mail box receive the new messages.( refer to webhoocks/subscription)

Categories