How to read exchange email with unattended console app - c#

I have been trying to implement a solution for this for days. It's my first experiment with Microsoft Graph. I had our network admin register the app and went through the quick start code in console-app-quickstart.
I looked at active-directory-dotnetcore-daemon-v2 and active-directory-dotnet-iwa-v2.
var App = PublicClientApplicationBuilder
.Create("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
.WithTenantId("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
.Build();
The PublicClientApplication has the AcquireTokenByIntegratedWindowsAuth function. This sounds good because we can launch the console app as whatever user we want to use with a scheduled task. But it errors out with WS-Trust endpoint not found. Where's WS-Trust endpoint defined?
The sample also includes the line var accounts = await App.GetAccountsAsync() but that always returns zero accounts. Some responses to searches for this say that we have to use the global tenant admin. The company doesn't like that idea at all. How can that be safe? Do we create a new user as an admin tenant just for that?
The other option is this
var App = ConfidentialClientApplicationBuilder.Create("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
.WithClientSecret("aeiou~XXXXXXXXXXX")
.WithAuthority(new Uri("https://login.microsoftonline.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"))
.Build();
The ConfidentialClientApplication doesn't have the integrated windows auth version. I can get connected and get MailFolders and Messages and process those, but it seems to work only when we use App.AcquireTokenForClient(scopes) and API permissions that allow the app to read everyone's email. Security doesn't like that much either.
I also looked at impersonation-and-ews-in-exchange. I read in some places that ExchangeWebService is deprecated and use MS Graph instead. Is the MS Graph API permissions in the EWS category mean that it's going to be around?
Can anyone out there show me the right combination of pieces needed to do this? (api permissions, client application type, scopes, authority, etc). It needs to be unattended (launched by scheduled task), needs to have permissions to read only one email box, and save the attachments.
(sorry so long)
Thanks, Mike

WS-Trust endpoint not found
The WS-Trust endpoint is your ADFS endpoint, if you have ADFS 2019 then MSAL does support that using WithAdfsAuthority see https://github.com/MicrosoftDocs/azure-docs/blob/main/articles/active-directory/develop/msal-net-initializing-client-applications.md
There are some other restriction around using WIA that are listed at the top of https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Integrated-Windows-Authentication-in-MSAL-2.x . If the constraints don't affect you it should work okay.
With the Client Credentials flow which is what your using above you can restrict the scope of the mailboxes it can access see https://learn.microsoft.com/en-us/graph/auth-limit-mailbox-access
I would stick with the Graph rather then EWS as the later is being phased out and requires more permissions as its a legacy API.

The tutorial you shared in the question is an asp.net core console app. Since you want to have a console app and use it to read exchange mails.
Therefore, what we can confirm is that: We need to use MS Graph API to read the exchange mails. Graph API required an Azure AD application with correct API permissions to generate Access token to call the API. API permissions have 2 types, Delegated for Web app because it required users to sign in to obtain the token, Application for daemon app like console application which don't require an user-sign-in.
Since you are using the asp.net core console application, you can only using Application API permission. Using Application permission means the console app has the permission to query messages of any email address in your tenant. You can't control the Graph API itself to query some specific users only. But you can write your own business logic to set authorization.
Then we can make the console application authorized to access the API, we can generate an Access token and use it in the HTTP request header to call the API, we can also use the Graph SDK. Using SDK will help to troubleshoot when met error.
using Microsoft.Graph;
using Azure.Identity;
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "tenant_id";
var clientId = "Azure_AD_app_id";
var clientSecret = "Azure_AD_client_secret";
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
var messages = await graphClient.Users["{email_address/user_account/user_id}"].Messages.Request().Select("sender,subject").GetAsync();

Related

Create a 'event as online meeting' or only the onlineMeeting using Microsoft Graph API

Any user who logged into our system (IdentityServer as Auth) under a specific tenant should be able to create an event as an online meeting (MS Teams).
We followed Build ASP.NET Core MVC apps with Microsoft Graph and Create and enable an event as an online meeting to create an application that authenticates an AD user of an organization and allow him to create an event as an online meeting.
We are able to implement it successfully and was able to create the event as an online meeting.
But the exact scenario here is any user who is authenticated in our web application (not a AD user) should be able create a MS Teams meeting event and share it with other participants who should be able to join the meeting.
I am not sure how to achieve this.
Edit
Or at least how do I create onlineMeeting ? I tried with Client credentials provider as below
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create("<<App_Id>>")
.WithTenantId("<<Tenant_Id>>")
.WithClientSecret("<<Client_Secret>>")
.Build();
ClientCredentialProvider authenticationProvider = new ClientCredentialProvider(confidentialClientApplication);
GraphServiceClient graphClient = new GraphServiceClient(authenticationProvider);
var onlineMeeting = new OnlineMeeting
{
StartDateTime = DateTimeOffset.Parse("2020-01-15T21:30:34.2444915+05:30"),
EndDateTime = DateTimeOffset.Parse("2020-01-15T22:00:34.2464912+05:30"),
Subject = "User Token Meeting"
};
var meeting = graphClient.Me.OnlineMeetings
.Request()
.AddAsync(onlineMeeting).Result;
but it was throwing
Code: Forbidden
Inner error:
AdditionalData:
request-id: <<some_id>>
date: 2020-07-09T16:42:23
ClientRequestId: <<some_id>>
I been working on your question in few days, I was going to mention some of the suggestions comes with the other answer. But in addition, the main challenge here, the system need to know who is authorized to do what.
So IMO The Best choice to solve this is creating a guest login in AzureAD, than you can use that to create a Team Events. Further more you can added an extra step after guest user logon, so that guest should enter his/her name and use it as reference.
You will need to take these two steps.
Get the right token
Create an event (but change the url to
https://graph.microsoft.com/v1.0/users/the_user#domain.com/events)
The hard part is getting the right token, you have multiple options.
Use the client credentials flow this will force an admin from every new tenant to authorize your application for their organization. You can then use the tenant from the user info to request a token for that tenant and use the user id to create the right url to post to.
Make IdentityServer save the access token and allow you to access it. At coonfiguration level you have access to token callback and there you can also save the Azure AD access token. I think you can add it to a reference token, that way it isn't transmitted everytime but your web application is still able to access it.
Use the on-behalf-of flow, this would require you to pass the Azure AD access token token retrieved from azure AD by the IdentityServer to be passed to your application.
Just remove the identity server from the flow and have your web application logging straight with Azure AD. That way you'll have the right token available all the time.
Edit
After reading your editted question, what you want is a website where the user doesn't have to be an member of your Azure AD, just wants access to some new online meeting?
Best option is to created a shared mailbox, authorize an application (with Calendar.ReadWrite). Get a token with client credentials and call Create Event and then extract the meeting url from the event (that you'll get back when the posts completes succesfully.
To create an online meeting for the "Client Credentials" flow, I used the following:
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create("my-app-client-id")
.WithTenantId("my-aad-tenant-id")
.WithClientSecret("my-client-secret")
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authProvider);
var meetingGuid = Guid.NewGuid();
var createMeetingResponse = await graphClient.Users["my-aad-user-object-id"].OnlineMeetings
.CreateOrGet(meetingGuid.ToString())
.Request()
.PostAsync();
The issue with your code is that referencing graphClient.Me causes the Graph requests to go to https://graph.microsoft.com/v1.0/me/onlineMeetings, which is not what you want in the "Client Credentials" flow. See this screenshot from the documentation found here: https://learn.microsoft.com/en-us/graph/api/onlinemeeting-createorget?view=graph-rest-1.0&tabs=csharp
I had to grant "application" permissions in Azure Portal to allow my app to access the online meetings API, and I had to create a client secret. I also had to follow this article to create a policy and grant it to specific users using Microsoft Teams PowerShell:
https://learn.microsoft.com/en-us/graph/cloud-communication-online-meeting-application-access-policy
For users not in your organization, you can invite them as a guest user to your tenant.
I had issues using the Microsoft Teams Powershell commands due to settings in Windows Remote Management, which I did something like this to work around:
https://lonesysadmin.net/2017/08/10/fix-winrm-client-issues/

How to get OAuth2 access token for EWS managed API in service/daemon application

Scenario
I have an Exchange Online environment and service/daemin (no interactive user) application on the Azure VM. Service uses EWS managed API to work with emails in the mailbox of any tenant user. Now EWS client uses Basic authentication that, according to Microsoft, will become unsupported in EWS to access Exchange Online.
Question/Issue
So, I need to find a way to get valid access token for service/daemon application to use with EWS managed API.
My findings
The following article shows an example of using OAuth 2.0 with EWS managed API. This example works, but it uses interactive method of getting consent (sign-in form appears allowing user authenticate themselves and grant requested permission to application) that is not suitable for service/daemon app scenario, because there is no interactive user.
For service/daemon application I need to use client credential authentication flow.
Registered application
Using admin account on https://aad.portal.azure.com portal I registered application with Azure Active Directory. Added client secret for registered application.
Aforementioned article uses https://outlook.office.com/EWS.AccessAsUser.All as a scope. But I did not find permission with such a URL on the portal. I found only the following permissions under Office 365 Exchange Online > Application permissions > Mail:
https://outlook.office365.com/Mail.Read Allows the app to read mail in all mailboxes without a signed-in user
https://outlook.office365.com/Mail.ReadWrite Allows the app to create, read, update, and delete mail in all mailboxes without a signed-in user.
I added both of them and granted admin consent for all users.
Getting access token
For testing purposes and simplicity I did not use any auth libraries (ADAL, MSAL etc.). I used Postman to get access token, then set token variable in debug (see code snippet later in the post).
I tried different endpoints to get acess token.
OAuth 2.0 token endpoint (v2)
POST: https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token
grant_type=client_credentials
client_id=***
client_secret=***
scope=https://outlook.office.com/EWS.AccessAsUser.All
Sending this request produces the following error response:
AADSTS70011: The provided request must include a 'scope' input parameter. The provided value for the input parameter 'scope' is not valid. The scope https://outlook.office.com/EWS.AccessAsUser.All is not valid.
I tried changing scope to https://outlook.office.com/.default. Access token was returned, but it appeared to be invalid for EWS. EWS client throws 401 error with the following value of x-ms-diagnostics response header:
2000008;reason="The token contains no permissions, or permissions can not be understood.";error_category="invalid_grant"
OAuth 2.0 token endpoint (v1)
POST: https://login.microsoftonline.com/<TENANT_ID>/oauth2/token
grant_type=client_credentials
client_id=***
client_secret=***
resource=https://outlook.office.com
Access token was returned, but also appeared to be invalid for EWS. EWS client throws 401 error with the same value of x-ms-diagnostics response header as described ealier in #1.
Use aquired access token with EWS managed API
Here is code sample that I used to test EWS client with access token acquired in Postman:
var token = "...";
var client = new ExchangeService
{
Url = new Uri("https://outlook.office365.com/EWS/Exchange.asmx"),
Credentials = new OAuthCredentials(token),
ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddress,
"user#domain.onmicrosoft.com"),
};
var folder = Folder.Bind(client, WellKnownFolderName.SentItems);
We had a similar problem: We wanted to use a Service Account to connect to a single mailbox and just doing some stuff with the EWS API (e.g. searching in the GAL) and the full_access_as_app seems like an overkill.
Fortunately it is possible:
Follow the normal "delegate" steps
And use this to get a token via username/password:
...
var cred = new NetworkCredential("UserName", "Password");
var authResult = await pca.AcquireTokenByUsernamePassword(new string[] { "https://outlook.office.com/EWS.AccessAsUser.All" }, cred.UserName, cred.SecurePassword).ExecuteAsync();
...
To make this work you need to enable the "Treat application as public client" under "Authentication" > "Advanced settings" because this uses the "Resource owner password credential flow". (This SO answer helped me alot!)
With that setup we could use a "tradional" username/password way, but using OAuth and the EWS API.
You can protect your client application with either a certificate or a secret. The two permissions that I needed to get this to work were Calendars.ReadWrite.All and full_access_as_app. I never tried acquiring my token via PostMan, but use AcquireTokenAsync in Microsoft.IdentityModel.Clients.ActiveDirectory. In that call, the resource parameter I use is https://outlook.office365.com/. It's pretty simple once you know all the little twists and turns. And full disclosure: I was one lost puppy until MSFT support helped me through this. The doc on the web is often outdated, conflicting, or at best, confusing.
You need to register your app in Azure and use certificate based authentication. https://blogs.msdn.microsoft.com/emeamsgdev/2018/09/11/authenticating-against-exchange-web-services-using-certificate-based-oauth2-tokens/
I run into the same issue while following Microsoft official docs for OAuth 2.0 client credentials flow
According to the Microsoft identity platform and the OAuth 2.0 client credentials flow, the scope "should be the resource identifier (application ID URI) of the resource you want, affixed with the .default suffix" (see default scope doc).
So the question is how to convert https://outlook.office.com/EWS.AccessAsUser.All into the resource identifier.
Experimentally I manage to make it working using scope=https://outlook.office365.com/.default. I granted full_access_as_app (Office 365 Exchange Online / Application permissions) and got administrator consent for it.
I did face this issue while implementing OAuth for EWS. My application is not using EWS Managed API. Here is what all I did to make it working.
Added permission Office 365 Exchange Online > full_access_as_app to application.
Acquired access token for scope https://outlook.office365.com/.default.
POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
form-data = {
client_id,
client_secret,
grant_type: 'client_credentials',
scope: 'https://outlook.office365.com/.default',
};
Added access token as Authorization header and ExchangeImpersonation SOAP header to the request.
<SOAP-ENV:Header>
<t:ExchangeImpersonation>
<t:ConnectingSID>
<t:PrimarySmtpAddress>user#domain.com</t:PrimarySmtpAddress>
</t:ConnectingSID>
</t:ExchangeImpersonation>
</SOAP-ENV:Header>
Late answer, but since this seems to come up, and I was just working with this... why not.
If you use Microsoft's v2.0 URLs for OAUTH2 (https://login.microsoftonline.com/common/oauth2/v2.0/authorize and .../common/oauth2/v2.0/token) then the scope for Office 365 EWS is:
https://outlook.office365.com/EWS.AccessAsUser.All
You'll probably want to combine this scope with "openid" (to get the signed in user's identity) and "offline_access" (to get a refresh token). But then offline_access may not be necessary when using client credentials (because you don't have to prompt a human user for them every time you need an access token).
In other words:
params.add("client_id", "...")
...
params.add("scope", "openid offline_access https://outlook.office365.com/EWS.AccessAsUser.All")
If using v1 OAUTH2 URLs (https://login.microsoftonline.com/common/oauth2/authorize and .../common/oauth2/token) then you can use a "resource" instead of a "scope". The resource for Office 365 is https://outlook.office365.com/.
Or in other words:
params.add("resource", "https://outlook.office365.com/")
Note that in the latter case, you're not asking for any scopes (it's not possible to combine "resource" with scopes). But the token will automatically cover offline_access and openid scopes.
I used this method successfully:
Install Microsoft Authentication Library module ( MSAL.PS)
https://www.powershellgallery.com/packages/MSAL.PS/4.2.1.3
Configure Delegate Access as per MSFT instructions: https://learn.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-authenticate-an-ews-application-by-using-oauth
Configure ApplicationImpersonation for a service account as normal
Grab your token
$cred = Get-Credential
$clientid = ""
$tenantid = ""
$tok = Get-MsalToken -ClientId $clientid -TenantId $tenantid -UserCredential $cred -Scopes "https://outlook.office.com/EWS.AccessAsUser.All"

Value of 'scope' to use when getting OAuth token to read Outlook email

I have an existing application (a console app that runs as a WebJob) that uses Exchange Web Services to read emails in a shared Outlook 365 mailbox. This works, but it's using basic authentication and I want to use OAuth instead. I'm attempting to do this using Microsoft.Identity.Client.ConfidentialClientApplicationBuilder to get an access token. I've read various articles and posts online which seem to give conflicting advice about what the 'scope' parameter should be when calling AcquireTokenForClient. Some say https://graph.microsoft.com/.default, others say https://outlook.office.com/.default or https://outlook.office365.com/.default. Others seem to suggest that it should be Mail.Read rather than .Default. I've tried all of the above without success. Can anyone tell me what the correct value for 'scope' is?
I assume that you have registered your app for an Office 365 tenant. We are using EWS with modern authentication successfully for some time now. To access the users' mailboxes in your tenant using OAuth authentication you have to grant the registered application the API permission Exchange - full_access_as_app and use https://outlook.office.com/.default as scope.
var clientApp = ConfidentialClientApplicationBuilder
.Create("applicationId")
.WithTenantId("tenantId")
.WithClientSecret("secret")
.Build();
var authenticationResult = await clientApp.AcquireTokenForClient(new[] { "https://outlook.office.com/.default" }).ExecuteAsync();
var accessToken = authenticationResult.AccessToken;
Then add the token to the authorization header of the EWS requests.

MSAL + Azure App Services

I've posted this before, but the thread became pretty extensive and confusing and a resolution was never met. I'm reposting with a clear and concise block of code and my desired outcome.
I'm looking to use client-flow authentication for an Azure App Services backend.
I'd like to use MSAL, to support both Microsoft Accounts (MSA) and AAD accounts. Been stuck on this for weeks with no resolution in sight.
PublicClientApplication myApp = new PublicClientApplication("registered-app-id-in-apps.dev-portal");
string[] scopes = new string[] { "User.Read" };
AuthenticationResult authenticationResult = await myApp.AcquireTokenAsync(scopes);
JObject payload = new JObject();
payload["access_token"] = authenticationResult.AccessToken;
payload["id_token"] = authenticationResult.IdToken;
user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory, payload);
Why doesn't this work?
What do I have to do to get it to work?
Getting a 401 exception, tried with MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory as well as MobileServiceAuthenticationProvider.Microsoftaccount
--App Service Auth Config for Microsoft Account:
ClientID and ClientSecret as it appears in apps.dev.microsoft.com
--App Service Auth Config for AAD:
ClientID as it appears in apps.dev.microsoft.com
Issuer URL: https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
Client Secret: (Blank)
Been having the same issue, having set up Azure Active Directory authentication on the App Service & attempting to authenticate from a WinForms client using MSAL. Turns out that, as of the time of this writing, Azure App Service does not support AAD V2 (including MSAL). Found the below note here:
At this time, AAD V2 (including MSAL) is not supported for Azure App Services and Azure Functions. Please check back for updates.
So ADAL seems to be the only viable option at the moment, unless you handle the authentication inside your backend code yourself.

Azure AD: user_interaction_required issue when authenticating a native application

I have Exchange Online from Office 365 with a mailbox and I need to access this mailbox with my console C# application that uses Managed EWS. The requirement is that the console application should use OAuth authentication to access the Exchange Online.
I have Azure AD set up, and created an application there, received clientid and redirect uri. I have given full permissions to the application - please have a look at the screenshot below:
I'm using Active Directory Authentication Library for .NET (latest version from NuGet) to issue a token, but having a problem to get it running...
My code is:
AuthenticationContext authenticationContext = new AuthenticationContext("https://login.windows.net/rsoftgroup.onmicrosoft.com", false);
AuthenticationResult authenticationResult = null;
try
{
var authenticationTask = authenticationContext.AcquireTokenAsync(
"outlook.office365.com",
"c4fa7d60-df1e-4664-a8f8-fb072d0bb287",
new Uri(redirectUri),
new PlatformParameters(PromptBehavior.Never)
);
authenticationTask.Wait();
authenticationResult = authenticationTask.Result;
exchangeService.Credentials = new OAuthCredentials(authenticationResult.AccessToken);
}
catch (AdalException)
{
// Exception occured on the authentication process.
}
I get AdalException with message: "user_interaction_required: One of two conditions was encountered: 1. The PromptBehavior.Never flag was passed, but the constraint could not be honored, because user interaction was required. 2. An error occurred during a silent web authentication that prevented the http authentication flow from completing in a short enough time frame"
Can somebody help me how to solve it?
I need the OAuth authentication to work without user interaction, as this will be a command line application...
Any suggestions highly appreciated.
Your application still needs to authenticate as some user, currently if you look at your code you don't authenticate because of PromptBehavior.Never and you don't specify any user-credentials and use the implicit auth flow eg http://www.cloudidentity.com/blog/2014/07/08/using-adal-net-to-authenticate-users-via-usernamepassword/
For a standard Console apps where you are going to authenticate (eg ask for credentials when the app is run) I would use out of band call urn:ietf:wg:oauth:2.0:oob (you then don't need a redirection endpoint) and set your code to prompt eg
AuthenticationContext ac = new AuthenticationContext("https://login.windows.net/Common");
var authenticationTask = ac.AcquireTokenAsync(
"https://graph.windows.net",
"5471030d-f311-4c5d-91ef-74ca885463a7",
new Uri("urn:ietf:wg:oauth:2.0:oob"),
new PlatformParameters(PromptBehavior.Always)
).Result;
Console.WriteLine(authenticationTask.AccessToken);
When you run the Console app windows and the ADAL library will handle the plumbing and show the correct authentication prompts and get the Token back and you get the benefits of reduce attack surface over prompting for the credentials yourself in your code (or as parameters etc)
As Venkat comments suggests if you don't need to use EWS (eg no existing code base investment etc) then using the REST endpoints maybe a better solution if your building a daemon type application as you can take advantage of this type of auth flow eg https://blogs.msdn.microsoft.com/exchangedev/2015/01/21/building-daemon-or-service-apps-with-office-365-mail-calendar-and-contacts-apis-oauth2-client-credential-flow/

Categories

Resources