Create Microsoft Graph GraphServiceClient with user/password unattended - c#

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.

Related

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

Web application requiring admin consent during token retrieval

My goal is to bypass the login screen and use Azure AD as the Identity Provider.
Given I am already logged in with my Azure AD user, I'd like to retrieve the authorization token using the silent flow and use this for resources that are protected.
I have a web application running on ASP.NET MVC 5 and a user-managed and backed by Azure AD (i.e. federated user). As a starting point, I have followed the steps in this article: Integrated Windows Authentication.
If I understand correctly, I should be able to use silent authentication since my users are federated and my application is registered as a public application.
In Azure AD, my app is registered with the following properties:
The code is straightforward, from the url.
var app = PublicClientApplicationBuilder.CreateWithApplicationOptions(
new PublicClientApplicationOptions()
{
ClientId = "<clientId>",
TenantId = "<tenantId>",
LogLevel = LogLevel.Verbose,
AzureCloudInstance = AzureCloudInstance.AzurePublic,
})
.Build();
var scopes = new [] { "User.Read" };
var result = await app.AcquireTokenByIntegratedWindowsAuth(scopes)
.WithUsername("<username>")
.ExecuteAsync();
The call for acquiring the token throws the following exception:
"AADSTS65001: The user or administrator has not consented to use the application with ID 'xxx' named 'xxx'. Send an interactive authorization request for this user and resource"
Per the documentation, User.Read doesn't need Admin Consent.
So what am I doing wrong here?
EDIT:
I have constructed a URL that prompts for user consent: https://login.microsoftonline.com/tenantId/oauth2/authorize?client_id=clientId&response_type=code&redirect_uri=<myApp>&nonce=1234&resource=User.Read&prompt=consent
It takes me to the screen where I need to pick my account, and after that, I get redirected to my app, where I get the same exception again.
So it doesn't show any consent screen, just asking me to pick the Microsoft account I want to use. Is this because of consent for User.Read is already granted by the admin?
But why am I still receiving the error? I'm a bit confused at this point.
You're mixing two different AAD OAuth mechanisms (aka v1 and v2 endpoints). The v1 endpoint uses Resources (https://graph.microsoft.com) while the v2 endpoint use Scopes (user.read). So when you request resource=User.Read, you are passing it an invalid resource name.
I would recommend using the v2 endpoint with the following URI prototype (line breaks for readability):
https://login.microsoftonline.com/common/oauth2/v2.0/authorize
?client_id={clientId}
&response_type=id_token
&redirect_uri={your_app}
&response_mode=fragment
&scope=user.read
Because Integrated Windows Authentication is a silent flow, the user of your application must have previously consented to use the application or the tenant admin must have previously consented to all users in the tenant to use the application.
You can force user consent through a URL request with prompt=consent, the url will look like:
https://login.microsoftonline.com/<tenant-id>/oauth2/authorize?client_id=<client id>&response_type=code&redirect_uri=<Your-Redirect-URI-Https-Encoded>&nonce=1234&resource=<your-resource-Https-encoded>&prompt=consent
If it needs admin consent, use $prompt=admin_consent instead.(need to use admin account to sign in)
For more details about fix error AADSTS65001, you could refer to this article.
With delegated permissions, every user will need to consent to the application. If you do not want that requirement, a tenant administrator can consent for all users from the Azure Portal.

Adal.NET acquiretoken for current application

Is it possible to acquire a jwt token for the current application and not based on another Azure app or keep it through the entire lifecycle of signed in?
In my app I'm authenticating my user to Azure with Owin and OpenIdConnect. Authentication succeeded and I received a token from Azure.
In later methods I need to pass the token as parameter, so now I store it in session variable, but this expires earlier and results in an empty variable.
Now I'm trying to acquire a token from Azure with ADAL.
string userObjectId = ((ClaimsIdentity)User.Identity).Claims.FirstOrDefault(c => c.ToString() == "http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
AuthenticationContext authContext = new AuthenticationContext(ConfigurationManager.AppSettings["ida:AADInstance"] + ConfigurationManager.AppSettings["ida:TenantId"], new TokenCache());
ClientCredential credential = new ClientCredential(ConfigurationManager.AppSettings["ida:ClientId"], "client_secret");
var result = authContext.AcquireTokenAsync(ConfigurationManager.AppSettings["ida:ClientId"], credential).Result.AccessToken;
This results in an error:
{"error":"invalid_grant","error_description":"AADSTS50105: Application >
'4337e286-7b84-4377-8843-82ea9504606b' is not assigned to a role for the application '4337e286-7b84-4377-8843-82ea9504606b'}
For other apps, it is set up with another app which has permissions for the resource, but I don't want to depend on another app to get the token.
Anyone an idea?
EDIT
I've followed the suggestion of #Jean-MarcPrieur, but on the line
var accounts = await application.GetAccountsAsync();
it returns no accounts which results in an empty result and no accessToken.
Here you are trying to use the client credentials flow (which requires your app to register permissions to call the Web API you want to call - tenant admin permissions).
If you want to access resources in the name of the user, you need to acquire a token for your app to call the API in the name of the signed-in user.
See for instance this sample: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/aspnetcore2-2-signInAndCallGraph which signs-in an user and calls the graph
It's an ASP.NET Core sample using the Azure AD v2.0 endpoint (the latest technos). If you have other constraints, don't hesitate to ask.

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

invalid_grant - type = password: user or admin has not consented to use the application

I am getting a consent error when trying to obtain a token. Because of our application, we can't show an interactive dialog to give consent.
"AADSTS65001: The user or administrator has not consented to use the
application with ID <'my native client app id'>. Send an
interactive authorization request for this user and resource.
AuthenticationContext ctx = new AuthenticationContext(
string.Format("https://login.microsoftonline.com/{0}","mytenant.onmicrosoft.com"));
UserPasswordCredential cred = new UserPasswordCredential("login#mytenant.onmicrosoft.com", "Password");
var result = ctx.AcquireTokenAsync("my api uri", "my native client id", cred);
We are using the grant_type=password and client_id is a Native app id, and resource is the Web API app URI.
Permissions-wise, from the client app, a delegated permission has been given to access the api app and have also tried setting oauth2AllowImplicitFlow : true in the manifest.
All applications have been created in the new preview Azure AD section of the new portal (portal.azure.com)
Unfortunately if your application needs access to certain resources like the Graph API, you will need to prompt for consent at least one time.
Even if your app doesn't have an interactive login experience, you should be able to prompt this once to unblock your scenario in your tenant.
Use the following URL:
https://login.microsoftonline.com/<TenantID>/oauth2/authorize?client_id=<AppID>&response_type=code&redirect_uri=<RedirectURI>&resource=<ResourceURI>&prompt=admin_consent
You can see here we have just simply generated the login URL which would be generated as part of an interactive login experience. You will need to fill out your own specific data like Reply URL, App ID, Resource URI, etc...
Note that we added a final query string at the end where we are forcing a "consent" prompt. This should be done by an Administrator, who would be able to consent on behalf of the whole tenant. Once you have done that, the username/password flow should start working for you.
Also, as an additional note, implicit grant flow has nothing to do with consent.
Please read this section in the OAuth 2 spec:
https://www.rfc-editor.org/rfc/rfc6749#section-1.3.2
You should only use this setting if you are creating a single-page application with something like JavaScript; Otherwise, there are significant security concerns with this setting on applications that should not have it.

Categories