Constructing Microsoft Graph with delegate access in .net full framework - c#

I am trying to successfully construct the graph client with delegate permissions in .net 4.5 framework. I have literally tried all the ways I have found on the internet, and all of them are not working.
I have my application registered in azure ad with delegate permissions, but have not had any luck constructing it. Here is the latest that I have tried:
//private string[] _scopes = new string[] { "https://graph.microsoft.com/.default" };
//private string[] _scopes = new string[] { "https://graph.microsoft.com/User.ReadWrite.All" };
//private readonly string[] _scopes = new string[] { "User.Read" };
private readonly string[] _scopes = new string[] { "User.Read.All" };
public GraphServiceClient GetAuthenticatedGraphClient(ClaimsIdentity userIdentity) =>
new GraphServiceClient(new DelegateAuthenticationProvider(
async requestMessage =>
{
// Passing tenant ID to the sample auth provider to use as a cache key
var accessToken = await _authProvider.GetUserAccessTokenAsync();
// Append the access token to the request
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}));
}
public async Task<string> GetUserAccessTokenAsync()
{
try
{
var result = await _app.AcquireTokenForClient(_scopes)
.ExecuteAsync();
return result.AccessToken;
}
// Unable to retrieve the access token silently.
catch (Exception ex)
{
throw new ServiceException(new Error
{
Code = GraphErrorCode.AuthenticationFailure.ToString(),
Message = "Caller needs to authenticate. Unable to retrieve the access token silently."
});
}
}
Can I get a full code snipet to construct the Microsoft graph client properly for delegate permission for a full framework web application?

I found a valid example finally that had the proper setup for delegate permissions for microsoft graph:
https://developer.microsoft.com/en-us/graph/quick-start?appID=383cb18f-fedb-4165-a82f-e9ed354d124c&appName=My%20ASP.NET%20App&redirectUrl=https://localhost:44375/&platform=option-dotnet

Related

Obtaining access to Graph API without user interaction

I'm new to graph API. I'm creating an app that can access user email using Graph API. I've been using the device token method which is used here.
The above code is working with my application. But I want to automate this process. I found some help from Microsoft documents here, from the sample codes here, and from this SO post.
But I can't get my code to obtain the token automatically. I thought it was an API permissions issue so I set them up like below.
I was able to get the AcquireTokenInteractive but AcquireTokenSilent is not working.
UPDATE: I've managed to get an exception the message is this "No account or login hint was passed to the AcquireTokenSilent call.". Also found that variable FirstAccount seems to be empty.
Below is my code for obtaining the token.
using Azure.Core;
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.Identity.Client;
using System.Net.Http.Headers;
namespace DEA
{
public class GraphHelper
{
private static GraphServiceClient? graphClient;
private static AuthenticationResult token;
private static IPublicClientApplication? application;
public static async void InitializeAuto(string ClientID, string InstanceID, string TenantID, string GraphUrl, string[] scopes)
{
string auth = string.Concat(InstanceID, TenantID);
application = PublicClientApplicationBuilder.Create(ClientID)
.WithAuthority(auth)
.WithDefaultRedirectUri()
.Build();
try
{
var accounts = await application.GetAccountsAsync();
graphClient = new GraphServiceClient(GraphUrl,
new DelegateAuthenticationProvider(async (requestMessage) =>
{
token = await application.AcquireTokenInteractive(scopes, accounts.FirstOrDefault()).ExecuteAsync();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token.AccessToken);
}
));
}
catch (Exception ex)
{
Console.WriteLine("Exception thrown: {0}", ex.Message);
}
}
}
The above function is called from my "Program.cs".
using DEA;
using Microsoft.Extensions.Configuration;
var appConfig = LoadAppSettings();
if (appConfig == null)
{
Console.WriteLine("Set the graph API pemissions. Using dotnet user-secrets set .... They don't exsits in this computer.");
return;
}
var appId = appConfig["appId"];
var TenantId = appConfig["TenantId"];
var Instance = appConfig["Instance"];
var GraphApiUrl = appConfig["GraphApiUrl"];
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
GraphHelper.InitializeAuto(appId, Instance, TenantId, GraphApiUrl, scopes);
static IConfigurationRoot? LoadAppSettings()
{
var appConfig = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.Build();
// Check for required settings
if (string.IsNullOrEmpty(appConfig["appId"]) ||
string.IsNullOrEmpty(appConfig["scopes"]))
{
return null;
}
return appConfig;
}
I don't know what I'm doing wrong. I used a try ... catch but still nothing. The only error I get is an exception thrown by the graph client call when I press option 2 of my app.
Can someone please help me to solve this, please?
After reading #Marc and #Jeremy Lakeman I rewrote the code using IConfidentialClientApplication. And used this as a guide from Microsoft guides.
And came up with the below code and it works now.
public static async void InitializeAuto(string ClientID, string InstanceID, string TenantID, string GraphUrl, string ClientSecret, string[] scopes)
{
string auth = string.Concat(InstanceID, TenantID);
application = ConfidentialClientApplicationBuilder.Create(ClientID)
.WithClientSecret(ClientSecret)
.WithAuthority(new Uri(auth))
.Build();
Console.WriteLine("Auth: {0}", auth);
Console.WriteLine("Client Secrets: {0}", ClientSecret);
try
{
graphClient = new GraphServiceClient(GraphUrl,
new DelegateAuthenticationProvider(async (requestMessage) =>
{
AuthToken = await application.AcquireTokenForClient(scopes).ExecuteAsync();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", AuthToken.AccessToken);
}
));
/*result = await application.AcquireTokenForClient(scopes)
.ExecuteAsync();
Console.WriteLine("Token: {0}", result.AccessToken);*/
}
catch (MsalUiRequiredException ex)
{
// The application doesn't have sufficient permissions.
// - Did you declare enough app permissions during app creation?
// - Did the tenant admin grant permissions to the application?
Console.WriteLine("Exception: {0}", ex.Message);
}
catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011"))
{
// Invalid scope. The scope has to be in the form "https://resourceurl/.default"
// Mitigation: Change the scope to be as expected.
Console.WriteLine("Scope provided is not supported");
}
}
Could you please do a fresh start and try again with AcquireTokenSilent(), lets see if you got any error .
{
var accounts = await application.GetAccountsAsync();
result = await application.AcquireTokenSilent(scopes,
accounts.FirstOrDefault())
.ExecuteAsync();
}
catch (MsalUiRequiredException ex)
{
result = await application.AcquireTokenInteractive(scopes)
.WithClaims(ex.Claims)
.ExecuteAsync();
}
doc - https://github.com/Azure-Samples/ms-identity-dotnet-desktop-tutorial/tree/master/1-Calling-MSGraph/1-1-AzureAD

OAuth 2.0 Mailkit "Authentication failed" in MVC, but c# console-app works fine

Because Microsoft ends the support for Basic Authentication access for IMAP in Office 365 I try to update our application to use OAuth 2.0. We use MailKit in a MVC .Net web-application to access an IMAP mailbox, but I get an error saying Authentication failed. However, as a test, I can get it to work in a c# console-application.
The strange thing is:
If I copy the access-token I acquired using the console-application and use it in my web-application I can successfully authenticate and read emails. So that part works.
The authentication itself seems to be successful in the web-application. Our webapp redirects to the Microsoft login-page, MFA works, I see successful audits in Azure A/D and I do get a token in the callback. However, this token gives the Authentication failed error by Mailkit.
In Azure A/D I see some of these errors between the successful audits, but I'm not sure whether they are related or not: Error AADSTS16000 SelectUserAccount - This is an interrupt thrown by Azure AD, which results in UI that allows the user to select from among multiple valid SSO sessions. This error is fairly common and may be returned to the application if prompt=none is specified.
I already verified that the scope for which I acquire a token is the same for both console and web.
The main difference is that I use pca.AcquireTokenInteractive(scopes) in the console application to acquire the token, but I use a webclient call with a call-back in the MVC-controller.
Here is my code (MVC):
public ActionResult Index()
{
string clientID = "[client-id here]";
string clientSecret = "[client-secret here]";
string redirectUri = "[redirectUri here]";
AuthorizationServerDescription server = new AuthorizationServerDescription
{
AuthorizationEndpoint = new Uri("https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize"),
TokenEndpoint = new Uri("https://login.microsoftonline.com/organizations/oauth2/v2.0/token"),
ProtocolVersion = ProtocolVersion.V20,
};
List<string> scopes = new List<string>
{
"email",
"offline_access",
"https://outlook.office365.com/IMAP.AccessAsUser.All"
};
WebServerClient consumer = new WebServerClient(server, clientID, clientSecret);
OutgoingWebResponse response = consumer.PrepareRequestUserAuthorization(
scopes, new Uri(redirectUri));
return response.AsActionResultMvc5();
}
public async Task<ActionResult> Authorized(string code, string state, string session_state)
{
List<string> scopes = new List<string>
{
"IMAP.AccessAsUser.All",
"User.Read",
"offline_access"
};
HttpClient httpClient = new HttpClient();
var values = new Dictionary<string, string>
{
{ "Host", "https://login.microsoftonline.com" },
{ "Content-Type", "application/x-www-form-urlencoded" },
{ "client_id", "[client-id here]" },
{ "scope", string.Join(" ",scopes) },
{ "code", code },
{ "redirect_uri", [redirectUri here] },
{ "grant_type", "authorization_code" },
{ "client_secret", "[client-secret here]" },
{ "state", state },
};
var content = new FormUrlEncodedContent(values);
var response = await httpClient.PostAsync("https://login.microsoftonline.com/organizations/oauth2/v2.0/token", content);
var jsonString = await response.Content.ReadAsStringAsync();
var oathToken = JsonConvert.DeserializeObject<OathToken>(jsonString);
var oauth2 = new SaslMechanismOAuth2("[Email here]", oathToken.access_token);
var stringBuilder = new StringBuilder();
using (var client = new ImapClient())
{
try
{
await client.ConnectAsync("outlook.office365.com", 993, SecureSocketOptions.Auto);
await client.AuthenticateAsync(oauth2);
var inbox = client.Inbox;
inbox.Open(FolderAccess.ReadOnly);
for (int i = 0; i < inbox.Count; i++)
{
var message = inbox.GetMessage(i);
stringBuilder.AppendLine($"Subject: {message.Subject}");
}
await client.DisconnectAsync(true);
return Content(stringBuilder.ToString());
}
catch (Exception e)
{
return Content(e.Message);
}
}
}
The error Authentication failed occurs at the line
await client.AuthenticateAsync(oauth2);
The problem was the scope "email".
We had to remove that. Exactly why, I don't know. It was no problem when used in the console app. Maybe it had to do with the fact we used pca.AcquireTokenInteractive(scopes) in that.

Calling MS Graph API has error CompactToken parsing failed with error code: 80049217

We implement to get the phone numbers being used in MFA of the signed-in user. We use password grant flow where we have a service account(with Global admin role) that will call MS Graph API on behalf of the user.
We are able to get the access token. However, when making a call to MS Graph encounters the error below.
Error:
ServiceException: Code: InvalidAuthenticationToken
Message: CompactToken parsing failed with error code: 80049217
MS Graph API call:
MicrosoftGraphClientSDK client = new MicrosoftGraphClientSDK();
var graphClient = client.GetAuthenticatedClient();
// Error encountered here:
var phones = await graphClient.Me.Authentication.PhoneMethods[{objectiD of the user}].Request().GetAsync();
This is how we get the access token in GetAuthenticatedClient
public MicrosoftGraphClientSDK()
{
_app_public = PublicClientApplicationBuilder.Create(clientID)
.WithAuthority("https://login.microsoftonline.com/{tenantID}")
.Build();
}
public Beta.GraphServiceClient GetAuthenticatedClient()
{
var accessToken = GetUserAccessTokenAsync();
var delegateAuthProvider = new DelegateAuthenticationProvider((requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.ToString());
return Task.FromResult(0);
});
_graphClient = new Beta.GraphServiceClient(delegateAuthProvider);
return _graphClient;
}
public async Task<string> GetUserAccessTokenAsync()
{
AuthenticationResult result;
var accounts = await _app_public.GetAccountsAsync();
if (accounts.Any())
{
result = await _app_public.AcquireTokenSilent(_scopes, accounts.FirstOrDefault())
.ExecuteAsync();
}
else
{
SecureString password = new SecureString();
foreach (char c in pass)
password.AppendChar(c);
result = await _app_public
.AcquireTokenByUsernamePassword(_scopes, username, password)
.ExecuteAsync();
}
return result.AccessToken;
}
I have search online about the error but could not get figure out the solution.
I appreciate your response. Thanks.

Using Microsoft Graph to get on premises Mail using C#

I am trying to get my mail on premises using GraphServiceClient. I am getting the token from Microsoft online account then use that token to get my emails. But because my mails are on premises I cannot access the as it throws the following error "code": "ResourceNotFound", "message": "Resource could not be discovered.". So how can I change my code to use the same token I get from online to on premises Mail using GraphServiceClient (if it is possible).
public async Task<ActionResult> Inbox()
{
string token = await GetAccessToken();
if (string.IsNullOrEmpty(token))
{
// If there's no token in the session, redirect to Home
return Redirect("/");
}
GraphServiceClient client = new GraphServiceClient(
new DelegateAuthenticationProvider(
(requestMessage) =>
{
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
return Task.FromResult(0);
}));
var mailResults = await
client.Me.MailFolders.Inbox.Messages.Request()
.OrderBy("receivedDateTime DESC")
.Select("subject,receivedDateTime,from")
.Top(10)
.GetAsync();
return View(mailResults.CurrentPage);
}
public async Task<string> GetAccessToken()
{
string accessToken = null;
// Load the app config from web.config
string appId = ConfigurationManager.AppSettings["ida:AppId"];
string appPassword = ConfigurationManager.AppSettings["ida:AppPassword"];
string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
string[] scopes = ConfigurationManager.AppSettings["ida:AppScopes"]
.Replace(' ', ',').Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
// Get the current user's ID
string userId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
if (!string.IsNullOrEmpty(userId))
{
// Get the user's token cache
SessionTokenCache tokenCache = new SessionTokenCache(userId, HttpContext);
ConfidentialClientApplication cca = new ConfidentialClientApplication(
appId, redirectUri, new ClientCredential(appPassword), tokenCache.GetMsalCacheInstance(), null);
// Call AcquireTokenSilentAsync, which will return the cached
// access token if it has not expired. If it has expired, it will
// handle using the refresh token to get a new one.
IEnumerable<IAccount> accounts = await cca.GetAccountsAsync();
IAccount fisrtAccount = accounts.FirstOrDefault();
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes, fisrtAccount);
accessToken = result.AccessToken;
}
return accessToken;
}
By default you can't access any on-prem information with the Microsoft Graph. The only exception to that is a preview feature in Exchange 2016 that allows you to access Calendar, Mail and Contact items transparently.
This feature has a number of infrastructure pre-requisites so I suggest you connect with your infrastructure team first to validate those. Once those pre-requisites are in place, you should not have to change any code from the application's perspective to access the content. Keep in mind this is a preview feature, so not supported for production workloads.

Failing to get Team details from a c# console app while it works from Graph Explorer

I need to collect Microsoft Teams data from a C# console application using Microsoft Graph.
I am using ADAL and cloned the authentication methods from the https://github.com/microsoftgraph/console-csharp-connect-sample sample.
The only difference is that I am using an HttpClient client and not a GraphServiceClient that does not implement Teams objects.
The list of required permissions have been determined with a Fiddler trace of a request made with Graph Explorer (no need for User.Read.All or User.Write.All) :
User.Read, Mail.Send, Files.ReadWrite, User.ReadWrite, User.ReadBasic.All, Sites.ReadWrite.All, Contacts.ReadWrite, People.Read, Notes.ReadWrite.All, Tasks.ReadWrite, Mail.ReadWrite, Files.ReadWrite.All, Calendars.ReadWrite
Everything works fine with my console app as long as I am not requesting any Teams resource:
I can get the list of groups "that are Teams" with the following
request: https://graph.microsoft.com/beta/groups?$filter=resourceProvisioningOptions/any(v:v eq 'Team')&$select=id,displayname,groupTypes,resourceBehaviorOptions,resourceProvisioningOptions
I can successfully get the group details with: https://graph.microsoft.com/beta/groups/{groupId}
But when I try to get the team view of that group (which I am member of) it fails with HTTP
403-Unautorized:
https://graph.microsoft.com/beta/groups/{groupId}/team
Very
frustrating to see that this last step is working well from the
Graph Explorer
My problem is very similiar with Access Denied when querying Teams in Microsoft Graph but in my case I am member of the teams I am trying to access and the request works with Graph Explorer.
Code details:
class AuthenticationHelper
{
// The Client ID is used by the application to uniquely identify itself to the v2.0 authentication endpoint.
static string clientId = Constants.ClientId;
// The list of required permissions have been determined with a Fiddler trace of a request made with Graph Explorer
// e.g. below are the permissions Grap Explorer requires to run the sample requests
public static string[] Scopes = {
"User.Read"
, "Mail.Send"
, "Files.ReadWrite"
, "User.ReadWrite"
, "User.ReadBasic.All"
, "Sites.ReadWrite.All"
, "Contacts.ReadWrite"
, "People.Read"
, "Notes.ReadWrite.All"
, "Tasks.ReadWrite"
, "Mail.ReadWrite"
, "Files.ReadWrite.All"
, "Calendars.ReadWrite"
};
public static PublicClientApplication IdentityClientApp = new PublicClientApplication(clientId);
public static string UserToken = null;
public static DateTimeOffset Expiration;
//-----------------------------------------------------------------------------------------------------------------
public static async Task<HttpClient> GetAuthenticatedHttpClient()
{
HttpClient client = null;
try
{
client= new HttpClient(new HttpClientHandler { UseCookies = true });
var token = await GetTokenForUserAsync();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// This header has been added to identify our sample in the Microsoft Graph service. If extracting this code for your project please remove.
client.DefaultRequestHeaders.Add("SampleID", "TestCSharp-AzureToken");
return client;
}
catch (Exception ex)
{
Debug.WriteLine("Could not create a graph client: " + ex.Message);
}
return client;
}
//-----------------------------------------------------------------------------------------------------------------
public static async Task<string> GetTokenForUserAsync()
{
AuthenticationResult authResult;
try
{
IEnumerable<IAccount> accounts = await IdentityClientApp.GetAccountsAsync();
IAccount firstAccount = accounts.FirstOrDefault();
authResult = await IdentityClientApp.AcquireTokenSilentAsync(Scopes, firstAccount);
UserToken = authResult.AccessToken;
}
catch (Exception)
{
if (UserToken == null || Expiration <= DateTimeOffset.UtcNow.AddMinutes(5))
{
authResult = await IdentityClientApp.AcquireTokenAsync(Scopes );
UserToken = authResult.AccessToken;
Expiration = authResult.ExpiresOn;
}
}
return UserToken;
}
}
//----------------------------------------------------
// Console entry point
class Program
{
//public static GraphServiceClient client;
public static HttpClient _client;
static async Task<string> GetHttpResponse(string url)
{
string responseBody = null;
_client = await AuthenticationHelper.GetAuthenticatedHttpClient();
HttpResponseMessage response = await _client.GetAsync(url);
response.EnsureSuccessStatusCode();
using (HttpContent content = response.Content)
{
responseBody = await response.Content.ReadAsStringAsync();
logger.Trace(responseBody);
}
return responseBody;
}
static void Main(string[] args)
{
// call 1 is working: list groups that "are Microsoft Teams"
string s;
string url = "https://graph.microsoft.com/beta/groups?$filter=resourceProvisioningOptions/any(v:v eq 'Team')&$select=id,displayname,groupTypes,resourceBehaviorOptions,resourceProvisioningOptions";
s = await GetHttpResponse(url);
Console.WriteLine(s);
// call 2 is working: Display details of one of these groups
Console.Write($"Enter the id of the group/teams to search for: ");
string groupId = Console.ReadLine().Trim().ToLower();
url = $"https://graph.microsoft.com/beta/groups/{groupId}";
s = await GetHttpResponse(url);
Console.WriteLine(s);
// call 3 is failing: Display the team view of this groups
url = url + "/team";
s = await GetHttpResponse(url);
Console.WriteLine(s);
}
}
You're missing a scope. You need to have Group.Read.All in order to read a Group or Team.

Categories

Resources