Authenticate Office 365 IMAP Account using Unattended C# Console - c#

I am developing a .NET Core Console Application (C#) that needs to authenticate to an Office 365 IMAP account. The purpose is to retrieve mail and process CSV file attachments unattended.
The app has been registered on Azure as a Mobile/Desktop app with the RedirectUri set as http://localhost.
The following code causes a new tab to open in Chrome web browser and asks for the Outlook account to use for login. I need to stop the browser from opening and handle authentication completely from code.
Current Code:
using var client = new ImapClient(new ProtocolLogger("imap.log"));
var options = new PublicClientApplicationOptions
{
ClientId = _options.ClientId,
TenantId = _options.TenantId,
RedirectUri = "http://localhost"
};
var publicClientApplication = PublicClientApplicationBuilder
.CreateWithApplicationOptions(options)
.Build();
var scopes = new[]
{
"email",
"offline_access",
"https://outlook.office.com/IMAP.AccessAsUser.All" // Only needed for IMAP
//"https://outlook.office.com/POP.AccessAsUser.All", // Only needed for POP
//"https://outlook.office.com/SMTP.AccessAsUser.All", // Only needed for SMTP
};
var cancellationToken = new CancellationToken();
var authToken = await publicClientApplication
.AcquireTokenInteractive(scopes)
.ExecuteAsync(cancellationToken);
await publicClientApplication
.AcquireTokenSilent(scopes, authToken.Account)
.ExecuteAsync(cancellationToken);
SaslMechanism oauth2;
if (client.AuthenticationMechanisms.Contains("OAUTHBEARER"))
{
oauth2 = new SaslMechanismOAuthBearer(authToken.Account.Username, authToken.AccessToken);
}
else
{
oauth2 = new SaslMechanismOAuth2(authToken.Account.Username, authToken.AccessToken);
}
await client.AuthenticateAsync(oauth2);
await client.DisconnectAsync (true);
This line triggers a browser window to open https://login.microsoftonline.com/:
var authToken = await publicClientApplication
.AcquireTokenInteractive(scopes)
.ExecuteAsync(cancellationToken);
This console application will be run unattended. How do I obtain the token and authenticate without a web browser opening up?

This is an answear to your latest comment, as it's my final recommendation. So, first of all, you should decide if you want to acess the data on behalf of user, or as an app granted permissions by admin.
First step is to register your app.
Second step is getting the acess token. This is going to differ based on the method you chose. Tutorial for each: acting on behalf of the user or acting without the user, but granted permission from admin.
Once you have the acess token, you can call the Microsoft Graph API. The important thing is, you always have to call Microsoft Graph API. There is no other official way (as far as I know) of comunicating with Microsoft's services. You can try the requests with the Microsoft Graph Explorer, however it's VERY limited with it's defaul urls/parameters, so I suggest taking a look at the docs.
From what you've described, you first want to obtain UserID. The way of doing this is going to vary based on what type of auth you chose.
If you chose to act on behalf of user, you should be able to get that (ID) using this endpoint: https://graph.microsoft.com/v1.0/me/
If you chose to act as an app with admin consent, you should be able to search for user using the https://graph.microsoft.com/v1.0/me/people/?$search= with search query parameters. Here are the docs for this endpoint
Now, the only thing left, is to supply that ID to one of the Outlook api methods. You can find docs for them here. Specifically, it seems like you want to list all messages and then read a specific message.
Also, keep an eye on what methods you use with which type of auth. On behalf of user, you usually want url's that contain /me, on behalf of app with given admin privelages, you usually want some endpoint that enables you to pass user id.
Hope I helped!
PS: There is no code in this response, because there is a lot of stuff that just cannot be coded without your decisions, actions on Azure and so on. I suggest you read a little bit about auth and graph api using microsoft docs I linked earlier.

This code worked for me using MSAL after registering the app in azure and getting a client secret.
var options = new ConfidentialClientApplicationOptions
{
ClientId = "<ClientID or Application ID>",
TenantId = "<Azure TenantId>",
RedirectUri = "http://localhost"
};
string clientSecret = "<Client Secret Goes here>";
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.CreateWithApplicationOptions(options)
.WithClientSecret(clientSecret)
.Build();
var scopes = new string[] {
"https://outlook.office365.com/.default"
};
var authToken = await confidentialClientApplication.AcquireTokenForClient(scopes).ExecuteAsync();

Related

Presence endpoint returns Forbidden with application permissions

With help from a fellow stacker, I was able to make successful calls to thre MS graph api, at least users, but I have been wanting to get the teams status of a user using the Presence function. I have not been able to get around the 403 Forbidden error. I have read much of the prose MS has written on OAuth 2.0 and have tried sample apps, graph explorer, and tried Postman as well.
I have checked my app permissions in Azure portal and according to the display, Presence is application able, not just delegate:
As I can perform a User.Read.All it is confusing that I cannot call Presence without the error. This is the basic function I got help in writing and it fails on the presence call:
static async Task MainAsync()
{
var tenantId = "some giud";
var clientId = "more guid";
var clientSecret = "even more guid";
try
{
string[] scopes = { "https://graph.microsoft.com/.default" };
ClientSecretCredential clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret);
GraphServiceClient graphClient = new GraphServiceClient(clientSecretCredential, scopes);
//var users = await graphClient.Users.Request().GetAsync();
//foreach (var user in users)
// Console.WriteLine(user.UserPrincipalName);
var userid = await graphClient.Users["my.name#company.com"].Request().GetAsync();
Console.WriteLine(userid.Id);
var presence = await graphClient.Users["{user id}"].Presence.Request().GetAsync();
// Console.WriteLine(presence)
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
I scoured google and MS looking for examples and I found one from MS and this is the link to github:
git clone https://github.com/Azure-Samples/active-directory-dotnetcore-devicecodeflow-v2.git
It will ask to perform a login via a browser and code. I got this code working and it does ask that I log in. I had to alter my registered app to this setting:
https://i.stack.imgur.com/Uz9IK.jpg
All this I did and I still get the 403 forbidden error. I am wondering if anyone has a working set of code that calls Presence and can share either what I am missing, or is this only something done in teams.
Why they have it so restricted is beyond reason given I can get more interesting user data just calling users/{emails}
I tried to reproduce the scenario and get the present status of the user .
I got the similar error forbidden when I gave a client secret which is expired and does not have Presence.Read.All delegated permissions .I only had application permissions
Then I added delegated permissions and user.read permissions (also granted admin consent).
I could get the status successfully with below code and query through graph where it uses access token (as authorization header is mandatory parameter to be passed ).
Note: Getting the presence requires users signed in.So while requesting On behalf of user, delegated permissions are must.
GraphServiceClient graphClient = new GraphServiceClient( authProvider );
var presence = await graphClient.Users["c4xx3cf2axxxxa6df-d2xxxx391"].Presence
.Request()
.GetAsync();
Presence of some user with Id
Current user’s presence:
Please make sure to go through all the required constraints to Resolve Microsoft Graph authorization errors - Microsoft Graph | Microsoft Learn
It requires Presence.Read or Presence.Read.All Delegated permissions to query the presence of the user .
Reference : Get presence - Microsoft Graph v1.0 | Microsoft Learn

Mailkit Office365 AcquireTokenInteractive requires client_secret

So this is an issue that has been plaguing me for a bit and my deadline is coming up. I'm working on an application that sends emails and my workplace uses Office365 via Exchange. I'm using a C# webapp and using Mailkit to deliver emails.
The issue (not really an issue but good practice that's getting in my way) is that we made an email account to deliver mail yet our organization requires MFA. After talking about it with my director, creating an app password would not be a good idea for how this program is deployed so I'm trying to find ways to authenticate properly.
I eventually landed on using the Microsoft.Identity.Client library to require logging in via a registered Azure application. I could then cache this and refresh as needed, this way making sure the access is still valid.
However, I'm stuck on something. I have the app registration set to public with no client secrets or certificates with all of the necessary permissions. However right at the var oauth2 step, it fails while giving the error "Original exception: AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret'."
The issue is that the application is public and is defined to allow public client flows. So I'm not getting how the request could still require a client secret when that's not how I'm building the request at all. I tried using private, but because of the MFA requirement, that fails too.
Below is what I have. Ignore that I'm hard coding stuff; it's temporary until I can get this sorted out. I'm also only scoping the SMTP permissions because all this application needs to do is send an email; IMAP isn't needed since it's not reading or anything else.
var options = new PublicClientApplicationOptions
{
ClientId = "[clientID]",
TenantId = "[tenandID]",
RedirectUri = "http://localhost"
};
var publicClientApplication = PublicClientApplicationBuilder
.CreateWithApplicationOptions(options)
.Build();
var scopes = new string[] {
"email",
"offline_access",
"https://outlook.office.com/SMTP.Send" // Only needed for SMTP
};
var authToken = await publicClientApplication.AcquireTokenInteractive(scopes).ExecuteAsync();
//Here is where it returns that it needs a client_secret and won't advance.
// The login window appears and states it was successfully authenticated,
// but the application crashes with that error at this step.
var oauth2 = new SaslMechanismOAuth2(Config.Env.smtpUser, authToken.AccessToken);

Retrieve 2 Access Tokens on Interactive Authentication in Azure AD

Development Information: Azure Registered Mobile Application, Azure hosted API, Visual Studio 2019, mobile application is built in Xamarin Forms targeting .Net-5 and API is built in Asp.Net targeting .Net-5.
Desired Workflow: User does Authorization code flow authentication interactive if AcquireTokenSilently fails, utilizes MS Authenticator. Once authenticated, the user gets an access token for Graph and for the custom hosted API.
What is working: I can do the MFA authentication piece using the MSAL library and an access token is received in the mobile application, but the token is only good for making calls to Graph. I need to be able to call the protected resource (hosted API) as well. My thoughts originally was that the JWT access token would work with the hosted API as well as Graph, but after reading more, I have read that you have to acquire two separate tokens as to prevent issues where two resources may have the same scope(s).
The problem I am facing is that I thought upon Authenticating there would be an easy way to get another resources access token without having to authenticate a second time. Isn't that the point of the OpenID authentication?? My last thought was to do an implicit authentication from the mobile app to the protected API using a client id, client secret, but I don't want to store a client secret in the mobile app for fear it may expose something sensitive to a user. I tried following the documentation of Microsoft and set up specific scopes for the hosted API and registered it in the Azure portal, but this didn't seem to have any affect on the authentication piece. I still have to authenticate against the API which isn't the desired result.
Is it possible to authenticate once for Graph and then knowing that the user authenticated correctly as they now have a valid token for Graph then somehow call this protected API without having to force them to do authentication again for the protected API and if so, how to do this without exposing any sensitive information (client secret) in the mobile application?
CODE
// Microsoft Authentication client for native/mobile apps
public static IPublicClientApplication PCA;
//Setting up the PublicClientApplicationBuilder
//OAuthSettings is a static class with required values (app id, tenant id, etc)
var builder = PublicClientApplicationBuilder
.Create(OAuthSettings.ApplicationId)
.WithTenantId(OAuthSettings.TenantId)
.WithBroker()
.WithRedirectUri(OAuthSettings.RedirectUri);
PCA = builder.Build();
//Scopes being used in initial authentication
//I tried adding to this with a custom scope, I added on the
//protected api and it caused an exception, so I didn't think
//I could use those together with the scopes for Graph
//Custom scope for the protected API is as follows:
//XXX.User.Common (where XXX is our company name)
public const string Scopes = "User.Read MailboxSettings.Read Calendars.ReadWrite";
try
{
var accounts = await PCA.GetAccountsAsync();
var silentAuthResult = await PCA
.AcquireTokenSilent(Scopes.Split(' '), accounts.FirstOrDefault())
.ExecuteAsync();
}
catch (MsalUiRequiredException msalEx)
{
var windowLocatorService = DependencyService.Get<IParentWindowLocatorService>();
// Prompt the user to sign-in
var interactiveRequest = PCA.AcquireTokenInteractive(Scopes);
AuthUIParent = windowLocatorService?.GetCurrentParentWindow();
if (AuthUIParent != null)
{
interactiveRequest = interactiveRequest
.WithParentActivityOrWindow(AuthUIParent);
}
var interactiveAuthResult = await interactiveRequest.ExecuteAsync();
var accounts = await PCA.GetAccountsAsync();
//at this point, I have a valid Graph token, but I need to
//get a valid token for the protected API,
//unsure of how to do this last piece
}
Much of this answer is thanks to Gaurav Mantri but he was too modest to accept credit, and he asked me to post an answer.
First thing I needed to change was the flow of the authentication.
Authenticate the user against the protected API, then get the Graph token, as the Graph authentication works well with the AcquireTokenSilent(IEnumerable<string> scopes, IAccount account) method of MSAL library. I was doing this backwards by authenticating against Graph then trying to get the protected API token.
So, the code for authenticating then looks like the following:
public async Task SignIn()
{
try
{
//check for any accounts which are authenticated
var accounts = await PCA.GetAccountsAsync();
//try to Authenticate against the protected API silently
var silentAuthResult = await PCA
.AcquireTokenSilent(new string[] { "api://{your api client id}/.default" }, accounts.FirstOrDefault())
.ExecuteAsync();
}
catch (MsalUiRequiredException msalEx)
{
// This exception is thrown when an interactive sign-in is required.
var windowLocatorService = DependencyService.Get<IParentWindowLocatorService>();
// Prompt the user to sign-in
var interactiveRequest = PCA.AcquireTokenInteractive(new string[] { "api://{your api client id}/.default" });
AuthUIParent = windowLocatorService?.GetCurrentParentWindow();
if (AuthUIParent != null)
{
interactiveRequest = interactiveRequest
.WithParentActivityOrWindow(AuthUIParent);
}
var interactiveAuthResult = await interactiveRequest.ExecuteAsync();
var accounts = await PCA.GetAccountsAsync();
//Now we can get the Graph token silently
//We now have valid tokens for both Graph and our protected API
var graphtokenresult = await PCA.AcquireTokenSilent(Scopes, accounts.First()).ExecuteAsync();
}
catch (Exception ex)
{
}
}
In the Azure portal there were a few things I needed to ensure were set in the configurations.
Make sure you set some custom scopes for the protected API - quickstart-configure-app-expose-web-apis
(This one isn't documented in the MSDN documentation)
On the same page you can add scopes for the protected API, there is a section called Add a client application, you would think by clicking add and then selecting an application which you want to grant permission to access your API would suffice, but it is not.
You also need to go into the protected API's Manifest and add the client id of the application you want to grant access manually, as clicking the add button and selecting the client application does not modify the manifest. So, open the manifest of your protected API and add the client application id to the section of the manifect labeled knownClientApplications:
Once all of this has been done, you can now receive an access token which will authorize against the protected API as well as a Graph token for getting user information. I hope this is helpful and thanks again to Gaurav Mantri. If anyone has more questions about this, please contact me and I'll do my best to pass on what I have learned.

How to access Microsoft Graph's services with service-user credentials

I have created a special service account in AAD that I want to use to send email notifications to users.
In asp.net core 2 web app, how do I get access token for that service account?
The samples I've seen uses user's identity, but that is not my case.
I will have probably some background process, so there cannot be any user interactivity.
I will have probably some background process, so there cannot be any user interactivity.
you could use OAuth 2 Resource Owner Password Credentials grant. Note: The resource owner password grant doesn't provide consent and doesn't support MFA either. Detailed tutorial, you could follow here. Moreover, you could use ADAL to retrieve the access_token instead of constructing the HttpClient by yourself.
The code for acquire the token via ADAL would look like:
var result = await authContext.AcquireTokenAsync("https://graph.microsoft.com","<clientId>", new UserPasswordCredential("<userName>", "<password>"));
Moreover, as juunas commented that you could use the service to service scenario and use the application permissions instead of the user delegated permissions. The code would look like this:
var result = await authContext.AcquireTokenAsync("https://graph.microsoft.com", new ClientCredential("<clientId>", "<clientSecrets>"));
For your AAD app on Azure Portal, you need to configure the required permissions for accessing Microsoft Graph API as follows:
Note: For Send mail Microsoft graph API, you need to set the related permission. For Resource Owner Password Credentials grant flow, you need to choose the delegated permission Send mail as a user (Mail.Send). While for client credential flow, you need to choose the application permission Send mail as any user (Mail.Send), then click grant permissions (this permission needs global admin to consent).
After retrieved the access_token, you could use Microsoft Graph Client Library for .NET to communicate with Microsoft Graph API to send the email. The initialization for GraphServiceClient would look like this:
//initialize the GraphServiceClient instance
var graphClient = new GraphServiceClient(
"https://graph.microsoft.com/v1.0",
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
var token = await GetAccessTokenAsync();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
}));

Create Microsoft Graph GraphServiceClient with user/password unattended

I am creating a console application that connects to Microsoft Graph using the Microsoft Graph API (as shown in https://github.com/microsoftgraph/console-csharp-connect-sample).
Everything is working fine, but I wonder if there is a way where I can authenticate a user (when I already know their user/password) without them needing to manually enter their credentials on the "Sing in to your account" window rendered on the desktop.
The idea is basically to run the application unattended, so there is no need for the user to be entering their credentials when the application starts. I can´t find any relevant information on the subject.
Is that even possible?
EDIT
After following the link #DanSilver posted about geting access without a user, I tried the sample suggested in that link (https://github.com/Azure-Samples/active-directory-dotnet-daemon-v2). Although that is an MVC application that forces users to authenticate (precisely what I wanted to avoid) I have managed to use part of the authentication code in that sample with my console application. After giving authorization to the application manually through a request to https://login.microsoftonline.com/myTenantId/adminconsent I can create a GraphServiceClient in my console app that connects to Graph without user interaction. So I mark the answer as valid.
Just in case someone is in the same situation, the GraphServiceclient is created as:
GraphServiceClient graphServiceClientApplication = new GraphServiceClient("https://graph.microsoft.com/v1.0", new DelegateAuthenticationProvider(
async (requestMessage) =>
{
string clientId = "yourClientApplicationId";
string authorityFormat = "https://login.microsoftonline.com/{0}/v2.0";
string tenantId = "yourTenantId";
string msGraphScope = "https://graph.microsoft.com/.default";
string redirectUri = "msalXXXXXX://auth"; // Custom Redirect URI asigned in the Application Registration Portal in the native Application Platform
string clientSecret = "passwordGenerated";
ConfidentialClientApplication daemonClient = new ConfidentialClientApplication(clientId, String.Format(authorityFormat, tenantId), redirectUri, new ClientCredential(clientSecret), null, new TokenCache());
AuthenticationResult authResult = await daemonClient.AcquireTokenForClientAsync(new string[] { msGraphScope });
string token = authResult.AccessToken;
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
}
));
One idea is using the "app only" authorization flow. The idea is that you can have long running apps access the Microsoft Graph without user authentication. The main difference is instead of the access token granting access to a particular user, it grants your app access to resources that you've consented to in advance. There will be no user login dialog and you can programmatically fetch access tokens to call the Graph API.
To reiterate that these tokens aren't for a particular user, consider making a GET request to 'https://graph.microsoft.com/v1.0/me'. This will return an error since the access token isn't for a particular user and "me" doesn't mean anything. Requests should be sent with full user ids "like graph.microsoft.com/users/someuser#contosos.com".
More information on this can be found at the Get access without a user documentation page.
Another idea is to let the user authenticate the first time they use your app and then store a refresh token. These tokens live longer (a few months IIRC) and then you won't need to prompt for user consent each time the app runs. Refresh tokens can be exchanged for access tokens that live 60 minutes and those can be used to call Graph API on behalf of users.
More info on refresh tokens: https://developer.microsoft.com/en-us/graph/docs/concepts/auth_v2_user#5-use-the-refresh-token-to-get-a-new-access-token
I did want to come back out here and share, since I ran into this problem yesterday, and the idea of granting read/write mailbox access for my application... to EVERYONE'S EMAIL BOX IN THE ENTIRE ORGANIZATION... was way over the top for my needs. (And that is exactly what happens when you start talking about granting Application level permissions instead of delegated permissions to your registered app).
It's a simple use case: I had a nightly process that needed to automate sending of emails from a shared mailbox using a traditional AD service account.
Thankfully... even though they are on the march to eliminate passwords (lol)... someone at Microsoft still recognizes my use case, and it's lack of apples-to-apples alternatives in Azure AD. There is still an extension method we can lean on to get the job done:
private AuthenticationContext authContext = null;
authContext = new AuthenticationContext("https://login.microsoftonline.com/ourmail.onmicrosoft.com",
new TokenCache());
result = authContext.AcquireTokenAsync("https://graph.microsoft.com/",
"12345678-1234-1234-1234-1234567890",
new UserPasswordCredential(
Environment.GetEnvironmentVariable("UID", EnvironmentVariableTarget.User),
Environment.GetEnvironmentVariable("UPD", EnvironmentVariableTarget.User)
)).Result;
You can replace those GetEnvironmentVariable calls with your Username (UID) and Password (UPD). I just stuff them in the environment variables of the service account so I didn't have to check anything into source control.
AcquireTokenAsync is an extension method made available from the Microsoft.IdentityModel.Clients.ActiveDirectory namespace. From there, it's a simple business to fire up a GraphClient.
string sToken = result.AccessToken;
Microsoft.Graph.GraphServiceClient oGraphClient = new GraphServiceClient(
new DelegateAuthenticationProvider((requestMessage) => {
requestMessage
.Headers
.Authorization = new AuthenticationHeaderValue("bearer", sToken);
return Task.FromResult(0);
}));
The last bit of magic was to add these permissions to Application registration I created in Azure AD (where that GUID came from). The application has be defined as a Public client (there's a radio button for that towards the bottom of the authentication tab). I added the following 5 DELEGATED permissions (NOT application permissions):
Microsoft Graph
1. Mail.ReadWrite.Shared
2. Mail.Send.Shared
3. User.Read
4. email
5. openid
Since user consents are actually blocked in our organization, another permissions admin had to review my application definition and then do an admin level grant of those rights, but once he did, everything lit up and worked like I needed: limited access by a service account to a single shared mailbox, with the actual security of that access being managed in Office 365 and not Azure AD.

Categories