MS Graph API on behalf of user - unsuccess - c#

On this page https://learn.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS#IntegratedWindowsProvider it is said "The interactive flow is used by mobile applications (Xamarin and UWP) and desktops applications to call Microsoft Graph in the name of a user."
So I developed a C# console app to login and query some data:
var clientId = "<APP GUID GOES HERE>";
var tenantId = "<APP TENANT GUID GOES HERE>";
var scopes = new[] {"user.read","Calendars.Read"};
var clientApplication = PublicClientApplicationBuilder
.Create(clientId)
.Build();
var authProvider = new InteractiveAuthenticationProvider(clientApplication, scopes);
var graphClient = new GraphServiceClient(authProvider);
User me = graphClient.Me.Request()
.GetAsync()
.Result;
During running the console app a login "page" comes out, I entered my credentials, but at the end the pagse says error "AADSTS500113: No reply address is registered for the application.", and the code got "user cancelled the login"
BTW: I dont want to login manually each time, I added my password to the code:
var scopes = new[] {"offline_access","user.read","Calendars.Read"};
var clientApplication = PublicClientApplicationBuilder
.Create(clientId)
.Build();
var authProvider = new UsernamePasswordProvider(clientApplication, scopes);
var graphClient = new GraphServiceClient(authProvider);
var pwd = ConvertToSecureString("<MYPASSWORD GOES HERE>");
User me = graphClient.Me.Request()
.WithUsernamePassword("<MY EMAIL GOES HERE>", pwd)
.GetAsync()
.Result;
In this case no login page shows up (good), but an exception raises: "The grant type is not supported over the /common or /consumers endpoints. Please use the /organizations or tenant-specific endpoint."
Then I added a WithTenantId(...) to the Build(), now I got different exception: "MsalUiRequiredException: AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access '00000003-0000-0000-c000-000000000000'." but the multi-factor auth request does not come to my phone.
What goes wrong? What should I do to get this app work?
What I want is to execute this c# console app regularly on my desktop computer, without any interactions (logins) as my user to query some data using graph api. How to do that correctly?
Thanks in advance!

This error AADSTS500113: No reply address is registered for the application indicates that the reply URL is not available and AAD does not know where to send the token. To fix this, you need to add a valid redirect URI in your app registration in AAD.
The next error : MsalUiRequiredException in your case happens because the user needs to perform multiple factor authentication based your Azure AD policies. To do this, you need to change your flow from the current username/password provider to interactive authentication provider since in the former case, users who need to do MFA won't be able to sign-in (as there is no interaction).
This would look something like this:
IPublicClientApplication publicClientApplication = PublicClientApplicationBuilder
.Create(clientId)
.Build();
InteractiveAuthenticationProvider authenticationProvider = new InteractiveAuthenticationProvider(publicClientApplication, scopes);
You can then acquire the token interactively :
string[] scopes = new string[] {"user.read"};
var app = PublicClientApplicationBuilder.Create(clientId).Build();
var accounts = await app.GetAccountsAsync();
AuthenticationResult result;
try
{
result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
.ExecuteAsync();
}
catch(MsalUiRequiredException)
{
result = await app.AcquireTokenInteractive(scopes)
.ExecuteAsync();
}
To authenticate without the user, your app can implement client credentials acquisition methods - these suppose that the app has previously registered a secret (application password or certificate) with Azure AD, which it then shares with this call. Please note that no user interactions means you can't use delegated permissions.
Let me know if this helps and if you have further questions.

Related

Microsoft Graph list buckets with clientsecretcredentials

I am trying to manipulate microsoft planner tasks (end goal is to create a task in a certain Scope and bucket).
I am already failing at listing a Plan or the buckets for a plan. I want to make this connection from a background service (daemon) so no interactive user login should take place. (with interactive login credentials i can make it work, but that's not what i need/want).
So i Created a new App Registration in Azure with the Api Permissions:
Group.Read.All (Delegated)
Group.ReadWrite.All (Delegated)
Tasks.Read (Delegated)
Tasks.Read.Shared (Delegated)
Tasks.ReadWrite (Delegated)
Tasks.ReadWrite.Shared (Delegated)
User.Read (Delegated)
Group.ReadWrite.All (Application)
Tasks.ReadWrite.All (Application)
User.ManageIdentities.All (Application)
User.ReadWrite.All (Application)
I also checked the "Allow public client flows" setting on the App registration Authentication tab.
I started by adding the ones prescribed on the official microsoft doc website about this topic. And then started adding some because i was still receiving Access Denied messages. Thus reaching this list. It should be enough according to microsoft.
Then i have this code to authenticate with Microsoft graph, giving me a graphclient instance which is successfully initialized:
private GraphServiceClient initializeTeamsGraphConnection(string TenantId, string ApplicationId, string ClientSecret)
{
// The client credentials flow requires that you request the
// /.default scope, and preconfigure your permissions on the
// app registration in Azure. An administrator must grant consent
// to those permissions beforehand.
var scopes = new[] { ScopeGraph };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
var tenantId = TenantId;
// Values from app registration
var clientId = ApplicationId;
var clientSecret = ClientSecret;
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// https://docs.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
return graphClient;
}
So authentication seems to be succesful, but when i then try to list a plan using the code below:
private void CreateTask(GraphServiceClient client)
{
var graphTask = client.Planner.Plans["Sdonp-JNB0aInPxDcxMowZgACZ59"]
.Request()
.GetAsync();
while (!graphTask.IsCompleted)
{
graphTask.Wait(10000);
}
var plans = graphTask.Result;
I get following error:
403 - Forbidden: Access is denied.
You do not have permission to view this directory or page using the credentials that you supplied.
Access Permissions should be well above what is needed to do this. Any idea on what I am doing wrong?
Again this code is working because when i change authentication to some sort of interactive login type, i get this plan info no problem
Planner API currently supports only delegated permissions that's the reason why it returns 403 for daemon (background service).
According to this announcement, support for application permissions is coming soon.

Microsoft Graph API UnkownError

What am I doing wrong? I want to list the files in the root of my OneDrive. But I always get a 401 Unauthorized.
I used Fiddler to track the requests and requesting the OAuth token seems to work fine. But when I try to request https://graph.microsoft.com/v1.0/me/drive/root/children I get Unauthorized as response with the code UnknownError
private static GraphServiceClient GetAuthenticatedGraphClient()
{
List<string> scopes = new List<string>
{
"https://graph.microsoft.com/.default",
};
var cca = ConfidentialClientApplicationBuilder.Create(CLIENT_ID)
.WithAuthority(AadAuthorityAudience.PersonalMicrosoftAccount)
.WithClientSecret(SECRET)
.Build();
GraphServiceClient graphServiceClient =
new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) =>
{
// Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
var authResult = await cca.AcquireTokenForClient(scopes).ExecuteAsync();
// Add the access token in the Authorization header of the API
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
})
);
return graphServiceClient;
}
var drive = GraphClient.Me.Drive.Root.Children.
Request().
GetAsync();
FYI: I am using .NET MVC 5 and I want to access my personal onedrive without user interaction. I seem to be a bit lost with what flow I should use for this.
You are calling /me/drive/root/children endpoint, so you should use a user token rather than application token.
You are using auth code flow with:
var cca = ConfidentialClientApplicationBuilder.Create(CLIENT_ID)
.WithAuthority(AadAuthorityAudience.PersonalMicrosoftAccount)
.WithClientSecret(SECRET)
.Build();
Here you need to add .WithRedirectUri(redirectUri). See sample here.
And you should not use AcquireTokenForClient method here because it is requiring an application token with client credential flow.
If you are trying to call Microsoft Graph in a .net core MVC, please refer to this sample.
Acquire the access token:
string token = await _tokenAcquisition
.GetAccessTokenForUserAsync(GraphConstants.Scopes);
If your application is .net MVC, please refer to this document.
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithClientSecret(appSecret)
.Build();
string message;
string debug;
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code).ExecuteAsync();
message = "Access token retrieved.";
debug = result.AccessToken;
}
UPDATE:
There are 2 scenes that we could connect to OneDrive without any further human interaction.
Use client credential flow, which allow us to call Microsoft Graph with an application token. You need to add Application Permission into your Azure AD app. You should choose Client credentials provider and use GraphClient.Users["{userId or UPN}"].Drive.Root.Children instead of GraphClient.Me.Drive.Root.Children because there is no user(/me) in this case.
Corresponding code:
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantID)
.WithClientSecret(clientSecret)
.Build();
ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
var children = await graphClient.Users["{userId or UPN}"].Drive.Root.Children
.Request()
.GetAsync();
If you want to use GraphClient.Me.Drive.Root.Children but don't want sign-in interactively, you could choose Username/password provider, which uses OAuth 2.0 Resource Owner Password Credentials. This scene also uses user token rather than application token.
Please note that:
Microsoft recommends you do not use the ROPC flow. In most scenarios,
more secure alternatives are available and recommended. This flow
requires a very high degree of trust in the application, and carries
risks which are not present in other flows. You should only use this
flow when other more secure flows can't be used.
You need to add Delegated Permission in this case.
Corresponding code:
IPublicClientApplication publicClientApplication = PublicClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantID)
.Build();
var email = "{your username}";
var str = "{your password}";
var password = new SecureString();
foreach (char c in str) password.AppendChar(c);
UsernamePasswordProvider authProvider = new UsernamePasswordProvider(publicClientApplication, scopes);
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
var children= await graphClient.Me.Drive.Root.Children.Request()
.WithUsernamePassword(email, password)
.GetAsync();
UPDATE 2:
Unfortunately, both Client Credential flow and ROPC(Resource Owner Password Credentials) flow don't support personal accounts. For personal accounts, you have to use auth code flow I mentioned at the beginning and it requires you to sign-in interactively. In summary, it's impossible to access personal Onedrive without any further human interaction.

User lookup by user id failed in AAD

I'm trying to create MS Teams Meeting using Microsoft.Graph.Beta SDK in .NET core application. I'm using the following code to create the meeting.
static void Main(string[] args)
{
var clientId = "<Enter you Client ID here>";
var tenantId = "<Enter your tenand ID here>";
var clientSecret = "<Enter your client secret here>";
var scopes = new string[] { "https://graph.microsoft.com/.default" };
///The client credential flow enables service applications to run without user interaction.
///Access is based on the identity of the application.
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantId)
.WithClientSecret(clientSecret)
.Build();
ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);
var onlinemeeting = CreateTeamsMeeting(authProvider).GetAwaiter().GetResult();
Console.ReadLine();
}
public static async Task<OnlineMeeting> CreateTeamsMeeting(IAuthenticationProvider authProvider)
{
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
var onlineMeeting = new OnlineMeeting
{
StartDateTime = DateTimeOffset.Parse("2020-11-12T21:30:34.2444915+00:00"),
EndDateTime = DateTimeOffset.Parse("2020-11-12T22:00:34.2464912+00:00"),
Subject = "User Token Meeting",
};
return await graphClient.Me.OnlineMeetings
.Request()
.AddAsync(onlineMeeting);
}
However, upon running the above code, I receive the following error.
Server error: User lookup by user id failed in AAD. Client exception:
Processing of the HTTP request resulted in an exception. Please see
the HTTP response returned by the 'Response' property of this
exception for details.
Inner error: AdditionalData: date: 2020-11-09T12:19:53 request-id:
02cd2168-d0d8-437a-9eee-117f1924a387 client-request-id:
02cd2168-d0d8-437a-9eee-117f1924a387 ClientRequestId:
02cd2168-d0d8-437a-9eee-117f1924a387
How do we fix this error??
#juusnas's answer is correct. I just organize my comments here for easy reference.
You are using client credential flow based on Client credentials provider. So you should use graphClient.Users["{user id/upn}"].OnlineMeetings.Request().AddAsync(onlineMeeting) rather than graphClient.Me.OnlineMeetings.Request().AddAsync(onlineMeeting).
And if you want to implement Get the token as a user, you should choose Authorization code provider or Username/password provider. Then you could keep using graphClient.Me.OnlineMeetings.Request().AddAsync(onlineMeeting).
For permission error, you need to add Application permission (not delegated permission) OnlineMeetings.ReadWrite.All into your App registration.
And based on the Important tip here, you also must create an application access policy to allow applications to access online meetings on behalf of a user.
You have acquired a token as the application, with client credentials flow.
This does not make sense:
graphClient.Me.
Who is "me"?
Normally that is the user on behalf of who the app is making the API call.
But because you acquired the token as the app, there isn't a user.
You need to either:
Get the token as a user (don't use client credentials)
Use an API that is not dependent on user identity (not sure if Teams has those)

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);

Microsoft Graph API - Sending email as another user

In our application, we need to send notifications to users by email for various event triggers.
I'm able to send email if I send as "Me" the current user, but trying to send as another user account returns an error message and I'd prefer it if notifications didn't come users' themselves and may contain info we don't want floating around in Sent folders.
What works:
await graphClient.Me.SendMail(email, SaveToSentItems: false).Request().PostAsync();
What doesn't work:
string FromUserEmail = "notifications#contoso.com";
await graphClient.Users[FromUserEmail].SendMail(email, SaveToSentItems: false).Request().PostAsync();
Also tried using the user object id directly:
await graphClient.Users["cd8cc59c-0815-46ed-aa45-4d46c8a89d72"].SendMail(email, SaveToSentItems: false).Request().PostAsync();
My application has permissions for the Graph API to "Send mail as any user" enabled and granted by the owner/administrator.
The error message returned by the API:
Code: ErrorFolderNotFound Message: The specified folder could not be
found in the store.
I thought this error might have been because the notifications account didn't have a sent folder, so I set the SaveToSentItems value to false, but I still get the same error.
Are there any settings I need to check on the account itself to allow the app to send mail on this account or should this work?
I have checked out the documentation here:
https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_sendmail
Which appears to support what I'm trying to do, but doesn't reference any folder except for the sent items folder which I'm telling the API not to save to anyway.
We aren't intending to impersonate any actual user here, just send notification emails from within the app from this specific account (which I know is technically impersonation, but not of a real entity).
So like Schwarzie2478 we used a noreply#ourcompany.com address. But our AD is federated which means you can't use Username\Password auth and we didn't want to use the Application Mail.Send permission since it literally can send as anyone and there is no way IT Security would let that fly. So we used Windows Authentication instead.
This requires that you grant consent to the app to use the mail.send and user.read delegate permissions by going to https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize?client_id={clientId}&response_type=code&scope=user.read%20mail.send and logging in with the windows user that the app will run as.
More info on using windows auth here: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Integrated-Windows-Authentication
// method call
var t = SendEmailUsingGraphAPI();
t.Wait();
// method
static async Task<Boolean> SendEmailUsingGraphAPI() {
// AUTHENTICATION
var tenantID = "YOUR_TENANT_ID"; //azure ad tenant/directory id
var clientID = "YOUR_APPS_CLIENT_ID"; // registered app clientID
var scopes = "user.read mail.send"; // DELEGATE permissions that the request will need
string authority = $"https://login.microsoftonline.com/{tenantID}";
string[] scopesArr = new string[] { scopes };
try {
IPublicClientApplication app = PublicClientApplicationBuilder
.Create(clientID)
.WithAuthority(authority)
.Build();
var accounts = await app.GetAccountsAsync();
AuthenticationResult result = null;
if (accounts.Any()) {
result = await app.AcquireTokenSilent(scopesArr, accounts.FirstOrDefault())
.ExecuteAsync();
}
else {
// you could acquire a token by username/password authentication if you aren't federated.
result = await app.AcquireTokenByIntegratedWindowsAuth(scopesArr)
//.WithUsername(fromAddress)
.ExecuteAsync(CancellationToken.None);
}
Console.WriteLine(result.Account.Username);
// SEND EMAIL
var toAddress = "EMAIL_OF_RECIPIENT";
var message = "{'message': {'subject': 'Hello from Microsoft Graph API', 'body': {'contentType': 'Text', 'content': 'Hello, World!'}, 'toRecipients': [{'emailAddress': {'address': '" + result.Account.Username + "'} } ]}}";
var restClient = new RestClient("https://graph.microsoft.com/v1.0/users/" + result.Account.Username + "/sendMail");
var request = new RestRequest(Method.POST);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Authorization", "Bearer " + result.AccessToken);
request.AddParameter("", message, ParameterType.RequestBody);
IRestResponse response = restClient.Execute(request);
Console.WriteLine(response.Content);
}
catch (Exception e) {
Console.WriteLine(e.Message);
throw e;
}
return true;
}
Whenever you are using delegated permissions (i.e. when a user is logged in), even though your admin has consented to the Mail.Send.Shared, it does NOT grant access to all mailboxes in the tenant. These OAuth permissions do not override the permissions (and restrictions) in place for the user.
If the user is not already configured with permissions to be able to "Send As" the notifications#contoso.com user, then you'll see this error.
To make it work, you'd need to actually grant "Send As" rights to all users that will be using your application.
This is a subtle thing, and granted it's a bit confusing. In the Azure portal, the permissions have slightly different descriptions, depending on if you're looking at the Application Permissions or the Delegated Permissions.
Application: Send mail as any user
Delegated: Send mail on behalf of others
Since you're using delegated, the permission doesn't allow you to send as any user, only send on behalf of any folks that the logged on user has rights to send as.
Another approach you could use here to avoid having to grant these rights to all users (which would allow them to send via Outlook, etc.) would be to have your backend app use the client credentials flow to get an app-only token. In that case, the app itself would have the permission to send as any user.
I don't know what others will have done for this, but I contacted Microsoft about this exact scenario: I want to send a mail as a fixed user ( noreply#mycompany.com) which has a mailbox in Azure. I want to send this mail from different applications or services.
The person there told me that sending a mail with no user logging in, is only possible with an delegated user token.
So we configured our application as an Native application in Azure like for mobile apps. Logging in for this application with the technical user during a setup phase gives me a delegated user token for that specific user which can be stored in a mailing service or component. This token does not expire ( at least not until the security changes of the user like password or something) and can be used to call the graph api to send mails when you give permission for this account to be sending mails from.
Next to that we even associated other shared mailboxes to this accounts to be able to send mails for those mailboxes too.
Documentation:
First You need a native app registration in Azure ( not an Web API):
https://learn.microsoft.com/en-us/azure/active-directory/develop/native-app
This app only requires an one-time login and approval from an user to get a token which can represent that user indefinitly. We set up a mail user account to be used for this. That token is then used to get access token to Graph Api for sending mails and such
Token Handling example:
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/token-cache-serialization
With an identitytoken stored ( usually a .cache file somewhere) you can request an accesstoken:
Identity Client:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.identity.client.publicclientapplication?view=azure-dotnet
_clientApp = new PublicClientApplication(ClientId, "https://login.microsoftonline.com/{xxx-xxx-xx}, usertoken,...
authResult = await _clientApp.AcquireTokenSilentAsync(scopes,...
private static string graphAPIEndpoint = "https://graph.microsoft.com/v1.0/me";
//Set the scope for API call to user.read
private static string[] scopes = new string[] { "user.read", "mail.send" };
private const string GraphApi = "https://graph.microsoft.com/";
var graphclient = new GraphServiceClient($"{GraphApi}/beta",
new DelegateAuthenticationProvider(
(requestMessage) =>
{
// inject bearer token for auth
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", authResult.AccessToken);
return Task.FromResult(0);
}));
var sendmail = graphclient.Users[User].SendMail(mail), true);
try
{
await sendmail.Request().PostAsync();
}
catch (Exception e)
{

Categories

Resources