Overview of Users, Groups and Permissions in Microsoft Graph – Part 2
Last Updated on
Nov 30, 2020
In part 1 of the series, we utilized the Microsoft Graph to create a user and unified group in Office 365. In this blog, we will explore the Microsoft Graph to assign permissions to users by adding them to the required Office 365 groups. To authenticate and call Microsoft Graph, we will create a console application based on the .NET core to achieve the functionality in less number of steps. The application will use client secret retrieved earlier and Microsoft Authentication Library (MSAL) to establish a context for authentication with the Microsoft Graph API.
Prerequisites
You will require a few prerequisites to execute the process:
- Visual Studio code installed in your development environment.
- C# extension for Visual Studio Code.
- .NET Core SDK: .NET Core installer > dotnet-sdk-2.1.700-win-x64.exe.
- Microsoft work or school account.
Add and validate user permissions in Office groups
Once Office 365 users are successfully created, we can add the permissions needed for the users in the respective groups. SharePoint developers also granted permission on the SharePoint Online group, if any.
We can get groups and directory roles that a user is a member of using Microsoft Graph endpoint /users/{id|userPrincipalName}/memberOf. Also, we can add users to a group using endpoint /groups/{id}/members/$ref. We already have the necessary permissions granted in the Azure app earlier for accessing both the Graph endpoints.
Create a .NET Core Console Application
- Create a folder named GraphNETCore in your development environment file explorer.
- Navigate to the folder in the command prompt and run the below command: dotnet new console
- Install below NuGet packages that you will use later using commands.
- Identity.Client > dotnet add package Microsoft.Identity.Client –version 2.3.1-preview
- Graph > dotnet add package Microsoft.Graph
- Extensions.Configuration > dotnet add package Microsoft.Extensions.Configuration
- Extensions.Configuration.FileExtensions > dotnet add package Microsoft.Extensions.Configuration.FileExtensions
- Extensions.Configuration.Json > dotnet add package Microsoft.Extensions.Configuration.Json
Note: We will use the same Azure web application AzureADUserManagement registered earlier while provisioning users and groups in part 1 of this blog.
Extend the app for Azure AD Authentication
To obtain a necessary OAuth access token to call Microsoft Graph, we will extend the registered application to support authentication with Azure AD. Here, we will integrate Microsoft Authentication Library into the application.
Create a configuration file
- Open the project in Visual Studio Code by running the below command
code.
- Create a file having a name as appsettings.json and its content as below. Drop in the values obtained earlier to add Application (Client) ID, Application (Client) Secret, Tenant (Domain) ID, and application redirects URI. It is generally a best practice to define configuration, i.e., Tenant (Domain) Id, Application (Client) Id, Application (Client) Secret independent of the actual code.
appsettings.json
{ "applicationId": " ", "applicationSecret": " ", "tenantId": " ", "redirectUri": "https://localhost:8080" } |
{
“applicationId”: ” “,
“applicationSecret”: ” “,
“tenantId”: ” “,
“redirectUri”: “https://localhost:8080”
}
Note: The SharePoint Online access token’s default lifespan is 1 hour. The Refresh tokens are valid for up to 14 days and can be valid for up to 90 days with ongoing use. After 90 days, the users are required to re-authenticate.
Create helper classes
- Add a new folder inside the project called Helpers.
- Create a new file AuthHandler.cs under the Helper folder and add the below content inside the file.AuthHandler.cs
using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Graph; using Microsoft.Extensions.Configuration; using System.Linq; using System.Threading; namespace GraphNETCore { // This class allows an implementation of IAuthenticationProvider to be inserted into the DelegatingHandler // pipeline of an HttpClient instance. In future versions of GraphSDK, many cross-cutting concernts will // be implemented as DelegatingHandlers. This AuthHandler will come in the box. public class AuthHandler : DelegatingHandler { private IAuthenticationProvider _authenticationProvider; public AuthHandler(IAuthenticationProvider authenticationProvider, HttpMessageHandler innerHandler) { InnerHandler = innerHandler; _authenticationProvider = authenticationProvider; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { await _authenticationProvider.AuthenticateRequestAsync(request); return await base.SendAsync(request,cancellationToken); } } }
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Graph;
using Microsoft.Extensions.Configuration;
using System.Linq;
using System.Threading;namespace GraphNETCore
{
// This class allows an implementation of IAuthenticationProvider to be inserted into the DelegatingHandler
// pipeline of an HttpClient instance. In future versions of GraphSDK, many cross-cutting concernts will
// be implemented as DelegatingHandlers. This AuthHandler will come in the box.
public class AuthHandler : DelegatingHandler {
private IAuthenticationProvider _authenticationProvider;public AuthHandler(IAuthenticationProvider authenticationProvider, HttpMessageHandler innerHandler)
{
InnerHandler = innerHandler;
_authenticationProvider = authenticationProvider;
}protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
await _authenticationProvider.AuthenticateRequestAsync(request);
return await base.SendAsync(request,cancellationToken);
}
}
}
- Add a new file, MsalAuthenticationProvider.cs, in the Helpers folder, and add the below content inside the file.MsalAuthenticationProvider.cs
using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Graph; using Microsoft.Extensions.Configuration; using System.Linq; namespace GraphNETCore { // This class encapsulates the details of getting a token from MSAL and exposes it via the // IAuthenticationProvider interface so that GraphServiceClient or AuthHandler can use it. // A significantly enhanced version of this class will in the future be available from // the GraphSDK team. It will supports all the types of Client Application as defined by MSAL. public class MsalAuthenticationProvider : IAuthenticationProvider { private IConfidentialClientApplication _clientApplication; private string[] _scopes; public MsalAuthenticationProvider(IConfidentialClientApplication clientApplication, string[] scopes) { _clientApplication = clientApplication; _scopes = scopes; } /// <summary> /// Update HttpRequestMessage with credentials /// </summary> public async Task AuthenticateRequestAsync(HttpRequestMessage request) { var token = await GetTokenAsync(); request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token); } /// <summary> /// Acquire Token /// </summary> public async Task<string> GetTokenAsync() { AuthenticationResult authResult = null; authResult = await _clientApplication.AcquireTokenForClient(_scopes) .ExecuteAsync(); return authResult.AccessToken; } } }
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Graph;
using Microsoft.Extensions.Configuration;
using System.Linq;namespace GraphNETCore
{
// This class encapsulates the details of getting a token from MSAL and exposes it via the
// IAuthenticationProvider interface so that GraphServiceClient or AuthHandler can use it.
// A significantly enhanced version of this class will in the future be available from
// the GraphSDK team. It will supports all the types of Client Application as defined by MSAL.
public class MsalAuthenticationProvider : IAuthenticationProvider
{
private IConfidentialClientApplication _clientApplication;
private string[] _scopes;public MsalAuthenticationProvider(IConfidentialClientApplication clientApplication, string[] scopes) {
_clientApplication = clientApplication;
_scopes = scopes;
}/// <summary>
/// Update HttpRequestMessage with credentials
/// </summary>
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
var token = await GetTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue(“bearer”, token);
}/// <summary>
/// Acquire Token
/// </summary>
public async Task<string> GetTokenAsync()
{
AuthenticationResult authResult = null;
authResult = await _clientApplication.AcquireTokenForClient(_scopes)
.ExecuteAsync();
return authResult.AccessToken;
}
}
}
Extend the app for Microsoft Graph
To make calls to the Microsoft Graph, we will use the Microsoft Graph Client Library for. NET. Here we’ll see how the Microsoft Graph can be incorporated into the application.
Get user information from the tenant
- Add the following statements at the top of the Program.cs file.Program.cs
using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Graph; using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Graph;
using Microsoft.Extensions.Configuration;
- Add static variables for GraphServiceClient and HttpClient inside Program class to make calls against Microsoft Graph.Program.cs
private static GraphServiceClient _graphServiceClient; private static HttpClient _httpClient;
private static GraphServiceClient _graphServiceClient;
private static HttpClient _httpClient;
- Add a new method LoadAppSettings inside Program class to fetch configuration values from appsettings.json file.Program.cs
private static IConfigurationRoot LoadAppSettings() { try { var config = new ConfigurationBuilder() .SetBasePath(System.IO.Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", false, true) .Build(); // Validate required settings if (string.IsNullOrEmpty(config["applicationId"]) || string.IsNullOrEmpty(config["applicationSecret"]) || string.IsNullOrEmpty(config["redirectUri"]) || string.IsNullOrEmpty(config["tenantId"]) || string.IsNullOrEmpty(config["domain"])) { return null; } return config; } catch (System.IO.FileNotFoundException) { return null; } }
private static IConfigurationRoot LoadAppSettings()
{
try
{
var config = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile(“appsettings.json”, false, true)
.Build();// Validate required settings
if (string.IsNullOrEmpty(config[“applicationId”]) ||
string.IsNullOrEmpty(config[“applicationSecret”]) ||
string.IsNullOrEmpty(config[“redirectUri”]) ||
string.IsNullOrEmpty(config[“tenantId”]) ||
string.IsNullOrEmpty(config[“domain”]))
{
return null;
}return config;
}
catch (System.IO.FileNotFoundException)
{
return null;
}
}
- To make calls against Microsoft Graph, create a new method named CreateAuthorizationProvider inside the Program class, and this method will be consumed later. It makes use of ConfidentialClientApplication to get configuration data.
Program.csprivate static IAuthenticationProvider CreateAuthorizationProvider(IConfigurationRoot config) { var clientId = config["applicationId"]; var clientSecret = config["applicationSecret"]; var redirectUri = config["redirectUri"]; var authority = $"https://login.microsoftonline.com/{config["tenantId"]}/v2.0"; List<string> scopes = new List<string>(); scopes.Add("https://graph.microsoft.com/.default"); var cca = ConfidentialClientApplicationBuilder.Create(clientId) .WithAuthority(authority) .WithRedirectUri(redirectUri) .WithClientSecret(clientSecret) .Build(); return new MsalAuthenticationProvider(cca, scopes.ToArray()); }
private static IAuthenticationProvider CreateAuthorizationProvider(IConfigurationRoot config)
{
var clientId = config[“applicationId”];
var clientSecret = config[“applicationSecret”];
var redirectUri = config[“redirectUri”];
var authority = $”https://login.microsoftonline.com/{config[“tenantId”]}/v2.0″;List<string> scopes = new List<string>();
scopes.Add(“https://graph.microsoft.com/.default”);var cca = ConfidentialClientApplicationBuilder.Create(clientId)
.WithAuthority(authority)
.WithRedirectUri(redirectUri)
.WithClientSecret(clientSecret)
.Build();
return new MsalAuthenticationProvider(cca, scopes.ToArray());
}
- Generate a new method GetAuthenticatedGraphClient inside Program class to create an instance of GraphServiceClient from the static reference defined earlier. Utilize the returned configuration of the previous method.Program.cs
private static GraphServiceClient GetAuthenticatedGraphClient(IConfigurationRoot config) { var authenticationProvider = CreateAuthorizationProvider(config); _graphServiceClient = new GraphServiceClient(authenticationProvider); return _graphServiceClient; }
private static GraphServiceClient GetAuthenticatedGraphClient(IConfigurationRoot config)
{
var authenticationProvider = CreateAuthorizationProvider(config);
_graphServiceClient = new GraphServiceClient(authenticationProvider);
return _graphServiceClient;
}
- Create a new method called GetAuthenticatedHTTPClient inside the Program class to create an instance of HTTPClient from the static reference declared earlier. Utilize the returned configuration of the previous method.
Program.csprivate static HttpClient GetAuthenticatedHTTPClient(IConfigurationRoot config) { var authenticationProvider = CreateAuthorizationProvider(config); _httpClient = new HttpClient(new AuthHandler(authenticationProvider, new HttpClientHandler())); return _httpClient; }
private static HttpClient GetAuthenticatedHTTPClient(IConfigurationRoot config)
{
var authenticationProvider = CreateAuthorizationProvider(config);
_httpClient = new HttpClient(new AuthHandler(authenticationProvider, new HttpClientHandler()));
return _httpClient;
}
- Place the below code snippet instead of “Console.WriteLine(“Hello World!”);” inside the Main method.Program.js -> Main
// Load appsettings.json var config = LoadAppSettings(); if (null == config) { Console.WriteLine("Missing or invalid appsettings.json file. Please see README.md for configuration instructions."); return; }
// Load appsettings.json
var config = LoadAppSettings();
if (null == config)
{
Console.WriteLine(“Missing or invalid appsettings.json file. Please see README.md for configuration instructions.”);
return;
}
- Add below code continuing inside the Main method to retrieve an authenticated instance of GraphServiceClient.
Program.csGraphServiceClient graphClient = GetAuthenticatedGraphClient(config);
GraphServiceClient graphClient = GetAuthenticatedGraphClient(config);
Get list of groups a user belongs to
- Create a file named PermissionHelper.cs under Helpers folder to fetch all the groups that a user is a member of.
- Create a UserMemberOf helper method inside the file.PermissionHelper.cs
using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Graph; namespace GraphNETCore { public class PermissionHelper { private GraphServiceClient _graphClient; public PermissionHelper(GraphServiceClient graphClient) { if (null == graphClient) throw new ArgumentNullException(nameof(graphClient)); _graphClient = graphClient; } //Returns a list of groups that the given user belongs to public async Task<List<ResultsItem>> UserMemberOf(string alias) { User user = FindByAlias(alias).Result; List<ResultsItem> items = new List<ResultsItem>(); IUserMemberOfCollectionWithReferencesPage groupsCollection = await _graphClient.Users[user.Id].MemberOf.Request().GetAsync(); if (groupsCollection?.Count > 0) { foreach (DirectoryObject dirObject in groupsCollection) { if (dirObject is Group) { Group group = dirObject as Group; items.Add(new ResultsItem { Display = group.DisplayName, Id = group.Id }); } } } return items; } } }
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Graph;namespace GraphNETCore
{
public class PermissionHelper
{
private GraphServiceClient _graphClient;public PermissionHelper(GraphServiceClient graphClient)
{
if (null == graphClient) throw new ArgumentNullException(nameof(graphClient));
_graphClient = graphClient;
}//Returns a list of groups that the given user belongs to
public async Task<List<ResultsItem>> UserMemberOf(string alias)
{
User user = FindByAlias(alias).Result;
List<ResultsItem> items = new List<ResultsItem>();IUserMemberOfCollectionWithReferencesPage groupsCollection = await _graphClient.Users[user.Id].MemberOf.Request().GetAsync();
if (groupsCollection?.Count > 0)
{
foreach (DirectoryObject dirObject in groupsCollection)
{
if (dirObject is Group)
{
Group group = dirObject as Group;
items.Add(new ResultsItem
{
Display = group.DisplayName,
Id = group.Id
});
}
}
}
return items;
}
}
}
Determine group and add user permissions to the group
- Add below helper methods to add users inside the unified Office 365 group “Test Azure” created earlier in part 1, while also checking if the user is not already part of that group.PermissionHelper.cs
//Adds the user to the given group if not already a member of public async Task AddUserToGroup(string alias, string groupId) { User user = FindByAlias(alias).Result; List<ResultsItem> items = UserMemberOf(alias).Result; if (items.FindIndex(f => f.Id == groupId) >= 0) Console.WriteLine("User already belongs to this group"); else await _graphClient.Groups[groupId].Members.References.Request().AddAsync(user); } //Returns the first unified group with the given suffix public async Task<string> GetGroupByName(string groupNameSuffix) { string groupId = string.Empty; var groups = await _graphClient.Groups.Request().Filter("groupTypes/any(c:c%20eq%20'unified') AND startswith(displayName,'" + groupNameSuffix + "')").Select("displayName,description,id").GetAsync(); if (groups?.Count > 0) { groupId = (groups[0] as Group).Id; } else { groupId = CreateGroup().Result; } return groupId; } //Returns the User object for the given alias public async Task<User> FindByAlias(string alias) { List<QueryOption> queryOptions = new List<QueryOption> { new QueryOption("$filter", $@"mailNickname eq '{alias}'") }; var userResult = await _graphClient.Users.Request(queryOptions).GetAsync(); if (userResult.Count != 1) throw new ApplicationException($"Unable to find a user with the alias {alias}"); return userResult[0]; }
//Adds the user to the given group if not already a member of
public async Task AddUserToGroup(string alias, string groupId)
{
User user = FindByAlias(alias).Result;
List<ResultsItem> items = UserMemberOf(alias).Result;
if (items.FindIndex(f => f.Id == groupId) >= 0)
Console.WriteLine(“User already belongs to this group”);
else
await _graphClient.Groups[groupId].Members.References.Request().AddAsync(user);
}//Returns the first unified group with the given suffix
public async Task<string> GetGroupByName(string groupNameSuffix)
{
string groupId = string.Empty;
var groups = await _graphClient.Groups.Request().Filter(“groupTypes/any(c:c%20eq%20’unified’) AND startswith(displayName,'” + groupNameSuffix + “‘)”).Select(“displayName,description,id”).GetAsync();
if (groups?.Count > 0)
{
groupId = (groups[0] as Group).Id;
}
else
{
groupId = CreateGroup().Result;
}
return groupId;
}//Returns the User object for the given alias
public async Task<User> FindByAlias(string alias)
{
List<QueryOption> queryOptions = new List<QueryOption>
{
new QueryOption(“$filter”, $@”mailNickname eq ‘{alias}'”)
};var userResult = await _graphClient.Users.Request(queryOptions).GetAsync();
if (userResult.Count != 1) throw new ApplicationException($”Unable to find a user with the alias {alias}”);
return userResult[0];
}
- Create a below helper method to create a group if it doesn’t already exist.PermissionHelper.cs
//Creates a Unified O365 Group public async Task<string> CreateGroup() { string guid = Guid.NewGuid().ToString(); string groupPrefix = "Test Azure -"; Group group = await _graphClient.Groups.Request().AddAsync(new Group { GroupTypes = new List<string> { "Unified" }, DisplayName = groupPrefix + guid.Substring(0, 11), Description = groupPrefix + guid, MailNickname = groupPrefix.Replace(" ", "").ToLower() + guid.Substring(0, 11), MailEnabled = false, SecurityEnabled = false }); if (null == group) throw new ApplicationException($"Unable to create a unified group"); return group.Id; }
//Creates a Unified O365 Group
public async Task<string> CreateGroup()
{
string guid = Guid.NewGuid().ToString();
string groupPrefix = “Test Azure -“;
Group group = await _graphClient.Groups.Request().AddAsync(new Group
{
GroupTypes = new List<string> { “Unified” },
DisplayName = groupPrefix + guid.Substring(0, 11),
Description = groupPrefix + guid,
MailNickname = groupPrefix.Replace(” “, “”).ToLower() + guid.Substring(0, 11),
MailEnabled = false,
SecurityEnabled = false
});
if (null == group)
throw new ApplicationException($”Unable to create a unified group”);return group.Id;
}
- Now that the helper methods are ready, we can consume them in Program.cs class.Program.cs
private static void PermissionHelperExampleScenario() { const string alias = " "; ListUnifiedGroupsForUser(alias); string groupId = GetUnifiedGroupStartswith("Test Azure"); AddUserToUnifiedGroup(alias, groupId); ListUnifiedGroupsForUser(alias); } private static void ListUnifiedGroupsForUser(string alias) { var permissionHelper = new PermissionHelper(_graphServiceClient); List<ResultsItem> items = permissionHelper.UserMemberOf(alias).Result; Console.WriteLine("User is member of "+ items.Count +" group(s)."); foreach(ResultsItem item in items) { Console.WriteLine(" Group Name: "+ item.Display); } } private static string GetUnifiedGroupStartswith(string groupPrefix) { var permissionHelper = new PermissionHelper(_graphServiceClient); var groupId = permissionHelper.GetGroupByName(groupPrefix).Result; return groupId; } private static void AddUserToUnifiedGroup(string alias, string groupId) { var permissionHelper = new PermissionHelper(_graphServiceClient); permissionHelper.AddUserToGroup(alias, groupId).GetAwaiter().GetResult(); }
private static void PermissionHelperExampleScenario()
{
const string alias = ” “;
ListUnifiedGroupsForUser(alias);
string groupId = GetUnifiedGroupStartswith(“Test Azure”);
AddUserToUnifiedGroup(alias, groupId);
ListUnifiedGroupsForUser(alias);
}private static void ListUnifiedGroupsForUser(string alias)
{
var permissionHelper = new PermissionHelper(_graphServiceClient);
List<ResultsItem> items = permissionHelper.UserMemberOf(alias).Result;
Console.WriteLine(“User is member of “+ items.Count +” group(s).”);
foreach(ResultsItem item in items)
{
Console.WriteLine(” Group Name: “+ item.Display);
}
}private static string GetUnifiedGroupStartswith(string groupPrefix)
{
var permissionHelper = new PermissionHelper(_graphServiceClient);
var groupId = permissionHelper.GetGroupByName(groupPrefix).Result;
return groupId;
}private static void AddUserToUnifiedGroup(string alias, string groupId)
{
var permissionHelper = new PermissionHelper(_graphServiceClient);
permissionHelper.AddUserToGroup(alias, groupId).GetAwaiter().GetResult();
}
- PermissionHelperExampleScenario first retrieves all the unified groups a user is a member of, then it examines the passed unified group and tries to add the specified user inside the group.
- Call the PermissionHelperExampleScenario method inside the Main method. Program.cs -> Main
static void Main(string[] args) { // Load appsettings.json var config = LoadAppSettings(); if (null == config) { Console.WriteLine("Missing or invalid appsettings.json file. Please see README.md for configuration instructions."); return; } GraphServiceClient graphClient = GetAuthenticatedGraphClient(config); //Executes the scenario that shows how to add user to unified group //Validate the user permissions to the group which also implies the associated SPO site PermissionHelperExampleScenario(); }
static void Main(string[] args)
{
// Load appsettings.json
var config = LoadAppSettings();
if (null == config)
{
Console.WriteLine(“Missing or invalid appsettings.json file. Please see README.md for configuration instructions.”);
return;
}
GraphServiceClient graphClient = GetAuthenticatedGraphClient(config);//Executes the scenario that shows how to add user to unified group
//Validate the user permissions to the group which also implies the associated SPO site
PermissionHelperExampleScenario();
}
After ensuring that all files are saved, run the below command in the command prompt to test the console application.
dotnet build
dotnet run
Conclusion
SharePoint developer can use the Microsoft Graph to build intuitive app experiences based on users and their relationships with unified groups. Albeit, Microsoft Graph also provides API’s to make calls to manage Office 365 groups and users’ access to those groups according to your organization’s scenario. Microsoft Graph makes it possible for SharePoint developers and IT Pros to create Custom Software solutions, and automation tools using various Microsoft services without adding multiple authentication contexts or generating individual calls to services.
Comments