I am having trouble sending an email from a daemon app. I can get the token with the Client Credential flow but I am unable to send an email with the Microsoft Graph API. I am getting the following error:
Code: BadRequest
Message: Found a function 'microsoft.graph.sendMail' on an open property. Functions on open properties are not supported.
Inner error:
AdditionalData:
request-id: e2e3bb60-2212-4c99-8858-d109aaf4f1cd
date: 2020-01-30T11:18:21
ClientRequestId: e2e3bb60-2212-4c99-8858-d109aaf4f1cd
}
Below is the coding for sending an email through Microsoft Graph.
private readonly IClientCredentialProvider _clientCredentialProvider;
public MailTransmitter()
{
AuthenticationConfig config = AuthenticationConfig.ReadFromJsonFile("appsettings.json"); // contains the tenantId, clientSecret and clientId
_clientCredentialProvider = new ClientCredentialProvider(config);
}
public async Task<bool> SendMail(List<UserEntitlement> sortedListByLastAccessDate)
{
//GraphServiceClient graphClient = new GraphServiceClient(_clientCredentialProvider.GetAuthorizationCodeProvider());
var result = await _clientCredentialProvider.GetClientToken(); // Get token using Client Credentials flow
var accessToken = result.AccessToken;
//should I pass the URL to the graphServiceClient like below? Is the URL right?
var graphServiceClient = new GraphServiceClient("https://graph.microsoft.com/v1.0/0a181b4b-a2fb-4e38-b23b-2c72adc882f2/users/c26d8491-82f8-4f08-990e-35a73ad61ede/memberOf", new DelegateAuthenticationProvider(async (requestMessage) => {
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}));
var message = new Message
{
Subject = "Meet for lunch?",
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = "The new cafeteria is open."
},
ToRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "Bla#hotmail.com"
}
}
},
From = new Recipient
{
EmailAddress = new EmailAddress {
Address = "bla.bla#test.nl"
}
}
};
var saveToSentItems = false;
//Error occurs here
await graphServiceClient.Users["c26d8491-82f8-4f08-990e-35a73ad61ede"]
.SendMail(message, saveToSentItems)
.Request()
.PostAsync();
return true;
}
In case you are wondering how I used the client credentials flow, take a look at: https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/1-Call-MSGraph/daemon-console.
What is the problem exactly?
Thanks in advance!
The provided error occurs since invalid url is provided for GraphServiceClient, it expects the first argument to be service root url:
public GraphServiceClient(string baseUrl,IAuthenticationProvider authenticationProvider,IHttpProvider httpProvider = null)
in case of Microsoft Graph API, service root url consist of:
https://graph.microsoft.com is the Microsoft Graph API endpoint.
{version} is the target service version, for example, v1.0 or beta.
for instance, https://graph.microsoft.com/v1.0. Refer Calling the Microsoft Graph API for a details
Example
var app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority(new Uri(authority))
.Build();
var scopes = new string[] {"https://graph.microsoft.com/.default"};
var result = await app.AcquireTokenForClient(scopes)
.ExecuteAsync();
var client = new GraphServiceClient(
"https://graph.microsoft.com/v1.0",
new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", result.AccessToken);
}));
or via Client credentials provider for that matter:
var app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority(new Uri(authority))
.Build();
var authProvider = new ClientCredentialProvider(app);
var client = new GraphServiceClient(authProvider);
Prerequisites: requires Microsoft.Graph.Auth package
Related
I would like to read e-mails from a signed in user using IMAP.
I have created a console application for testing purposes and making sure the app-registration settings in azure are correct.
The console application is working as intended.
A Microsoft login window is shown where the user can enter their credentials.
An access token is received and is passed to MailKit in order to get the user's emails.
The problem
When I try to authenticate using MailKit in a MVC .net standard web-application, I get an error saying "Authentication failed".
However, when I copy the access-token I acquired using the console- application and use it in my web-application I do not get the authorization error and can successfully authenticate and read emails. (I use the access-token as second parameter in
var oauth2 = new SaslMechanismOAuth2("[Email here]", oathToken.access_token);).
I have used DotNetOpenAuth in order to show a Microsoft login window.
(I could not find a MSAL example for a web-application where i didn't have to add OWIN as middleware. I only want to authenticate in order to get emails, not for application wide authentication and authorization.)
Console application code (this works):
// Using Microsoft.Identity.Client 4.22.0
// Configure the MSAL client to get tokens
var pcaOptions = new PublicClientApplicationOptions
{
ClientId = "[client-id here]",
AadAuthorityAudience = AadAuthorityAudience.AzureAdMultipleOrgs,
};
var pca = PublicClientApplicationBuilder
.CreateWithApplicationOptions(pcaOptions).Build();
var scopes = new string[] {
"email",
"offline_access",
"https://outlook.office365.com/IMAP.AccessAsUser.All" };
// Make the interactive token request
var authResult = await pca.AcquireTokenInteractive(scopes).ExecuteAsync();
var oauth2 = new SaslMechanismOAuth2(authResult.Account.Username, authResult.AccessToken);
using (var client = new ImapClient())
{
await client.ConnectAsync("outlook.office365.com", 993, SecureSocketOptions.Auto);
await client.AuthenticateAsync(oauth2);
var inbox = client.Inbox;
inbox.Open(FolderAccess.ReadOnly);
for (int i = 0; i < inbox.Count; i++)
{
var message = inbox.GetMessage(i);
Console.WriteLine("Subject: {0}", message.Subject);
}
await client.DisconnectAsync(true);
}
Web-application (this doesn't work):
public ActionResult Index()
{
string clientID = "[client-id here]";
string clientSecret = "[client-secret here]";
string redirectUri = "[redirectUri here]";
AuthorizationServerDescription server = new AuthorizationServerDescription
{
AuthorizationEndpoint = new Uri("https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize"),
TokenEndpoint = new Uri("https://login.microsoftonline.com/organizations/oauth2/v2.0/token"),
ProtocolVersion = ProtocolVersion.V20,
};
List<string> scopes = new List<string>
{
"email",
"offline_access",
"https://outlook.office365.com/IMAP.AccessAsUser.All"
};
WebServerClient consumer = new WebServerClient(server, clientID, clientSecret);
OutgoingWebResponse response = consumer.PrepareRequestUserAuthorization(
scopes, new Uri(redirectUri));
return response.AsActionResultMvc5();
}
public async Task<ActionResult> Authorized(string code, string state, string session_state)
{
List<string> scopes = new List<string>
{
"IMAP.AccessAsUser.All",
"User.Read",
"offline_access"
};
HttpClient httpClient = new HttpClient();
var values = new Dictionary<string, string>
{
{ "Host", "https://login.microsoftonline.com" },
{ "Content-Type", "application/x-www-form-urlencoded" },
{ "client_id", "[client-id here]" },
{ "scope", string.Join(" ",scopes) },
{ "code", code },
{ "redirect_uri", [redirectUri here] },
{ "grant_type", "authorization_code" },
{ "client_secret", "[client-secret here]" },
{ "state", state },
};
var content = new FormUrlEncodedContent(values);
var response = await httpClient.PostAsync("https://login.microsoftonline.com/organizations/oauth2/v2.0/token", content);
var jsonString = await response.Content.ReadAsStringAsync();
var oathToken = JsonConvert.DeserializeObject<OathToken>(jsonString);
var oauth2 = new SaslMechanismOAuth2("[Email here]", oathToken.access_token);
var stringBuilder = new StringBuilder();
using (var client = new ImapClient())
{
try
{
await client.ConnectAsync("outlook.office365.com", 993, SecureSocketOptions.Auto);
await client.AuthenticateAsync(oauth2);
var inbox = client.Inbox;
inbox.Open(FolderAccess.ReadOnly);
for (int i = 0; i < inbox.Count; i++)
{
var message = inbox.GetMessage(i);
stringBuilder.AppendLine($"Subject: {message.Subject}");
}
await client.DisconnectAsync(true);
return Content(stringBuilder.ToString());
}
catch (Exception e)
{
return Content(e.Message);
}
}
}
The problems occurs on this line: await client.AuthenticateAsync(oauth2);
I receive an error saying "Authentication failed"
However, when using the access-token from the console application in the web-application i do not get this error and can successfully authenticate and read emails in the web-application.
Can anyone point me in the right direction?
Thanks.
I am trying to create a channel using MS Graph within a BotFramework bot. I get what appears to be a valid access Token. However the code below generates the following error:
The collection type 'Microsoft.Graph.IChannelMembersCollectionPage' on 'Microsoft.Graph.Channel.Members' is not supported.
var credential = new DefaultAzureCredential();
var token = credential.GetToken(
new Azure.Core.TokenRequestContext(
new[] { "https://graph.microsoft.com/.default" }));
var accessToken = token.Token;
Logger.LogWarning($"Token:{accessToken.ToString()}");
var graphServiceClient = new GraphServiceClient(
new DelegateAuthenticationProvider((requestMessage) =>
{
requestMessage
.Headers
.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
return Task.CompletedTask;
}));
try
{
var chan = new Channel
{
DisplayName = $"Chan1",
Description = "This channel is where we debate all future world domination plans",
MembershipType = ChannelMembershipType.Standard
};
await graphServiceClient.Teams["{GroupID}"].Channels.Request().AddAsync(chan);
}
You can use Graph SDK to generate token internally. Please try providing application permissions in azure portal and use the below code to create a channel in MS Teams. Below are the packages you need to install.
This is an example for application permissions. You can try the same code with minor changes/to no changes for delegate permissions.
string clientId = "";
string clientSecret = "1";
string tenantId = "";
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantId)
.WithClientSecret(clientSecret) // or .WithCertificate(certificate)
.Build();
//AuthorizationCodeProvider authProvider = new AuthorizationCodeProvider(confidentialClientApplication, scopes);
ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
var channel = new Channel
{
DisplayName = "Topic Discussion",
Description = "This channel is where we debate all future architecture plans",
MembershipType = ChannelMembershipType.Standard
};
await graphClient.Teams["{Your-teams-id}"].Channels
.Request()
.AddAsync(channel);
The goal it use Graph API to send an email.
I am able to get the Authorization token by using the below code - https://login.microsoftonline.com/Some_Tenant_ID/oauth2/v2.0/authorize?client_id=SOME_Client_ID&response_type=code&redirect_uri=https://localhost&response_mode=query&scope=offline_access%20user.read%20mail.read&state=12345
The scope is user.read and mail.send with offline access. Now from using this authorization code, I want to get the refresh token. From my understanding this should work without any problem but for some reason the code is breaking at this line var httpResponse = (HttpWebResponse)request.GetResponse(); and I not sure why.
The exception code error 400 Bad Request.
my console output.
Can any one help me here and is there another way to get the Access token and/or refresh token from Authorization token. the end goal is to send email from graph API.
string tokenUrl = "https://login.microsoftonline.com/myAppTenantID/oauth2/token";
//string tokenUrl = "https://login.microsoftonline.com/myAppTenantID/oauth2/v2.0/token"; I have tried this URL too
string grant_type= "authorization_code";
string ClientID = "MyAppClientID";
string Auth_Code = "My Auth Code";
string RedirectURI = "https://localhost";
string ClientSecret = "my App secret";
Dictionary<string, string> res_dic = null;
string TargetURL = String.Format(tokenUrl);
var request = (System.Net.HttpWebRequest)WebRequest.Create(TargetURL);
request.ContentType = "application/x-www-form-urlencoded";
string RefreshToken = null;
string requestBody = String.Format(#"client_id={0}&scope=user.read%20mail.read&code={1}&redirect_uri={2}&grant_type=authorization_code&client_secret={3}", ClientID, Auth_Code,RedirectURI, ClientSecret);
request.Method = "POST";
using (var streamwriter = new StreamWriter(request.GetRequestStream()))
{
Console.WriteLine("stage 0....");
streamwriter.Write(requestBody);
streamwriter.Flush();
streamwriter.Close();
}
try
{
Console.WriteLine("stage 1....");
//Console.WriteLine("prting response"+ (HttpWebResponse)request.GetResponse());
var httpResponse = (HttpWebResponse)request.GetResponse();
Console.WriteLine("Stage 2....");
using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
{
Console.WriteLine("Stage 3");
string Result = streamReader.ReadToEnd();
string StatusCode = httpResponse.StatusCode.ToString();
res_dic = JsonConvert.DeserializeObject<Dictionary<string, string>>(Result);
}
}
catch (WebException ex)
{
Console.WriteLine("stage 4");
string ErrorMessage = ex.Message.ToString();
Console.WriteLine(ErrorMessage);
}
RefreshToken = res_dic["refresh_token"].ToString();
}
This is better for you to debug for full error message, there are many situations where this error occurs.
The scope in your code needs to add offline_access because refresh_token will be only provided if offline_access scope is requested.
You could use SDK. Code sample here:
string[] scopes = new string[] { "", "" };
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithRedirectUri(redirectUri)
.WithClientSecret(clientSecret)
.WithAuthority(authority)
.Build();
AuthorizationCodeProvider auth = new AuthorizationCodeProvider(app, scopes);
GraphServiceClient graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) => {
// Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
var authResult = await app.AcquireTokenByAuthorizationCode(scopes, auth_code).ExecuteAsync();
// Add the access token in the Authorization header of the API request.
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
})
);
And this API is used for sending mail, you need to add Mail.Send permission first.
var message = new Message
{
Subject = "Meet for lunch?",
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = "The new cafeteria is open."
},
ToRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "fannyd#contoso.onmicrosoft.com"
}
}
},
CcRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "danas#contoso.onmicrosoft.com"
}
}
}
};
var saveToSentItems = false;
await graphClient.Me
.SendMail(message,saveToSentItems)
.Request()
.PostAsync();
I followed these 2 links to create a console app for sending emails using Graph API:
https://learn.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0&tabs=csharp
Microsoft Graph API unable to Send Email C# Console
I have added & granted the required permissions in Azure AD app:
I made sure to provide the client id, tenant id, client secret.
However, I see this error on running the console:
What am I missing?
Here is the code I tried from Microsoft Graph API unable to Send Email C# Console
static void Main(string[] args)
{
// Azure AD APP
string clientId = "<client Key Here>";
string tenantID = "<tenant key here>";
string clientSecret = "<client secret here>";
Task<GraphServiceClient> callTask = Task.Run(() => SendEmail(clientId, tenantID, clientSecret));
// Wait for it to finish
callTask.Wait();
// Get the result
var astr = callTask;
}
public static async Task<GraphServiceClient> SendEmail(string clientId, string tenantID, string clientSecret)
{
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantID)
.WithClientSecret(clientSecret)
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authProvider);
var message = new Message
{
Subject = "Meet for lunch?",
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = "The new cafeteria is open."
},
ToRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "myToEmail#gmail.com"
}
}
},
CcRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "myCCEmail#gmail.com"
}
}
}
};
var saveToSentItems = true;
await graphClient.Me
.SendMail(message, saveToSentItems)
.Request()
.PostAsync();
return graphClient;
}
Based on your code which generates the confidentialClientApplication, you are using Client credentials provider.
But the way you send the email is:
await graphClient.Me
.SendMail(message, saveToSentItems)
.Request()
.PostAsync()
It is calling https://graph.microsoft.com/v1.0/me/sendMail in fact.
But Client credentials flow doesn't support /me endpoint. You should call https://graph.microsoft.com/v1.0/users/{id | userPrincipalName}/sendMail endpoint in this case.
So the code should be:
await graphClient.Users["{id or userPrincipalName}"]
.SendMail(message, saveToSentItems)
.Request()
.PostAsync();
Or if you want to use /me/sendMail, choose Authorization code provider, where you should implement interactive login.
You can learn about the scenarios and differences between authorization code flow and client credentials flow.
I have previously been adding users programmatically using Active Directory Authentication Library (ADAL), but now I need to define "signInNames" (= users email), and that doesn't seem to be possible with ADAL (please tell me if im wrong).
Now I'm trying to add a new user (local account) programmatically using HTTP POST, following the documentation on MSDN.
//Get access token (using ADAL)
var authenticationContext = new AuthenticationContext(AuthString, false);
var clientCred = new ClientCredential(ClientId, ClientSecret);
var authenticationResult = authenticationContext.AcquireTokenAsync(ResourceUrl, clientCred);
var token = authenticationResult.Result.AccessToken;
//HTTP POST CODE
const string mail = "new#email.com";
// Create a new user object.
var user = new CustomUser
{
accountEnabled = true,
country = "MS",
creationType = "LocalAccount",
displayName = mail,
passwordPolicies = "DisablePasswordExpiration,DisableStrongPassword",
passwordProfile = new passwordProfile { password = "jVPmEm)6Bh", forceChangePasswordNextLogin = true },
signInNames = new signInNames { type = "emailAddress", value = mail }
};
var url = "https://graph.windows.net/" + TenantId + "/users?api-version=1.6";
var jsonObject = JsonConvert.SerializeObject(user);
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = client.PostAsync(url,
new StringContent(JsonConvert.SerializeObject(user).ToString(),
Encoding.UTF8, "application/json"))
.Result;
if (response.IsSuccessStatusCode)
{
dynamic content = JsonConvert.DeserializeObject(
response.Content.ReadAsStringAsync()
.Result);
// Access variables from the returned JSON object
var appHref = content.links.applications.href;
}
}
But i have no success, getting this response:
{StatusCode: 400, ReasonPhrase: 'Bad Request', Version: 1.1, Content:....}
Any ideas what i should do? I succeeded using Powershell-script, but I need to do this in my C# app.
Thank you for your response Fei Xue, i believe i had the right permissions. What i did to solvem my problem.
First off i removed my own custom class "NewUser", then i downloaded this sample-project: https://github.com/AzureADQuickStarts/B2C-GraphAPI-DotNet/blob/master/B2CGraphClient/B2CGraphClient.cs to eliminate the risk that my code was wrong. I modified it to support my needs, then i created a simple JObject:
var jsonObject = new JObject
{
{"accountEnabled", true},
{"country", customer.CustomerBase.Company},
{"creationType", "LocalAccount"},
{"displayName", pendingCustomer.Email.Trim()},
{"passwordPolicies", "DisablePasswordExpiration,DisableStrongPassword"},
{"passwordProfile", new JObject
{
{"password", pwd},
{"forceChangePasswordNextLogin", true}
} },
{"signInNames", new JArray
{
new JObject
{
{"value", pendingCustomer.Email.Trim()},
{"type", "emailAddress"}
}
}
}
};
client = new B2CGraphClient(ClientId, ClientSecret, TenantId);
var response = await client.CreateUser(jsonObject.ToString());
var newUser = JsonConvert.DeserializeObject<User>(response);
From B2CGraphClient.cs
private async Task<string> SendGraphPostRequest(string api, string json)
{
// NOTE: This client uses ADAL v2, not ADAL v4
var result = authContext.AcquireToken(Globals.aadGraphResourceId, credential);
var http = new HttpClient();
var url = Globals.aadGraphEndpoint + tenant + api + "?" + Globals.aadGraphVersion;
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
var formatted = JsonConvert.DeserializeObject(error);
//Console.WriteLine("Error Calling the Graph API: \n" + JsonConvert.SerializeObject(formatted, Formatting.Indented));
Logger.Error("Error Calling the Graph API: \n" + JsonConvert.SerializeObject(formatted, Formatting.Indented));
}
Logger.Info((int)response.StatusCode + ": " + response.ReasonPhrase);
return await response.Content.ReadAsStringAsync();
}
This finally solved all my problems, it was probably an format-error in the serialization of my NewCustomer-class, which then got rejected by the API.
Did you grant the app sufficient permission to operate users? The create user REST API works well for me for the B2C tenant.
Here are the steps I tested:
1.Create the app via the PowerShell below
PowerShell:
$bytes = New-Object Byte[] 32
$rand = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$rand.GetBytes($bytes)
$rand.Dispose()
$newClientSecret = [System.Convert]::ToBase64String($bytes)
New-MsolServicePrincipal -DisplayName "My New B2C Graph API App" -Type password -Value
2.Grant the app to User Account Administrator role.
Add-MsolRoleMember -RoleObjectId fe930be7-5e62-47db-91af-98c3a49a38b1 -RoleMemberObjectId 7311370c-dac3-4f34-b2ce-b22c2a5a811e -RoleMemberType servicePrincipal
3.Get the token for the app with client credential flow
POST: https://login.microsoftonline.com/adb2cfei.onmicrosoft.com/oauth2/token
grant_type=client_credentials&client_id={AppPrincipalId return by PowerShell}&client_secret={client_secret}&resource=https%3A%2F%2Fgraph.windows.net
4.Create the user with REST below:
POST: https://graph.windows.net/adb2cfei.onmicrosoft.com/users?api-version=1.6
authorization: bearer {token}
content-type: application/json
{
"accountEnabled": true,
"creationType": "LocalAccount",
"displayName": "Alex Wu",
"passwordProfile": {
"password": "Test1234",
"forceChangePasswordNextLogin": false
},
"signInNames": [
{
"type": "userName",
"value": "AlexW"
},
{
"type": "emailAddress",
"value": "AlexW#example.com"
}
]
}