Authentication failed exception with MailKit OAuth2.0 - c#

I'm using the following code to get an access token and connect to the mail folder:
var confidentialClientApplicationBuilder = ConfidentialClientApplicationBuilder.Create(clientId).WithClientSecret(clientSecret).WithTenantId(tenantId).Build();
var scopes = new string[] { ".default" };
var authToken = await confidentialClientApplicationBuilder.AcquireTokenForClient(scopes).ExecuteAsync();
var oauth2 = new SaslMechanismOAuth2(username, authToken.AccessToken);
using (ImapClient client = new ImapClient())
{
await client.ConnectAsync("outlook.office365.com", 993, SecureSocketOptions.SslOnConnect);
await client.AuthenticateAsync(oauth2);
//TODO
await client.DisconnectAsync(true);
}
Everything seems to work correctly here, the ImapClient is connected and I can see oauth2.Credentials.Password is populated with the access token. However, when I run it the AuthenticateAsync method throws the error:
MailKit.Security.AuthenticationException: 'Authentication failed.'
I have noticed that the authToken.Account is null and that's why I'm passing the account name in by the string username. Also it seems I have to use the .default scope as anything else causes an error on AcquireTokenForClient as per this question.
Any ideas what I'm doing wrong here?

It seems that what you want is not possible at this time. See this Github issue for details.
Basically, using ConfidentialClientApplicationBuilder can only use scopes defined as "API permissions" on your AppRegistration. If you have registered IMAP.AccessAsUser.All or Mail.Read Graph permissions, and requested them using the https://graph.microsoft.com/.default scope, you will get an access token, but it can only be used by the Graph API REST endpoints Microsoft has exposed.
MailKit does not support these Graph API endpoints (as the linked issue describes).
In order to use the IMAP support in MailKit it seems you must get an access token using PublicClientApplicationOptions as demonstrated in the MailKit example. This has the disadvantage of popping up the browser asking the user to authenticate themselves.
It is, however, uncertain how long this will work, as it seems Microsoft will deprecate their IMAP endpoints (as mentioned in the previously linked issue)

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

Get Microsoft accessToken silently

I'm trying to connect to Graph API and get user access token.
My problem is that I don't know how to connect to Graph API with credentials silently (without browser).
I currently use MSLogin() for get access token but it open a browser where you can authorize an AzureAD app to get some access to your account. A library in Java is litteraly what I want in c# https://github.com/Litarvan/OpenAuth
I need something like: MSGraph.ConnectAsync(email, pass).getAccessToken();
Here my current code (Through a browser)
private const string ClientId = "520f6e8e-xxxxxxxxxxxxxxxxxxxx";
private string[] scopes = { "https://graph.microsoft.com/user.read" };
private static AuthenticationResult authResult;
public static IPublicClientApplication PublicClientApp;
private async Task<AuthenticationResult> MSLogin()
{
PublicClientApp = PublicClientApplicationBuilder.Create(ClientId).WithRedirectUri("msal520f6e8e-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx://auth").Build();
authResult = await PublicClientApp.AcquireTokenInteractive(scopes).ExecuteAsync();
return authResult;
}
If you are using Microsoft Graph .NET Client Library you can check documentation with example how to implement username/password authentication flow.
string[] scopes = {"User.Read"};
var usernamePasswordCredential = new UsernamePasswordCredential("username#domain.com", "password", tenantId, clientId);
var graphClient = new GraphServiceClient(usernamePasswordCredential, scopes);
var me = await graphClient.Me.Request().GetAsync();
You can use AcquireTokenByUsernamePassword() for that, see MSDN.
Note however that Microsoft discourages usage of this flow and depending on your AzureAD setup there might be restrictions (i.e. you can aquire tokens only within a certain IP range etc).
Well, you can get the access token silently but not at the first time, First a user must authorize your app by going through Microsoft's Login flow and for your subsequent calls to Microsoft, you can get the access token without the intervention of user.
I would just give a basic idea, without focusing on a specific SDK that you might be using. For which, you can decide which ever method suits your needs.
I assume, you already have your credentials and desired scopes with you, otherwise you need to obtain those.
Formulate a proper URL using the credentials you obtained, plus you need to add an extra scope in the URL which is offline_access. Then you need to redirect the user to Microsoft for the initial authorization.
If the user logs in successfully, Microsoft will redirect the user back to your website with an Authorization Code.
Grab that Authorization Code and exchange it for an Access Token using /oauth2/{version}/token api.
You will receive a response from above call which will contain an Access Token along with a Refresh Token. You need to store the refresh token somewhere for future use.
Now comes the interesting part.
Using the refresh token, you can renew the access token when it expires without user's intervention. You can use oauth2/v2.0/token api with parameters:
client_id={your_client_id}&scope={your_scopes}&refresh_token={refresh_token_obtained}&grant_type=refresh_token&client_secret={your_client_secret}
The resultant response would look something like this:
{
"access_token": "new access token",
"token_type": "Bearer",
"expires_in": 3599,
"scope": "your scopes",
"refresh_token": "refresh token",
}
REF: https://learn.microsoft.com/en-us/graph/auth-v2-user#authorization-request

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

Authenticate Office 365 IMAP Account using Unattended C# Console

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

Get Plans returns 401 - Unauthorized

I am new to Microsoft Graph. I wanted to create a console app which will fetch all the Group Ids based on a Group Name and then fetch a particular Plan Id with that Group Id and Plan Name (I may or may not be a member of that group).
I have written a code which is able to fetch group Id but when I try to use it to fetch the Plan Id:
var targetGroup = await _graphClient
.Groups
.Request()
.Filter($"startsWith(displayName,'{groupName}')")
.GetAsync();
var groupId = targetGroup.First().Id;
var plans = await _graphClient
.Groups[groupId]
.Planner
.Plans
.Request()
.GetAsync();
This last line fires an exception:
401 - Unauthorized: Access is denied due to invalid credentials.
I am using the O365 E3 Trial version. I have read all the solutions posted here and tried them but the error is still there. I have allowed all the permissions (Delegated and Application) for Users, Tasks, Directory, Files, and Groups.
Could it be a problem with permissions or the trail version?
Update:
I also tried this:
GraphHttpClient.MicrosoftGraphV1BaseUri + $"groups/{groupId}/planner/plans";
It works fine for groupId but when I add /planner/plans it throws the same exception.
Are you using User access token or Application token? Because that api (GET /groups/{group-id}/planner/plans) it's not supported for Application token according to documentation.
I'm not sure for User access token but I think you must be a member of the group to reach out to plans.
The sample code you've refferenced is using the Client Credentials OAuth grant. This will result in assigning Application scopes to your token rather than Delegate scopes:
AuthenticationResult authResult = null;
authResult = await _clientApplication
.AcquireTokenForClient(_scopes) // This is "Client Credentials"
.ExecuteAsync();
return authResult.AccessToken;
Specifically, the AcquireTokenForClient call is where this is happening. From the documentation:
Acquires a token from the authority configured in the app, for the confidential client itself (in the name of no user) using the client credentials flow. See https://aka.ms/msal-net-client-credentials.
As Martin Jones stated in his answer, /plans only supports Delegated permission scopes:
Delegated (work or school account): Group.Read.All, Group.ReadWrite.All
Delegated (personal Microsoft account): Not supported.
Application: Not supported.
In order to call /plans, you will need to acquire a token using a different OAuth Grant which supports Delegated permissions such as AcquireTokenOnBehalfOf or AcquireTokenByAuthorizationCode.

Categories