From a6a7aea93afd332256e2e1f74e8e4bd62ff249db Mon Sep 17 00:00:00 2001 From: Kalyan Krishna Date: Mon, 27 May 2019 22:23:55 +1000 Subject: [PATCH] Minor fixes and enhancements --- AppCreationScripts/AppCreationScripts.md | 146 +++++++++ AppCreationScripts/Cleanup.ps1 | 70 ++++ AppCreationScripts/Configure.ps1 | 306 ++++++++++++++++++ AppCreationScripts/sample.json | 106 ++++++ .../Controllers/TodoListController.cs | 2 +- .../AzureAdAuthenticationBuilderExtensions.cs | 9 +- .../JwtBearerMiddlewareDiagnostics.cs | 91 ++++++ TodoListService/Startup.cs | 15 +- TodoListService/TodoListService.csproj | 6 +- TodoListWebApp/Controllers/TodoController.cs | 7 +- .../AzureAdAuthenticationBuilderExtensions.cs | 2 +- TodoListWebApp/Properties/launchSettings.json | 4 +- 12 files changed, 748 insertions(+), 16 deletions(-) create mode 100644 AppCreationScripts/AppCreationScripts.md create mode 100644 AppCreationScripts/Cleanup.ps1 create mode 100644 AppCreationScripts/Configure.ps1 create mode 100644 AppCreationScripts/sample.json create mode 100644 TodoListService/JwtBearerMiddlewareDiagnostics.cs diff --git a/AppCreationScripts/AppCreationScripts.md b/AppCreationScripts/AppCreationScripts.md new file mode 100644 index 0000000..68a7817 --- /dev/null +++ b/AppCreationScripts/AppCreationScripts.md @@ -0,0 +1,146 @@ +# Registering the Azure Active Directory applications and updating the configuration files for this sample using PowerShell scripts + +## Overview + +### Quick summary + +1. On Windows run PowerShell 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 accordinly. (Other ways of running the scripts are described below) + ```PowerShell + .\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) + +## Goal of the scripts + +### Presentation of the scripts + +This sample comes with two PowerShell scripts, which automate the creation of the Azure Active Directory applications, and the configuration of the code for this sample. Once you run them, you will only need to build the solution and you are good to test. + +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 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). + +### Usage pattern for tests and DevOps scenarios + +The `Configure.ps1` will stop if it tries to create an Azure AD application which already exists in the tenant. For this, if you are using the script to try/test the sample, or in DevOps scenarios, you might want to run `Cleanup.ps1` just before `Configure.ps1`. This is what is shown in the steps below. + +## How to use the app creation scripts ? + +### 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: + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process + ``` +### (Optionally) install AzureAD 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: + +4. 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, + ```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 + +You're done. this just works! + +### Four 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. + +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). +- 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 indentity 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) + + 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 +- Go to **Properties** and copy the content of the **Directory Id** property +- Then use the full syntax to run the scripts: + +```PowerShell +$tenantId = "yourTenantIdGuid" +. .\Cleanup.ps1 -TenantId $tenantId +. .\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 +``` diff --git a/AppCreationScripts/Cleanup.ps1 b/AppCreationScripts/Cleanup.ps1 new file mode 100644 index 0000000..c103f51 --- /dev/null +++ b/AppCreationScripts/Cleanup.ps1 @@ -0,0 +1,70 @@ +[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 +) + +if ((Get-Module -ListAvailable -Name "AzureAD") -eq $null) { + Install-Module "AzureAD" -Scope CurrentUser +} +Import-Module AzureAD +$ErrorActionPreference = 'Stop' + +Function Cleanup +{ +<# +.Description +This function removes the Azure AD applications for the sample. These applications were created by the Configure.ps1 script +#> + + # $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 + } + else + { + if (!$TenantId) + { + $creds = Connect-AzureAD -Credential $Credential + } + else + { + $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential + } + } + + if (!$tenantId) + { + $tenantId = $creds.Tenant.Id + } + $tenant = Get-AzureADTenantDetail + $tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name + + # Removes the applications + Write-Host "Cleaning-up applications from tenant '$tenantName'" + + Write-Host "Removing 'service' (TodoListService-aspnetcore) if needed" + $app=Get-AzureADApplication -Filter "DisplayName eq 'TodoListService-aspnetcore'" + + if ($app) + { + Remove-AzureADApplication -ObjectId $app.ObjectId + Write-Host "Removed TodoListService-aspnetcore." + } + Write-Host "Removing 'client' (TodoListWebApp-aspnetcore) if needed" + $app=Get-AzureADApplication -Filter "DisplayName eq 'TodoListWebApp-aspnetcore'" + + if ($app) + { + Remove-AzureADApplication -ObjectId $app.ObjectId + Write-Host "Removed TodoListWebApp-aspnetcore." + } + } + +Cleanup -Credential $Credential -tenantId $TenantId diff --git a/AppCreationScripts/Configure.ps1 b/AppCreationScripts/Configure.ps1 new file mode 100644 index 0000000..1a171ce --- /dev/null +++ b/AppCreationScripts/Configure.ps1 @@ -0,0 +1,306 @@ +[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 +) + +<# + 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 + + 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) +{ + $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 + return $key +} + +# Adds the requiredAccesses (expressed as a pipe separated string) to the requiredAccess structure +# The exposed permissions are in the $exposedPermissions collection, and the type of permission (Scope | Role) is +# described in $permissionType +Function AddResourcePermission($requiredAccess, ` + $exposedPermissions, [string]$requiredAccesses, [string]$permissionType) +{ + foreach($permission in $requiredAccesses.Trim().Split("|")) + { + 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) + } + } + } +} + +# +# Exemple: GetRequiredPermissions "Microsoft Graph" "Graph.Read|User.Read" +# See also: http://stackoverflow.com/questions/42164581/how-to-configure-a-new-azure-ad-application-through-powershell +Function GetRequiredPermissions([string] $applicationDisplayName, [string] $requiredDelegatedPermissions, [string]$requiredApplicationPermissions, $servicePrincipal) +{ + # If we are passed the service principal we use it directly, otherwise we find it from the display name (which might not be unique) + if ($servicePrincipal) + { + $sp = $servicePrincipal + } + else + { + $sp = Get-AzureADServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'" + } + $appid = $sp.AppId + $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess + $requiredAccess.ResourceAppId = $appid + $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess] + + # $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" + } + + # $sp.AppRoles | Select Id,AdminConsentDisplayName,Value: To see the list of all the Application permissions for the application + if ($requiredApplicationPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role" + } + return $requiredAccess +} + + +Function UpdateLine([string] $line, [string] $value) +{ + $index = $line.IndexOf('=') + $delimiter = ';' + if ($index -eq -1) + { + $index = $line.IndexOf(':') + $delimiter = ',' + } + if ($index -ige 0) + { + $line = $line.Substring(0, $index+1) + " "+'"'+$value+'"'+$delimiter + } + return $line +} + +Function UpdateTextFile([string] $configFilePath, [System.Collections.HashTable] $dictionary) +{ + $lines = Get-Content $configFilePath + $index = 0 + while($index -lt $lines.Length) + { + $line = $lines[$index] + foreach($key in $dictionary.Keys) + { + if ($line.Contains($key)) + { + $lines[$index] = UpdateLine $line $dictionary[$key] + } + } + $index++ + } + + Set-Content -Path $configFilePath -Value $lines -Force +} + +Set-Content -Value "" -Path createdApps.html +Add-Content -Value "" -Path createdApps.html + +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" + + # $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 + } + else + { + if (!$TenantId) + { + $creds = Connect-AzureAD -Credential $Credential + } + else + { + $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential + } + } + + if (!$tenantId) + { + $tenantId = $creds.Tenant.Id + } + + $tenant = Get-AzureADTenantDetail + $tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name + + # Get the user running the script + $user = Get-AzureADUser -ObjectId $creds.Account.Id + + # Create the service AAD application + Write-Host "Creating the AAD application (TodoListService-aspnetcore)" + $serviceAadApplication = New-AzureADApplication -DisplayName "TodoListService-aspnetcore" ` + -HomePage "https://localhost:44351/" ` + -ReplyUrls "https://localhost:44351/" ` + -AvailableToOtherTenants $True ` + -PublicClient $False + $serviceIdentifierUri = 'api://'+$serviceAadApplication.AppId + Set-AzureADApplication -ObjectId $serviceAadApplication.ObjectId -IdentifierUris $serviceIdentifierUri + + $currentAppId = $serviceAadApplication.AppId + $serviceServicePrincipal = New-AzureADServicePrincipal -AppId $currentAppId -Tags {WindowsAzureActiveDirectoryIntegratedApp} + + # add the user running the script as an app owner if needed + $owner = Get-AzureADApplicationOwner -ObjectId $serviceAadApplication.ObjectId + if ($owner -eq $null) + { + Add-AzureADApplicationOwner -ObjectId $serviceAadApplication.ObjectId -RefObjectId $user.ObjectId + Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($serviceServicePrincipal.DisplayName)'" + } + + Write-Host "Done creating the service application (TodoListService-aspnetcore)" + + # URL of the AAD application in the Azure portal + # Future? $servicePortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$serviceAadApplication.AppId+"/objectId/"+$serviceAadApplication.ObjectId+"/isMSAApp/" + $servicePortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$serviceAadApplication.AppId+"/objectId/"+$serviceAadApplication.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 'service' to 'Microsoft Graph') + Write-Host "Getting access from 'service' to 'Microsoft Graph'" + $requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" ` + -requiredDelegatedPermissions "User.Read" ` + + $requiredResourcesAccess.Add($requiredPermissions) + + + Set-AzureADApplication -ObjectId $serviceAadApplication.ObjectId -RequiredResourceAccess $requiredResourcesAccess + Write-Host "Granted permissions." + # Create the client AAD application + Write-Host "Creating the AAD application (TodoListWebApp-aspnetcore)" + # Get a 2 years application key for the client Application + $pw = ComputePassword + $fromDate = [DateTime]::Now; + $key = CreateAppKey -fromDate $fromDate -durationInYears 2 -pw $pw + $clientAppKey = $pw + $clientAadApplication = New-AzureADApplication -DisplayName "TodoListWebApp-aspnetcore" ` + -HomePage "https://localhost:44377/" ` + -LogoutUrl "https://localhost:44371/Account/EndSession" ` + -ReplyUrls "https://localhost:44377/", "https://localhost:44377/signin-oidc" ` + -AvailableToOtherTenants $True ` + -PasswordCredentials $key ` + -Oauth2AllowImplicitFlow $true ` + -PublicClient $False + + $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 + Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($clientServicePrincipal.DisplayName)'" + } + + Write-Host "Done creating the client application (TodoListWebApp-aspnetcore)" + + # 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" ` + + $requiredResourcesAccess.Add($requiredPermissions) + + # Add Required Resources Access (from 'client' to 'service') + Write-Host "Getting access from 'client' to 'service'" + $requiredPermissions = GetRequiredPermissions -applicationDisplayName "TodoListService-aspnetcore" ` + -requiredDelegatedPermissions "user_impersonation" ` + + $requiredResourcesAccess.Add($requiredPermissions) + + + Set-AzureADApplication -ObjectId $clientAadApplication.ObjectId -RequiredResourceAccess $requiredResourcesAccess + Write-Host "Granted permissions." + + # Configure known client applications for service + Write-Host "Configure known client applications for the 'service'" + $knowApplications = New-Object System.Collections.Generic.List[System.String] + $knowApplications.Add($clientAadApplication.AppId) + Set-AzureADApplication -ObjectId $serviceAadApplication.ObjectId -KnownClientApplications $knowApplications + Write-Host "Configured." + + + # Update config file for 'service' + $configFile = $pwd.Path + "\..\TodoListService\appsettings.json" + Write-Host "Updating the sample code ($configFile)" + $dictionary = @{ "Domain" = $tenantName;"TenantId" = $tenantId;"ClientId" = $serviceAadApplication.AppId }; + UpdateTextFile -configFilePath $configFile -dictionary $dictionary + + # Update config file for 'client' + $configFile = $pwd.Path + "\..\TodoListWebApp\appsettings.json" + Write-Host "Updating the sample code ($configFile)" + $dictionary = @{ "Domain" = $tenantName;"TenantId" = $tenantId;"ClientId" = $clientAadApplication.AppId;"ClientSecret" = $clientAppKey;"TodoListResourceId" = $serviceIdentifierUri }; + UpdateTextFile -configFilePath $configFile -dictionary $dictionary + + Add-Content -Value "
ApplicationAppIdUrl in the Azure portal
service$currentAppIdTodoListService-aspnetcore
client$currentAppIdTodoListWebApp-aspnetcore
" -Path createdApps.html +} + +# Pre-requisites +if ((Get-Module -ListAvailable -Name "AzureAD") -eq $null) { + Install-Module "AzureAD" -Scope CurrentUser +} +Import-Module AzureAD + +# Run interactively (will ask you for the tenant ID) +ConfigureApplications -Credential $Credential -tenantId $TenantId \ No newline at end of file diff --git a/AppCreationScripts/sample.json b/AppCreationScripts/sample.json new file mode 100644 index 0000000..df24a1e --- /dev/null +++ b/AppCreationScripts/sample.json @@ -0,0 +1,106 @@ +{ + "Sample": { + "Title": "An ASP.NET Core web application that authenticates Azure AD users and calls a web API using OAuth 2.0 access tokens.", + "Level": 300, + "Client": "ASP.NET Core 2.0", + "Service": "ASP.NET Core 2.0", + "RepositoryUrl": "active-directory-dotnet-webapp-webapi-openidconnect-aspnetcore", + "Endpoint": "AAD v1.0" + }, + + /* + This section describes the Azure AD Applications to configure, and their dependencies + */ + "AADApps": [ + { + "Id": "service", + "Name": "TodoListService-aspnetcore", + "Kind": "WebApi", + "HomePage": "https://localhost:44351/", + "ReplyUrls": "https://localhost:44351/", + "AvailableToOtherTenants": "False", + "RequiredResourcesAccess": [ + { + "Resource": "Microsoft Graph", + "DelegatedPermissions": [ "User.Read" ] + } + ] + }, + { + "Id": "client", + "Name": "TodoListWebApp-aspnetcore", + "Kind": "WebApp", + "HomePage":"https://localhost:44377/", + "ReplyUrls": "https://localhost:44377/, https://localhost:44377/signin-oidc", + "LogoutUrl": "https://localhost:44371/Account/EndSession", + "AvailableToOtherTenants": "False", + "PasswordCredentials": "Auto", + "RequiredResourcesAccess": [ + { + "Resource": "Microsoft Graph", + "DelegatedPermissions": [ "User.Read" ] + }, + { + "Resource": "service", + "DelegatedPermissions": [ "user_impersonation" ] + } + ] + } + ], + + /* + This section describes how to update the code in configuration files from the apps coordinates, once the apps + are created in Azure AD. + Each section describes a configuration file, for one of the apps, it's type (XML, JSon, plain text), its location + with respect to the root of the sample, and the mappping (which string in the config file is mapped to which value + */ + "CodeConfiguration": [ + { + "App": "service", + "SettingKind": "Text", + "SettingFile": "\\..\\TodoListService\\appsettings.json", + "Mappings": [ + { + "key": "Domain", + "value": "$tenantName" + }, + { + "key": "TenantId", + "value": "$tenantId" + }, + { + "key": "ClientId", + "value": "service.AppId" + } + ] + }, + + { + "App": "client", + "SettingKind": "Text", + "SettingFile": "\\..\\TodoListWebApp\\appsettings.json", + "Mappings": [ + { + "key": "Domain", + "value": "$tenantName" + }, + { + "key": "TenantId", + "value": "$tenantId" + }, + { + "key": "ClientId", + "value": "client.AppId" + }, + { + "key": "ClientSecret", + "value": "client.AppKey" + }, + { + "key": "TodoListResourceId", + "value": "$serviceIdentifierUri" + } + ] + } + ] +} diff --git a/TodoListService/Controllers/TodoListController.cs b/TodoListService/Controllers/TodoListController.cs index b41b303..6eef551 100644 --- a/TodoListService/Controllers/TodoListController.cs +++ b/TodoListService/Controllers/TodoListController.cs @@ -8,7 +8,7 @@ namespace TodoListService.Controllers { - [Authorize] + [Authorize] [Route("api/[controller]")] public class TodoListController : Controller { diff --git a/TodoListService/Extensions/AzureAdAuthenticationBuilderExtensions.cs b/TodoListService/Extensions/AzureAdAuthenticationBuilderExtensions.cs index b8c4e08..a5ed27c 100644 --- a/TodoListService/Extensions/AzureAdAuthenticationBuilderExtensions.cs +++ b/TodoListService/Extensions/AzureAdAuthenticationBuilderExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using TodoListService; namespace Microsoft.AspNetCore.Authentication { @@ -30,8 +31,14 @@ public ConfigureAzureOptions(IOptions azureOptions) public void Configure(string name, JwtBearerOptions options) { - options.Audience = _azureOptions.ClientId; + //options.Audience = _azureOptions.ClientId; options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}"; + + // The valid audiences are both the Client ID(options.Audience) and api://{ClientID} + options.TokenValidationParameters.ValidAudiences = new string[] { _azureOptions.ClientId, $"api://{_azureOptions.ClientId}" }; + + // If you want to debug, or just understand the JwtBearer events, uncomment the following line of code + // options.Events = JwtBearerMiddlewareDiagnostics.Subscribe(options.Events); } public void Configure(JwtBearerOptions options) diff --git a/TodoListService/JwtBearerMiddlewareDiagnostics.cs b/TodoListService/JwtBearerMiddlewareDiagnostics.cs new file mode 100644 index 0000000..457831d --- /dev/null +++ b/TodoListService/JwtBearerMiddlewareDiagnostics.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace TodoListService +{ + /// + /// Diagnostics for the JwtBearer middleware (used in Web APIs) + /// + public class JwtBearerMiddlewareDiagnostics + { + /// + /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. + /// + static Func onAuthenticationFailed; + + /// + /// Invoked when a protocol message is first received. + /// + static Func onMessageReceived; + + /// + /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. + /// + static Func onTokenValidated; + + /// + /// Invoked before a challenge is sent back to the caller. + /// + static Func onChallenge; + + /// + /// Subscribes to all the JwtBearer events, to help debugging, while + /// preserving the previous handlers (which are called) + /// + /// Events to subscribe to + public static JwtBearerEvents Subscribe(JwtBearerEvents events) + { + if (events == null) + { + events = new JwtBearerEvents(); + } + + onAuthenticationFailed = events.OnAuthenticationFailed; + events.OnAuthenticationFailed = OnAuthenticationFailed; + + onMessageReceived = events.OnMessageReceived; + events.OnMessageReceived = OnMessageReceived; + + onTokenValidated = events.OnTokenValidated; + events.OnTokenValidated = OnTokenValidated; + + onChallenge = events.OnChallenge; + events.OnChallenge = OnChallenge; + + return events; + } + + static async Task OnMessageReceived(MessageReceivedContext context) + { + Debug.WriteLine($"1. Begin {nameof(OnMessageReceived)}"); + // Place a breakpoint here and examine the bearer token (context.Request.Headers.HeaderAuthorization / context.Request.Headers["Authorization"]) + // Use https://jwt.ms to decode the token and observe claims + await onMessageReceived(context); + Debug.WriteLine($"1. End - {nameof(OnMessageReceived)}"); + } + + static async Task OnAuthenticationFailed(AuthenticationFailedContext context) + { + Debug.WriteLine($"99. Begin {nameof(OnAuthenticationFailed)}"); + // Place a breakpoint here and examine context.Exception + await onAuthenticationFailed(context); + Debug.WriteLine($"99. End - {nameof(OnAuthenticationFailed)}"); + } + + static async Task OnTokenValidated(TokenValidatedContext context) + { + Debug.WriteLine($"2. Begin {nameof(OnTokenValidated)}"); + await onTokenValidated(context); + Debug.WriteLine($"2. End - {nameof(OnTokenValidated)}"); + } + + static async Task OnChallenge(JwtBearerChallengeContext context) + { + Debug.WriteLine($"55. Begin {nameof(OnChallenge)}"); + await onChallenge(context); + Debug.WriteLine($"55. End - {nameof(OnChallenge)}"); + } + } +} diff --git a/TodoListService/Startup.cs b/TodoListService/Startup.cs index 6026678..dc5ade4 100644 --- a/TodoListService/Startup.cs +++ b/TodoListService/Startup.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.AzureAD.UI; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace TodoListService { @@ -31,6 +26,8 @@ public void ConfigureServices(IServiceCollection services) }) .AddAzureAdBearer(options => Configuration.Bind("AzureAd", options)); + + services.AddMvc(); } @@ -46,4 +43,4 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) app.UseMvc(); } } -} +} \ No newline at end of file diff --git a/TodoListService/TodoListService.csproj b/TodoListService/TodoListService.csproj index 91dbaef..942eebc 100644 --- a/TodoListService/TodoListService.csproj +++ b/TodoListService/TodoListService.csproj @@ -7,11 +7,15 @@ - + + + + + diff --git a/TodoListWebApp/Controllers/TodoController.cs b/TodoListWebApp/Controllers/TodoController.cs index 6423332..01329e5 100644 --- a/TodoListWebApp/Controllers/TodoController.cs +++ b/TodoListWebApp/Controllers/TodoController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -68,8 +69,10 @@ public async Task Index() return ProcessUnauthorized(itemList, authContext); } } - catch (Exception) + catch (Exception ex) { + Debug.WriteLine(ex); + if (HttpContext.Request.Query["reauth"] == "True") { // @@ -79,6 +82,7 @@ public async Task Index() // return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme); } + // // The user needs to re-authorize. Show them a message to that effect. // @@ -88,6 +92,7 @@ public async Task Index() ViewBag.ErrorMessage = "AuthorizationRequired"; return View(itemList); } + // // If the call failed for any other reason, show the user an error. // diff --git a/TodoListWebApp/Extensions/AzureAdAuthenticationBuilderExtensions.cs b/TodoListWebApp/Extensions/AzureAdAuthenticationBuilderExtensions.cs index 0392da2..71fbbfd 100644 --- a/TodoListWebApp/Extensions/AzureAdAuthenticationBuilderExtensions.cs +++ b/TodoListWebApp/Extensions/AzureAdAuthenticationBuilderExtensions.cs @@ -38,7 +38,7 @@ public void Configure(string name, OpenIdConnectOptions options) options.CallbackPath = _azureOptions.CallbackPath; options.RequireHttpsMetadata = false; options.ClientSecret = _azureOptions.ClientSecret; - options.Resource = "https://graph.windows.net"; // AAD graph + options.Resource = "https://graph.microsoft.com"; // AAD graph // Without overriding the response type (which by default is id_token), the OnAuthorizationCodeReceived event is not called. // but instead OnTokenValidated event is called. Here we request both so that OnTokenValidated is called first which diff --git a/TodoListWebApp/Properties/launchSettings.json b/TodoListWebApp/Properties/launchSettings.json index 874305f..151e5db 100644 --- a/TodoListWebApp/Properties/launchSettings.json +++ b/TodoListWebApp/Properties/launchSettings.json @@ -3,8 +3,8 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:17945/", - "sslPort": 0 + "applicationUrl": "https://localhost:44377/", + "sslPort": 44377 } }, "profiles": {