diff --git a/.gitignore b/.gitignore index dfcfd56..2553e8d 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,6 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ +/TodoListClient/Properties/ServiceDependencies/CAAuthNContextAppDemo - Web Deploy +/TodoListClient/Properties/serviceDependencies.CAAuthNContextAppDemo - Web Deploy.json +/AppCreationScripts/createdApps.html diff --git a/AppCreationScripts/AppCreationScripts.md b/AppCreationScripts/AppCreationScripts.md index 7512ab4..6e60a27 100644 --- a/AppCreationScripts/AppCreationScripts.md +++ b/AppCreationScripts/AppCreationScripts.md @@ -1,45 +1,35 @@ -# Registering the sample apps with the Microsoft identity platform and updating the configuration files using PowerShell +# Registering sample apps with the Microsoft identity platform and updating configuration files using PowerShell ## Overview ### Quick summary -1. On Windows run PowerShell as **Administrator** 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) +1. Run the script to create your Azure AD application and configure the code of the sample application accordingly. ```PowerShell cd .\AppCreationScripts\ - .\Configure.ps1 + .\Configure.ps1 -TenantId "your test tenant's id" -AzureEnvironmentName "[Optional] - Azure environment, defaults to 'Global'" ``` -1. Open the Visual Studio solution and click start - ### More details -The following paragraphs: - -- [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](#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 (Interactive, but create apps in a specified tenant)](#option-3-Interactive-but-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 @@ -50,14 +40,14 @@ This sample comes with two PowerShell scripts, which automate the creation of th These scripts are: - `Configure.ps1` which: - - creates Azure AD applications and their related objects (permissions, dependencies, secrets), - - changes the configuration files in the C# and JavaScript projects. + - creates Azure AD applications and their related objects (permissions, dependencies, secrets, app roles), + - changes the configuration files in the sample projects. - creates a summary file named `createdApps.html` in the folder from which you ran the script, and containing, for each Azure AD application it created: - the identifier of the application - the AppId of the application - the url of its registration in the [Azure portal](https://portal.azure.com). -- `Cleanup.ps1` which cleans-up the Azure AD objects created by `Configure.ps1`. Note that this script does not revert the changes done in the configuration files, though. You will need to undo the change from source control (from Visual Studio, or from the command line using, for instance, git reset). +- `Cleanup.ps1` which cleans-up the Azure AD objects created by `Configure.ps1`. Note that this script does not revert the changes done in the configuration files, though. You will need to undo the change from source control (from Visual Studio, or from the command line using, for instance, `git reset`). ### Usage pattern for tests and DevOps scenarios @@ -75,23 +65,23 @@ The `Configure.ps1` will stop if it tries to create an Azure AD application whic Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process ``` -### (Optionally) install AzureAD PowerShell modules +### (Optionally) install Microsoft.Graph.Applications PowerShell modules -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: +The scripts install the required PowerShell module (Microsoft.Graph.Applications) 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: -1. 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 Microsoft.Graph.Applications 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). + 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 + Install-Module Microsoft.Graph.Applications ``` or if you cannot be administrator on your machine, run: - + ```PowerShell - Install-Module AzureAD -Scope CurrentUser + Install-Module Microsoft.Graph.Applications -Scope CurrentUser ``` ### Run the script and start running @@ -106,44 +96,29 @@ The scripts install the required PowerShell module (AzureAD) for the current use 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! +You're done! -### Four ways to run the script +### Two ways to run the script We advise four ways of running the script: - Interactive: you will be prompted for credentials, and the scripts decide in which tenant to create the objects, -- non-interactive: you will provide credentials, and the scripts decide in which tenant to create the objects, -- Interactive in specific tenant: you will provide the tenant in which you want to create the objects and then you will be prompted for credentials, and the scripts will create the objects, -- non-interactive in specific tenant: you will provide tenant in which you want to create the objects and credentials, and the scripts will create the objects. +- Interactive in specific tenant: you will provide the tenant in which you want to create the objects and then you will be prompted for credentials, and the scripts will create the objects, Here are the details on how to do this. #### Option 1 (interactive) -- Just run ``. .\Configure.ps1``, and you will be prompted to sign-in (email address, password, and if needed MFA). +- Just run ``.\Configure.ps1``, and you will be prompted to sign-in (email address, password, and if needed MFA). - The script will be run as the signed-in user and will use the tenant in which the user is defined. Note that the script will choose the tenant in which to create the applications, based on the user. Also to run the `Cleanup.ps1` script, you will need to re-sign-in. -#### Option 2 (non-interactive) - -When you know the identity and credentials of the user in the name of whom you want to create the applications, you can use the non-interactive approach. It's more adapted to DevOps. Here is an example of script you'd want to run in a PowerShell Window - -```PowerShell -$secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force -$mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd) -. .\Cleanup.ps1 -Credential $mycreds -. .\Configure.ps1 -Credential $mycreds -``` - -Of course, in real life, you might already get the password as a `SecureString`. You might also want to get the password from KeyVault. - -#### Option 3 (Interactive, but create apps in a specified tenant) +#### Option 2 (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) +- 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 - Go to **Properties** and copy the content of the **Directory Id** property @@ -155,21 +130,9 @@ $tenantId = "yourTenantIdGuid" . .\Configure.ps1 -TenantId $tenantId ``` -#### Option 4 (non-interactive, and create apps in a specified tenant) - -This option combines option 2 and option 3: it creates the application in a specific tenant. See option 3 for the way to get the tenant Id. Then run: - -```PowerShell -$secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force -$mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd) -$tenantId = "yourTenantIdGuid" -. .\Cleanup.ps1 -Credential $mycreds -TenantId $tenantId -. .\Configure.ps1 -Credential $mycreds -TenantId $tenantId -``` - ### Running the script on Azure Sovereign clouds -All the four options listed above, can be used on any Azure Sovereign clouds. By default, the script targets `AzureCloud`, but it can be changed using the parameter `-AzureEnvironmentName`. +All the four options listed above can be used on any Azure Sovereign clouds. By default, the script targets `AzureCloud`, but it can be changed using the parameter `-AzureEnvironmentName`. The acceptable values for this parameter are: diff --git a/AppCreationScripts/Cleanup.ps1 b/AppCreationScripts/Cleanup.ps1 index 25adc2a..334888f 100644 --- a/AppCreationScripts/Cleanup.ps1 +++ b/AppCreationScripts/Cleanup.ps1 @@ -1,26 +1,17 @@ + [CmdletBinding()] param( - [PSCredential] $Credential, [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] [string] $tenantId, - [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script (it defaults to AzureCloud)')] + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script. Default = Global')] [string] $azureEnvironmentName ) -#Requires -Modules AzureAD -RunAsAdministrator - - -if ($null -eq (Get-Module -ListAvailable -Name "AzureAD")) { - Install-Module "AzureAD" -Scope CurrentUser -} -Import-Module AzureAD -$ErrorActionPreference = "Stop" - Function Cleanup { if (!$azureEnvironmentName) { - $azureEnvironmentName = "AzureCloud" + $azureEnvironmentName = "Global" } <# @@ -31,63 +22,63 @@ Function Cleanup # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD. - # Login to Azure PowerShell (interactive if credentials are not already provided: - # you'll need to sign-in with creds enabling your to create apps in the tenant) - if (!$Credential -and $TenantId) - { - $creds = Connect-AzureAD -TenantId $tenantId -AzureEnvironmentName $azureEnvironmentName + # Connect to the Microsoft Graph API + Write-Host "Connecting to Microsoft Graph" + if ($tenantId -eq "") { + Connect-MgGraph -Scopes "Application.ReadWrite.All" -Environment $azureEnvironmentName + $tenantId = (Get-MgContext).TenantId } - else - { - if (!$TenantId) - { - $creds = Connect-AzureAD -Credential $Credential -AzureEnvironmentName $azureEnvironmentName - } - else - { - $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential -AzureEnvironmentName $azureEnvironmentName - } - } - - if (!$tenantId) - { - $tenantId = $creds.Tenant.Id + else { + Connect-MgGraph -TenantId $tenantId -Scopes "Application.ReadWrite.All" -Environment $azureEnvironmentName } - $tenant = Get-AzureADTenantDetail - $tenantName = ($tenant.VerifiedDomains | Where-Object { $_._Default -eq $True }).Name # Removes the applications - Write-Host "Cleaning-up applications from tenant '$tenantName'" + Write-Host "Cleaning-up applications from tenant '$tenantId'" Write-Host "Removing 'client' (TodoListClient-authContext-webapp) if needed" try { - Get-AzureADApplication -Filter "DisplayName eq 'TodoListClient-authContext-webapp'" | ForEach-Object {Remove-AzureADApplication -ObjectId $_.ObjectId } + Get-MgApplication -Filter "DisplayName eq 'TodoListClient-authContext-webapp'" | ForEach-Object {Remove-MgApplication -ApplicationId $_.Id } } catch { - Write-Host "Unable to remove the 'TodoListClient-authContext-webapp' . Try deleting manually." -ForegroundColor White -BackgroundColor Red + Write-Host "Unable to remove the application 'TodoListClient-authContext-webapp' . Try deleting manually." -ForegroundColor White -BackgroundColor Red } - $apps = Get-AzureADApplication -Filter "DisplayName eq 'TodoListClient-authContext-webapp'" + + Write-Host "Making sure there are no more (TodoListClient-authContext-webapp) applications found, will remove if needed..." + $apps = Get-MgApplication -Filter "DisplayName eq 'TodoListClient-authContext-webapp'" + if ($apps) { - Remove-AzureADApplication -ObjectId $apps.ObjectId + Remove-MgApplication -ApplicationId $apps.Id } foreach ($app in $apps) { - Remove-AzureADApplication -ObjectId $app.ObjectId + Remove-MgApplication -ApplicationId $app.Id Write-Host "Removed TodoListClient-authContext-webapp.." } + # also remove service principals of this app try { - Get-AzureADServicePrincipal -filter "DisplayName eq 'TodoListClient-authContext-webapp'" | ForEach-Object {Remove-AzureADServicePrincipal -ObjectId $_.Id -Confirm:$false} + Get-MgServicePrincipal -filter "DisplayName eq 'TodoListClient-authContext-webapp'" | ForEach-Object {Remove-MgServicePrincipal -ApplicationId $_.Id -Confirm:$false} } catch { - Write-Host "Unable to remove ServicePrincipal 'TodoListClient-authContext-webapp' . Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red + Write-Host "Unable to remove ServicePrincipal 'TodoListClient-authContext-webapp' . Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red } } -Cleanup -Credential $Credential -tenantId $TenantId \ No newline at end of file +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Applications")) { + Install-Module "Microsoft.Graph.Applications" -Scope CurrentUser +} +Import-Module Microsoft.Graph.Applications +$ErrorActionPreference = "Stop" + + +Cleanup -tenantId $tenantId -environment $azureEnvironmentName + +Write-Host "Disconnecting from tenant" +Disconnect-MgGraph + diff --git a/AppCreationScripts/Configure.ps1 b/AppCreationScripts/Configure.ps1 index 11887f7..5bb5380 100644 --- a/AppCreationScripts/Configure.ps1 +++ b/AppCreationScripts/Configure.ps1 @@ -1,49 +1,32 @@ + [CmdletBinding()] param( - [PSCredential] $Credential, [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] [string] $tenantId, - [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script (it defaults to AzureCloud)')] + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script. Default = Global')] [string] $azureEnvironmentName ) -#Requires -Modules AzureAD -RunAsAdministrator - <# This script creates the Azure AD applications needed for this sample and updates the configuration files for the visual Studio projects from the data in the Azure AD applications. - Before running this script you need to install the AzureAD cmdlets as an administrator. - For this: - 1) Run Powershell as an administrator - 2) in the PowerShell window, type: Install-Module AzureAD - + In case you don't have Microsoft.Graph.Applications already installed, the script will automatically install it for the current user + There are four ways to run this script. For more information, read the AppCreationScripts.md file in the same folder as this script. #> -# Create a password that can be used as an application key -Function ComputePassword -{ - $aesManaged = New-Object "System.Security.Cryptography.AesManaged" - $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC - $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros - $aesManaged.BlockSize = 128 - $aesManaged.KeySize = 256 - $aesManaged.GenerateKey() - return [System.Convert]::ToBase64String($aesManaged.Key) -} - # Create an application key # See https://www.sabin.io/blog/adding-an-azure-active-directory-application-and-key-using-powershell/ -Function CreateAppKey([DateTime] $fromDate, [double] $durationInYears, [string]$pw) +Function CreateAppKey([DateTime] $fromDate, [double] $durationInMonths) { - $endDate = $fromDate.AddYears($durationInYears) - $keyId = (New-Guid).ToString(); - $key = New-Object Microsoft.Open.AzureAD.Model.PasswordCredential - $key.StartDate = $fromDate - $key.EndDate = $endDate - $key.Value = $pw - $key.KeyId = $keyId + $key = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphPasswordCredential + + $key.StartDateTime = $fromDate + $key.EndDateTime = $fromDate.AddMonths($durationInMonths) + $key.KeyId = (New-Guid).ToString() + $key.DisplayName = "app secret" + return $key } @@ -53,19 +36,19 @@ Function CreateAppKey([DateTime] $fromDate, [double] $durationInYears, [string]$ Function AddResourcePermission($requiredAccess, ` $exposedPermissions, [string]$requiredAccesses, [string]$permissionType) { - foreach($permission in $requiredAccesses.Trim().Split("|")) + foreach($permission in $requiredAccesses.Trim().Split("|")) + { + foreach($exposedPermission in $exposedPermissions) { - foreach($exposedPermission in $exposedPermissions) - { - if ($exposedPermission.Value -eq $permission) - { - $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess - $resourceAccess.Type = $permissionType # Scope = Delegated permissions | Role = Application permissions - $resourceAccess.Id = $exposedPermission.Id # Read directory data - $requiredAccess.ResourceAccess.Add($resourceAccess) - } - } + if ($exposedPermission.Value -eq $permission) + { + $resourceAccess = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphResourceAccess + $resourceAccess.Type = $permissionType # Scope = Delegated permissions | Role = Application permissions + $resourceAccess.Id = $exposedPermission.Id # Read directory data + $requiredAccess.ResourceAccess += $resourceAccess + } } + } } # @@ -80,17 +63,17 @@ Function GetRequiredPermissions([string] $applicationDisplayName, [string] $requ } else { - $sp = Get-AzureADServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'" + $sp = Get-MgServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'" } $appid = $sp.AppId - $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess + $requiredAccess = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess $requiredAccess.ResourceAppId = $appid - $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess] + $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphResourceAccess] # $sp.Oauth2Permissions | Select Id,AdminConsentDisplayName,Value: To see the list of all the Delegated permissions for the application: if ($requiredDelegatedPermissions) { - AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" + AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2PermissionScopes -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" } # $sp.AppRoles | Select Id,AdminConsentDisplayName,Value: To see the list of all the Application permissions for the application @@ -104,16 +87,14 @@ Function GetRequiredPermissions([string] $applicationDisplayName, [string] $requ Function UpdateLine([string] $line, [string] $value) { - $index = $line.IndexOf('=') - $delimiter = ';' - if ($index -eq -1) - { - $index = $line.IndexOf(':') - $delimiter = ',' - } + $index = $line.IndexOf(':') + $lineEnd = '' + + if($line[$line.Length - 1] -eq ','){ $lineEnd = ',' } + if ($index -ige 0) { - $line = $line.Substring(0, $index+1) + " "+'"'+$value+'"'+$delimiter + $line = $line.Substring(0, $index+1) + " " + '"' + $value+ '"' + $lineEnd } return $line } @@ -138,137 +119,121 @@ Function UpdateTextFile([string] $configFilePath, [System.Collections.HashTable] Set-Content -Path $configFilePath -Value $lines -Force } -Set-Content -Value "" -Path createdApps.html -Add-Content -Value "" -Path createdApps.html - -$ErrorActionPreference = "Stop" - Function ConfigureApplications { -<#.Description - This function creates the Azure AD applications for the sample in the provided Azure AD tenant and updates the - configuration files in the client and service project of the visual studio solution (App.Config and Web.Config) - so that they are consistent with the Applications parameters -#> - $commonendpoint = "common" + <#.Description + This function creates the Azure AD applications for the sample in the provided Azure AD tenant and updates the + configuration files in the client and service project of the visual studio solution (App.Config and Web.Config) + so that they are consistent with the Applications parameters + #> if (!$azureEnvironmentName) { - $azureEnvironmentName = "AzureCloud" + $azureEnvironmentName = "Global" } - # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant - # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD. - - # Login to Azure PowerShell (interactive if credentials are not already provided: - # you'll need to sign-in with creds enabling your to create apps in the tenant) - if (!$Credential -and $TenantId) - { - $creds = Connect-AzureAD -TenantId $tenantId -AzureEnvironmentName $azureEnvironmentName + # Connect to the Microsoft Graph API, non-interactive is not supported for the moment (Oct 2021) + Write-Host "Connecting to Microsoft Graph" + if ($tenantId -eq "") { + Connect-MgGraph -Scopes "Application.ReadWrite.All" -Environment $azureEnvironmentName + $tenantId = (Get-MgContext).TenantId } - else - { - if (!$TenantId) - { - $creds = Connect-AzureAD -Credential $Credential -AzureEnvironmentName $azureEnvironmentName - } - else - { - $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential -AzureEnvironmentName $azureEnvironmentName - } + else { + Connect-MgGraph -TenantId $tenantId -Scopes "Application.ReadWrite.All" -Environment $azureEnvironmentName } - - if (!$tenantId) - { - $tenantId = $creds.Tenant.Id - } - - $tenant = Get-AzureADTenantDetail - $tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name - - # Get the user running the script to add the user as the app owner - $user = Get-AzureADUser -ObjectId $creds.Account.Id - # Create the client AAD application Write-Host "Creating the AAD application (TodoListClient-authContext-webapp)" - # Get a 2 years application key for the client Application - $pw = ComputePassword + # Get a 6 months application key for the client Application $fromDate = [DateTime]::Now; - $key = CreateAppKey -fromDate $fromDate -durationInYears 2 -pw $pw - $clientAppKey = $pw + $key = CreateAppKey -fromDate $fromDate -durationInMonths 6 + + # create the application - $clientAadApplication = New-AzureADApplication -DisplayName "TodoListClient-authContext-webapp" ` - -HomePage "https://localhost:44321/" ` - -LogoutUrl "https://localhost:44321/signout-oidc" ` - -ReplyUrls "https://localhost:44321/", "https://localhost:44321/signin-oidc" ` - -IdentifierUris "https://$tenantName/TodoListClient-authContext-webapp" ` - -PasswordCredentials $key ` - -PublicClient $False - - # create the service principal of the newly created application - $currentAppId = $clientAadApplication.AppId - $clientServicePrincipal = New-AzureADServicePrincipal -AppId $currentAppId -Tags {WindowsAzureActiveDirectoryIntegratedApp} - - # add the user running the script as an app owner if needed - $owner = Get-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId - if ($owner -eq $null) - { - Add-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId -RefObjectId $user.ObjectId + $clientAadApplication = New-MgApplication -DisplayName "TodoListClient-authContext-webapp" ` + -Web ` + @{ ` + RedirectUris = "https://localhost:44321/", "https://localhost:44321/signin-oidc"; ` + HomePageUrl = "https://localhost:44321/"; ` + LogoutUrl = "https://localhost:44321/signout-oidc"; ` + } ` + -SignInAudience AzureADMyOrg ` + #end of command + #add a secret to the application + $pwdCredential = Add-MgApplicationPassword -ApplicationId $clientAadApplication.Id -PasswordCredential $key + $clientAppKey = $pwdCredential.SecretText + + $tenantName = (Get-MgApplication -ApplicationId $clientAadApplication.Id).PublisherDomain + Update-MgApplication -ApplicationId $clientAadApplication.Id -IdentifierUris @("https://$tenantName/TodoListClient-authContext-webapp") + + # create the service principal of the newly created application + $currentAppId = $clientAadApplication.AppId + $clientServicePrincipal = New-MgServicePrincipal -AppId $currentAppId -Tags {WindowsAzureActiveDirectoryIntegratedApp} + + # add the user running the script as an app owner if needed + $owner = Get-MgApplicationOwner -ApplicationId $clientAadApplication.Id + if ($owner -eq $null) + { + New-MgApplicationOwnerByRef -ApplicationId $clientAadApplication.Id -BodyParameter = @{"@odata.id" = "htps://graph.microsoft.com/v1.0/directoryObjects/$user.ObjectId"} Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($clientServicePrincipal.DisplayName)'" - } - - - Write-Host "Done creating the client application (TodoListClient-authContext-webapp)" - - # URL of the AAD application in the Azure portal - # Future? $clientPortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" - $clientPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" - Add-Content -Value "" -Path createdApps.html - - $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess] - - # Add Required Resources Access (from 'client' to 'Microsoft Graph') - Write-Host "Getting access from 'client' to 'Microsoft Graph'" - $requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" ` - -requiredDelegatedPermissions "User.Read|Policy.Read.ConditionalAccess|Policy.ReadWrite.ConditionalAccess" ` - - $requiredResourcesAccess.Add($requiredPermissions) - + } + Write-Host "Done creating the client application (TodoListClient-authContext-webapp)" - Set-AzureADApplication -ObjectId $clientAadApplication.ObjectId -RequiredResourceAccess $requiredResourcesAccess - Write-Host "Granted permissions." + # URL of the AAD application in the Azure portal + # Future? $clientPortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.Id+"/isMSAApp/" + $clientPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.Id+"/isMSAApp/" + Add-Content -Value "" -Path createdApps.html + $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] - # Update config file for 'client' - $configFile = $pwd.Path + "\..\TodoListClient\appsettings.json" - Write-Host "Updating the sample code ($configFile)" - $dictionary = @{ "Domain" = $tenantName;"TenantId" = $tenantId;"ClientId" = $clientAadApplication.AppId;"ClientSecret" = $clientAppKey }; - 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 'client'" - Write-Host " - Navigate to '$clientPortalUrl'" - Write-Host " - Navigate to the API Permissions page and select 'Grant admin consent for (your tenant)'" -ForegroundColor Red + + # Add Required Resources Access (from 'client' to 'Microsoft Graph') + Write-Host "Getting access from 'client' to 'Microsoft Graph'" + $requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" ` + -requiredDelegatedPermissions "User.Read|Policy.Read.ConditionalAccess|Policy.ReadWrite.ConditionalAccess" ` + - Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" - if($isOpenSSL -eq 'Y') - { + $requiredResourcesAccess.Add($requiredPermissions) + Update-MgApplication -ApplicationId $clientAadApplication.Id -RequiredResourceAccess $requiredResourcesAccess + Write-Host "Granted permissions." + + # Update config file for 'client' + $configFile = $pwd.Path + "\..\TodoListClient\appsettings.json" + $dictionary = @{ "Domain" = $tenantName;"TenantId" = $tenantId;"ClientId" = $clientAadApplication.AppId;"ClientSecret" = $clientAppKey }; + + Write-Host "Updating the sample code ($configFile)" + + UpdateTextFile -configFilePath $configFile -dictionary $dictionary + 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 client" + Write-Host " - Navigate to $clientPortalUrl" + Write-Host " - Navigate to the API Permissions page and select 'Grant admin consent for (your tenant)'" -ForegroundColor Red + Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" + if($isOpenSSL -eq 'Y') + { Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" Write-Host "You have generated certificate using OpenSSL so follow below steps: " Write-Host "Install the certificate on your system from current folder." Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" - } - Add-Content -Value "
ApplicationAppIdUrl in the Azure portal
client$currentAppIdTodoListClient-authContext-webapp
client$currentAppIdTodoListClient-authContext-webapp
" -Path createdApps.html + } + Add-Content -Value "" -Path createdApps.html } # Pre-requisites -if ((Get-Module -ListAvailable -Name "AzureAD") -eq $null) { - Install-Module "AzureAD" -Scope CurrentUser +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Applications")) { + Install-Module "Microsoft.Graph.Applications" -Scope CurrentUser } -Import-Module AzureAD +Import-Module Microsoft.Graph.Applications + +Set-Content -Value "" -Path createdApps.html +Add-Content -Value "" -Path createdApps.html + +$ErrorActionPreference = "Stop" # Run interactively (will ask you for the tenant ID) -ConfigureApplications -Credential $Credential -tenantId $TenantId \ No newline at end of file +ConfigureApplications -tenantId $tenantId -environment $azureEnvironmentName + +Write-Host "Disconnecting from tenant" +Disconnect-MgGraph \ No newline at end of file diff --git a/AppCreationScripts/apps.json b/AppCreationScripts/apps.json deleted file mode 100644 index 2706190..0000000 --- a/AppCreationScripts/apps.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "Sample": { - "Title": "This sample demonstrates using the Conditional Access auth context to perform step-up authentication for high-privilege and sensitive operations in a web app.", - "Level": 300, - "Client": "ASP.NET Core Web App" - }, - "AppRegistrations": [ - { - "x-ms-id": "client", - "x-ms-name": "TodoListClient-authContext-webapp", - "x-ms-version": "2.0", - "replyUrlsWithType": [ - { - "url": "https://localhost:44321/", - "type": "Web" - }, - { - "url": " https://localhost:44321/signin-oidc", - "type": "Web" - } ], - "oauth2AllowImplicitFlow": false, - "oauth2AllowIdTokenImplicitFlow": false, - "requiredResourceAccess": [ - { - "x-ms-resourceAppName": "Microsoft Graph", - "resourceAppId": "00000003-0000-0000-c000-000000000000", - "resourceAccess": [ - { - "id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d", - "type": "Scope", - "x-ms-name": "User.Read" - }, - { - "id": "unknown", - "type": "Scope", - "x-ms-name": "Policy.Read.ConditionalAccess" - }, - { - "id": "unknown", - "type": "Scope", - "x-ms-name": "Policy.ReadWrite.ConditionalAccess" - } ] - } - ], - "codeConfigurations": [ - { - "settingFile": "/TodoListClient/appsettings.json", - "replaceTokens": - { - /** - * Note: The following 'key-value' pairs are for illustration only; you may - * not have all of them in your configuration file. Azure portal will replace - * the values (i.e. text) below with the actual app credentials. - * Finally, don't forget to remove this comment. - */ - "appId": "Enter_the_Application_Id_Here", - "redirectUri": "Enter_the_Redirect_Uri_Here", - "tenantId": "Enter_the_Tenant_Info_Here", - "clientSecret": "Enter_the_Client_Secret_Here", - "authorityEndpointHost": "Enter_the_Cloud_Instance_Id_Here", - "msgraphEndpointHost": "Enter_the_Graph_Endpoint_Here", - "signInAudience": "Enter_the_Sign-in_Audience_Here" - } - } ] - } - ] -} diff --git a/AppCreationScripts/sample.json b/AppCreationScripts/sample.json index 8c6190a..bfa6a1e 100644 --- a/AppCreationScripts/sample.json +++ b/AppCreationScripts/sample.json @@ -1,10 +1,13 @@ { "Sample": { - "Title": "This sample demonstrates using the Conditional Access auth context to perform step-up authentication for high-privilege and sensitive operations in a web app.", + "Title": "Use the Conditional Access auth context to perform step-up authentication for high-privilege operations in a Web app", "Level": 300, "Client": "ASP.NET Core Web App", "RepositoryUrl": "ms-identity-dotnetcore-ca-auth-context-app", - "Endpoint": "AAD v2.0" + "Endpoint": "AAD v2.0", + "Description":"This sample demonstrates using the Conditional Access auth context to perform step-up authentication for high-privilege and sensitive operations in a web app.", + "Languages": [ "csharp" ], + "Products": [ "aspnet-core", "azure-active-directory", "ms-graph" ] }, /* diff --git a/README.md b/README.md index de0ccea..0dd4308 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ description: "This sample demonstrates using the Conditional Access auth context This code sample uses the Conditional Access Auth Context to demand a higher bar of authentication for certain high-privileged and sensitive operations in a Web App. > To use the CA Auth context in a Web API, please try the [Use the Conditional Access auth context to perform step-up authentication for high-privilege operations in a Web API](https://github.com/Azure-Samples/ms-identity-ca-auth-context/blob/main/README.md) code sample - + ## Scenario 1. The client ASP.NET Core Web App uses the [Microsoft.Identity.Web](https://aka.ms/microsoft-identity-web) and Microsoft Authentication Library for .NET ([MSAL.NET](https://aka.ms/msal-net)) to sign-in a user with **Azure AD**. @@ -40,7 +40,7 @@ This code sample uses the Conditional Access Auth Context to demand a higher bar ![Overview](./ReadmeFiles/topology.png) > :information_source: Check out the recorded session on this topic: [Use Conditional Access Auth Context in your app for step-up authentication](https://www.youtube.com/watch?v=_iO7CfoktTY&ab_channel=Microsoft365Community) -> + ## Prerequisites - [Visual Studio](https://visualstudio.microsoft.com/downloads/) @@ -149,6 +149,8 @@ Open the project in your IDE (like Visual Studio or Visual Studio Code) to confi 1. Find the key `ClientId` and replace the existing value with the application ID (clientId) of `TodoListClient-authContext-webapp` app copied from the Azure portal. 1. Find the key `ClientSecret` and replace the existing value with the key you saved during the creation of `TodoListClient-authContext-webapp` copied from the Azure portal. + > You'd note in the *appsettings.json* file that Microsoft Graph settings are for the beta version of Microsoft Graph. This is because the API is only available in MS Graph beta at the time of writing this sample. You would update your code to use the MS Graph production (v1.0) version when the API becomes GA. + ## Running the sample > For Visual Studio Users @@ -223,6 +225,12 @@ If an operation was saved for a certain authContext and there is a CA policy con .AddInMemoryTokenCaches(); ``` + We also add a class that helps us when working with Microsoft Graph using the following line + + ```csharp + services.AddScoped(); + ``` + 1. In `AdminController.cs`, the method **GetAuthenticationContextValues** returns a default set of AuthN context values for the app to work with, either from Graph or a default hard coded set. ```csharp @@ -237,20 +245,15 @@ If an operation was saved for a certain authContext and there is a CA policy con string sessionKey = "ACRS"; - if (HttpContext.Session.Get>(sessionKey) != default) - { + if (HttpContext.Session.Get>(sessionKey) != default) { dictACRValues = HttpContext.Session.Get>(sessionKey); } - else - { + else { var existingAuthContexts = await _authContextClassReferencesOperations.ListAuthenticationContextClassReferencesAsync(); - if (existingAuthContexts.Count() > 0) - { + if (existingAuthContexts.Count() > 0) { dictACRValues.Clear(); - - foreach (var authContext in existingAuthContexts) - { + foreach (var authContext in existingAuthContexts) { dictACRValues.Add(authContext.Id, authContext.DisplayName); } @@ -342,7 +345,7 @@ If an operation was saved for a certain authContext and there is a CA policy con 1. In `TodoListController.cs`, the method **CheckForRequiredAuthContext** retrieves the acrsvalues from database for the request method. Then checks if the access token has `acrs` claim with acrsValue. If does not exists then it creates a **claims** payload to be sent back to Azure AD. ```csharp - public string CheckForRequiredAuthContext(string method) + public string CheckForRequiredAuthContext(string method) { string claimsChallenge = string.Empty; @@ -364,17 +367,13 @@ If an operation was saved for a certain authContext and there is a CA policy con if (acrsClaim?.Value != savedAuthContextId) { claimsChallenge = "{\"id_token\":{\"acrs\":{\"essential\":true,\"value\":\"" + savedAuthContextId + "\"}}}"; - } - } - - return claimsChallenge; - } + } ``` ### Code for the Web App (TodoListClient) -Methods in `TodoListController.cs` challenges the user to re-authenticate if a claims payload is returned by the CheckforRequiredAuthContext(): +Methods in `TodoListController.cs` challenges the user to re-authenticate if a claims payload is returned by the *CheckforRequiredAuthContext()*: ```csharp string claimsChallenge = CheckForRequiredAuthContext("Delete"); @@ -395,55 +394,45 @@ Take a look into the example of using session state. ```csharp // GET: TodoList/Create - public ActionResult Create() - { - string claimsChallenge = CheckForRequiredAuthContext(Request.Method); - if (!string.IsNullOrWhiteSpace(claimsChallenge)) - { - _consentHandler.ChallengeUser(new string[] { "user.read" }, claimsChallenge); - return new EmptyResult(); - } - var todoObject = TodoSessionState(SessionAction.Get); - if (todo != null && todoObject.IsInitialized()) - { - PersistTodo(todoObject); - TodoSessionState(SessionAction.Set); - return RedirectToAction("Index"); - } - Todo todo = new Todo() { Owner = HttpContext.User.Identity.Name }; - return View(todo); - } - // POST: TodoList/Create - [HttpPost] - [ValidateAntiForgeryToken] public ActionResult Create([Bind("Title,Owner")] Todo todo) { - string claimsChallenge = CheckForRequiredAuthContext(Request.Method); - if (!string.IsNullOrWhiteSpace(claimsChallenge)) + //add owner accountid to new todo + todo.AccountId = HttpContext.User.GetMsalAccountId(); + todo.Owner = GetCurrentUsersName(); + + if (ChallengeUser(HttpMethods.Post)) { - _consentHandler.ChallengeUser(new string[] { "user.read" }, claimsChallenge); + //save in session state before redirecting to GET handler TodoSessionState(SessionAction.Set, todo); - return new EmptyResult(); + + return View(); } - PersistTodo(new Todo() { Owner = HttpContext.User.Identity.Name, Title = todo.Title }); + + SaveToDoToDatabase(new Todo() { Owner = todo.Owner, Title = todo.Title, AccountId = todo.AccountId }); + return RedirectToAction("Index"); } - private Todo TodoSessionState(SessionAction action, Todo todo = null) + // POST: TodoList/Delete + public ActionResult Delete(int id, [Bind("Id,Title,Owner")] Todo todo) { - string todoObject = "Todo"; - switch (action) - { - case SessionAction.Set: - HttpContext.Session.SetString(todoObject, todo != null ? JsonSerializer.Serialize(todo) : ""); - break; - case SessionAction.Get: - var obj = HttpContext.Session.GetString(todoObject); - return !string.IsNullOrEmpty(obj) ? JsonSerializer.Deserialize(obj) : null; - default: - break; - } - return todo; + if (ChallengeUser(HttpMethods.Delete)) + { + //save in session state before redirecting to GET handler + TodoSessionState(SessionAction.Set, new Todo { Id = id }); + + return View(); + } + + //make sure the received todo is inside database before deleting + var todoFromDb = _commonDBContext.Todo.Find(id); + if (todoFromDb != null) + { + DeleteToDoFromDatabase(todoFromDb); + } + + return RedirectToAction("Index"); } + ``` ## More information diff --git a/ReadmeFiles/Teams icon 1.png b/ReadmeFiles/Teams icon 1.png new file mode 100644 index 0000000..ca866fb Binary files /dev/null and b/ReadmeFiles/Teams icon 1.png differ diff --git a/TodoListClient/Controllers/AdminController.cs b/TodoListClient/Controllers/AdminController.cs index 3273810..de41006 100644 --- a/TodoListClient/Controllers/AdminController.cs +++ b/TodoListClient/Controllers/AdminController.cs @@ -125,12 +125,22 @@ public IActionResult ViewDetails() return View(authContexts); } + /// + /// Delete an configuration item by key + /// + /// Always will be in form of AuthContextId_Operation + /// public ActionResult Delete(string id) { + var authContextId = id.Split("_")[0]; + var operationName = id.Split("_")[1]; + AuthContext authContext = null; using (var commonDBContext = new CommonDBContext(_configuration)) { - authContext = commonDBContext.AuthContext.FirstOrDefault(x => x.AuthContextId == id && x.TenantId == TenantId); + authContext = commonDBContext + .AuthContext + .FirstOrDefault(x => x.AuthContextId == authContextId && x.Operation == operationName && x.TenantId == TenantId); } return View(authContext); } diff --git a/TodoListClient/Controllers/HomeController.cs b/TodoListClient/Controllers/HomeController.cs index cc3ed33..89f429d 100644 --- a/TodoListClient/Controllers/HomeController.cs +++ b/TodoListClient/Controllers/HomeController.cs @@ -9,13 +9,6 @@ namespace TodoListClient.Controllers [Authorize] public class HomeController : Controller { - private readonly ITokenAcquisition tokenAcquisition; - - public HomeController(ITokenAcquisition tokenAcquisition) - { - this.tokenAcquisition = tokenAcquisition; - } - public IActionResult Index() { return View(); diff --git a/TodoListClient/Controllers/TodoListController.cs b/TodoListClient/Controllers/TodoListController.cs index fbfaf2f..6fd49d9 100644 --- a/TodoListClient/Controllers/TodoListController.cs +++ b/TodoListClient/Controllers/TodoListController.cs @@ -22,13 +22,48 @@ public TodoListController(IHttpContextAccessor contextAccessor, IConfiguration c { _configuration = configuration; _commonDBContext = commonDBContext; - this._consentHandler = consentHandler; + _consentHandler = consentHandler; + + EnsureDatabaseIsAwakeAndAvailable(); + } + + /// + /// Makes sure Database is available + /// + /// + private void EnsureDatabaseIsAwakeAndAvailable() + { + + // give the database some time to wake up + var retryTimes = 3; + while (retryTimes-- > 0) + { + try + { + _commonDBContext.Todo.Take(2); + } + catch (Exception ex) + { + //throw exception if database didn't wakeup after 3 attempts + if (retryTimes == 0) + { + throw new Exception( + $"Unable to reach the database after multiple tries. The app will not be able to function as expected. {ex}"); + } + } + } + } + + private string GetCurrentUsersName() + { + return HttpContext?.User?.Identity?.Name; } // GET: api/values [HttpGet] public IEnumerable Get() { + EnsureDatabaseIsAwakeAndAvailable(); return _commonDBContext.Todo.ToList(); } @@ -46,6 +81,8 @@ public ActionResult Index() //reset session on every entry to TODO's list TodoSessionState(SessionAction.Set); + EnsureDatabaseIsAwakeAndAvailable(); + return View(_commonDBContext.Todo.Where(l => l.AccountId.Equals(HttpContext.User.GetMsalAccountId())).ToList()); } @@ -58,27 +95,17 @@ public ActionResult Details(int id) // GET: TodoList/Create public ActionResult Create() { - string claimsChallenge = CheckForRequiredAuthContext(Request.Method); + //get todo from session state (if available then this means we were redirected from POST and have to save this todo) + var todoFromSessionState = TodoSessionState(SessionAction.Get); - if (!string.IsNullOrWhiteSpace(claimsChallenge)) + if (todoFromSessionState != null && todoFromSessionState.IsInitialized) { - _consentHandler.ChallengeUser(new string[] { "user.read" }, claimsChallenge); - - return new EmptyResult(); - } - - var todoObject = TodoSessionState(SessionAction.Get); - - if (todoObject != null && todoObject.IsInitialized()) - { - PersistTodo(todoObject); - TodoSessionState(SessionAction.Set); - - return RedirectToAction("Index"); + return Create(todoFromSessionState); } - Todo todo = new Todo() { Owner = HttpContext.User.Identity.Name }; + Todo todo = new Todo() { Owner = GetCurrentUsersName() }; + return View(todo); } @@ -87,20 +114,19 @@ public ActionResult Create() [ValidateAntiForgeryToken] public ActionResult Create([Bind("Title,Owner")] Todo todo) { + //add owner accountid to new todo todo.AccountId = HttpContext.User.GetMsalAccountId(); + todo.Owner = GetCurrentUsersName(); - string claimsChallenge = CheckForRequiredAuthContext(Request.Method); - - if (!string.IsNullOrWhiteSpace(claimsChallenge)) + if (ChallengeUser(HttpMethods.Post)) { - _consentHandler.ChallengeUser(new string[] { "user.read" }, claimsChallenge); - + //save in session state before redirecting to GET handler TodoSessionState(SessionAction.Set, todo); - return new EmptyResult(); + return View(); } - PersistTodo(new Todo() { Owner = HttpContext.User.Identity.Name, Title = todo.Title, AccountId = todo.AccountId }); + SaveToDoToDatabase(new Todo() { Owner = todo.Owner, Title = todo.Title, AccountId = todo.AccountId }); return RedirectToAction("Index"); } @@ -108,7 +134,18 @@ public ActionResult Create([Bind("Title,Owner")] Todo todo) // GET: TodoList/Edit/5 public ActionResult Edit(int id) { - return View(_commonDBContext.Todo.FirstOrDefault(t => t.Id == id)); + //get todo from session state (if available then this means we were redirected from POST and have to update this todo) + var todoFromSessionState = TodoSessionState(SessionAction.Get); + + if (todoFromSessionState != null && todoFromSessionState.IsInitialized && todoFromSessionState.Id == id) + { + UpdateToDoInDatabase(todoFromSessionState); + return Edit(todoFromSessionState.Id, todoFromSessionState); + } + else + { + return View(_commonDBContext.Todo.FirstOrDefault(t => t.Id == id)); + } } // POST: TodoList/Edit/5 @@ -116,7 +153,6 @@ public ActionResult Edit(int id) [ValidateAntiForgeryToken] public ActionResult Edit(int id, [Bind("Id,Title,Owner")] Todo todo) { - //await _todoListService.EditAsync(todo); if (id != todo.Id) { return NotFound(); @@ -124,8 +160,15 @@ public ActionResult Edit(int id, [Bind("Id,Title,Owner")] Todo todo) todo.AccountId = HttpContext.User.GetMsalAccountId(); - _commonDBContext.Todo.Update(todo); - _commonDBContext.SaveChanges(); + if (ChallengeUser(HttpMethods.Post)) + { + //save in session state before redirecting to GET handler + TodoSessionState(SessionAction.Set, todo); + + return View(); + } + + UpdateToDoInDatabase(todo); return RedirectToAction("Index"); } @@ -133,7 +176,19 @@ public ActionResult Edit(int id, [Bind("Id,Title,Owner")] Todo todo) // GET: TodoList/Delete/5 public ActionResult Delete(int id) { - return View(_commonDBContext.Todo.FirstOrDefault(t => t.Id == id)); + //get todo from session state (if available then this means we were redirected from POST and have to save this todo) + var todoFromSessionState = TodoSessionState(SessionAction.Get); + + if (todoFromSessionState != null && todoFromSessionState.Id == id) + { + //clean session state + TodoSessionState(SessionAction.Set); + return Delete(todoFromSessionState.Id, todoFromSessionState); + } + else + { + return View(_commonDBContext.Todo.FirstOrDefault(t => t.Id == id)); + } } // POST: TodoList/Delete/5 @@ -141,19 +196,19 @@ public ActionResult Delete(int id) [ValidateAntiForgeryToken] public ActionResult Delete(int id, [Bind("Id,Title,Owner")] Todo todo) { - string claimsChallenge = CheckForRequiredAuthContext("Delete"); - - if (!string.IsNullOrWhiteSpace(claimsChallenge)) + if (ChallengeUser(HttpMethods.Delete)) { - _consentHandler.ChallengeUser(new string[] { "user.read" }, claimsChallenge); - return new EmptyResult(); + //save in session state before redirecting to GET handler + TodoSessionState(SessionAction.Set, new Todo { Id = id }); + + return View(); } - var todo2 = _commonDBContext.Todo.Find(id); - if (todo2 != null) + //make sure the received todo is inside database before deleting + var todoFromDb = _commonDBContext.Todo.Find(id); + if (todoFromDb != null) { - _commonDBContext.Todo.Remove(todo2); - _commonDBContext.SaveChanges(); + DeleteToDoFromDatabase(todoFromDb); } return RedirectToAction("Index"); @@ -188,16 +243,27 @@ public string CheckForRequiredAuthContext(string method) if (acrsClaim?.Value != savedAuthContextId) { claimsChallenge = "{\"id_token\":{\"acrs\":{\"essential\":true,\"value\":\"" + savedAuthContextId + "\"}}}"; - } } return claimsChallenge; } - private void PersistTodo(Todo todo) + private void DeleteToDoFromDatabase(Todo todoToRemove) + { + _commonDBContext.Todo.Remove(todoToRemove); + _commonDBContext.SaveChanges(); + } + + private void SaveToDoToDatabase(Todo todoToSave) { - _commonDBContext.Todo.Add(todo); + _commonDBContext.Todo.Add(todoToSave); + _commonDBContext.SaveChanges(); + } + + private void UpdateToDoInDatabase(Todo todoToUpdate) + { + _commonDBContext.Todo.Update(todoToUpdate); _commonDBContext.SaveChanges(); } @@ -227,6 +293,29 @@ private Todo TodoSessionState(SessionAction action, Todo todo = null) return todo; } + /// + /// Create a user challenge for the specified scope if it was requested by CAE + /// + /// + /// + private bool ChallengeUser(string actionName) + { + //get challenge from token or from session state + string claimsChallenge = CheckForRequiredAuthContext(actionName); + + if (!string.IsNullOrWhiteSpace(claimsChallenge)) + { + _consentHandler.ChallengeUser(new string[] { _configuration["GraphBeta:Scopes"] }, claimsChallenge); + + return true; + } + + return false; + } + + /// + /// Enumerator to distinguish between session state actions + /// private enum SessionAction { Set, diff --git a/TodoListClient/Models/TodoItem.cs b/TodoListClient/Models/TodoItem.cs index 0cab635..3949e7c 100644 --- a/TodoListClient/Models/TodoItem.cs +++ b/TodoListClient/Models/TodoItem.cs @@ -12,9 +12,9 @@ public class Todo public string AccountId { get; set; } //Return true only if both Title and Owners are not empty strings - public bool IsInitialized() + public bool IsInitialized { - return !string.IsNullOrEmpty(Title) && !string.IsNullOrEmpty(Owner) && !string.IsNullOrEmpty(AccountId); + get { return !string.IsNullOrEmpty(Title) && !string.IsNullOrEmpty(Owner) && !string.IsNullOrEmpty(AccountId); } } } } diff --git a/TodoListClient/Properties/PublishProfiles/Woodgrove Deploy.pubxml b/TodoListClient/Properties/PublishProfiles/Woodgrove Deploy.pubxml deleted file mode 100644 index 3cfe0b4..0000000 --- a/TodoListClient/Properties/PublishProfiles/Woodgrove Deploy.pubxml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - MSDeploy - /subscriptions/9ac91505-2026-4193-a58c-05a599583f61/resourceGroups/Thefirstresourcegroup/providers/Microsoft.Web/sites/AuthContextDotNetApp - Thefirstresourcegroup - AzureWebSite - Release - Any CPU - https://authcontextdotnetapp.azurewebsites.net - true - false - 8f331ce4-767d-488d-a33b-e6f129abc198 - authcontextdotnetapp.scm.azurewebsites.net:443 - AuthContextDotNetApp - - true - WMSVC - true - true - $AuthContextDotNetApp - <_SavePWD>true - <_DestinationType>AzureWebSite - false - - \ No newline at end of file diff --git a/TodoListClient/Properties/ServiceDependencies/Woodgrove Deploy/profile.arm.json b/TodoListClient/Properties/ServiceDependencies/Woodgrove Deploy/profile.arm.json deleted file mode 100644 index d1c4e12..0000000 --- a/TodoListClient/Properties/ServiceDependencies/Woodgrove Deploy/profile.arm.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_dependencyType": "compute.appService.windows" - }, - "parameters": { - "resourceGroupName": { - "type": "string", - "defaultValue": "Thefirstresourcegroup", - "metadata": { - "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." - } - }, - "resourceGroupLocation": { - "type": "string", - "defaultValue": "eastus", - "metadata": { - "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." - } - }, - "resourceName": { - "type": "string", - "defaultValue": "AuthContextDotNetApp", - "metadata": { - "description": "Name of the main resource to be created by this template." - } - }, - "resourceLocation": { - "type": "string", - "defaultValue": "[parameters('resourceGroupLocation')]", - "metadata": { - "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." - } - } - }, - "variables": { - "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", - "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]" - }, - "resources": [ - { - "type": "Microsoft.Resources/resourceGroups", - "name": "[parameters('resourceGroupName')]", - "location": "[parameters('resourceGroupLocation')]", - "apiVersion": "2019-10-01" - }, - { - "type": "Microsoft.Resources/deployments", - "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", - "resourceGroup": "[parameters('resourceGroupName')]", - "apiVersion": "2019-10-01", - "dependsOn": [ - "[parameters('resourceGroupName')]" - ], - "properties": { - "mode": "Incremental", - "template": { - "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [ - { - "location": "[parameters('resourceLocation')]", - "name": "[parameters('resourceName')]", - "type": "Microsoft.Web/sites", - "apiVersion": "2015-08-01", - "tags": { - "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" - }, - "dependsOn": [ - "[variables('appServicePlan_ResourceId')]" - ], - "kind": "app", - "properties": { - "name": "[parameters('resourceName')]", - "kind": "app", - "httpsOnly": true, - "reserved": false, - "serverFarmId": "[variables('appServicePlan_ResourceId')]", - "siteConfig": { - "metadata": [ - { - "name": "CURRENT_STACK", - "value": "dotnetcore" - } - ] - } - }, - "identity": { - "type": "SystemAssigned" - } - }, - { - "location": "[parameters('resourceLocation')]", - "name": "[variables('appServicePlan_name')]", - "type": "Microsoft.Web/serverFarms", - "apiVersion": "2015-08-01", - "sku": { - "name": "S1", - "tier": "Standard", - "family": "S", - "size": "S1" - }, - "properties": { - "name": "[variables('appServicePlan_name')]" - } - } - ] - } - } - } - ] -} \ No newline at end of file diff --git a/TodoListClient/Properties/serviceDependencies.json b/TodoListClient/Properties/serviceDependencies.json deleted file mode 100644 index a4e7aa3..0000000 --- a/TodoListClient/Properties/serviceDependencies.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "dependencies": { - "secrets1": { - "type": "secrets" - } - } -} \ No newline at end of file diff --git a/TodoListClient/Properties/serviceDependencies.local.json b/TodoListClient/Properties/serviceDependencies.local.json deleted file mode 100644 index 09b109b..0000000 --- a/TodoListClient/Properties/serviceDependencies.local.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "dependencies": { - "secrets1": { - "type": "secrets.user" - } - } -} \ No newline at end of file diff --git a/TodoListClient/Startup.cs b/TodoListClient/Startup.cs index 6f99a12..07ac0bb 100644 --- a/TodoListClient/Startup.cs +++ b/TodoListClient/Startup.cs @@ -14,6 +14,7 @@ using Microsoft.Identity.Web.UI; using TodoListClient.Models; using System; +using System.IdentityModel.Tokens.Jwt; using Microsoft.EntityFrameworkCore; namespace TodoListClient @@ -55,7 +56,7 @@ public void ConfigureServices(IServiceCollection services) // By default, the claims mapping will map claim names in the old format to accommodate older SAML applications. // 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role' instead of 'roles' // This flag ensures that the ClaimsIdentity claims collection will be built from the claims in the token - // JwtSecurityTokenHandler.DefaultMapInboundClaims = false; + JwtSecurityTokenHandler.DefaultMapInboundClaims = false; // Adds Microsoft Identity platform (AAD v2.0) support to authenticate users services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) @@ -87,11 +88,7 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - using (var serviceScope = app.ApplicationServices.GetService().CreateScope()) - { - var context = serviceScope.ServiceProvider.GetRequiredService(); - context.Database.EnsureCreated(); - } + EnsureDatabaseAvailability(app); if (env.IsDevelopment()) { @@ -129,5 +126,37 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) endpoints.MapRazorPages(); }); } + + /// + /// Hits the Datbase multiple times to ensure that its waken up and available for the app + /// + /// + /// + private static void EnsureDatabaseAvailability(IApplicationBuilder app) + { + using (var serviceScope = app.ApplicationServices.GetService().CreateScope()) + { + // give the database should have time to wake up + var retryTimes = 3; + while (retryTimes-- > 0) + { + try + { + var context = serviceScope.ServiceProvider.GetRequiredService(); + context.Database.EnsureCreated(); + } + catch (Exception ex) + { + //throw exception if database didn't wakeup after 3 attempts + if (retryTimes == 0) + { + throw new Exception( + $"Unable to reach the database after multiple tries. The app will not be able to function as expected. {ex}"); + } + + } + } + } + } } } \ No newline at end of file diff --git a/TodoListClient/TodoListClient.csproj b/TodoListClient/TodoListClient.csproj index 57285ca..c031751 100644 --- a/TodoListClient/TodoListClient.csproj +++ b/TodoListClient/TodoListClient.csproj @@ -8,26 +8,31 @@ + + + + + <_WebToolingArtifacts Remove="Properties\ServiceDependencies\**" /> - + - - + + - - - - - + + + + + diff --git a/TodoListClient/Views/Admin/Index.cshtml b/TodoListClient/Views/Admin/Index.cshtml index 66f0ae4..5922e56 100644 --- a/TodoListClient/Views/Admin/Index.cshtml +++ b/TodoListClient/Views/Admin/Index.cshtml @@ -3,6 +3,11 @@ ViewData["Title"] = "Index"; } +
+
+

Configure Azure AD Step-up authentication

+
+

Admin

Select Create Or Fetch button to perform following steps: diff --git a/TodoListClient/Views/Admin/ViewDetails.cshtml b/TodoListClient/Views/Admin/ViewDetails.cshtml index dc2aff8..59dac81 100644 --- a/TodoListClient/Views/Admin/ViewDetails.cshtml +++ b/TodoListClient/Views/Admin/ViewDetails.cshtml @@ -43,7 +43,7 @@ @item.Operation
diff --git a/TodoListClient/Views/Home/Index.cshtml b/TodoListClient/Views/Home/Index.cshtml index 6fb485b..9ce2e52 100644 --- a/TodoListClient/Views/Home/Index.cshtml +++ b/TodoListClient/Views/Home/Index.cshtml @@ -1,6 +1,10 @@ @{ ViewData["Title"] = "Home Page"; } +
+
+

Microsoft Azure AD Step-up authentication demo

+
@if (!User.Identity.IsAuthenticated) {

diff --git a/TodoListClient/Views/TodoList/Delete.cshtml b/TodoListClient/Views/TodoList/Delete.cshtml index b2f304c..f7b6148 100644 --- a/TodoListClient/Views/TodoList/Delete.cshtml +++ b/TodoListClient/Views/TodoList/Delete.cshtml @@ -1,10 +1,9 @@ @model TodoListClient.Models.Todo - @{ ViewData["Title"] = "Delete"; } -

Delete ToDo

+

Delete ToDo

Are you sure you want to delete this?

diff --git a/TodoListClient/Views/TodoList/Details.cshtml b/TodoListClient/Views/TodoList/Details.cshtml index 0261e67..93a833b 100644 --- a/TodoListClient/Views/TodoList/Details.cshtml +++ b/TodoListClient/Views/TodoList/Details.cshtml @@ -2,12 +2,16 @@ @{ ViewData["Title"] = "Details"; + var todoId = Model.Id; }

ToDo Details

- Create New + @Html.ActionLink("Create","Create") | + @Html.ActionLink("Back to List", "Index") | + @Html.ActionLink("Edit", "Edit", new { id = todoId }) | + @Html.ActionLink("Delete", "Delete", new { id = todoId })

ApplicationAppIdUrl in the Azure portal
- @Html.ActionLink("Delete", "Delete", new { id = @item.AuthContextId }) + @Html.ActionLink("Delete", "Delete", new { id = @item.AuthContextId + "_" + @item.Operation })
diff --git a/TodoListClient/Views/TodoList/Edit.cshtml b/TodoListClient/Views/TodoList/Edit.cshtml index 98927a8..3a505ca 100644 --- a/TodoListClient/Views/TodoList/Edit.cshtml +++ b/TodoListClient/Views/TodoList/Edit.cshtml @@ -5,7 +5,7 @@

Edit ToDo

-
+
@@ -20,12 +20,9 @@
- + | + Back to List
-
- Back to List -
- diff --git a/TodoListClient/appsettings.json b/TodoListClient/appsettings.json index 21e7df9..5e15537 100644 --- a/TodoListClient/appsettings.json +++ b/TodoListClient/appsettings.json @@ -19,7 +19,7 @@ "Scopes": "Policy.Read.ConditionalAccess Policy.ReadWrite.ConditionalAccess" }, "ConnectionStrings": { - "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CommonDBContext;Trusted_Connection=True;MultipleActiveResultSets=true" + "DefaultConnection": "your database connection string here" // For example, "Server=(localdb)\\MSSQLLocalDB;Integrated Security=true" }, "Logging": { "LogLevel": {