Azure & Microsoft Graph: Access denied accessing inbox, using Client Secret - c#

We tried to setup the simplest possible application for reading emails from a mailbox in our organization, using Microsoft Graph.
It's a background service so we used ClientSecretCredential as authentication strategy.
Here is the code:
var credentials = new ClientSecretCredential("xxx-tenantID","xxx-clientId","xxx-clientSecret");
var graphServiceClient = new GraphServiceClient(credentials, new string [] {"https://graph.microsoft.com/.default"});
var result = await graphServiceClient.Users["myaddress#mycompany.com"].MailFolders["Inbox"] .Messages.Request() .GetAsync();
It seems to login correctly but then it gets an access denied when tring to access the specific inbox:
Unhandled exception. Status Code: Forbidden
Microsoft.Graph.ServiceException: Code: ErrorAccessDenied
Message: Access is denied. Check credentials and try again.
I think we miss some authorization on the azure side; in particular, we can't find the place where the application is authorized to access that specific mail box (myaddress#mycompany.com).
Update:
When we give to the application the authorization showed below, it works.
Unfortunately this gives access to all the mailboxes in the organization and this is unacceptable since this app should only access the service mail box it was designed to manage.
The question now is how to limit the access to the only mailbox required.
Sorry for italian language in the screenshot, the highlighted text means "Adminisrator consent" and is the settig that made the application work.

To summarize you want to use client credentials to access a single mailbox?
You can use this powershell command to create an access policy to restrict this application to a single mailbox or group of mailboxes.
https://learn.microsoft.com/en-us/powershell/module/exchange/new-applicationaccesspolicy
Connect-ExchangeOnline
New-ApplicationAccessPolicy -AccessRight RestrictAccess -AppId <String[]> -PolicyScopeGroupId your.service#account.com

Related

Accessing Guest outlook.office365.com mailbox via IMAP- Authenticate Failed Error

I have followed all steps to use client credentials grant flow to authenticate IMAP.
Registered my app on Azure AD (multitenant)
Set App permissions (Screenshot attached)
Set Service Principals
Acquiring token with scope- "https://outlook.office365.com/.default" and doing post- https://login.microsoftonline.com/{TenantId}/oauth2/v2.0/token
I do get the token but it throws "NO AUTHENTICATE FAILED" error.
Here's the issue- The app was created and permissions set using TESTDOMAIN1 account. So it works/authenticates without any problems for emails with abc#TESTDOMAIN1.com.
But if I try to access a guest account(account added in Azure using invitation) like xyz#TESTDOMAIN2.com, it generates the token but throws NO AUTHENTICATE FAILED error.
I tried updating the service principal as well to access this emailbox but I got error there too. (Screenshot attached)
Please suggest if I'm missing something here. All I want to do is access any of my users (Azure AD Users or external users) to access my app and be able to use the api and give access to mailbox.
ApiPermissions
GuestAccount
ServicePrincipalError
I tried to reproduce the same in my environment and got the below results:
I created an Azure AD application and added permission like below:
I generated the authorization code with below parameters.
GET
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
client_id=e626f30a-80ea-4530-81e9-ebxxxxx
&response_type=code
&redirect_uri=https://jwt.ms
&response_mode=query
&scope=https://graph.microsoft.com/IMAP.AccessAsUser.All offline_access
&state=12345
I generated access token successfully like below.
https://login.microsoftonline.com/common/oauth2/v2.0/token
grant_type:authorization_code
client_id:e626f30a-80ea-4530-81e9-ebxxxxx
client_secret:client_secret
scope://graph.microsoft.com/IMAP.AccessAsUser.All offline_access
redirect_uri:https://jwt.ms
code:code

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.

How can I return user- specific information from MS Graph API?

I am trying to get a list of planner plans associated with a user. I can do this in Graph Explorer (logged in with my work Microsoft account) using the following API call:
https://graph.microsoft.com/v1.0/users/<my-email>/planner/plans
However, I need to use a token from my console app, as I need to run it as a scheduled task with no user intervention. From my app I get an access denied error:
401 - Unauthorized: Access is denied due to invalid credentials. You do not have permission to view this directory or page using the
credentials that you supplied.
I can successfully call "generic" methods (such as https://graph.microsoft.com/v1.0/groups ) but not something specific to a user.
I am getting my token using:
context = new AuthenticationContext("https://login.microsoftonline.com/<my_tenant");
context.AcquireTokenAsync("https://graph.microsoft.com", new ClientCredential(clientId, appKey));
How can I give this token the necessary permissions to read stuff like planner? I've tried in the Azure Portal under the Azure AD blade:
According to your description, I assume you want to list the plans of a user.
Based on my test, we can use the follow simple code:
public PlanClientService()
{
_serviceClient = GraphSdkHelper.GetGraphServiceClient();
}
public async Task<IList<PlannerPlan>> PlannerPlansAsync()
{
var plans = await _serviceClient.Me.Planner.Plans.Request().GetAsync();
return plans;
}
We can refer to the simple code for more detail.
And according to the error description, you should add the Group.Read.All, Group.ReadWrite.All permission when you get the accesstoken.

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