Query only basic UserInfo from Azure AD with limited permissions - c#

I am trying to build a list of emailadresses of all employees in our company.
This is in a Web App running on Azure, with Azure AD authentication. The app uses .NET Framework 4.6, and uses the Microsoft.Owin.Security packages to connect to Azure AD.
I can authenticate, I am authorized by my role as defined in the app registration. So far so good.
I can check out my own User Profile. This uses the Microsoft.Azure.ActiveDirectory.GraphClient package to build the queries, instead of manually creating the HTTPRequests.
string userObjectID = ClaimsPrincipal.Current
.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
ActiveDirectoryClient activeDirectoryClient = new ActiveDirectoryClient(serviceRoot,
async () => await authorizationHelper.GetTokenForApplication());
var result = await activeDirectoryClient.Users
.Where(u => u.ObjectId.Equals(userObjectID))
.ExecuteAsync();
IUser user = result.CurrentPage.ToList().First();
AD computes my query, sees that it only returns me, and deduces that I am allowed to view my own info. Execute succeeds, returning just me.
On a different page, I want to show the mail addresses used in the company (to tag a user, to typeahead, to check for existance, etc.) I try the following:
result = await activeDirectoryClient.Users
.ExecuteAsync();
return result.CurrentPage
.ToList();
This fails, "insufficient privileges". I am not allowed to view the entire User I guess. My delegated permissions are to "Read all users' basic profiles", meaning:
Allows the app to read the basic profile of all users in the
organization on behalf of the signed-in user. The following properties
comprise a user’s basic profile: display name, first and last name,
photo, and email address.
So, I try to filter:
result = await activeDirectoryClient.Users
.Where(u => u.Mail.StartsWith(str))
.ExecuteAsync();
return result.CurrentPage
.ToList();
but that still gives me the entire User, and I'm yet again hit with "insufficient privileges". So I try
result = await activeDirectoryClient.Users
.Where(u => u.Mail.StartsWith(str))
.Select (u => u.Mail)
.ExecuteAsync();
return result.CurrentPage
.ToList();
Which gives the cryptic runtime error (free translation from Dutch)
GenericArguments[1], System.String, Microsoft.Azure.ActiveDirectory.GraphClient.Extensions
.ReadOnlyQueryableSet.`2[TSource,TISource] conflicts with type TISource.
And I am at a loss. No source code seems to be available for ActiveDirectoryClient, and the usual Graph API doesn't even mention selecting only basic info.
The "easy" solution would be "just get more app permissions", but that needs a lot of time to go through the bureaucracy. And I SHOULD be able to get all Users' mail, right?

First and foremost, you don't need to do anything special to "Query only basic UserInfo from Azure AD with limited permissions". You just query Users, and it will initialize any fields you do not have access to as null. Insufficient privileges means you're not allowed to look at the user at all. Red flag.
At first I thought my tokencache was faulty. Which it was. Then I fixed it. Still errors.
By looking at several tokens in different applications with the online tool linked by Nan Yu I found out I was missing the actual permission to look at users' basic profiles in my token.
I still didn't get the permission in the token, and found out that even though the checkbox under "Delegated Permissions" was checked, the "Grant Permissions" option was not succesfully used afterwards.
PSA: don't assume all checked boxes are actually granted. Check in the App Service which permissions it has. If you're allowed that by your company, that is.

Related

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.

Can I merge permissions of delegated GraphServiceClient and application GraphServiceClient?

I'm trying to implement a search algorithm for our people using Microsoft Graph. What I would like to do is have 1 (not 2) instance of GraphServiceClient and use that to query People. Then, if no search results come up, I query the Users.
Currently, I use the delegated GraphServiceClient instance to use the people search, and I use the other application GraphServiceClient to search Users. But when I try to use the delegated GraphServiceClient to search Users, it gives me an unauthorized error.
Both use the same App ID registered into Microsoft so why can't I just have one instance instead of having 2 different instances? I've already received admin permissions to read our users. I just want to have 1 instance of Microsoft Graph. Not 2.
Edit: Here's the error message I get when trying to query users using a delegated GraphServiceClient instance:
Code: Authorization_RequestDenied\nMessage: Insufficient privileges to complete the operation.\n\nInner error\n
Found the answer. I just requested Directory.Read.All permissions from the user and I was able to access the list of Users to perform my search.
I don't even need application permissions anymore!

ADAL query security groups

I have a system that logs into a native wpf app via an web api on azure.
AuthenticationContext authContext = new AuthenticationContext(string.Format("https://login.windows.net/{0}", authority));
AuthenticationResult tokenAuthResult = authContext.AcquireTokenAsync(resource, clientId, new Uri(redirectUri), new PlatformParameters(PromptBehavior.Auto)).Result;
if (tokenAuthResult == null) return;
Credentials.RestCredentials = new TokenCredentials(tokenAuthResult.AccessToken);
Credentials.UserName = string.Concat(tokenAuthResult.UserInfo.GivenName, " ", tokenAuthResult.UserInfo.FamilyName);
This all works perfect, with returning token etc.
The users are all in ADAL, and have associated groups against them (these are all O365 users).
I want to be able to query what the logged in users associated group(s) are.
Do I need to make a new call out using the Graph api?
Do I use the returned token?
I'm a little lost here.
Thanks in advance
Scott
to add to #juunas's answer, the following sample active-directory-dotnet-webapp-groupclaims explains things in details.
In particular Step 3: Configure your application to receive group claims explains the app configuration in the manifest.
See also the claims related to groups in https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-token-and-claims#claims-in-idtokens
An easy way is to go to your app registration in Azure AD, click on Manifest, and modify the groupMembershipClaims property:
"groupMembershipClaims": "SecurityGroup",
This will result in the Id token containing the user's group ids.
It does have a limit though, only a certain amount of groups can be included, in which case you would have to get them from Graph API instead.
Here is the endpoint you would need to call in that case: https://msdn.microsoft.com/en-us/library/azure/ad/graph/api/functions-and-actions#getMemberGroups.

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.

My AzureAD - registered application doesn't have the rights to do anything ...?

I'm writing a c# program right now that tries to authenticate with Azure to make a generic http request. I finally got the code working and I wanted to test the features but for every request I make I get the following error code in response:
{"error":{"code": "AuthorizationFailed", "message":"The client "(id of the app I registered in AzureAD)" with object id "(same value as before)" does not have authorization to perform action 'Microsoft.Authorization/roleAssignments/read' over scope '/subscriptions/(mysubscriptionid)'."}}.
The thing is ... The account I use to set everything up is a global admin. I checked every permission box in AzureAD I could find...
(that's 8 Application Permissions and 9 Delegated permissions in the Windows Azure Active Directory API and 1 delegated Permission in the Windows Azure Service Management API, though I don't know why there aren't more possible permissions for Azure Service Management)
the relevant code is rather basic but it works so I don't feel like I need post it, I'll just say that I obtain the Token using Microsoft.IdentityModel.Clients.ActiveDirectory.AcquireTokenAsync() with
authorityUri = "https://login.windows.net/(mytenantid)",
string resourceUri = "https://management.azure.com/";
AuthenticationContext authContext = new AuthenticationContext(authorityUri);
var res = authContext.AcquireTokenAsync(resourceUri, new
ClientCredential(clientId,clientSecret));
return res.Result;
and make the Request to
https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Authorization/roleAssignments?api-version=2016-03-01&$filter=atScope()
(as an example, this one is supposed to call the roles).
Tried several different types of GET Requests to different URIs, all give similar errors.
I'm thinking it might not be an issue with the code but a setting in the AzurePortal but everything I found there seems set up right (or rather "universal access").
According to your description, you forget to grant your service principal. You could add it on Azure Portal. You could grant Contributor or Owner role to it.
Please refer to this link:Assign application to role.

Categories