diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index e915a9b3..676e421d 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -27,7 +27,10 @@ Please add an 'x' for the scenario(s) where you found an issue 1. [ ] With specific token caches: [2-WebApp-graph-user/2-2-TokenCache](../blob/master/2-WebApp-graph-user/2-2-TokenCache) 1. [ ] Calling Microsoft Graph in national clouds: [2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph](../blob/master/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph) 1. [ ] Web app calling several APIs [3-WebApp-multi-APIs](../blob/master/3-WebApp-multi-APIs) -1. [ ] Web app calling your own Web API [4-WebApp-your-API](../blob/master/4-WebApp-your-API) +1. [ ] Web app calling your own Web API + 1. [ ] with a work and school account in your organization: [4-WebApp-your-API/4-1-MyOrg](../blob/master/4-WebApp-your-API/4-1-MyOrg) + 1. [ ] with B2C users: [4-WebApp-your-API/4-2-B2C](../blob/master/4-WebApp-your-API/4-2-B2C) + 1. [ ] with any work and school account: [4-WebApp-your-API/4-3-AnyOrg](../blob/master/4-WebApp-your-API/4-3-AnyOrg) 1. Web app restricting users 1. [ ] by Roles: [5-WebApp-AuthZ/5-1-Roles](../blob/master/5-WebApp-AuthZ/5-1-Roles) 1. [ ] by Groups: [5-WebApp-AuthZ/5-2-Groups](../blob/master/5-WebApp-AuthZ/5-2-Groups) diff --git a/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/AppCreationScripts.md b/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/AppCreationScripts.md index 80a81355..d49f8ef4 100644 --- a/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/AppCreationScripts.md +++ b/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/AppCreationScripts.md @@ -1,35 +1,47 @@ -# Registering the sample apps with Microsoft identity platform and updating the configuration files using PowerShell scripts +# Registering the sample apps with the Microsoft identity platform and updating the configuration files using PowerShell ## Overview ### Quick summary -1. On Windows run PowerShell and navigate to the root of the cloned directory +1. On Windows run PowerShell as **Administrator** and navigate to the root of the cloned directory 1. In PowerShell run: + ```PowerShell Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force ``` + 1. Run the script to create your Azure AD application and configure the code of the sample application accordingly. (Other ways of running the scripts are described below) + ```PowerShell cd .\AppCreationScripts\ .\Configure.ps1 ``` + 1. Open the Visual Studio solution and click start ### More details The following paragraphs: -- [Present the scripts](#presentation-of-the-scripts) and explain their [usage patterns](#usage-pattern-for-tests-and-devops-scenarios) for test and DevOps scenarios. -- Explain the [pre-requisites](#pre-requisites) -- Explain [four ways of running the scripts](#four-ways-to-run-the-script): - - [Interactively](#option-1-interactive) to create the app in your home tenant - - [Passing credentials](#option-2-non-interactive) to create the app in your home tenant - - [Interactively in a specific tenant](#option-3-interactive-but-create-apps-in-a-specified-tenant) - - [Passing credentials in a specific tenant](#option-4-non-interactive-and-create-apps-in-a-specified-tenant) - - [Passing environment name, for Sovereign clouds](#running-the-script-on-azure-sovereign-clouds) - -## Goal of the scripts +- [Registering the sample apps with the Microsoft identity platform and updating the configuration files using PowerShell](#Registering-the-sample-apps-with-the-Microsoft-identity-platform-and-updating-the-configuration-files-using-PowerShell) + - [Overview](#Overview) + - [Quick summary](#Quick-summary) + - [More details](#More-details) + - [Goal of the provided scripts](#Goal-of-the-provided-scripts) + - [Presentation of the scripts](#Presentation-of-the-scripts) + - [Usage pattern for tests and DevOps scenarios](#Usage-pattern-for-tests-and-DevOps-scenarios) + - [How to use the app creation scripts?](#How-to-use-the-app-creation-scripts) + - [Pre-requisites](#Pre-requisites) + - [Run the script and start running](#Run-the-script-and-start-running) + - [Four ways to run the script](#Four-ways-to-run-the-script) + - [Option 1 (interactive)](#Option-1-interactive) + - [Option 2 (non-interactive)](#Option-2-non-interactive) + - [Option 3 (Interactive, but create apps in a specified tenant)](#Option-3-Interactive-but-create-apps-in-a-specified-tenant) + - [Option 4 (non-interactive, and create apps in a specified tenant)](#Option-4-non-interactive-and-create-apps-in-a-specified-tenant) + - [Running the script on Azure Sovereign clouds](#Running-the-script-on-Azure-Sovereign-clouds) + +## Goal of the provided scripts ### Presentation of the scripts @@ -56,36 +68,43 @@ The `Configure.ps1` will stop if it tries to create an Azure AD application whic ### Pre-requisites 1. Open PowerShell (On Windows, press `Windows-R` and type `PowerShell` in the search window) -2. Navigate to the root directory of the project. -3. Until you change it, the default [Execution Policy](https:/go.microsoft.com/fwlink/?LinkID=135170) for scripts is usually `Restricted`. In order to run the PowerShell script you need to set the Execution Policy to `RemoteSigned`. You can set this just for the current PowerShell process by running the command: +1. Navigate to the root directory of the project. +1. Until you change it, the default [Execution Policy](https:/go.microsoft.com/fwlink/?LinkID=135170) for scripts is usually `Restricted`. In order to run the PowerShell script you need to set the Execution Policy to `RemoteSigned`. You can set this just for the current PowerShell process by running the command: + ```PowerShell Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process ``` -### (Optionally) install AzureAD PowerShell modules + +1. ### (Optionally) install AzureAD PowerShell modules +2. The scripts install the required PowerShell module (AzureAD) for the current user if needed. However, if you want to install if for all users on the machine, you can follow the following steps: -4. If you have never done it already, in the PowerShell window, install the AzureAD PowerShell modules. For this: +1. If you have never done it already, in the PowerShell window, install the AzureAD PowerShell modules. For this: 1. Open PowerShell as admin (On Windows, Search Powershell in the search bar, right click on it and select Run as administrator). 2. Type: + ```PowerShell Install-Module AzureAD ``` or if you cannot be administrator on your machine, run: + ```PowerShell Install-Module AzureAD -Scope CurrentUser ``` ### Run the script and start running -5. Go to the `AppCreationScripts` sub-folder. From the folder where you cloned the repo, +1. Go to the `AppCreationScripts` sub-folder. From the folder where you cloned the repo, + ```PowerShell cd AppCreationScripts ``` -6. Run the scripts. See below for the [four options](#four-ways-to-run-the-script) to do that. -7. Open the Visual Studio solution, and in the solution's context menu, choose **Set Startup Projects**. -8. select **Start** for the projects + +1. Run the scripts. See below for the [four options](#four-ways-to-run-the-script) to do that. +1. Open the Visual Studio solution, and in the solution's context menu, choose **Set Startup Projects**. +1. select **Start** for the projects You're done. this just works! @@ -123,6 +142,7 @@ Of course, in real life, you might already get the password as a `SecureString`. #### Option 3 (Interactive, but create apps in a specified tenant) if you want to create the apps in a particular tenant, you can use the following option: + - open the [Azure portal](https://portal.azure.com) - Select the Azure Active directory you are interested in (in the combo-box below your name on the top right of the browser window) - Find the "Active Directory" object in this tenant diff --git a/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/BulkCreateGroups.ps1 b/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/BulkCreateGroups.ps1 index 7a8c6b25..9f1f21a9 100644 --- a/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/BulkCreateGroups.ps1 +++ b/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/BulkCreateGroups.ps1 @@ -6,10 +6,11 @@ $ErrorActionPreference = "Stop" + # ObjectId of the user to be assigned to these security groups. The ObjectId can be obtained via Graph Explorer or in the "Users" blade on the portal. +$usersobjectId = "695a3e1d-2e9f-4d24-aa3c-ac795c16f25c" + Get-AzureADUser -ObjectId $usersobjectId - # ObjectId of the user to be assigned to these security groups. The ObjectId can be obtained via Graph Explorer or in the "Users" blade on the portal. -$usersobjectId = "5b6e08a5-7789-4ae0-a4cb-3d73b4097752" $groupNamePrefix = "TestGroup" $numberOfGroupsToCreate = 222; diff --git a/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/BulkRemoveGroups.ps1 b/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/BulkRemoveGroups.ps1 index 1c5fe016..330813ab 100644 --- a/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/BulkRemoveGroups.ps1 +++ b/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/BulkRemoveGroups.ps1 @@ -10,7 +10,12 @@ $numberOfGroupsToDelete = 222; for($i = 1; $i -le $numberOfGroupsToDelete; $i++) { $groupName = $groupNamePrefix + $i - $group = Get-AzureADGroup -SearchString $groupName - Remove-AzureADGroup -ObjectId $group.ObjectId - Write-Host "Successfully deleted $($group.DisplayName)" + $groups = Get-AzureADGroup -SearchString $groupName + + Foreach ($group in $groups) + { + Write-Host "Trying to delete group $($group.DisplayName)" + Remove-AzureADGroup -ObjectId $group.ObjectId + Write-Host "Successfully deleted $($group.DisplayName)" + } } diff --git a/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/Cleanup.ps1 b/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/Cleanup.ps1 index de9d8120..0d01416b 100644 --- a/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/Cleanup.ps1 +++ b/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/Cleanup.ps1 @@ -7,7 +7,7 @@ param( [string] $azureEnvironmentName ) -#Requires -Modules AzureAD +#Requires -Modules AzureAD -RunAsAdministrator if ($null -eq (Get-Module -ListAvailable -Name "AzureAD")) { diff --git a/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/Configure.ps1 b/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/Configure.ps1 index 58c24387..6e8eb2a1 100644 --- a/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/Configure.ps1 +++ b/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/Configure.ps1 @@ -7,7 +7,7 @@ param( [string] $azureEnvironmentName ) -#Requires -Modules AzureAD +#Requires -Modules AzureAD -RunAsAdministrator <# This script creates the Azure AD applications needed for this sample and updates the configuration files @@ -206,7 +206,6 @@ Function ConfigureApplications -IdentifierUris "https://$tenantName/WebApp-GroupClaims" ` -PasswordCredentials $key ` -GroupMembershipClaims "SecurityGroup" ` - -Oauth2AllowImplicitFlow $true ` -PublicClient $False # create the service principal of the newly created application @@ -234,7 +233,7 @@ Function ConfigureApplications # Add Required Resources Access (from 'webApp' to 'Microsoft Graph') Write-Host "Getting access from 'webApp' to 'Microsoft Graph'" $requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" ` - -requiredDelegatedPermissions "Directory.Read.All" ` + -requiredDelegatedPermissions "User.Read|GroupMember.Read.All" ` $requiredResourcesAccess.Add($requiredPermissions) @@ -244,10 +243,21 @@ Function ConfigureApplications # Update config file for 'webApp' $configFile = $pwd.Path + "\..\appsettings.json" + Write-Host "Updating the sample code ($configFile)" $dictionary = @{ "ClientId" = $webAppAadApplication.AppId;"TenantId" = $tenantId;"Domain" = $tenantName;"ClientSecret" = $webAppAppKey }; UpdateTextFile -configFilePath $configFile -dictionary $dictionary - + Write-Host "" + Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" + Write-Host "IMPORTANT: Please follow the instructions below to complete a few manual step(s) in the Azure portal": + Write-Host "- For 'webApp'" + Write-Host " - Navigate to '$webAppPortalUrl'" + Write-Host " - Navigate to the API Permissions page and select 'Grant admin consent for (your tenant)'" -ForegroundColor Red + Write-Host " - On Azure Portal, create a security group named GroupAdmin, assign some users to it, and configure your ID and Access token to emit GroupID in your app registration. Configure the value for 'GroupAdmin' key in appsettings.json." -ForegroundColor Red + Write-Host " - On Azure Portal, create a security group named GroupMember, assign some users to it, and configure your ID and Access token to emit GroupID in your app registration. Configure the value for 'GroupMember' key in appsettings.json." -ForegroundColor Red + + Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" + Add-Content -Value "" -Path createdApps.html } diff --git a/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/sample.json b/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/sample.json index 67aa8359..70e500db 100644 --- a/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/sample.json +++ b/5-WebApp-AuthZ/5-2-Groups/AppCreationScripts/sample.json @@ -25,7 +25,18 @@ "RequiredResourcesAccess": [ { "Resource": "Microsoft Graph", - "DelegatedPermissions": [ "Directory.Read.All" ] + "DelegatedPermissions": [ "User.Read", "GroupMember.Read.All" ] + } + ], + "ManualSteps": [ + { + "Comment": "Navigate to the API Permissions page and select 'Grant admin consent for (your tenant)'" + }, + { + "Comment": "On Azure Portal, create a security group named GroupAdmin, assign some users to it, and configure your ID and Access token to emit GroupID in your app registration. Configure the value for 'GroupAdmin' key in appsettings.json." + }, + { + "Comment": "On Azure Portal, create a security group named GroupMember, assign some users to it, and configure your ID and Access token to emit GroupID in your app registration. Configure the value for 'GroupMember' key in appsettings.json." } ] } @@ -40,7 +51,7 @@ "CodeConfiguration": [ { "App": "webApp", - "SettingKind": "JSon", + "SettingKind": "JSON", "SettingFile": "\\..\\appsettings.json", "Mappings": [ { diff --git a/5-WebApp-AuthZ/5-2-Groups/Controllers/AccountController.cs b/5-WebApp-AuthZ/5-2-Groups/Controllers/AccountController.cs new file mode 100644 index 00000000..d898cc92 --- /dev/null +++ b/5-WebApp-AuthZ/5-2-Groups/Controllers/AccountController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace WebApp_OpenIDConnect_DotNet.Controllers +{ + public class AccountController : Controller + { + [Authorize] + public IActionResult SignOut() + { + HttpContext.Session.Clear(); + return RedirectToAction("SignOut", "Account", new { area = "MicrosoftIdentity" }); + } + } +} diff --git a/5-WebApp-AuthZ/5-2-Groups/Controllers/HomeController.cs b/5-WebApp-AuthZ/5-2-Groups/Controllers/HomeController.cs index 832cdaee..1e4690cf 100644 --- a/5-WebApp-AuthZ/5-2-Groups/Controllers/HomeController.cs +++ b/5-WebApp-AuthZ/5-2-Groups/Controllers/HomeController.cs @@ -1,8 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Diagnostics; -using System.Security.Claims; using WebApp_OpenIDConnect_DotNet.Models; +using Microsoft.AspNetCore.Http; +using WebApp_OpenIDConnect_DotNet.Services; namespace WebApp_OpenIDConnect_DotNet.Controllers { @@ -16,6 +17,13 @@ public HomeController() public IActionResult Index() { ViewData["User"] = HttpContext.User; + + // Calls method GetSessionGroupList to get groups from session. + var groups = GraphHelper.GetUserGroupsFromSession(HttpContext.Session); + if (groups?.Count > 0) + { + ViewData.Add("groupClaims", groups ); + } return View(); } @@ -23,7 +31,7 @@ public IActionResult Index() [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { - return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier}); + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } } } \ No newline at end of file diff --git a/5-WebApp-AuthZ/5-2-Groups/Controllers/UserProfileController.cs b/5-WebApp-AuthZ/5-2-Groups/Controllers/UserProfileController.cs index 160900dc..7961170c 100644 --- a/5-WebApp-AuthZ/5-2-Groups/Controllers/UserProfileController.cs +++ b/5-WebApp-AuthZ/5-2-Groups/Controllers/UserProfileController.cs @@ -2,9 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Graph; using Microsoft.Identity.Web; -using System.Collections.Generic; using System.Threading.Tasks; -using WebApp_OpenIDConnect_DotNet.Services.MicrosoftGraph; using Constants = WebApp_OpenIDConnect_DotNet.Infrastructure.Constants; namespace WebApp_OpenIDConnect_DotNet.Controllers @@ -13,39 +11,28 @@ namespace WebApp_OpenIDConnect_DotNet.Controllers //[Authorize(Roles = "8873daa2-17af-4e72-973e-930c94ef7549")] public class UserProfileController : Controller { - private readonly ITokenAcquisition tokenAcquisition; - private readonly IMSGraphService graphService; + private readonly GraphServiceClient graphServiceClient; - public UserProfileController(ITokenAcquisition tokenAcquisition, IMSGraphService MSGraphService) + public UserProfileController(GraphServiceClient graphServiceClient) { - this.tokenAcquisition = tokenAcquisition; - this.graphService = MSGraphService; + this.graphServiceClient= graphServiceClient; } - - [AuthorizeForScopes(Scopes = new[] { Constants.ScopeUserRead, Constants.ScopeDirectoryReadAll })] + [Authorize(Policy = "GroupAdmin")] + [AuthorizeForScopes(Scopes = new[] { Constants.ScopeUserRead })] public async Task Index() { - // This is how group ids/names are used in the IsInRole method - // var isinrole = User.IsInRole("8873daa2-17af-4e72-973e-930c94ef7549"); - - string accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(new[] { Constants.ScopeUserRead, Constants.ScopeDirectoryReadAll }); - - User me = await graphService.GetMeAsync(accessToken); + User me = await graphServiceClient.Me.Request().GetAsync(); ViewData["Me"] = me; try { - var photo = await graphService.GetMyPhotoAsync(accessToken); + var photo = await graphServiceClient.Me.Photo.Request().GetAsync(); ViewData["Photo"] = photo; } catch { //swallow } - - IList groups = await graphService.GetMyMemberOfGroupsAsync(accessToken); - ViewData["Groups"] = groups; - return View(); } } diff --git a/5-WebApp-AuthZ/5-2-Groups/Infrastructure/Constants.cs b/5-WebApp-AuthZ/5-2-Groups/Infrastructure/Constants.cs index ecd4f441..991955d0 100644 --- a/5-WebApp-AuthZ/5-2-Groups/Infrastructure/Constants.cs +++ b/5-WebApp-AuthZ/5-2-Groups/Infrastructure/Constants.cs @@ -3,7 +3,8 @@ namespace WebApp_OpenIDConnect_DotNet.Infrastructure public static class Constants { public const string ScopeUserRead = "User.Read"; - public const string ScopeDirectoryReadAll = "Directory.Read.All"; + + public const string ScopeGroupMemberRead = "GroupMember.Read.All"; public const string BearerAuthorizationScheme = "Bearer"; } diff --git a/5-WebApp-AuthZ/5-2-Groups/Infrastructure/CustomAuthorization.cs b/5-WebApp-AuthZ/5-2-Groups/Infrastructure/CustomAuthorization.cs new file mode 100644 index 00000000..85aa9630 --- /dev/null +++ b/5-WebApp-AuthZ/5-2-Groups/Infrastructure/CustomAuthorization.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using System.Threading.Tasks; +using WebApp_OpenIDConnect_DotNet.Services; + +namespace WebApp_OpenIDConnect_DotNet.Infrastructure +{ + /// + /// GroupPolicyHandler deals with custom Policy-based authorization. + /// GroupPolicyHandler evaluates the GroupPolicyRequirement against AuthorizationHandlerContext + /// by calling CheckUsersGroupMembership method to determine if authorization is allowed. + /// + public class GroupPolicyHandler : AuthorizationHandler + { + private IHttpContextAccessor _httpContextAccessor; + + public GroupPolicyHandler(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + /// + /// Makes a decision if authorization is allowed based on GroupPolicyRequirement. + /// + /// + /// + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, + GroupPolicyRequirement requirement) + { + // Calls method to check if requirement exists in user claims or session. + if (GraphHelper.CheckUsersGroupMembership(context, requirement.GroupName, _httpContextAccessor)) + { + context.Succeed(requirement); + } + return Task.CompletedTask; + } + } + + /// + /// GroupPolicyRequirement contains data parameter that + /// GroupPolicyHandler uses to evaluate against the current user principal or session data. + /// + public class GroupPolicyRequirement : IAuthorizationRequirement + { + public string GroupName { get; } + public GroupPolicyRequirement(string GroupName) + { + this.GroupName = GroupName; + } + } +} diff --git a/5-WebApp-AuthZ/5-2-Groups/Infrastructure/SessionExtensions.cs b/5-WebApp-AuthZ/5-2-Groups/Infrastructure/SessionExtensions.cs new file mode 100644 index 00000000..120c19c6 --- /dev/null +++ b/5-WebApp-AuthZ/5-2-Groups/Infrastructure/SessionExtensions.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text.Json; +using System.Threading.Tasks; + +namespace WebApp_OpenIDConnect_DotNet.Infrastructure +{ + public static class SessionExtensions + { + public static void SetAsByteArray(this ISession session, string key, object toSerialize) + { + var binaryFormatter = new BinaryFormatter(); + var memoryStream = new MemoryStream(); + binaryFormatter.Serialize(memoryStream, toSerialize); + + session.Set(key, memoryStream.ToArray()); + } + + public static object GetAsByteArray(this ISession session, string key) + { + var memoryStream = new MemoryStream(); + var binaryFormatter = new BinaryFormatter(); + + var objectBytes = session.Get(key) as byte[]; + memoryStream.Write(objectBytes, 0, objectBytes.Length); + memoryStream.Position = 0; + + return binaryFormatter.Deserialize(memoryStream); + + } + } +} \ No newline at end of file diff --git a/5-WebApp-AuthZ/5-2-Groups/README-incremental-instructions.md b/5-WebApp-AuthZ/5-2-Groups/README-incremental-instructions.md index 78867b68..59e79093 100644 --- a/5-WebApp-AuthZ/5-2-Groups/README-incremental-instructions.md +++ b/5-WebApp-AuthZ/5-2-Groups/README-incremental-instructions.md @@ -1,20 +1,15 @@ --- -services: active-directory -platforms: dotnet -author: kalyankrishna1 -level: 300 -client: ASP.NET Core Web App -service: Microsoft Graph -endpoint: Microsoft identity platform page_type: sample languages: - - csharp + - csharp products: - - azure + - azure-active-directory - - dotnet - - office-ms-graph -description: "Add authorization using groups & group claims to an ASP.NET Core Web app that signs-in users with the Microsoft identity platform" + - dotnet + - aspnet-core + - ms-graph +name: Add authorization using groups & group claims to an ASP.NET Core Web app that signs-in users with the Microsoft identity platform +description: "This sample demonstrates a ASP.NET Core Web App application calling The Microsoft Graph" --- # Add authorization using groups & group claims to an ASP.NET Core Web app that signs-in users with the Microsoft identity platform @@ -42,17 +37,13 @@ This sample first leverages the ASP.NET Core OpenID Connect middleware to sign i To run this sample, you'll need: -- [Visual Studio 2019](https://aka.ms/vsdownload) or just the [.NET Core SDK](https://www.microsoft.com/net/learn/get-started) -- An Internet connection -- A Windows machine (necessary if you want to run the app on Windows) -- An OS X machine (necessary if you want to run the app on Mac) -- A Linux machine (necessary if you want to run the app on Linux) +- [Visual Studio](https://visualstudio.microsoft.com/downloads/) - An Azure Active Directory (Azure AD) tenant. For more information on how to get an Azure AD tenant, see [How to get an Azure AD tenant](https://azure.microsoft.com/documentation/articles/active-directory-howto-tenant/) -- A user account in your Azure AD tenant. This sample will not work with a Microsoft account (formerly Windows Live account). Therefore, if you signed in to the [Azure portal](https://portal.azure.com) with a Microsoft account and have never created a user account in your directory before, you need to do that now. +- A user account in your Azure AD tenant. This sample will not work with a **personal Microsoft account**. Therefore, if you signed in to the [Azure portal](https://portal.azure.com) with a personal account and have never created a user account in your directory before, you need to do that now. > Please make sure to have one or more user accounts in the tenant assigned to a few security groups in your tenant. Please follow the instructions in [Create a basic group and add members using Azure Active Directory](https://docs.microsoft.com/azure/active-directory/fundamentals/active-directory-groups-create-azure-portal) to create a few groups and assign users to them if not already done. -### Step 1: Clone or download this repository +### Step 1: In the downloaded folder From your shell or command line: @@ -62,14 +53,14 @@ Navigate to the `"5-WebApp-AuthZ"` folder cd 5-WebApp-AuthZ\5-2-Groups ``` -### Step 3: Configure your application to receive the **groups** claim +### Step 2: Configure your application to receive the **groups** claim -Now you have two different options available to you on how you can further configure your application to receive the `groups` claim. +You have two different options available to you on how you can further configure your application to receive the `groups` claim. 1. [Receive **all the groups** that the signed-in user is assigned to in an Azure AD tenant, included nested groups](#configure-your-application-to-receive-all-the-groups-the-signed-in-user-is-assigned-to-included-nested-groups). -1. [Receive the **groups** claim values from a **filtered set of groups** that your application is programmed to work with](#configure-your-application-to-receive-the-groups-claim-values-from-a-filtered-set-of-groups-a-user-may-be-assigned-to). (Not available in the [Azure AD Free edition](https://azure.microsoft.com/pricing/details/active-directory/)). +1. [Receive the **groups** claim values from a **filtered set of groups** that your application is programmed to work with](#configure-your-application-to-receive-the-groups-claim-values-from-a-filtered-set-of-groups-a-user-may-be-assigned-to) (Not available in the [Azure AD Free edition](https://azure.microsoft.com/pricing/details/active-directory/)). -> To get the on-premise group's `samAccountName` or `On Premises Group Security Identifier` instead of Group id, please refer to the document [Configure group claims for applications with Azure Active Directory](https://docs.microsoft.com/azure/active-directory/hybrid/how-to-connect-fed-group-claims#prerequisites-for-using-group-attributes-synchronized-from-active-directory). +> To get the on-premise group's `samAccountName` or `On Premises Group Security Identifier` instead of Group ID, please refer to the document [Configure group claims for applications with Azure Active Directory](https://docs.microsoft.com/azure/active-directory/hybrid/how-to-connect-fed-group-claims#prerequisites-for-using-group-attributes-synchronized-from-active-directory). #### Configure your application to receive **all the groups** the signed-in user is assigned to, included nested groups @@ -94,8 +85,8 @@ Now you have two different options available to you on how you can further confi 1. Select `Groups assigned to the application`. 1. Choosing additional options like `Security Groups` or `All groups (includes distribution lists but not groups assigned to the application)` will negate the benefits your app derives from choosing to use this option. 1. Under the **ID** section, select `Group ID`. This will result in Azure AD sending the object [id](https://docs.microsoft.com/graph/api/resources/group?view=graph-rest-1.0) of the groups the user is assigned to in the `groups` claim of the [ID Token](https://docs.microsoft.com/azure/active-directory/develop/id-tokens) that your app receives after signing-in a user. -1. If you are exposing a Web API using the **Expose an API** option, then you can also choose the `Group ID` option under the **Access** section. This will result in Azure AD sending the object [id](https://docs.microsoft.com/graph/api/resources/group?view=graph-rest-1.0) of the groups the user is assigned to in the `groups` claim of the [Access Token](https://docs.microsoft.com/azure/active-directory/develop/access-tokens) issued to the client applications of your API. -1. In the app's registration screen, click on the **Overview** blade in the left to open the Application overview screen. Select the hyperlink with the name of your application in **Managed application in local directory** (note this field title can be truncated for instance `Managed application in ...`). When you select this link you will navigate to the **Enterprise Application Overview** page associated with the service principal for your application in the tenant where you created it. You can navigate back to the app registration page by using the back button of your browser. +1. If you are exposing a Web API using the **Expose an API** option, then you can also choose the `Group ID` option under the **Access** section. This will result in Azure AD sending the [Object ID](https://docs.microsoft.com/graph/api/resources/group?view=graph-rest-1.0) of the groups the user is assigned to in the `groups` claim of the [Access Token](https://docs.microsoft.com/azure/active-directory/develop/access-tokens) issued to the client applications of your API. +1. In the app's registration screen, click on the **Overview** blade in the left to open the Application overview screen. Select the hyperlink with the name of your application in **Managed application in local directory** (note this field title can be truncated for instance `Managed application in ...`). When you select this link you will navigate to the **Enterprise Application Overview** page associated with the service principal for your application in the tenant where you created it. You can navigate back to the app registration page by using the *back* button of your browser. 1. Select the **Users and groups** blade in the left to open the page where you can assign users and groups to your application. 1. Click on the **Add user** button on the top row. 1. Select **User and Groups** from the resultant screen. @@ -109,13 +100,43 @@ Now you have two different options available to you on how you can further confi > > When you set **User assignment required?** to **Yes**, Azure AD will check that only users assigned to your application in the **Users and groups** blade are able to sign-in to your app. You can assign users directly or by assigning security groups they belong to. -### Step 4: Run the sample +### Step 3: Run the sample -1. Clean and rebuild the solution, and run it. +#### Run the sample using Visual Studio -1. Open your web browser and make a request to the app. The app immediately attempts to authenticate you to the Microsoft identity platform. Sign in with a *work or school account* from the tenant where you created this app. -1. On the home page, the app lists the various claims it obtained from your ID token. You'd notice one more claims named `groups`. -1. On the top menu, click on the signed-in user's name **user@domain.com**, you should now see all kind of information about yourself including their picture. Beneath that, a list of all the security groups that the signed-in user is assigned to are listed as well. All of this was obtained by making calls to Microsoft Graph. This list is useful if the **Overage** scenario occurs with this signed-in user. The [overage](#groups-overage-claim) scenario is discussed later in this article. +> For Visual Studio Users +> +> Clean the solution, rebuild the solution, and run it. + +#### Run the sample using a command line interface such as VS Code integrated terminal + +##### Step 1. Install .NET Core dependencies + +```console + cd WebApp-GroupClaims + dotnet restore +``` + +##### Step 2. Trust development certificates + +```console + dotnet dev-certs https --clean + dotnet dev-certs https --trust +``` + +Learn more about [HTTPS in .NET Core](https://docs.microsoft.com/aspnet/core/security/enforcing-ssl). + +##### Step 3. Run the applications + +In the console window execute the below command: + +```console + dotnet run +``` + +1. Open your web browser and make a request to the app. The app immediately attempts to authenticate you to the Microsoft identity platform. You can sign-in with a *work or school account* from the tenant where you created this app. If admin consent to `GroupMember.Read.All` permission from portal is not done then sign-in with admin for the first time and consent for the permission. +1. On the home page, the app lists the various claims it obtained from your ID token. You'd notice one more claims named `groups`. +1. On the top menu, click on the signed-in user's name **user@domain.com**, you should now see all kind of information about yourself including their picture. > Did the sample not work for you as expected? Did you encounter issues trying this sample? Then please reach out to us using the [GitHub Issues](../../../../issues) page. @@ -135,29 +156,6 @@ The object id of the security groups the signed in user is member of is returned } ``` -### Support in ASP.NET Core middleware libraries - -The asp.net middleware supports roles populated from claims by specifying the claim in the `RoleClaimType` property of `TokenValidationParameters`. -Since the `groups` claim contains the object ids of the security groups than actual names by default, you'd use the group id's instead of group names. See [Role-based authorization in ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/authorization/roles) for more info. - -```CSharp -// Startup.cs - -// The following lines code instruct the asp.net core middleware to use the data in the "groups" claim in the [Authorize] attribute and for User.IsInrole() -// See https://docs.microsoft.com/aspnet/core/security/authorization/roles -services.Configure(OpenIdConnectDefaults.AuthenticationScheme, options => -{ - // Use the groups claim for populating roles - options.TokenValidationParameters.RoleClaimType = "groups"; -}); - -// In code..(Controllers & elsewhere) -[Authorize(Roles = "Group-object-id")] // In controllers -// or -User.IsInRole("Group-object-id"); // In methods - -``` - ### The groups overage claim To ensure that the token size doesn’t exceed HTTP header size limits, the Microsoft Identity Platform limits the number of object Ids that it includes in the **groups** claim. @@ -180,7 +178,7 @@ If a user is member of more groups than the overage limit (**150 for SAML tokens } ``` -##### Create the overage scenario in this sample for testing +#### Create the overage scenario in this sample for testing 1. You can use the `BulkCreateGroups.ps1` provided in the [App Creation Scripts](./AppCreationScripts/) folder to create a large number of groups and assign users to them. This will help test overage scenarios during development. Remember to change the user's objectId provided in the `BulkCreateGroups.ps1` script. 1. When you run this sample and an overage occurred, then you'd see the `_claim_names` in the home page after the user signs-in. @@ -194,7 +192,7 @@ If a user is member of more groups than the overage limit (**150 for SAML tokens > Developers who wish to gain good familiarity of programming for Microsoft Graph are advised to go through the [An introduction to Microsoft Graph for developers](https://www.youtube.com/watch?v=EBbnpFdB92A) recorded session. -##### When you are a single page application and using the implicit grant flow to authenticate +#### When you are a single page application and using the implicit grant flow to authenticate In case, you are authenticating using the [implicit grant flow](https://docs.microsoft.com/azure/active-directory/develop/v1-oauth2-implicit-grant-flow), the **overage** indication and limits are different than the apps using other flows. @@ -216,14 +214,24 @@ The following files have the code that would be of interest to you: 1. HomeController.cs 1. Passes the **HttpContext.User** (the signed-in user) to the view. -1. UserProfileController.cs - 1. Uses the **IMSGraphService** methods to fetch the signed-in user's group memberships. -1. IMSGraphService.cs, MSGraphService.cs and UserGroupsAndDirectoryRoles.cs - 1. Uses the [Microsoft Graph SDK](https://github.com/microsoftgraph/msgraph-sdk-dotnet) to carry out various operations with [Microsoft Graph](https://graph.microsoft.com). + 1. Calls method **GetSessionGroupList** of `GraphHelper.cs` to get groups from session and if groups are returned then pass them to the view. + + ```csharp + public IActionResult Index() + { + ViewData["User"] = HttpContext.User; + var groups = GraphHelper.GetUserGroupsFromSession(HttpContext.Session); + if (groups?.Count > 0) + { + ViewData.Add("groupClaims", groups ); + } + return View(); + } + ``` + 1. Home\Index.cshtml - 1. This has some code to print the current user's claims -1. UserProfile\Index.cshtml - 1. Has some client code that prints the signed-in user's information obtained from the [/me](https://docs.microsoft.com/graph/api/user-get?view=graph-rest-1.0), [/me/photo](https://docs.microsoft.com/graph/api/profilephoto-get) and [/memberOf](https://docs.microsoft.com/graph/api/user-list-memberof) endpoints. + 1. This has some code to print the current user's claims. + 1. Startup.cs - at the top of the file, add the following using directive: @@ -232,27 +240,118 @@ The following files have the code that would be of interest to you: using Microsoft.Identity.Web; ``` - - in the `ConfigureServices` method, the following lines: + - in the `ConfigureServices` method, the following lines: ```CSharp services.AddAuthentication(AzureADDefaults.AuthenticationScheme) .AddAzureAD(options => Configuration.Bind("AzureAd", options)); ``` - - have been replaced by these lines:: + - have been replaced by these lines: - ```CSharp - services.AddMicrosoftIdentityWebAppAuthentication(Configuration) - .EnableTokenAcquisitionToCallDownstreamApi( new string[] { "User.Read", "Directory.Read.All" }) + ```CSharp + services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp( + options => + { + Configuration.Bind("AzureAd", options); + options.Events = new OpenIdConnectEvents(); + options.Events.OnTokenValidated = async context => + { + var overageGroupClaims = await GraphHelper.GetSignedInUsersGroups(context); + }; + }, options => { Configuration.Bind("AzureAd", options); }) + .EnableTokenAcquisitionToCallDownstreamApi(options => Configuration.Bind("AzureAd", options), initialScopes) + .AddMicrosoftGraph(Configuration.GetSection("GraphAPI")) .AddInMemoryTokenCaches(); + ``` + + `OnTokenValidated` event calls **GetSignedInUsersGroups** method, that is defined in GraphHelper.cs, to process groups overage claim. + + `AddMicrosoftGraph` registers the service for `GraphServiceClient`. The values for BaseUrl and Scopes defined in `GraphAPI` section of **appsettings.json**. + + Following lines of code adds authorization policies that enforce authorization using group values. + + ```csharp + services.AddAuthorization(options => + { + options.AddPolicy("GroupAdmin", + policy => policy.Requirements.Add(new GroupPolicyRequirement(Configuration["Groups:GroupAdmin"]))); + options.AddPolicy("GroupMember", + policy => policy.Requirements.Add(new GroupPolicyRequirement(Configuration["Groups:GroupMember"]))); + }); + ``` + +1. In GraphHelper.cs, **GetSignedInUsersGroups** method checks if incoming token contains *Group Overage* claim then returns the list of groups from Microsoft Graph. First **GetUserGroupsFromSession** method is called to get group values from session if exists. If session does not contain groups claim then it will call **ProcessUserGroupsForOverage** method to retrieve groups. + + ```csharp + public static async Task> GetSignedInUsersGroups(TokenValidatedContext context) + { + List groupClaims = new List(); + if (HasOverageOccurred(context.Principal)) + { + // + groupClaims = GetUserGroupsFromSession(context.HttpContext.Session); + if (groupClaims?.Count > 0) + { + return groupClaims; + } + else + { + groupClaims = await ProcessUserGroupsForOverage(context); + } + } + return groupClaims; + } + ``` - services.AddMSGraphService(Configuration); // Adds the IMSGraphService as an available service for this app. + GraphHelper.cs contains a method **CheckUsersGroupMembership** that is called in `CustomAuthorization.cs` to check if value of GroupName parameter exists in either Session for Overage scenario or in User claims otherwise. + + ```csharp + public static bool CheckUsersGroupMembership(AuthorizationHandlerContext context, string GroupName, IHttpContextAccessor _httpContextAccessor) + { + bool result = false; + if (HasOverageOccurred(context.User)) + { + var groups = GetUserGroupsFromSession(_httpContextAccessor.HttpContext.Session); + if (groups?.Count > 0 && groups.Contains(GroupName)) + { + result = true; + } + } + else if (context.User.Claims.Any(x => x.Type == "groups" && x.Value == GroupName)) + { + result = true; + } + return result; + } ``` +1. In `CustomAuthorization.cs`, we have **GroupPolicyHandler** class that deals with custom Policy-based authorization. It evaluates the GroupPolicyRequirement against AuthorizationHandlerContext by overriding **HandleRequirementAsync** of **AuthorizationHandler**. + + HandleRequirementAsync calls **CheckUsersGroupMembership** method of `GraphHelper.cs` to determine if authorization is allowed. + + ```csharp + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, + GroupPolicyRequirement requirement) + { + if (GraphHelper.CheckUsersGroupMembership(context, requirement.GroupName, _httpContextAccessor)) + { + context.Succeed(requirement); + } + return Task.CompletedTask; + } + ``` + +1. UserProfileController.cs + 1. Checks authorization of signed-in user for ```[Authorize(Policy = "GroupAdmin")]```. If authorized successfully then obtain information from the [/me](https://docs.microsoft.com/graph/api/user-get?view=graph-rest-1.0) and [/me/photo](https://docs.microsoft.com/graph/api/profilephoto-get) endpoints by using `GraphServiceClient`. + +1. UserProfile\Index.cshtml + 1. Has some client code that prints the signed-in user's information. + 1. if you used the PowerShell scripts provided in the [AppCreationScripts](.\AppCreationScripts) folder, then note the extra parameter `-GroupMembershipClaims` in the `Configure.ps1` script. ```PowerShell - -Oauth2AllowImplicitFlow $true ` -GroupMembershipClaims "SecurityGroup" ` -PublicClient $False ``` @@ -261,7 +360,7 @@ The following files have the code that would be of interest to you: Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. -Make sure that your questions or comments are tagged with [ `msal` `azure-active-directory`]. +Make sure that your questions or comments are tagged with [ `msal` `azure-active-directory` `dotnet`]. If you find a bug in the sample, please raise the issue on [GitHub Issues](../../../../issues). @@ -273,7 +372,7 @@ To provide a recommendation, visit the following [User Voice page](https://feedb ## Learn more -- Learn how [Microsoft.Identity.Web](../../Microsoft.Identity.Web) works, in particular hooks-up to the ASP.NET Core ODIC events +- Learn how [Microsoft.Identity.Web](https://aka.ms/idweblib) works, in particular hooks-up to the ASP.NET Core ODIC events - To understand more about groups roles and the various claims in tokens, see: - [Configure group claims for applications with Azure Active Directory (Public Preview)](https://docs.microsoft.com/azure/active-directory/hybrid/how-to-connect-fed-group-claims#configure-the-azure-ad-application-registration-for-group-attributes) diff --git a/5-WebApp-AuthZ/5-2-Groups/README.md b/5-WebApp-AuthZ/5-2-Groups/README.md index 1861a5c7..b62982b1 100644 --- a/5-WebApp-AuthZ/5-2-Groups/README.md +++ b/5-WebApp-AuthZ/5-2-Groups/README.md @@ -1,20 +1,14 @@ --- -services: active-directory -platforms: dotnet -author: kalyankrishna1 -level: 300 -client: ASP.NET Core Web App -service: Microsoft Graph -endpoint: Microsoft identity platform page_type: sample languages: - - csharp + - csharp products: - - azure - azure-active-directory - - dotnet - - office-ms-graph -description: "Add authorization using groups & group claims to an ASP.NET Core Web app that signs-in users with the Microsoft identity platform" + - dotnet + - aspnet-core + - ms-graph +name: Add authorization using groups & group claims to an ASP.NET Core Web app that signs-in users with the Microsoft identity platform +description: "This sample demonstrates a ASP.NET Core Web App application calling The Microsoft Graph" --- # Add authorization using groups & group claims to an ASP.NET Core Web app that signs-in users with the Microsoft identity platform @@ -37,31 +31,27 @@ This sample first leverages the ASP.NET Core OpenID Connect middleware to sign i > An Identity Developer session covered Azure AD App roles and security groups, featuring this scenario and how to handle the overage claim. Watch the video [Using Security Groups and Application Roles in your apps](https://www.youtube.com/watch?v=LRoc-na27l0) -## How to run this sample +## Prerequisites -To run this sample, you'll need: - -- [Visual Studio 2019](https://aka.ms/vsdownload) or just the [.NET Core SDK](https://www.microsoft.com/net/learn/get-started) -- An Internet connection -- A Windows machine (necessary if you want to run the app on Windows) -- An OS X machine (necessary if you want to run the app on Mac) -- A Linux machine (necessary if you want to run the app on Linux) -- An Azure Active Directory (Azure AD) tenant. For more information on how to get an Azure AD tenant, see [How to get an Azure AD tenant](https://azure.microsoft.com/documentation/articles/active-directory-howto-tenant/) -- A user account in your Azure AD tenant. This sample will not work with a Microsoft account (formerly Windows Live account). Therefore, if you signed in to the [Azure portal](https://portal.azure.com) with a Microsoft account and have never created a user account in your directory before, you need to do that now. +- [Visual Studio](https://visualstudio.microsoft.com/downloads/) +- An **Azure AD** tenant. For more information see: [How to get an Azure AD tenant](https://azure.microsoft.com/documentation/articles/active-directory-howto-tenant/) +- A user account in your **Azure AD**. This sample will not work with a **personal Microsoft account**. Therefore, if you signed in to the [Azure portal](https://portal.azure.com) with a personal account and have never created a user account in your directory before, you need to do that now. > Please make sure to have one or more user accounts in the tenant assigned to a few security groups in your tenant. Please follow the instructions in [Create a basic group and add members using Azure Active Directory](https://docs.microsoft.com/azure/active-directory/fundamentals/active-directory-groups-create-azure-portal) to create a few groups and assign users to them if not already done. -### Step 1: Clone or download this repository +## Setup + +### Step 1: Clone or download this repository From your shell or command line: -```Shell +```console git clone https://github.com/Azure-Samples/microsoft-identity-platform-aspnetcore-webapp-tutorial.git ``` or download and extract the repository .zip file. -> Given that the name of the sample is quiet long, and so are the names of the referenced NuGet packages, you might want to clone it in a folder close to the root of your hard drive, to avoid file name length limitations on Windows. +> :warning: Given that the name of the sample is quite long, and so are the names of the referenced packages, you might want to clone it in a folder close to the root of your hard drive, to avoid maximum file path length limitations on Windows. Navigate to the `"5-WebApp-AuthZ"` folder @@ -69,19 +59,21 @@ Navigate to the `"5-WebApp-AuthZ"` folder cd 5-WebApp-AuthZ\5-2-Groups ``` -### Step 2: Register the sample application with your Azure Active Directory tenant +## Register the sample application with your Azure Active Directory tenant There is one project in this sample. To register it, you can: -- either follow the steps [Step 2: Register the sample with your Azure Active Directory tenant](#step-2-register-the-sample-with-your-azure-active-directory-tenant) and [Step 3: Configure the sample to use your Azure AD tenant](#choose-the-azure-ad-tenant-where-you-want-to-create-your-applications) +- either follow the steps below for manually register your apps - or use PowerShell scripts that: - - **automatically** creates the Azure AD applications and related objects (passwords, permissions, dependencies) for you. Note that this works for Visual Studio only. - - modify the Visual Studio projects' configuration files. + - **automatically** creates the Azure AD applications and related objects (passwords, permissions, dependencies) for you. + - modify the projects' configuration files.
Expand this section if you want to use this automation: -1. On Windows, run PowerShell and navigate to the root of the cloned directory +> :warning: If you have never used **Azure AD PowerShell** before, we recommend you go through the [App Creation Scripts](./AppCreationScripts/AppCreationScripts.md) once to ensure that your environment is prepared correctly for this step. + +1. On Windows, run PowerShell as **Administrator** and navigate to the root of the cloned directory 1. In PowerShell run: ```PowerShell @@ -99,23 +91,21 @@ There is one project in this sample. To register it, you can: > Other ways of running the scripts are described in [App Creation Scripts](./AppCreationScripts/AppCreationScripts.md) > The scripts also provide a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios. -1. Open the Visual Studio solution and click start to run the code. -
-Follow the steps below to manually walk through the steps to register and configure the application registration in the portal. +Follow the steps below to manually walk through the steps to register and configure the applications in the Azure portal. -#### Choose the Azure AD tenant where you want to create your applications +### Choose the Azure AD tenant where you want to create your applications As a first step you'll need to: -1. Sign in to the [Azure portal](https://portal.azure.com) using either a work or school account or a personal Microsoft account. -1. If your account is present in more than one Azure AD tenant, select your profile at the top right corner in the menu on top of the page. Then select **switch directory** to change your portal session to the desired Azure AD tenant. +1. Sign in to the [Azure portal](https://portal.azure.com). +1. If your account is present in more than one Azure AD tenant, select your profile at the top right corner in the menu on top of the page, and then **switch directory** to change your portal session to the desired Azure AD tenant. -#### Register the webApp app (WebApp-GroupClaims) +#### Register the web app (WebApp-GroupClaims) -1. Navigate to the Microsoft identity platform for developers [App registrations](https://go.microsoft.com/fwlink/?linkid=2083908) page. -1. Select **New registration**. +1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure AD** service. +1. Select the **App registrations** blade on the left, then select **New registration**. 1. In the **Register an application page** that appears, enter your application's registration information: - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `WebApp-GroupClaims`. - Under **Supported account types**, select **Accounts in this organizational directory only**. @@ -139,21 +129,36 @@ As a first step you'll need to: - Select one of the available key durations (**In 1 year**, **In 2 years**, or **Never Expires**) as per your security posture. - The generated key value will be displayed when you click the **Add** button. Copy the generated value for use in the steps later. - You'll need this key later in your code's configuration files. This key value will not be displayed again, and is not retrievable by any other means, so make sure to note it from the Azure portal before navigating to any other screen or blade. -1. In the app's registration screen, click on the **API permissions** blade in the left to open the page where we add access to the Apis that your application needs. +1. In the app's registration screen, click on the **API permissions** blade in the left to open the page where we add access to the APIs that your application needs. - Click the **Add a permission** button and then, - Ensure that the **Microsoft APIs** tab is selected. - In the *Commonly used Microsoft APIs* section, click on **Microsoft Graph** - - In the **Delegated permissions** section, select the **Directory.Read.All** in the list. Use the search box if necessary. + - In the **Delegated permissions** section, select the **User.Read** and **GroupMember.Read.All** in the list. Use the search box if necessary. - Click on the **Add permissions** button at the bottom. +1. At this stage permissions are assigned correctly and the **GroupMember.Read.All** requires admin to consent. + Click the **Grant/revoke admin consent for {tenant}** button, and then select **Yes** when you are asked if you want to grant consent for the + requested permissions for all account in the tenant. + You need to be an Azure AD tenant admin to do this. + +##### Configure the web app (WebApp-GroupClaims) to use your app registration + +Open the project in your IDE (like Visual Studio) to configure the code. +>In the steps below, "ClientID" is the same as "Application ID" or "AppId". + +1. Open the `appsettings.json` file +1. Find the app key `ClientId` and replace the existing value with the application ID (clientId) of the `WebApp-GroupClaims` application copied from the Azure portal. +1. Find the app key `TenantId` and replace the existing value with your Azure AD tenant ID. +1. Find the app key `Domain` and replace the existing value with your Azure AD tenant name. +1. Find the app key `ClientSecret` and replace the existing value with the key you saved during the creation of the `WebApp-GroupClaims` app, in the Azure portal. #### Configure your application to receive the **groups** claim -Now you have two different options available to you on how you can further configure your application to receive the `groups` claim. +You have two different options available to you on how you can further configure your application to receive the `groups` claim. 1. [Receive **all the groups** that the signed-in user is assigned to in an Azure AD tenant, included nested groups](#configure-your-application-to-receive-all-the-groups-the-signed-in-user-is-assigned-to-included-nested-groups). -1. [Receive the **groups** claim values from a **filtered set of groups** that your application is programmed to work with](#configure-your-application-to-receive-the-groups-claim-values-from-a-filtered-set-of-groups-a-user-may-be-assigned-to). (Not available in the [Azure AD Free edition](https://azure.microsoft.com/pricing/details/active-directory/)). +1. [Receive the **groups** claim values from a **filtered set of groups** that your application is programmed to work with](#configure-your-application-to-receive-the-groups-claim-values-from-a-filtered-set-of-groups-a-user-may-be-assigned-to) (Not available in the [Azure AD Free edition](https://azure.microsoft.com/pricing/details/active-directory/)). -> To get the on-premise group's `samAccountName` or `On Premises Group Security Identifier` instead of Group id, please refer to the document [Configure group claims for applications with Azure Active Directory](https://docs.microsoft.com/azure/active-directory/hybrid/how-to-connect-fed-group-claims#prerequisites-for-using-group-attributes-synchronized-from-active-directory). +> To get the on-premise group's `samAccountName` or `On Premises Group Security Identifier` instead of Group ID, please refer to the document [Configure group claims for applications with Azure Active Directory](https://docs.microsoft.com/azure/active-directory/hybrid/how-to-connect-fed-group-claims#prerequisites-for-using-group-attributes-synchronized-from-active-directory). ##### Configure your application to receive **all the groups** the signed-in user is assigned to, included nested groups @@ -178,8 +183,8 @@ Now you have two different options available to you on how you can further confi 1. Select `Groups assigned to the application`. 1. Choosing additional options like `Security Groups` or `All groups (includes distribution lists but not groups assigned to the application)` will negate the benefits your app derives from choosing to use this option. 1. Under the **ID** section, select `Group ID`. This will result in Azure AD sending the object [id](https://docs.microsoft.com/graph/api/resources/group?view=graph-rest-1.0) of the groups the user is assigned to in the `groups` claim of the [ID Token](https://docs.microsoft.com/azure/active-directory/develop/id-tokens) that your app receives after signing-in a user. -1. If you are exposing a Web API using the **Expose an API** option, then you can also choose the `Group ID` option under the **Access** section. This will result in Azure AD sending the object [id](https://docs.microsoft.com/graph/api/resources/group?view=graph-rest-1.0) of the groups the user is assigned to in the `groups` claim of the [Access Token](https://docs.microsoft.com/azure/active-directory/develop/access-tokens) issued to the client applications of your API. -1. In the app's registration screen, click on the **Overview** blade in the left to open the Application overview screen. Select the hyperlink with the name of your application in **Managed application in local directory** (note this field title can be truncated for instance `Managed application in ...`). When you select this link you will navigate to the **Enterprise Application Overview** page associated with the service principal for your application in the tenant where you created it. You can navigate back to the app registration page by using the back button of your browser. +1. If you are exposing a Web API using the **Expose an API** option, then you can also choose the `Group ID` option under the **Access** section. This will result in Azure AD sending the [Object ID](https://docs.microsoft.com/graph/api/resources/group?view=graph-rest-1.0) of the groups the user is assigned to in the `groups` claim of the [Access Token](https://docs.microsoft.com/azure/active-directory/develop/access-tokens) issued to the client applications of your API. +1. In the app's registration screen, click on the **Overview** blade in the left to open the Application overview screen. Select the hyperlink with the name of your application in **Managed application in local directory** (note this field title can be truncated for instance `Managed application in ...`). When you select this link you will navigate to the **Enterprise Application Overview** page associated with the service principal for your application in the tenant where you created it. You can navigate back to the app registration page by using the *back* button of your browser. 1. Select the **Users and groups** blade in the left to open the page where you can assign users and groups to your application. 1. Click on the **Add user** button on the top row. 1. Select **User and Groups** from the resultant screen. @@ -188,29 +193,57 @@ Now you have two different options available to you on how you can further confi 1. Click **Assign** to finish the group assignment process. 1. Your application will now receive these selected groups in the `groups` claim when a user signing in to your app is a member of one or more these **assigned** groups. 1. Select the **Properties** blade in the left to open the page that lists the basic properties of your application.Set the **User assignment required?** flag to **Yes**. - > **Important security tip** > > When you set **User assignment required?** to **Yes**, Azure AD will check that only users assigned to your application in the **Users and groups** blade are able to sign-in to your app. You can assign users directly or by assigning security groups they belong to. -##### Configure the webApp app (WebApp-GroupClaims) to use your app registration +### Configure the web app (WebApp-GroupClaims) to recognize Group IDs -Open the project in your IDE (like Visual Studio) to configure the code. ->In the steps below, "ClientID" is the same as "Application ID" or "AppId". +> :warning: +> During **Token Configuration**, if you have chosen any other option except **groupID** (e.g. like **DNSDomain\sAMAccountName**) you should enter the **group name** (for example `contoso.com\Test Group`) instead of the **object ID** below: -1. Open the `appsettings.json` file -1. Find the app key `ClientId` and replace the existing value with the application ID (clientId) of the `WebApp-GroupClaims` application copied from the Azure portal. -1. Find the app key `TenantId` and replace the existing value with your Azure AD tenant ID. -1. Find the app key `Domain` and replace the existing value with your Azure AD tenant name. -1. Find the app key `ClientSecret` and replace the existing value with the key you saved during the creation of the `WebApp-GroupClaims` app, in the Azure portal. +1. Open the `appsettings.json` file. +1. Find the app key `Groups.GroupAdmin` and replace the existing value with the object ID of the **GroupAdmin** group copied from the Azure portal. +1. Find the app key `Groups.GroupMember` and replace the existing value with the object ID of the **GroupMember** group copied from the Azure portal. + +## Running the sample + +### Run the sample using Visual Studio + +> For Visual Studio Users +> +> Clean the solution, rebuild the solution, and run it. + +### Run the sample using a command line interface such as VS Code integrated terminal + +#### Step 1. Install .NET Core dependencies + +```console + cd WebApp-GroupClaims + dotnet restore +``` + +#### Step 2. Trust development certificates + +```console + dotnet dev-certs https --clean + dotnet dev-certs https --trust +``` + +Learn more about [HTTPS in .NET Core](https://docs.microsoft.com/aspnet/core/security/enforcing-ssl). -### Step 4: Run the sample +#### Step 3. Run the applications -1. Clean and rebuild the solution, and run it. +In the console window execute the below command: -1. Open your web browser and make a request to the app. The app immediately attempts to authenticate you to the Microsoft identity platform. Sign in with a *work or school account* from the tenant where you created this app. +```console + dotnet run +``` + +1. Open your web browser and make a request to the app. The app immediately attempts to authenticate you to the Microsoft identity platform. You can sign-in with a *work or school account* from the tenant where you created this app. If admin consent to `GroupMember.Read.All` permission from portal is not done then sign-in with admin for the first time and consent for the permission. +1. If the **Overage** scenario occurs for the signed-in user then all the groups are retrieved from Microsoft Graph and added in a list. The [overage](#groups-overage-claim) scenario is discussed later in this article. 1. On the home page, the app lists the various claims it obtained from your ID token. You'd notice one more claims named `groups`. -1. On the top menu, click on the signed-in user's name **user@domain.com**, you should now see all kind of information about yourself including their picture. Beneath that, a list of all the security groups that the signed-in user is assigned to are listed as well. All of this was obtained by making calls to Microsoft Graph. This list is useful if the **Overage** scenario occurs with this signed-in user. The [overage](#groups-overage-claim) scenario is discussed later in this article. +1. On the top menu, click on the signed-in user's name **user@domain.com**, you should now see all kind of information about yourself including their picture. > Did the sample not work for you as expected? Did you encounter issues trying this sample? Then please reach out to us using the [GitHub Issues](../../../../issues) page. @@ -230,34 +263,11 @@ The object id of the security groups the signed in user is member of is returned } ``` -### Support in ASP.NET Core middleware libraries - -The ASP.NET middleware supports roles populated from claims by specifying the claim in the `RoleClaimType` property of `TokenValidationParameters`. -Since the `groups` claim contains the object IDs of the security groups than actual names by default, you'd use the group ID's instead of group names. See [Role-based authorization in ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/authorization/roles) for more info. - -```CSharp -// Startup.cs - -// The following lines code instruct the asp.net core middleware to use the data in the "groups" claim in the [Authorize] attribute and for User.IsInrole() -// See https://docs.microsoft.com/aspnet/core/security/authorization/roles -services.Configure(OpenIdConnectDefaults.AuthenticationScheme, options => -{ - // Use the groups claim for populating roles - options.TokenValidationParameters.RoleClaimType = "groups"; -}); - -// In code..(Controllers & elsewhere) -[Authorize(Roles = "Group-object-id")] // In controllers -// or -User.IsInRole("Group-object-id"); // In methods - -``` - ### The groups overage claim To ensure that the token size doesn’t exceed HTTP header size limits, the Microsoft Identity Platform limits the number of object Ids that it includes in the **groups** claim. -If a user is member of more groups than the overage limit (**150 for SAML tokens, 200 for JWT tokens, 6 for Single Page applications**, ), then the Microsoft Identity Platform does not emit the group ids in the `groups` claim in the token. Instead, it includes an **overage** claim in the token that indicates to the application to query the [Graph API](https://graph.microsoft.com) to retrieve the user’s group membership. +If a user is member of more groups than the overage limit (**150 for SAML tokens, 200 for JWT tokens, 6 for Single Page applications**), then the Microsoft Identity Platform does not emit the group IDs in the `groups` claim in the token. Instead, it includes an **overage** claim in the token that indicates to the application to query the [MS Graph API](https://graph.microsoft.com) to retrieve the user’s group membership. ```JSON { @@ -277,7 +287,7 @@ If a user is member of more groups than the overage limit (**150 for SAML tokens #### Create the overage scenario in this sample for testing -1. You can use the `BulkCreateGroups.ps1` provided in the [App Creation Scripts](./AppCreationScripts/) folder to create a large number of groups and assign users to them. This will help test overage scenarios during development. Remember to change the user's objectId provided in the `BulkCreateGroups.ps1` script. +1. You can use the `BulkCreateGroups.ps1` provided in the [App Creation Scripts](./AppCreationScripts/) folder to create a large number of groups and assign users to them. This will help test overage scenarios during development. Remember to change the user's **objectId** provided in the `BulkCreateGroups.ps1` script. 1. When you run this sample and an overage occurred, then you'd see the `_claim_names` in the home page after the user signs-in. 1. We strongly advise you use the [group filtering feature](#configure-your-application-to-receive-the-groups-claim-values-from-a-filtered-set-of-groups-a-user-may-be-assigned-to) (if possible) to avoid running into group overages. 1. In case you cannot avoid running into group overage, we suggest you use the following logic to process groups claim in your token. @@ -326,14 +336,24 @@ The following files have the code that would be of interest to you: 1. HomeController.cs 1. Passes the **HttpContext.User** (the signed-in user) to the view. -1. UserProfileController.cs - 1. Uses the **IMSGraphService** methods to fetch the signed-in user's group memberships. -1. IMSGraphService.cs, MSGraphService.cs and UserGroupsAndDirectoryRoles.cs - 1. Uses the [Microsoft Graph SDK](https://github.com/microsoftgraph/msgraph-sdk-dotnet) to carry out various operations with [Microsoft Graph](https://graph.microsoft.com). + 1. Calls method **GetSessionGroupList** of `GraphHelper.cs` to get groups from session and if groups are returned then pass them to the view. + + ```csharp + public IActionResult Index() + { + ViewData["User"] = HttpContext.User; + var groups = GraphHelper.GetUserGroupsFromSession(HttpContext.Session); + if (groups?.Count > 0) + { + ViewData.Add("groupClaims", groups ); + } + return View(); + } + ``` + 1. Home\Index.cshtml - 1. This has some code to print the current user's claims -1. UserProfile\Index.cshtml - 1. Has some client code that prints the signed-in user's information obtained from the [/me](https://docs.microsoft.com/graph/api/user-get?view=graph-rest-1.0), [/me/photo](https://docs.microsoft.com/graph/api/profilephoto-get) and [/memberOf](https://docs.microsoft.com/graph/api/user-list-memberof) endpoints. + 1. This has some code to print the current user's claims. + 1. Startup.cs - at the top of the file, add the following using directive: @@ -342,59 +362,154 @@ The following files have the code that would be of interest to you: using Microsoft.Identity.Web; ``` - - in the `ConfigureServices` method, the following lines: + - in the `ConfigureServices` method, the following lines: ```CSharp services.AddAuthentication(AzureADDefaults.AuthenticationScheme) .AddAzureAD(options => Configuration.Bind("AzureAd", options)); ``` - - have been replaced by these lines: - - + - have been replaced by these lines: + ```CSharp - services.AddMicrosoftIdentityWebAppAuthentication(Configuration) - .EnableTokenAcquisitionToCallDownstreamApi( new string[] { "User.Read", "Directory.Read.All" }) - .AddInMemoryTokenCaches(); + services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp( + options => + { + Configuration.Bind("AzureAd", options); + options.Events = new OpenIdConnectEvents(); + options.Events.OnTokenValidated = async context => + { + var overageGroupClaims = await GraphHelper.GetSignedInUsersGroups(context); + }; + }, options => { Configuration.Bind("AzureAd", options); }) + .EnableTokenAcquisitionToCallDownstreamApi(options => Configuration.Bind("AzureAd", options), initialScopes) + .AddMicrosoftGraph(Configuration.GetSection("GraphAPI")) + .AddInMemoryTokenCaches(); + ``` - services.AddMSGraphService(Configuration); // Adds the IMSGraphService as an available service for this app. + `OnTokenValidated` event calls **GetSignedInUsersGroups** method, that is defined in GraphHelper.cs, to process groups overage claim. + + `AddMicrosoftGraph` registers the service for `GraphServiceClient`. The values for BaseUrl and Scopes defined in `GraphAPI` section of **appsettings.json**. + + Following lines of code adds authorization policies that enforce authorization using group values. + + ```csharp + services.AddAuthorization(options => + { + options.AddPolicy("GroupAdmin", + policy => policy.Requirements.Add(new GroupPolicyRequirement(Configuration["Groups:GroupAdmin"]))); + options.AddPolicy("GroupMember", + policy => policy.Requirements.Add(new GroupPolicyRequirement(Configuration["Groups:GroupMember"]))); + }); + ``` + +1. In GraphHelper.cs, **GetSignedInUsersGroups** method checks if incoming token contains *Group Overage* claim then returns the list of groups from Microsoft Graph. First **GetUserGroupsFromSession** method is called to get group values from session if exists. If session does not contain groups claim then it will call **ProcessUserGroupsForOverage** method to retrieve groups. + + ```csharp + public static async Task> GetSignedInUsersGroups(TokenValidatedContext context) + { + List groupClaims = new List(); + if (HasOverageOccurred(context.Principal)) + { + // + groupClaims = GetUserGroupsFromSession(context.HttpContext.Session); + if (groupClaims?.Count > 0) + { + return groupClaims; + } + else + { + groupClaims = await ProcessUserGroupsForOverage(context); + } + } + return groupClaims; + } + ``` + + GraphHelper.cs contains a method **CheckUsersGroupMembership** that is called in `CustomAuthorization.cs` to check if value of GroupName parameter exists in either Session for Overage scenario or in User claims otherwise. + + ```csharp + public static bool CheckUsersGroupMembership(AuthorizationHandlerContext context, string GroupName, IHttpContextAccessor _httpContextAccessor) + { + bool result = false; + if (HasOverageOccurred(context.User)) + { + var groups = GetUserGroupsFromSession(_httpContextAccessor.HttpContext.Session); + if (groups?.Count > 0 && groups.Contains(GroupName)) + { + result = true; + } + } + else if (context.User.Claims.Any(x => x.Type == "groups" && x.Value == GroupName)) + { + result = true; + } + return result; + } ``` +1. In `CustomAuthorization.cs`, we have **GroupPolicyHandler** class that deals with custom Policy-based authorization. It evaluates the GroupPolicyRequirement against AuthorizationHandlerContext by overriding **HandleRequirementAsync** of **AuthorizationHandler**. + + HandleRequirementAsync calls **CheckUsersGroupMembership** method of `GraphHelper.cs` to determine if authorization is allowed. + + ```csharp + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, + GroupPolicyRequirement requirement) + { + if (GraphHelper.CheckUsersGroupMembership(context, requirement.GroupName, _httpContextAccessor)) + { + context.Succeed(requirement); + } + return Task.CompletedTask; + } + ``` + +1. UserProfileController.cs + 1. Checks authorization of signed-in user for ```[Authorize(Policy = "GroupAdmin")]```. If authorized successfully then obtain information from the [/me](https://docs.microsoft.com/graph/api/user-get?view=graph-rest-1.0) and [/me/photo](https://docs.microsoft.com/graph/api/profilephoto-get) endpoints by using `GraphServiceClient`. + +1. UserProfile\Index.cshtml + 1. Has some client code that prints the signed-in user's information. + ## How to deploy this sample to Azure This project has one WebApp project. To deploy that to Azure Web Sites, you'll need to: - create an Azure Web Site -- publish the Web App / Web APIs to the web site, and -- update its client(s) to call the web site instead of IIS Express. +- publish the project to the web site, and +- update its client(s) to call the web site instead of the local environment. ### Create and publish the `WebApp-GroupClaims` to an Azure Web Site 1. Sign in to the [Azure portal](https://portal.azure.com). 1. Click `Create a resource` in the top left-hand corner, select **Web** --> **Web App**, and give your web site a name, for example, `WebApp-GroupClaims-contoso.azurewebsites.net`. -1. Thereafter select the `Subscription`, `Resource Group`, `App service plan and Location`. `OS` will be **Windows** and `Publish` will be **Code**. +1. Next, select the `Subscription`, `Resource Group`, `App service plan and Location`. `OS` will be **Windows** and `Publish` will be **Code**. 1. Click `Create` and wait for the App Service to be created. 1. Once you get the `Deployment succeeded` notification, then click on `Go to resource` to navigate to the newly created App service. 1. Once the web site is created, locate it it in the **Dashboard** and click it to open **App Services** **Overview** screen. -1. From the **Overview** tab of the App Service, download the publish profile by clicking the **Get publish profile** link and save it. Other deployment mechanisms, such as from source control, can also be used. +1. From the **Overview** tab of the App Service, download the publish profile by clicking the **Get publish profile** link and save it. Other deployment mechanisms, such as from **source control**, can also be used. 1. Switch to Visual Studio and go to the WebApp-GroupClaims project. Right click on the project in the Solution Explorer and select **Publish**. Click **Import Profile** on the bottom bar, and import the publish profile that you downloaded earlier. -1. Click on **Configure** and in the `Connection tab`, update the Destination URL so that it is a `https` in the home page url, for example [https://WebApp-GroupClaims-contoso.azurewebsites.net](https://WebApp-GroupClaims-contoso.azurewebsites.net). Click **Next**. +1. Click on **Configure** and in the `Connection tab`, update the Destination URL so that it is a `https` in the home page URL, for example [https://WebApp-GroupClaims-contoso.azurewebsites.net](https://WebApp-GroupClaims-contoso.azurewebsites.net). Click **Next**. 1. On the Settings tab, make sure `Enable Organizational Authentication` is NOT selected. Click **Save**. Click on **Publish** on the main screen. 1. Visual Studio will publish the project and automatically open a browser to the URL of the project. If you see the default web page of the project, the publication was successful. -### Update the Active Directory tenant application registration for `WebApp-GroupClaims` +### Update the Azure AD app registration for `WebApp-GroupClaims` 1. Navigate back to the [Azure portal](https://portal.azure.com). In the left-hand navigation pane, select the **Azure Active Directory** service, and then select **App registrations (Preview)**. -1. In the resultant screen, select the `WebApp-GroupClaims` application. -1. In the **Authentication** | page for your application, update the Logout URL fields with the address of your service, for example [https://WebApp-GroupClaims-contoso.azurewebsites.net](https://WebApp-GroupClaims-contoso.azurewebsites.net) +1. In the resulting screen, select the `WebApp-GroupClaims` application. +1. In the **Authentication** page for your application, update the Logout URL fields with the address of your service, for example [https://WebApp-GroupClaims-contoso.azurewebsites.net](https://WebApp-GroupClaims-contoso.azurewebsites.net) 1. From the *Branding* menu, update the **Home page URL**, to the address of your service, for example [https://WebApp-GroupClaims-contoso.azurewebsites.net](https://WebApp-GroupClaims-contoso.azurewebsites.net). Save the configuration. -1. Add the same URL in the list of values of the *Authentication -> Redirect URIs* menu. If you have multiple redirect urls, make sure that there a new entry using the App service's Uri for each redirect url. +1. Add the same URL in the list of values of the *Authentication -> Redirect URIs* menu. If you have multiple redirect URIs, make sure that there a new entry using the App service's URI for each redirect URI. + +> :warning: If your app is using an *in-memory* storage, **Azure App Services** will spin down your web site if it is inactive, and any records that your app was keeping will emptied. +In addition, if you increase the instance count of your web site, requests will be distributed among the instances. Your app's records, therefore, will not be the same on each instance. ## Community Help and Support Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. -Make sure that your questions or comments are tagged with [ `msal` `azure-active-directory`]. +Make sure that your questions or comments are tagged with [ `msal` `azure-active-directory` `dotnet`]. If you find a bug in the sample, please raise the issue on [GitHub Issues](../../../../issues). diff --git a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/Bootstrapper.cs b/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/Bootstrapper.cs deleted file mode 100644 index 83967d19..00000000 --- a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/Bootstrapper.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using WebApp_OpenIDConnect_DotNet.Services.MicrosoftGraph; - -namespace WebApp_OpenIDConnect_DotNet.Services.GraphOperations -{ - public static class Bootstrapper - { - public static void AddGraphService(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(configuration); - // https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests - services.AddHttpClient(); - } - - /// Adds support for IMSGraphService, which provides interaction with Microsoft Graph using Graph SDK. - /// The services collection to add to - /// The app configuration - public static void AddMSGraphService(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(configuration); - services.AddSingleton(); - } - } -} \ No newline at end of file diff --git a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/GraphApiOperationService.cs b/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/GraphApiOperationService.cs deleted file mode 100644 index 899dedc0..00000000 --- a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/GraphApiOperationService.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using WebApp_OpenIDConnect_DotNet.Infrastructure; - -namespace WebApp_OpenIDConnect_DotNet.Services.GraphOperations -{ - public class GraphApiOperationService : IGraphApiOperations - { - private readonly HttpClient httpClient; - private readonly WebOptions webOptions; - - public GraphApiOperationService(HttpClient httpClient, IOptions webOptionValue) - { - this.httpClient = httpClient; - webOptions = webOptionValue.Value; - } - - public async Task GetUserInformation(string accessToken) - { - httpClient.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue(Constants.BearerAuthorizationScheme, - accessToken); - var response = await httpClient.GetAsync($"{webOptions.GraphApiUrl}/beta/me"); - if (response.StatusCode == HttpStatusCode.OK) - { - var content = await response.Content.ReadAsStringAsync(); - dynamic me = JsonConvert.DeserializeObject(content); - - return me; - } - - throw new - HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}."); - } - - public async Task GetPhotoAsBase64Async(string accessToken) - { - httpClient.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue(Constants.BearerAuthorizationScheme, - accessToken); - - var response = await httpClient.GetAsync($"{webOptions.GraphApiUrl}/beta/me/photo/$value"); - if (response.StatusCode == HttpStatusCode.OK) - { - byte[] photo = await response.Content.ReadAsByteArrayAsync(); - string photoBase64 = Convert.ToBase64String(photo); - - return photoBase64; - } - else - { - return null; - } - } - } -} \ No newline at end of file diff --git a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/GraphHelper.cs b/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/GraphHelper.cs new file mode 100644 index 00000000..9f27adff --- /dev/null +++ b/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/GraphHelper.cs @@ -0,0 +1,257 @@ +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Graph; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using WebApp_OpenIDConnect_DotNet.Infrastructure; + +namespace WebApp_OpenIDConnect_DotNet.Services +{ + public class GraphHelper + { + /// + /// This method inspects the claims collection created from the ID or Access token issued to a user and returns the groups that are present in the token. + /// If groups claims are already present in Session then it returns the list of groups by calling GetSessionGroupList method. + /// If it detects groups overage, the method then makes calls to ProcessUserGroupsForOverage method. + /// + /// TokenValidatedContext + public static async Task> GetSignedInUsersGroups(TokenValidatedContext context) + { + List groupClaims = new List(); + + // Checks if the incoming token contained a 'Group Overage' claim. + if (HasOverageOccurred(context.Principal)) + { + // Gets group values from session variable if exists. + groupClaims = GetUserGroupsFromSession(context.HttpContext.Session); + if (groupClaims?.Count > 0) + { + return groupClaims; + } + else + { + groupClaims = await ProcessUserGroupsForOverage(context); + } + } + return groupClaims; + } + + /// + /// Retrieves all the groups saved in Session. + /// + /// + /// + public static List GetUserGroupsFromSession(ISession _httpContextSession) + { + // Checks if Session contains data for groupClaims. + // The data will exist for 'Group Overage' claim. + if (_httpContextSession.Keys.Contains("groupClaims")) + { + return _httpContextSession.GetAsByteArray("groupClaims") as List; + } + return null; + } + + /// + /// Checks if 'Group Overage' claim exists for signed-in user. + /// + /// + /// + private static bool HasOverageOccurred(ClaimsPrincipal identity) + { + return identity.Claims.Any(x => x.Type == "hasgroups" || (x.Type == "_claim_names" && x.Value == "{\"groups\":\"src1\"}")); + } + + /// + /// ID Token does not contain 'scp' claim. + /// This claims exist for Access Token. + /// + /// + /// + private static bool IsAccessToken(ClaimsIdentity identity) + { + return identity.Claims.Any(x => x.Type == "scp" || x.Type == "http://schemas.microsoft.com/identity/claims/scope"); + } + + /// + /// This method is called for Groups overage scenario. + /// The method makes calls to Microsoft Graph to fetch the group membership of the authenticated user. + /// + /// + /// + private static async Task> ProcessUserGroupsForOverage(TokenValidatedContext context) + { + List groupClaims = new List(); + try + { + // Before instatntiating GraphServiceClient, the app should have granted admin consent for 'GroupMember.Read.All' permission. + var graphClient = context.HttpContext.RequestServices.GetService(); + + if (graphClient == null) + { + Console.WriteLine("No service for type 'Microsoft.Graph.GraphServiceClient' has been registered in the Startup."); + } + + // Checks if the SecurityToken is not null. + // For the Web App, SecurityToken contains value of the ID Token. + else if (context.SecurityToken != null) + { + // Checks if 'JwtSecurityTokenUsedToCallWebAPI' key already exists. + // This key is required to acquire Access Token for Graph Service Client. + if (!context.HttpContext.Items.ContainsKey("JwtSecurityTokenUsedToCallWebAPI")) + { + // For Web App, access token is retrieved using account identifier. But at this point account identifier is null. + // So, SecurityToken is saved in 'JwtSecurityTokenUsedToCallWebAPI' key. + // The key is then used to get the Access Token on-behalf of user. + context.HttpContext.Items.Add("JwtSecurityTokenUsedToCallWebAPI", context.SecurityToken as JwtSecurityToken); + } + + // The properties that we want to retrieve from MemberOf endpoint. + string select = "id,displayName,onPremisesNetBiosName,onPremisesDomainName,onPremisesSamAccountNameonPremisesSecurityIdentifier"; + + IUserMemberOfCollectionWithReferencesPage memberPage = new UserMemberOfCollectionWithReferencesPage(); + try + { + //Request to get groups and directory roles that the user is a direct member of. + memberPage = await graphClient.Me.MemberOf.Request().Select(select).GetAsync().ConfigureAwait(false); + } + catch (Exception graphEx) + { + var exMsg = graphEx.InnerException != null ? graphEx.InnerException.Message : graphEx.Message; + Console.WriteLine("Call to Microsoft Graph failed: " + exMsg); + } + + if (memberPage?.Count > 0) + { + // There is a limit to number of groups returned, below method make calls to Microsoft graph to get all the groups. + var allgroups = ProcessIGraphServiceMemberOfCollectionPage(memberPage); + + if (allgroups?.Count > 0) + { + var identity = (ClaimsIdentity)context.Principal.Identity; + + if (identity != null) + { + // Checks if token is not Access Token but 'ID Token'. + if (!IsAccessToken(identity)) + { + // Re-populate the `groups` claim with the complete list of groups fetched from MS Graph + foreach (Group group in allgroups) + { + // The following code adds group ids to the 'groups' claim. But depending upon your reequirement and the format of the 'groups' claim selected in + // the app registration, you might want to add other attributes than id to the `groups` claim, examples being; + + // For instance if the required format is 'NetBIOSDomain\sAMAccountName' then the code is as commented below: + // groupClaims.Add(group.OnPremisesNetBiosName+"\\"+group.OnPremisesSamAccountName)); + groupClaims.Add(group.Id); + } + + // Here we add the groups in a session variable that is used in authorization policy handler. + context.HttpContext.Session.SetAsByteArray("groupClaims", groupClaims); + } + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + // Checks if the key 'JwtSecurityTokenUsedToCallWebAPI' exists. + if (context.HttpContext.Items.ContainsKey("JwtSecurityTokenUsedToCallWebAPI")) + { + // Removes 'JwtSecurityTokenUsedToCallWebAPI' from Items collection. + // If not removed then it can cause failure to the application. + // Because this key is also added by StoreTokenUsedToCallWebAPI method of Microsoft.Identity.Web. + context.HttpContext.Items.Remove("JwtSecurityTokenUsedToCallWebAPI"); + } + } + + return groupClaims; + } + + /// + /// Returns all the groups that the user is a direct member of. + /// + /// First page having collection of directory roles and groups + /// List of groups + private static List ProcessIGraphServiceMemberOfCollectionPage(IUserMemberOfCollectionWithReferencesPage membersCollectionPage) + { + List allGroups = new List(); + + try + { + if (membersCollectionPage != null) + { + do + { + // Page through results + foreach (DirectoryObject directoryObject in membersCollectionPage.CurrentPage) + { + //Collection contains directory roles and groups of the user. + //Checks and adds groups only to the list. + if (directoryObject is Group) + { + allGroups.Add(directoryObject as Group); + } + } + + // are there more pages (Has a @odata.nextLink ?) + if (membersCollectionPage.NextPageRequest != null) + { + membersCollectionPage = membersCollectionPage.NextPageRequest.GetAsync().Result; + } + else + { + membersCollectionPage = null; + } + } while (membersCollectionPage != null); + } + } + catch (ServiceException ex) + { + Console.WriteLine($"We could not process the groups list: {ex}"); + return null; + } + return allGroups; + } + + /// + /// Checks if user is member of the required group. + /// + /// + /// + /// + /// + public static bool CheckUsersGroupMembership(AuthorizationHandlerContext context, string GroupName, IHttpContextAccessor _httpContextAccessor) + { + bool result = false; + // Checks if groups claim exists in claims collection of signed-in User. + if (HasOverageOccurred(context.User)) + { + // Calls method GetSessionGroupList to get groups from session. + var groups = GetUserGroupsFromSession(_httpContextAccessor.HttpContext.Session); + + // Checks if required group exists in Session. + if (groups?.Count > 0 && groups.Contains(GroupName)) + { + result = true; + } + } + else if (context.User.Claims.Any(x => x.Type == "groups" && x.Value == GroupName)) + { + result = true; + } + return result; + } + } +} \ No newline at end of file diff --git a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/IGraphApiOperations.cs b/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/IGraphApiOperations.cs deleted file mode 100644 index 0025fe86..00000000 --- a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/IGraphApiOperations.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; - -namespace WebApp_OpenIDConnect_DotNet.Services.GraphOperations -{ - public interface IGraphApiOperations - { - Task GetUserInformation(string accessToken); - Task GetPhotoAsBase64Async(string accessToken); - } -} \ No newline at end of file diff --git a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/IMSGraphService.cs b/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/IMSGraphService.cs deleted file mode 100644 index 3943db10..00000000 --- a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/IMSGraphService.cs +++ /dev/null @@ -1,71 +0,0 @@ -/************************************************************************************************ -The MIT License (MIT) - -Copyright (c) 2015 Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -***********************************************************************************************/ - -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Graph; - -namespace WebApp_OpenIDConnect_DotNet.Services.MicrosoftGraph -{ - /// - /// IMSGraph service has samples to show how to call Microsoft Graph using the Graph SDK - /// - public interface IMSGraphService - { - /// Gets the current user directory roles. - /// The access token for MS Graph. - /// A list of directory roles - Task> GetCurrentUserDirectoryRolesAsync(string accessToken); - - /// Gets the signed-in user groups and roles. A more efficient implementation that gets both group and role membership in one call - /// The access token for MS Graph. - /// A list of UserGroupsAndDirectoryRoles - Task GetCurrentUserGroupsAndRolesAsync(string accessToken); - - /// Gets the groups the signed-in user's is a member of. - /// The access token for MS Graph. - /// A list of Groups - Task> GetCurrentUsersGroupsAsync(string accessToken); - - /// Gets basic details about the signed-in user. - /// The access token for MS Graph. - /// A detail of the User object - Task GetMeAsync(string accessToken); - - /// Gets the groups the signed-in user's is a direct member of. - /// The access token for MS Graph. - /// A list of Groups - Task> GetMyMemberOfGroupsAsync(string accessToken); - - /// Gets the signed-in user's photo. - /// The access token for MS Graph. - /// The photo of the signed-in user as a base64 string - Task GetMyPhotoAsync(string accessToken); - - /// Gets the users in a tenant. - /// The access token for MS Graph. - /// A list of users - Task> GetUsersAsync(string accessToken); - } -} \ No newline at end of file diff --git a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/MSGraphService.cs b/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/MSGraphService.cs deleted file mode 100644 index 5a879275..00000000 --- a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/MSGraphService.cs +++ /dev/null @@ -1,381 +0,0 @@ -/************************************************************************************************ -The MIT License (MIT) - -Copyright (c) 2015 Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -***********************************************************************************************/ - -using Microsoft.Extensions.Options; -using Microsoft.Graph; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using WebApp_OpenIDConnect_DotNet.Services.GraphOperations; - -namespace WebApp_OpenIDConnect_DotNet.Services.MicrosoftGraph -{ - /// - /// MSGraph service has samples to show how to call Microsoft Graph using the Graph SDK - /// - public class MSGraphService : IMSGraphService - { - private readonly WebOptions webOptions; - - // the Graph SDK's GraphServiceClient - private GraphServiceClient graphServiceClient; - - /// Initializes a new instance of the class. - /// The web option value. - public MSGraphService(IOptions webOptionValue) - { - webOptions = webOptionValue.Value; - } - - /// Gets basic details about the signed-in user. - /// The access token for MS Graph. - /// A detail of the User object - public async Task GetMeAsync(string accessToken) - { - User currentUserObject; - - try - { - PrepareAuthenticatedClient(accessToken); - currentUserObject = await graphServiceClient.Me.Request().GetAsync(); - } - catch (ServiceException e) - { - Debug.WriteLine("We could not fetch details of the currently signed-in user: " + $"{e}"); - return null; - } - - return currentUserObject; - } - - /// Gets the signed-in user's photo. - /// The access token for MS Graph. - /// The photo of the signed-in user as a base64 string - public async Task GetMyPhotoAsync(string accessToken) - { - PrepareAuthenticatedClient(accessToken); - - try - { - var photo = await graphServiceClient.Me.Photo.Content.Request().GetAsync(); - - if (photo != null) - { - using (photo) - { - // Get byte[] for display. - using (BinaryReader reader = new BinaryReader(photo)) - { - byte[] data = reader.ReadBytes((int)photo.Length); - string photoBase64 = Convert.ToBase64String(data); - - return photoBase64; - } - } - } - } - catch (ServiceException sx) - { - if (sx.Error.Message == "The photo wasn't found.") - { - return null; - } - else - { - throw; - } - } - - return null; - } - - /// Gets the groups the signed-in user's is a member of. - /// The access token for MS Graph. - /// A list of Groups - public async Task> GetCurrentUsersGroupsAsync(string accessToken) - { - IUserMemberOfCollectionWithReferencesPage memberOfGroups = null; - IList groups = new List(); - - try - { - PrepareAuthenticatedClient(accessToken); - - memberOfGroups = await graphServiceClient.Me.MemberOf.Request().GetAsync(); - - if (memberOfGroups != null) - { - do - { - foreach (var directoryObject in memberOfGroups.CurrentPage) - { - if (directoryObject is Group) - { - Group group = directoryObject as Group; - // Trace.WriteLine("Got group: " + group.Id); - groups.Add(group as Group); - } - } - if (memberOfGroups.NextPageRequest != null) - { - memberOfGroups = await memberOfGroups.NextPageRequest.GetAsync(); - } - else - { - memberOfGroups = null; - } - } while (memberOfGroups != null); - } - - return groups; - } - catch (ServiceException e) - { - Trace.Fail("We could not get user groups: " + e.Error.Message); - return null; - } - } - - /// - /// Gets the current user directory roles. - /// - /// The access token for MS Graph. - /// - /// A list of directory roles - /// - public async Task> GetCurrentUserDirectoryRolesAsync(string accessToken) - { - IUserMemberOfCollectionWithReferencesPage memberOfDirectoryRoles = null; - IList DirectoryRoles = new List(); - - try - { - PrepareAuthenticatedClient(accessToken); - memberOfDirectoryRoles = await graphServiceClient.Me.MemberOf.Request().GetAsync(); - - if (memberOfDirectoryRoles != null) - { - do - { - foreach (var directoryObject in memberOfDirectoryRoles.CurrentPage) - { - if (directoryObject is DirectoryRole) - { - DirectoryRole DirectoryRole = directoryObject as DirectoryRole; - // Trace.WriteLine("Got DirectoryRole: " + DirectoryRole.Id); - DirectoryRoles.Add(DirectoryRole as DirectoryRole); - } - } - if (memberOfDirectoryRoles.NextPageRequest != null) - { - memberOfDirectoryRoles = await memberOfDirectoryRoles.NextPageRequest.GetAsync(); - } - else - { - memberOfDirectoryRoles = null; - } - } while (memberOfDirectoryRoles != null); - } - - return DirectoryRoles; - } - catch (ServiceException e) - { - Trace.Fail("We could not get user DirectoryRoles: " + e.Error.Message); - return null; - } - } - - /// - /// Gets the signed-in user groups and roles. A more efficient implementation that gets both group and role membership in one call - /// - /// The access token for MS Graph. - /// - /// A list of UserGroupsAndDirectoryRoles - /// - public async Task GetCurrentUserGroupsAndRolesAsync(string accessToken) - { - UserGroupsAndDirectoryRoles userGroupsAndDirectoryRoles = new UserGroupsAndDirectoryRoles(); - IUserMemberOfCollectionWithReferencesPage memberOfDirectoryRoles = null; - - try - { - PrepareAuthenticatedClient(accessToken); - memberOfDirectoryRoles = await graphServiceClient.Me.MemberOf.Request().GetAsync(); - - if (memberOfDirectoryRoles != null) - { - do - { - foreach (var directoryObject in memberOfDirectoryRoles.CurrentPage) - { - if (directoryObject is Group) - { - Group group = directoryObject as Group; - // Trace.WriteLine($"Got group: {group.Id}- '{group.DisplayName}'"); - userGroupsAndDirectoryRoles.Groups.Add(group); - } - else if (directoryObject is DirectoryRole) - { - DirectoryRole role = directoryObject as DirectoryRole; - // Trace.WriteLine($"Got DirectoryRole: {role.Id}- '{role.DisplayName}'"); - userGroupsAndDirectoryRoles.DirectoryRoles.Add(role); - } - } - if (memberOfDirectoryRoles.NextPageRequest != null) - { - userGroupsAndDirectoryRoles.HasOverageClaim = true; //check if this matches 150 per token limit - memberOfDirectoryRoles = await memberOfDirectoryRoles.NextPageRequest.GetAsync(); - } - else - { - memberOfDirectoryRoles = null; - } - } while (memberOfDirectoryRoles != null); - } - - return userGroupsAndDirectoryRoles; - } - catch (ServiceException e) - { - Trace.Fail("We could not get user groups and roles: " + e.Error.Message); - return null; - } - } - - /// - /// Gets the groups the signed-in user's is a direct member of. - /// - /// The access token for MS Graph. - /// - /// A list of Groups - /// - public async Task> GetMyMemberOfGroupsAsync(string accessToken) - { - List groups = new List(); - PrepareAuthenticatedClient(accessToken); - // Get groups the current user is a direct member of. - IUserMemberOfCollectionWithReferencesPage memberOfGroups = await graphServiceClient.Me.MemberOf.Request().GetAsync(); - if (memberOfGroups?.Count > 0) - { - foreach (var directoryObject in memberOfGroups) - { - // We only want groups, so ignore DirectoryRole objects. - if (directoryObject is Group) - { - Group group = directoryObject as Group; - groups.Add(group); - } - } - } - - // If paginating - while (memberOfGroups.NextPageRequest != null) - { - memberOfGroups = await memberOfGroups.NextPageRequest.GetAsync(); - - if (memberOfGroups?.Count > 0) - { - foreach (var directoryObject in memberOfGroups) - { - // We only want groups, so ignore DirectoryRole objects. - if (directoryObject is Group) - { - Group group = directoryObject as Group; - groups.Add(group); - } - } - } - } - - return groups; - } - - /// - /// Gets the users in a tenant. - /// - /// The access token for MS Graph. - /// - /// A list of users - /// - public async Task> GetUsersAsync(string accessToken) - { - List allUsers = new List(); - - try - { - PrepareAuthenticatedClient(accessToken); - IGraphServiceUsersCollectionPage users = await graphServiceClient.Users.Request().GetAsync(); - - // When paginating - //while(users.NextPageRequest != null) - //{ - // users = await users.NextPageRequest.GetAsync(); - //} - - if (users?.CurrentPage.Count > 0) - { - foreach (User user in users) - { - allUsers.Add(user); - } - } - } - catch (ServiceException e) - { - Debug.WriteLine("We could not retrieve the user's list: " + $"{e}"); - return null; - } - - return allUsers; - } - - /// - /// Prepares the authenticated client. - /// - /// The access token. - private void PrepareAuthenticatedClient(string accessToken) - { - try - { - graphServiceClient = new GraphServiceClient(webOptions.GraphApiUrl, - new DelegateAuthenticationProvider( - async (requestMessage) => - { - await Task.Run(() => - { - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken); - }); - })); - } - catch (Exception ex) - { - Debug.WriteLine($"Could not create a graph client {ex}"); - } - } - } -} diff --git a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/UserGroupsAndDirectoryRoles.cs b/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/UserGroupsAndDirectoryRoles.cs deleted file mode 100644 index e38b2e29..00000000 --- a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/UserGroupsAndDirectoryRoles.cs +++ /dev/null @@ -1,58 +0,0 @@ -/************************************************************************************************ -The MIT License (MIT) - -Copyright (c) 2015 Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -***********************************************************************************************/ - -using Microsoft.Graph; -using System.Collections.Generic; - -namespace WebApp_OpenIDConnect_DotNet.Services.MicrosoftGraph -{ - /// - /// An entity class that holds both groups and roles for a user. - /// - public class UserGroupsAndDirectoryRoles - { - public UserGroupsAndDirectoryRoles() - { - this.GroupIds = new List(); - this.Groups = new List(); - this.DirectoryRoles = new List(); - } - - /// Gets or sets a value indicating whether this user's groups claim will result in an overage - /// - /// true if this instance has overage claim; otherwise, false. - public bool HasOverageClaim { get; set; } - - /// Gets or sets the group ids. - /// The group ids. - public List GroupIds { get; set; } - - /// Gets or sets the groups. - /// The groups. - public List Groups { get; set; } - - /// Gets or sets the App roles - public List DirectoryRoles { get; set; } - } -} \ No newline at end of file diff --git a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/WebOptions.cs b/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/WebOptions.cs deleted file mode 100644 index f1318ed1..00000000 --- a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/WebOptions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace WebApp_OpenIDConnect_DotNet.Services.GraphOperations -{ - public class WebOptions - { - public string GraphApiUrl { get; set; } - } -} \ No newline at end of file diff --git a/5-WebApp-AuthZ/5-2-Groups/Startup.cs b/5-WebApp-AuthZ/5-2-Groups/Startup.cs index ddfc28f0..07aa82ab 100644 --- a/5-WebApp-AuthZ/5-2-Groups/Startup.cs +++ b/5-WebApp-AuthZ/5-2-Groups/Startup.cs @@ -8,9 +8,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Identity.Web; -using Microsoft.Identity.Web.TokenCacheProviders.InMemory; using Microsoft.Identity.Web.UI; -using WebApp_OpenIDConnect_DotNet.Services.GraphOperations; +using System; +using WebApp_OpenIDConnect_DotNet.Infrastructure; +using WebApp_OpenIDConnect_DotNet.Services; namespace WebApp_OpenIDConnect_DotNet { @@ -26,26 +27,49 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + var initialScopes = new string[] { Constants.ScopeUserRead, Constants.ScopeGroupMemberRead }; services.Configure(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.Unspecified; - // Handling SameSite cookie according to https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1 + // Handling SameSite cookie according to https://docs.microsoft.com/en-us/aspnet/core/security/samesite options.HandleSameSiteCookieCompatibility(); }); // Sign-in users with the Microsoft identity platform - services.AddMicrosoftIdentityWebAppAuthentication(Configuration) - .EnableTokenAcquisitionToCallDownstreamApi( new string[] { "User.Read", "Directory.Read.All" }) + services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp( + options => + { + Configuration.Bind("AzureAd", options); + options.Events = new OpenIdConnectEvents(); + options.Events.OnTokenValidated = async context => + { + //Calls method to process groups overage claim. + var overageGroupClaims = await GraphHelper.GetSignedInUsersGroups(context); + }; + }, options => { Configuration.Bind("AzureAd", options); }) + .EnableTokenAcquisitionToCallDownstreamApi(options => Configuration.Bind("AzureAd", options), initialScopes) + .AddMicrosoftGraph(Configuration.GetSection("GraphAPI")) .AddInMemoryTokenCaches(); - services.AddMSGraphService(Configuration); + // Adding authorization policies that enforce authorization using group values. + services.AddAuthorization(options => + { + options.AddPolicy("GroupAdmin", + policy => policy.Requirements.Add(new GroupPolicyRequirement(Configuration["Groups:GroupAdmin"]))); + options.AddPolicy("GroupMember", + policy => policy.Requirements.Add(new GroupPolicyRequirement(Configuration["Groups:GroupMember"]))); + }); + services.AddSingleton(); - services.Configure(OpenIdConnectDefaults.AuthenticationScheme, options => { - // The following code instructs the ASP.NET Core middleware to use the data in the "groups" claim in the [Authorize] attribute and for User.IsInRole() - // See https://docs.microsoft.com/en-us/aspnet/core/security/authorization/roles for more info. - options.TokenValidationParameters.RoleClaimType = "groups"; + services.AddDistributedMemoryCache(); + services.AddSession(options => + { + options.IdleTimeout = TimeSpan.FromMinutes(1); + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = true; }); services.AddControllersWithViews(options => @@ -74,12 +98,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseHttpsRedirection(); app.UseStaticFiles(); - app.UseCookiePolicy(); app.UseRouting(); + app.UseSession(); app.UseAuthentication(); app.UseAuthorization(); + app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( diff --git a/5-WebApp-AuthZ/5-2-Groups/Views/Home/Index.cshtml b/5-WebApp-AuthZ/5-2-Groups/Views/Home/Index.cshtml index df7210fc..4c860e7e 100644 --- a/5-WebApp-AuthZ/5-2-Groups/Views/Home/Index.cshtml +++ b/5-WebApp-AuthZ/5-2-Groups/Views/Home/Index.cshtml @@ -26,20 +26,44 @@ @foreach (var claim in user.Claims) { - - @{ - if (claim.Type == "groups") - { - @claim.Type - } - else - { - @claim.Type - } + + @{ + if (claim.Type == "groups") + { + @claim.Type + } + else + { + @claim.Type + } + } + + @claim.Value + + } + + + +@{ + List groupClaims = new List(); + + if (ViewData.ContainsKey("groupClaims")) + { + groupClaims = ViewData["groupClaims"] as List; } - @claim.Value - +} + + + + + + + @foreach (var group in groupClaims) + { + + + }
If groups overage occured, the groups fetched from Graph will be listed below
@group
\ No newline at end of file diff --git a/5-WebApp-AuthZ/5-2-Groups/Views/Shared/_Layout.cshtml b/5-WebApp-AuthZ/5-2-Groups/Views/Shared/_Layout.cshtml index 55ce50d6..db4314c0 100644 --- a/5-WebApp-AuthZ/5-2-Groups/Views/Shared/_Layout.cshtml +++ b/5-WebApp-AuthZ/5-2-Groups/Views/Shared/_Layout.cshtml @@ -31,7 +31,7 @@ diff --git a/5-WebApp-AuthZ/5-2-Groups/Views/Shared/_LoginPartial.cshtml b/5-WebApp-AuthZ/5-2-Groups/Views/Shared/_LoginPartial.cshtml index 9c076828..1560c941 100644 --- a/5-WebApp-AuthZ/5-2-Groups/Views/Shared/_LoginPartial.cshtml +++ b/5-WebApp-AuthZ/5-2-Groups/Views/Shared/_LoginPartial.cshtml @@ -3,7 +3,7 @@ { } else diff --git a/5-WebApp-AuthZ/5-2-Groups/Views/UserProfile/Index.cshtml b/5-WebApp-AuthZ/5-2-Groups/Views/UserProfile/Index.cshtml index 202b36d8..a387d1b3 100644 --- a/5-WebApp-AuthZ/5-2-Groups/Views/UserProfile/Index.cshtml +++ b/5-WebApp-AuthZ/5-2-Groups/Views/UserProfile/Index.cshtml @@ -1,6 +1,6 @@ @using Newtonsoft.Json.Linq @{ - ViewData["Title"] = "Profile & Groups"; + ViewData["Title"] = "Profile"; }

@ViewData["Title"]

@ViewData["Message"]

@@ -50,24 +50,4 @@ @user.Surname -

Your Current Groups:

-

Group Membership Acquired via GraphAPI Calls.

- - - - - - - - @foreach (Microsoft.Graph.Group group in (List - - )ViewData["Groups"]) - { - - - - - } - -
NameGroup's ObjectID
@group.DisplayName@group.Id
diff --git a/5-WebApp-AuthZ/5-2-Groups/WebApp-OpenIDConnect-DotNet.csproj b/5-WebApp-AuthZ/5-2-Groups/WebApp-OpenIDConnect-DotNet.csproj index 69f9af24..cfe17f3a 100644 --- a/5-WebApp-AuthZ/5-2-Groups/WebApp-OpenIDConnect-DotNet.csproj +++ b/5-WebApp-AuthZ/5-2-Groups/WebApp-OpenIDConnect-DotNet.csproj @@ -18,9 +18,11 @@ + - + + diff --git a/5-WebApp-AuthZ/5-2-Groups/appsettings.json b/5-WebApp-AuthZ/5-2-Groups/appsettings.json index c9b56b9d..171f3e8a 100644 --- a/5-WebApp-AuthZ/5-2-Groups/appsettings.json +++ b/5-WebApp-AuthZ/5-2-Groups/appsettings.json @@ -10,6 +10,14 @@ // To call an API "ClientSecret": "[Copy the client secret added to the app from the Azure portal]" }, + "GraphAPI": { + "BaseUrl": "https://graph.microsoft.com/v1.0", + "Scopes": "User.Read GroupMember.Read.All" + }, + "Groups": { + "GroupAdmin": "Enter the objectID for GroupAdmin group copied from Azure Portal", + "GroupMember": "Enter the objectID for GroupMember group copied from Azure Portal" + }, "Logging": { "LogLevel": { "Default": "Information", @@ -18,5 +26,5 @@ } }, "AllowedHosts": "*", - "GraphApiUrl": "https://graph.microsoft.com/beta" + "GraphApiUrl": "https://graph.microsoft.com/v1.0" }