diff --git a/.gitignore b/.gitignore index feb180eb..d021e5dd 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,4 @@ packages /4-WebApp-your-API/4-1-MyOrg/.vscode/settings.json /5-WebApp-AuthZ/5-1-Roles/.vscode/settings.json /.vscode +/2-WebApp-graph-user/2-1-Call-MSGraph/Properties/PublishProfiles diff --git a/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/AboutTheCode.md b/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/AboutTheCode.md new file mode 100644 index 00000000..e69de29b diff --git a/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/CAE.md b/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/CAE.md new file mode 100644 index 00000000..2887d0ea --- /dev/null +++ b/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/CAE.md @@ -0,0 +1,57 @@ +### Process the CAE challenge from Microsoft Graph + +To process the CAE challenge from Microsoft Graph, the controller actions need to extract it from the `wwwAuthenticate` header. It is returned when MS Graph rejects a seemingly valid Access tokens for MS Graph. For this you need to: + +1. Inject and instance of `MicrosoftIdentityConsentAndConditionalAccessHandler` in the controller constructor. The beginning of the HomeController becomes: + + ```CSharp + public class HomeController : Controller + { + private readonly ILogger _logger; + private readonly GraphServiceClient _graphServiceClient; + private readonly MicrosoftIdentityConsentAndConditionalAccessHandler _consentHandler; + private string[] _graphScopes = new[] { "user.read" }; + public HomeController(ILogger logger, + IConfiguration configuration, + GraphServiceClient graphServiceClient, + MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler) + { + _logger = logger; + _graphServiceClient = graphServiceClient; + this._consentHandler = consentHandler; + // Capture the Scopes for Graph that were used in the original request for an Access token (AT) for MS Graph as + // they'd be needed again when requesting a fresh AT for Graph during claims challenge processing + _graphScopes = configuration.GetValue("DownstreamApi:Scopes")?.Split(' '); + } + + // more code here + ``` +1. The process to handle CAE challenges from MS Graph comprises of the following steps: + 1. Catch a Microsoft Graph SDK's `ServiceException` and extract the required `claims`. This is done by wrapping the call to Microsoft Graph into a try/catch block that processes the challenge: + ```CSharp + currentUser = await _graphServiceClient.Me.Request().GetAsync(); + ``` + 1. Then redirect the user back to Azure AD with the new requested `claims`. Azure AD will use this `claims` payload to discern what or if any additional processing is required, example being the user needs to sign-in again or do multi-factor authentication. + ```CSharp + try + { + currentUser = await _graphServiceClient.Me.Request().GetAsync(); + } + // Catch CAE exception from Graph SDK + catch (ServiceException svcex) when (svcex.Message.Contains("Continuous access evaluation resulted in claims challenge")) + { + try + { + Console.WriteLine($"{svcex}"); + string claimChallenge = WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(svcex.ResponseHeaders); + _consentHandler.ChallengeUser(_graphScopes, claimChallenge); + return new EmptyResult(); + } + catch (Exception ex2) + { + _consentHandler.HandleException(ex2); + } + } + ``` + + The `AuthenticationHeaderHelper` class is available from the `Helpers\AuthenticationHeaderHelper.cs file`. diff --git a/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/Deployment.md b/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/Deployment.md new file mode 100644 index 00000000..45491952 --- /dev/null +++ b/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/Deployment.md @@ -0,0 +1,210 @@ +## Deployment + +### Deploying web app to Azure App Services + +There is one web app in this sample. To deploy it to **Azure App Services**, you'll need to: + +- create an **Azure App Service** +- publish the projects to the **App Services**, and +- update its client(s) to call the website instead of the local environment. + +#### Publish your files + +##### Publish using Visual Studio + +Follow the link to [Publish with Visual Studio](https://docs.microsoft.com/visualstudio/deployment/quickstart-deploy-to-azure). + +##### Publish using Visual Studio Code + +1. Open an instance of Visual Studio code set to the `WebApp-OpenIDConnect-DotNet-graph-v2` project folder. +1. Install the VS Code extension [Azure App Service](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azureappservice). +1. Using the extension you just installed, sign in to **Azure App Service** using your Azure AD account. +1. Choose `Terminal > New Terminal` from the VS Code menu to open a new terminal window in the project directory. +1. Run the following command + + ```console + dotnet publish WebApp-OpenIDConnect-DotNet-graph.csproj --configuration Release + ``` + +1. A `publish` folder is created within the following folder: `bin/Release/netcoreapp3.1/`. +1. From the VS Code file explorer, right-click on the **publish** folder and select **Deploy to Web App**. +1. Select **Create New Web App**. +1. Enter a unique name for the app, for example, `WebApp-OpenIDConnect-DotNet-graph-v2`. If you chose `example-domain` for your app name, your app's domain name will be `https://example-domain.azurewebsites.net`. +1. Select **Windows** as the OS. Press Enter. +1. Select **.NET Core 3.1 (LTS)** as runtime stack. +1. Select `Free` or any other option for your pricing tier. + +#### Update the Azure AD app registration (WebApp-OpenIDConnect-DotNet-graph-v2) + +1. Navigate back to to the [Azure portal](https://portal.azure.com). +1. Go to the **Azure Active Directory** section, and then select **App registrations**. +1. In the resulting screen, select the `WebApp-OpenIDConnect-DotNet-graph-v2` application. +1. In the app's registration screen, select **Authentication** in the menu. + - In the **Redirect URIs** section, update both of the reply URLs to match the site URL of your Azure deployment. Using the following examples as a guide, **replace** the text `example-domain` with the app name you created while deploying, for example: + - `https://example-domain.azurewebsites.net/` + - `https://example-domain.azurewebsites.net/signin-oidc` +1. Update the **Front-channel logout URL** fields with the address of your service, for example `https://example-domain.azurewebsites.net`. + +> :warning: If your app is using *in-memory* storage, **Azure App Services** will spin down your web site if it is inactive, and any records that your app was keeping will emptied. In addition, if you increase the instance count of your website, requests will be distributed among the instances. Your app's records, therefore, will not be the same on each instance. + +### Enabling your code to get secrets from Key Vault using Managed Identity + +One of the uber principals of security and **Zero Trust** is to place credentials out of your code and use in a manner that allows for credentials to be replaced or rotated without incurring a downtime. + +To achieve this we'd place our application's credentials in [Azure Key Vault](https://azure.microsoft.com/services/key-vault/) and access it via [managed Identities for Azure resources](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview). + +We will follow the steps broadly outlined in the guide: [Use Key Vault from App Service with Azure Managed Identity](https://github.com/Azure-Samples/app-service-msi-keyvault-dotnet/blob/master/README.md) + +#### Set up your Managed Identity + +1. Navigate to [Azure portal](https://portal.azure.com) and select the **Azure App Service**. +1. Find and select the App Service you've created previously. +1. On App Service portal, select **Identity**. +1. Within the **System assigned** tab, switch **Status** to **On**. Click **Save**. +1. Record the **Object Id** that will appear, as you will need it in the next step. + +For more information, see [Add a system-assigned identity](https://docs.microsoft.com/azure/app-service/overview-managed-identity?tabs=dotnet#add-a-system-assigned-identity) + +#### Set up your Key vault + +Before starting here, make sure: + +- You have an [Azure Subscription](https://azure.microsoft.com/free/). +- You have a working and deployed application as an Azure App Service following the steps listed at [Deploying web app to Azure App Services](#deploying-web-app-to-azure-app-services) above. +- Follow the guide to [create an Azure Key Vault](https://docs.microsoft.com/azure/key-vault/general/quick-create-portal). + +##### Upload your secret to KeyVault + +1. Navigate to your new key vault in the Azure portal. +1. On the Key Vault settings pages, select **Secrets**. +1. Click on **Generate/Import**. +1. On the **Create a secret** screen choose the following values: + - **Upload options**: Manual. + - **Name**: Type a name for the secret. The secret name must be unique within a Key Vault. For example, `myClientSecret` + - **Value**: Copy and paste the value for the `ClientSecret` property (without quotes!) from your `appsettings.json` file. + - Leave the other values to their defaults. Click **Create**. + +##### Provide the managed identity access to Key Vault + +1. Navigate to your Key Vault in the portal. +1. Select **Overview** > **Access policies**. +1. Click on **Add Access Policy**. +1. In the input box for **Secret permissions**, select **Get**. +1. Click on **Select Principal**, add the **system-assigned managed identity** that you have created in the [steps before](#set-up-your-managed-identity). You can use the **Object Id** you have recorded previously to search for it. +1. Click on **OK** to add the new Access Policy, then click **Save** to save the Access Policy. + +#### Modify your code to connect to Key Vault + +1. In the `appsettings.json` file, find and delete the `ClientSecret` property and its value. +1. In the `Properties\launchSettings.json` file, find the string `ENTER_YOUR_KEY_VAULT_URI` and replace it with the URI of your Key Vault, for example: `https://example-vault.vault.azure.net/` +1. Add the `Azure.Identity` NuGet package to the solution. This sample project has already added this package. +1. Add the following directives to your `startup.cs`. +

+ :information_source: this has been already added in the sample project. + +```CSharp +using Azure; +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; +``` + +5. In your `Startup.cs` file, you must create a `GetSecretFromKeyVault` method. This method sets up the Azure Key Vault client and returns the secret that is required. +

+ :information_source: this has already been added in the sample project. + +```CSharp + private string GetSecretFromKeyVault(string tenantId, string secretName) + { + // this should point to your vault's URI, like https://.vault.azure.net/ + string uri = Environment.GetEnvironmentVariable("KEY_VAULT_URI"); + DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions(); + + // Specify the tenant ID to use the dev credentials when running the app locally + options.VisualStudioTenantId = tenantId; + options.SharedTokenCacheTenantId = tenantId; + SecretClient client = new SecretClient(new Uri(uri), new DefaultAzureCredential(options)); + + // The secret name, for example if the full url to the secret is https://.vault.azure.net/secrets/Graph-App-Secret + Response secret = client.GetSecretAsync(secretName).Result; + + return secret.Value.Value; + } +``` + +6. In your `Startup.cs` file, find the `ConfigureServices` method. Add the following code to call the GetSecretFromKeyVault method, right after `services.AddAuthentication`. +

+ :information_source: In the sample project, this code is present but commented out by default. Uncomment it. +

+ :warning: Replace the string `ENTER_YOUR_SECRET_NAME_HERE` with the name of the client secret you entered into Azure Key Vault, for example `myClientSecret`. + +```CSharp + // uncomment the following 3 lines to get ClientSecret from KeyVault + string tenantId = Configuration.GetValue("AzureAd:TenantId"); + services.Configure( + options => { options.ClientSecret = GetSecretFromKeyVault(tenantId, "ENTER_YOUR_SECRET_NAME_HERE"); }); +``` + +7. Your `ConfigureServices` method should now look like the following snippet: + +```CSharp + public void ConfigureServices(IServiceCollection services) + { + string[] initialScopes = Configuration.GetValue("DownstreamApi:Scopes")?.Split(' '); + + services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(Configuration) + .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) + .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi")) + .AddInMemoryTokenCaches(); + + // uncomment the following 3 lines to get ClientSecret from KeyVault + string tenantId = Configuration.GetValue("AzureAd:TenantId"); + services.Configure( + options => { options.ClientSecret = GetSecretFromKeyVault(tenantId, "myClientSecret"); }); + + // ... more method code continues below + } +``` + +8. Add an environment variable to your App Service so your web app can find its key vault. + + 1. Go to the [Azure portal](https://portal.azure.com). Search for and select **App Service**, and then select your app. + 1. Select **Configuration** blade on the left, then select **New Application Settings**. + 1. Add a new variable, naming it **KEY_VAULT_URI**. Populate the value with the URI of your key vault, for example: `https://example-vault.vault.azure.net/` + +1. Re-deploy your project to Azure App Service. + + 1. Run the following command: + + ```console + dotnet publish WebApp-OpenIDConnect-DotNet-graph.csproj --configuration Release + ``` + + 1. Then, from the VS Code file explorer, right-click on the **bin/Release/netcoreapp3.1/publish** folder and select **Deploy to Web App**. If you are prompted to select an app, select one you created during this sample. + +1. The deployment status is available from the output window. Within a few minutes you'll be able to visit your now-secure app and sign in. + +## Optional - Handle Continuous Access Evaluation (CAE) challenge from Microsoft Graph + +Continuous access evaluation (CAE) enables web APIs to do just-in time token validation, for instance enforcing user session revocation in the case of password change/reset but there are other benefits. For details, see [Continuous access evaluation](https://docs.microsoft.com/azure/active-directory/conditional-access/concept-continuous-access-evaluation). + +Microsoft Graph is now CAE-enabled in Preview. This means that it can ask its clients for more claims when conditional access policies require it. Your can enable your application to be ready to consume CAE-enabled APIs by: + +1. Declaring that the client app is capable of handling claims challenges from the web API. +2. Processing these challenges when thrown. + +### Declare the CAE capability in the configuration + +This sample declares that it's CAE-capable by adding a `ClientCapabilities` property in the configuration, whose value is `[ "cp1" ]`. + +```Json +{ + "AzureAd": { + // ... + // the following is required to handle Continuous Access Evaluation challenges + "ClientCapabilities": [ "cp1" ], + // ... + }, + // ... +} +``` diff --git a/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/ExploreSample.md b/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/ExploreSample.md new file mode 100644 index 00000000..326aa818 --- /dev/null +++ b/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/ExploreSample.md @@ -0,0 +1,86 @@ +## Explore the sample + +1. Open your web browser and make a request to the app at url `https://localhost:44321`. The app immediately attempts to authenticate you via the Microsoft identity platform. Sign in with a work or school account. +2. Provide consent to the screen presented. +3. Click on the **Profile** link on the top menu. The web app will make a call to the Microsoft Graph `/me` endpoint. You should see information about the signed-in user's account, as well as its picture, if these values are set in the account's profile. + +> Did the sample not work for you as expected? Did you encounter issues trying this sample? Then please reach out to us using the [GitHub Issues](../../../../issues) page. + +> [Consider taking a moment to share your experience with us.](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbRz0h_jLR5HNJlvkZAewyoWxUNEFCQ0FSMFlPQTJURkJZMTRZWVJRNkdRMC4u) + +## About The code + +1. In this aspnetcore web project, first the packages `Microsoft.Identity.Web`, `Microsoft.Identity.Web.UI` and `Microsoft.Identity.Web.MicrosoftGraph` were added from NuGet. These libraries are used to simplify the process of signing-in a user and acquiring tokens for Microsoft Graph. + +2. Starting with the **Startup.cs** file : + + - at the top of the file, the following two using directives were added: + + ```CSharp + using Microsoft.Identity.Web; + using Microsoft.Identity.Web.UI; + ``` + + - in the `ConfigureServices` method, the following code was added, replacing any existing `AddAuthentication()` code: + + ```CSharp + + services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) + .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi")) + .AddInMemoryTokenCaches(); + + ``` + + `AddMicrosoftIdentityWebApp()` enables your application to sign-in a user with the Microsoft identity platform endpoint. This endpoint is capable of signing-in users both with their Work and School and Microsoft Personal accounts (if required). + + `EnableTokenAcquisitionToCallDownstreamApi()` and `AddMicrosoftGraph` adds support to call Microsoft Graph. This lines ensures that the GraphAPIService benefits from the optimized `HttpClient` management by ASP.NET Core. + +3. In the `Controllers\HomeController.cs` file, the following code is added to allow calling MS Graph: + + ```CSharp + private readonly ILogger _logger; + private readonly GraphServiceClient _graphServiceClient; + + private readonly GraphServiceClient _graphServiceClient; + public HomeController(ILogger logger, + IConfiguration configuration, + GraphServiceClient graphServiceClient) + { + _logger = logger; + _graphServiceClient = graphServiceClient; + this._consentHandler = consentHandler; + } + ``` + +4. In the `Profile()` action we make a call to the Microsoft Graph `/me` endpoint. In case a token cannot be acquired, a challenge is attempted to re-sign-in the user, and have them consent to the requested scopes. This is expressed declaratively by the `AuthorizeForScopes`attribute. This attribute is part of the `Microsoft.Identity.Web` project and automatically manages incremental consent. + + ```CSharp + [AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")] + public async Task Profile() + { + var me = await _graphServiceClient.Me.Request().GetAsync(); + ViewData["Me"] = me; + + try + { + // Get user photo + using (var photoStream = await _graphServiceClient.Me.Photo.Content.Request().GetAsync()) + { + byte[] photoByte = ((MemoryStream)photoStream).ToArray(); + ViewData["Photo"] = Convert.ToBase64String(photoByte); + } + } + catch (System.Exception) + { + ViewData["Photo"] = null; + } + + return View(); + } + ``` + +5. Update `launchSetting.json`. Change the following values in the `Properties\launchSettings.json` file to ensure that you start your web app from `https://localhost:44321`: + - update the `sslPort` of the `iisSettings` section to be `44321` + - update the `applicationUrl` property to `https://localhost:44321` diff --git a/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/MoreInformation.md b/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/MoreInformation.md new file mode 100644 index 00000000..7337db64 --- /dev/null +++ b/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/MoreInformation.md @@ -0,0 +1,15 @@ +## More information + +- [Microsoft identity platform (Azure Active Directory for developers)](https://docs.microsoft.com/azure/active-directory/develop/) +- [Overview of Microsoft Authentication Library (MSAL)](https://docs.microsoft.com/azure/active-directory/develop/msal-overview) +- [Microsoft.Identity.Web](https://aka.ms/microsoft-identity-web) +- [Quickstart: Register an application with the Microsoft identity platform (Preview)](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) +- [Understanding Azure AD application consent experiences](https://docs.microsoft.com/azure/active-directory/develop/application-consent-experience) +- [Understand user and admin consent](https://docs.microsoft.com/azure/active-directory/develop/howto-convert-app-to-be-multi-tenant#understand-user-and-admin-consent) +- [Application and service principal objects in Azure Active Directory](https://docs.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals) +- [National Clouds](https://docs.microsoft.com/azure/active-directory/develop/authentication-national-cloud#app-registration-endpoints) +- [Azure AD code samples](https://docs.microsoft.com/azure/active-directory/develop/sample-v2-code) +- [Managed Identities for Azure resources](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) +- [Azure Key Vault](https://azure.microsoft.com/services/key-vault/) +- [Use Key Vault from App Service with Azure Managed Identity](https://github.com/Azure-Samples/app-service-msi-keyvault-dotnet/blob/master/README.md) +- [Authentication Scenarios for Azure AD](https://docs.microsoft.com/azure/active-directory/develop/authentication-flows-app-scenarios). diff --git a/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/Overview.md b/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/Overview.md new file mode 100644 index 00000000..091cb9d9 --- /dev/null +++ b/2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/Overview.md @@ -0,0 +1,12 @@ +[![Build status](https://identitydivision.visualstudio.com/IDDP/_apis/build/status/AAD%20Samples/.NET%20client%20samples/ASP.NET%20Core%20Web%20App%20tutorial)](https://identitydivision.visualstudio.com/IDDP/_build/latest?definitionId=819) + +## Overview + +This sample demonstrates an ASP.NET Core web app that calls the Microsoft Graph API for a signed-in user. + +## Scenario + +1. The ASP.NET Core client web app uses the [Microsoft.Identity.Web](https://aka.ms/microsoft-identity-web) to sign a user in, and obtain a JWT [access Tokens](https://aka.ms/access-tokens) from **Azure AD**. +1. The access token is used by the client app as a bearer token to call Microsoft Graph. + +![Sign in with the Microsoft identity platform and call Graph](ReadmeFiles/sign-in.png) \ No newline at end of file diff --git a/2-WebApp-graph-user/2-5-HybridFlow/.vscode/launch.json b/2-WebApp-graph-user/2-5-HybridFlow/.vscode/launch.json new file mode 100644 index 00000000..97ef085e --- /dev/null +++ b/2-WebApp-graph-user/2-5-HybridFlow/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/net6.0/2-5-HybridFlow.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/2-WebApp-graph-user/2-5-HybridFlow/.vscode/tasks.json b/2-WebApp-graph-user/2-5-HybridFlow/.vscode/tasks.json new file mode 100644 index 00000000..124b5e60 --- /dev/null +++ b/2-WebApp-graph-user/2-5-HybridFlow/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/2-5-HybridFlow.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/2-5-HybridFlow.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/2-5-HybridFlow.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/2-WebApp-graph-user/2-5-HybridFlow/AppCreationScripts/sample1.json b/2-WebApp-graph-user/2-5-HybridFlow/AppCreationScripts/sample1.json new file mode 100644 index 00000000..bb5dbdc9 --- /dev/null +++ b/2-WebApp-graph-user/2-5-HybridFlow/AppCreationScripts/sample1.json @@ -0,0 +1,125 @@ +{ + "Sample": { + "Client": "ASP.NET Core Web App", + "Description": "Sign-in users interactively server-side (Confidential client) and silently acquire token for MS Graph for a Single-page app (SPA)", + "Endpoint": "AAD v2.0", + "Languages": [ + "csharp", + "javascript" + ], + "Level": 200, + "Products": [ + "aspnet-core", + "azure-active-directory" + ], + "RepositoryUrl": "active-directory-aspnetcore-webapp-openidconnect-v2", + "Service": "Microsoft Graph", + "Title": "active-directory-aspnetcore-webapp-openidconnect-v2" + }, + "ReadmeScenario": { + "IncludeFilePath": "../ReadmeFiles/ReadmeScenario.md", + "Image": "../ReadmeFiles/topology.png", + "AdditionalNotes": "" + }, + "ReadmePrerequirements": { + "FreeText": "", + "IncludeFilePath": "" + }, + "ReadmeSetup": { + "FreeText": "", + "IncludeFilePath": "../ReadmeFiles/ReadmeSetup.md", + "UseNewSetup": "1", + "CertificateOption": "1", + "CreateProjectIncludeFilePath": "", + "AppRegistrationIncludeFilePath": "", + "RunSampleIncludeFilePath": "" + }, + "ReadmeTroubleshooting": { + "IncludeFilePath": "../ReadmeFiles/ReadmeTroubleshooting.md" + }, + "ReadmeNextSteps": { + "FreeText": "", + "IncludeFilePath": "../ReadmeFiles/ReadmeNextSteps.md" + }, + "ReadmeContributing": { + "FreeText": "", + "IncludeFilePath": "../ReadmeFiles/ReadmeContributing.md" + }, + "ReadmeHowTheCodeWasCreated": { + "IncludeFilePath": "" + }, + "ReadmeAboutTheCode": { + "IncludeFilePath": "../ReadmeFiles/ReadmeAboutTheCode.md" + }, + "ReadmeAzureDeploy": { + "FreeText": "", + "IncludeFilePath": "" + }, + "ReadmeUsingTheSample": { + "IncludeFilePath": "../ReadmeFiles/ReadmeUsingTheSample.md" + }, + "ReadmeLearnMore": { + "IncludeFilePath": "../ReadmeFiles/ReadmeLearnMore.md" + }, + "AADApps": [ + { + "AllowImplicitFlow": true, + "EnableIdTokenIssuance": true, + "Audience": "AzureADMyOrg", + "PasswordCredentials": "Auto", + "HybridRedirectUris": [ + { + "ApplicationKind": "WebApp", + "Uri": "https://localhost:44321/signin-oidc" + }, + { + "ApplicationKind": "SinglePageApplication", + "Uri": "https://localhost:44321/" + } + ], + "Id": "HybridFlowAspNetCore", + "Kind": "WebApp", + "ManualSteps": [], + "Name": "HybridFlowAspNetCore", + "RequiredResourcesAccess": [ + { + "DelegatedPermissions": [ + "User.Read", + "Contacts.Read" + ], + "Resource": "Microsoft Graph" + } + ], + "Sample": { + "SampleSubPath": "2-WebApp-graph-user\\2-5-HybridFlow", + "ProjectDirectory": "." + }, + "CSharpConfigurations": [ + { + "AppSettingsJson": { + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "CallbackPath": "/signin-oidc", + "ClientCertificates": [], + "WithSpaAuthCode": true, + "ClientSecret": "Auto" + }, + "DownStreamApi": { + "BaseUrl": "https://graph.microsoft.com/v1.0", + "Scopes": "User.Read Contacts.Read" + }, + "RelativePath": "..\\" + } + } + ] + } + ], + "CodeConfiguration": [ + { + "App": "HybridFlowAspNetCore", + "Mappings": [], + "SettingFile": "\\..\\appsettings.json", + "SettingKind": "JSON" + } + ] + } \ No newline at end of file diff --git a/4-WebApp-your-API/4-1-MyOrg/4-1-MyOrg.sln b/4-WebApp-your-API/4-1-MyOrg/4-1-MyOrg.sln index a1316840..95b13f75 100644 --- a/4-WebApp-your-API/4-1-MyOrg/4-1-MyOrg.sln +++ b/4-WebApp-your-API/4-1-MyOrg/4-1-MyOrg.sln @@ -6,8 +6,8 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DDAC3758-9CAE-4790-84B1-5B6258BAE44E}" ProjectSection(SolutionItems) = preProject README-incremental-instructions.md = README-incremental-instructions.md - README-use-keyvault-certificate.md = README-use-keyvault-certificate.md README.md = README.md + README-use-certificate.md = README-use-certificate.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoListService", "TodoListService\TodoListService.csproj", "{A55C5542-9C7B-4A34-932C-05DAF36CED0C}" @@ -31,7 +31,12 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ReadmeFiles", "ReadmeFiles", "{2A7989B7-23E0-46D4-8F2A-3D069084F65D}" ProjectSection(SolutionItems) = preProject ReadmeFiles\ReadmeAboutTheCode.md = ReadmeFiles\ReadmeAboutTheCode.md + ReadmeFiles\ReadmeAzureDeploy.md = ReadmeFiles\ReadmeAzureDeploy.md + ReadmeFiles\ReadmeExploreTheSample.md = ReadmeFiles\ReadmeExploreTheSample.md + ReadmeFiles\ReadmeHowTheCodeWasCreated.md = ReadmeFiles\ReadmeHowTheCodeWasCreated.md + ReadmeFiles\ReadmeLearnMore.md = ReadmeFiles\ReadmeLearnMore.md ReadmeFiles\ReadmeScenario.md = ReadmeFiles\ReadmeScenario.md + ReadmeFiles\ReadmeTroubleshooting.md = ReadmeFiles\ReadmeTroubleshooting.md ReadmeFiles\topology.png = ReadmeFiles\topology.png EndProjectSection EndProject diff --git a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/AppCreationScripts.md b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/AppCreationScripts.md index 7512ab4c..6e062df6 100644 --- a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/AppCreationScripts.md +++ b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/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,32 +130,19 @@ $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: - AzureCloud - AzureChinaCloud - AzureUSGovernment -- AzureGermanyCloud Example: ```PowerShell - . .\Cleanup.ps1 -AzureEnvironmentName "AzureGermanyCloud" - . .\Configure.ps1 -AzureEnvironmentName "AzureGermanyCloud" + . .\Cleanup.ps1 -AzureEnvironmentName "AzureUSGovernment" + . .\Configure.ps1 -AzureEnvironmentName "AzureUSGovernment" ``` diff --git a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/Cleanup.ps1 b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/Cleanup.ps1 index ee69aa0d..9fb78000 100644 --- a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/Cleanup.ps1 +++ b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/Cleanup.ps1 @@ -1,3 +1,4 @@ + [CmdletBinding()] param( [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')] @@ -41,7 +42,7 @@ Function Cleanup } catch { - Write-Host "Unable to remove the application 'TodoListService-aspnetcore-webapi' . Try deleting manually." -ForegroundColor White -BackgroundColor Red + Write-Host "Unable to remove the application 'TodoListService-aspnetcore-webapi' . Try deleting manually." -ForegroundColor White -BackgroundColor Red } Write-Host "Making sure there are no more (TodoListService-aspnetcore-webapi) applications found, will remove if needed..." @@ -65,7 +66,7 @@ Function Cleanup } catch { - Write-Host "Unable to remove ServicePrincipal 'TodoListService-aspnetcore-webapi' . Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red + Write-Host "Unable to remove ServicePrincipal 'TodoListService-aspnetcore-webapi' . Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red } Write-Host "Removing 'client' (TodoListClient-aspnetcore-webapi) if needed" try @@ -74,7 +75,7 @@ Function Cleanup } catch { - Write-Host "Unable to remove the application 'TodoListClient-aspnetcore-webapi' . Try deleting manually." -ForegroundColor White -BackgroundColor Red + Write-Host "Unable to remove the application 'TodoListClient-aspnetcore-webapi' . Try deleting manually." -ForegroundColor White -BackgroundColor Red } Write-Host "Making sure there are no more (TodoListClient-aspnetcore-webapi) applications found, will remove if needed..." @@ -98,10 +99,11 @@ Function Cleanup } catch { - Write-Host "Unable to remove ServicePrincipal 'TodoListClient-aspnetcore-webapi' . Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red + Write-Host "Unable to remove ServicePrincipal 'TodoListClient-aspnetcore-webapi' . Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red } # remove self-signed certificate - Get-ChildItem -Path Cert:\CurrentUser\My | where { $_.subject -eq "the certificate will be named by application name" } | Remove-Item + Write-Host "Removing CN=TodoListClient-aspnetcore-webapi certificate from Cert:/CurrentUser/My" + Get-ChildItem -Path Cert:\CurrentUser\My | where { $_.subject -eq "CN=TodoListClient-aspnetcore-webapi" } | Remove-Item } if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Applications")) { @@ -115,4 +117,3 @@ Cleanup -tenantId $tenantId -environment $azureEnvironmentName Write-Host "Disconnecting from tenant" Disconnect-MgGraph - diff --git a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/Configure.ps1 b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/Configure.ps1 index 3767f514..2457e59a 100644 --- a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/Configure.ps1 +++ b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/Configure.ps1 @@ -1,4 +1,4 @@ - + [CmdletBinding()] param( [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')] @@ -16,20 +16,6 @@ param( There are four ways to run this script. For more information, read the AppCreationScripts.md file in the same folder as this script. #> -# 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] $durationInMonths) -{ - $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 -} - # 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 @@ -154,9 +140,24 @@ Function CreateAppRole([string] $types, [string] $name, [string] $description) $appRole.Value = $name; return $appRole } +Function CreateOptionalClaim([string] $name) +{ + <#.Description + This function creates a new Azure AD optional claims with default and provided values + #> + + $appClaim = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim + $appClaim.AdditionalProperties = New-Object System.Collections.Generic.List[string] + $appClaim.Source = $null + $appClaim.Essential = $false + $appClaim.Name = $name + return $appClaim +} Function ConfigureApplications { + $isOpenSSl = 'N' #temporary disable open certificate creation + <#.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) @@ -188,6 +189,10 @@ Function ConfigureApplications @{ ` HomePageUrl = "https://localhost:44351"; ` } ` + -Api ` + @{ ` + RequestedAccessTokenVersion = 2 ` + } ` -SignInAudience AzureADMyOrg ` #end of command $serviceIdentifierUri = 'api://'+$serviceAadApplication.AppId @@ -204,6 +209,28 @@ Function ConfigureApplications New-MgApplicationOwnerByRef -ApplicationId $serviceAadApplication.Id -BodyParameter = @{"@odata.id" = "htps://graph.microsoft.com/v1.0/directoryObjects/$user.ObjectId"} Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($serviceServicePrincipal.DisplayName)'" } + + # Add Claims + + $optionalClaims = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaims + $optionalClaims.AccessToken = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + $optionalClaims.IdToken = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + $optionalClaims.Saml2Token = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + + + # Add Optional Claims + + $newClaim = CreateOptionalClaim -name "idtyp" + $optionalClaims.AccessToken += ($newClaim) + Update-MgApplication -ApplicationId $serviceAadApplication.Id -OptionalClaims $optionalClaims + + # Add application Roles + $appRoles = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphAppRole] + $newRole = CreateAppRole -types "Application" -name "ToDoList.Read.All" -description "Allow application to read all ToDo list items" + $appRoles.Add($newRole) + $newRole = CreateAppRole -types "Application" -name "ToDoList.ReadWrite.All" -description "Allow application to read and write into ToDo list" + $appRoles.Add($newRole) + Update-MgApplication -ApplicationId $serviceAadApplication.Id -AppRoles $appRoles # rename the user_impersonation scope if it exists to match the readme steps or add a new scope @@ -228,14 +255,14 @@ Function ConfigureApplications -userConsentDisplayName "Access TodoListService-aspnetcore-webapi" ` -userConsentDescription "Allow the application to access TodoListService-aspnetcore-webapi on your behalf." ` -adminConsentDisplayName "Access TodoListService-aspnetcore-webapi" ` - -adminConsentDescription "Allows the app to have the same access to information in the directory on behalf of the signed-in user." + -adminConsentDescription "Allows the app to have the same access to information in the directory on behalf of an admin." $scopes.Add($scope) - $scope = CreateScope -value ToDoList.Write ` + $scope = CreateScope -value ToDoList.ReadWrite ` -userConsentDisplayName "Access TodoListService-aspnetcore-webapi" ` -userConsentDescription "Allow the application to access TodoListService-aspnetcore-webapi on your behalf." ` -adminConsentDisplayName "Access TodoListService-aspnetcore-webapi" ` - -adminConsentDescription "Allows the app to have the same access to information in the directory on behalf of the signed-in user." + -adminConsentDescription "Allows the app to have the same access to information in the directory on behalf of an admin." $scopes.Add($scope) @@ -250,60 +277,51 @@ Function ConfigureApplications # Create the client AAD application Write-Host "Creating the AAD application (TodoListClient-aspnetcore-webapi)" - # Get a 6 months application key for the client Application - $fromDate = [DateTime]::Now; - $key = CreateAppKey -fromDate $fromDate -durationInMonths 6 - # create the application $clientAadApplication = New-MgApplication -DisplayName "TodoListClient-aspnetcore-webapi" ` -Web ` @{ ` - RedirectUris = "https://localhost:44321/", "https://localhost:44321/signin-oidc"; ` + RedirectUris = "https://localhost:44321/signin-oidc"; ` HomePageUrl = "https://localhost:44321/"; ` LogoutUrl = "https://localhost:44321/signout-oidc"; ` } ` -SignInAudience AzureADMyOrg ` #end of command - #add password 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-aspnetcore-webapi") - - # Generate a certificate - Write-Host "Creating the client application (TodoListClient-aspnetcore-webapi)" + # Generate a certificate + Write-Host "Creating the client application (TodoListClient-aspnetcore-webapi)" - $certificateName = 'TodoListClient-aspnetcore-webapi' + $certificateName = 'TodoListClient-aspnetcore-webapi' - # temporarily disable the option and procees to certificate creation - #$isOpenSSL = Read-Host ' By default certificate is generated using New-SelfSignedCertificate. Do you want to generate cert using OpenSSL(Y/N)?' - $isOpenSSl = 'N' - if($isOpenSSL -eq 'Y') - { - $certificate=openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -keyout "$certificateName.key" -out "$certificateName.cer" -nodes -batch - openssl pkcs12 -export -out "$certificateName.pfx" -inkey $certificateName.key -in "$certificateName.cer" - } - else - { - $certificate=New-SelfSignedCertificate -Subject $certificateName ` - -CertStoreLocation "Cert:\CurrentUser\My" ` - -KeyExportPolicy Exportable ` - -KeySpec Signature - - $thumbprint = $certificate.Thumbprint - $certificatePassword = Read-Host -Prompt "Enter password for your certificate (Please remember the password, you will need it when uploading to KeyVault): " -AsSecureString - Write-Host "Exporting certificate as a PFX file" - Export-PfxCertificate -Cert "Cert:\Currentuser\My\$thumbprint" -FilePath "$pwd\$certificateName.pfx" -ChainOption EndEntityCertOnly -NoProperties -Password $certificatePassword - Write-Host "PFX written to:" - Write-Host "$pwd\$certificateName.pfx" - - # Add a Azure Key Credentials from the certificate for the application - $clientKeyCredentials = Update-MgApplication -ApplicationId $clientAadApplication.Id ` - -KeyCredentials @(@{Type = "AsymmetricX509Cert"; Usage = "Verify"; Key= $certificate.RawData; StartDateTime = $certificate.NotBefore; EndDateTime = $certificate.NotAfter;}) + # temporarily disable the option and procees to certificate creation + #$isOpenSSL = Read-Host ' By default certificate is generated using New-SelfSignedCertificate. Do you want to generate cert using OpenSSL(Y/N)?' + $isOpenSSl = 'N' + if($isOpenSSL -eq 'Y') + { + $certificate=openssl req -x509 -newkey rsa:2048 -days 365 -keyout "$certificateName.key" -out "$certificateName.cer" -subj "/CN=$certificateName.com" -nodes + openssl pkcs12 -export -out "$certificateName.pfx" -inkey $certificateName.key -in "$certificateName.cer" + } + else + { + $certificate=New-SelfSignedCertificate -Subject $certificateName ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -KeyExportPolicy Exportable ` + -KeySpec Signature + + $thumbprint = $certificate.Thumbprint + $certificatePassword = Read-Host -Prompt "Enter password for your certificate (Please remember the password, you will need it when uploading to KeyVault): " -AsSecureString + Write-Host "Exporting certificate as a PFX file" + Export-PfxCertificate -Cert "Cert:\Currentuser\My\$thumbprint" -FilePath "$pwd\$certificateName.pfx" -ChainOption EndEntityCertOnly -NoProperties -Password $certificatePassword + Write-Host "PFX written to:" + Write-Host "$pwd\$certificateName.pfx" + + # Add a Azure Key Credentials from the certificate for the application + $clientKeyCredentials = Update-MgApplication -ApplicationId $clientAadApplication.Id ` + -KeyCredentials @(@{Type = "AsymmetricX509Cert"; Usage = "Verify"; Key= $certificate.RawData; StartDateTime = $certificate.NotBefore; EndDateTime = $certificate.NotAfter;}) - } - + } # create the service principal of the newly created application $currentAppId = $clientAadApplication.AppId @@ -316,6 +334,20 @@ Function ConfigureApplications 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)'" } + + # Add Claims + + $optionalClaims = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaims + $optionalClaims.AccessToken = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + $optionalClaims.IdToken = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + $optionalClaims.Saml2Token = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + + + # Add Optional Claims + + $newClaim = CreateOptionalClaim -name "acct" + $optionalClaims.IdToken += ($newClaim) + Update-MgApplication -ApplicationId $clientAadApplication.Id -OptionalClaims $optionalClaims Write-Host "Done creating the client application (TodoListClient-aspnetcore-webapi)" # URL of the AAD application in the Azure portal @@ -323,12 +355,11 @@ Function ConfigureApplications $clientPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.Id+"/isMSAApp/" Add-Content -Value "client$currentAppIdTodoListClient-aspnetcore-webapi" -Path createdApps.html $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] - # Add Required Resources Access (from 'client' to 'service') Write-Host "Getting access from 'client' to 'service'" $requiredPermissions = GetRequiredPermissions -applicationDisplayName "TodoListService-aspnetcore-webapi" ` - -requiredDelegatedPermissions "ToDoList.Read|ToDoList.Write" ` + -requiredDelegatedPermissions "ToDoList.Read|ToDoList.ReadWrite" ` $requiredResourcesAccess.Add($requiredPermissions) @@ -345,12 +376,18 @@ Function ConfigureApplications # Update config file for 'client' $configFile = $pwd.Path + "\..\Client\appsettings.json" - $dictionary = @{ "Domain" = $tenantName;"TenantId" = $tenantId;"ClientId" = $clientAadApplication.AppId;"KeyVaultCertificateName" = $certificateName;"TodoListScopes" = "api://$($serviceAadApplication.AppId)/ToDoList.Read api://$($serviceAadApplication.AppId)/ToDoList.Write";"TodoListBaseAddress" = $serviceAadApplication.Web.HomePageUrl }; + $dictionary = @{ "Domain" = $tenantName;"TenantId" = $tenantId;"ClientId" = $clientAadApplication.AppId;"KeyVaultCertificateName" = $certificateName;"TodoListScopes" = "api://$($serviceAadApplication.AppId)/ToDoList.Read api://$($serviceAadApplication.AppId)/ToDoList.ReadWrite";"TodoListBaseAddress" = $serviceAadApplication.Web.HomePageUrl }; Write-Host "Updating the sample code ($configFile)" UpdateTextFile -configFilePath $configFile -dictionary $dictionary - if($isOpenSSL -eq 'Y') + 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 service" + Write-Host " - Navigate to $servicePortalUrl" + Write-Host " - Application 'service' publishes application permissions. Do remember to navigate to any client app(s) registration in the app portal and consent for those, if required" -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: " @@ -358,7 +395,7 @@ Function ConfigureApplications Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" } Add-Content -Value "" -Path createdApps.html -} +} # end of ConfigureApplications function # Pre-requisites if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Applications")) { diff --git a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/sample.json b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/sample.json index 54e3abe5..aed41d6f 100644 --- a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/sample.json +++ b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts-withCert/sample.json @@ -19,7 +19,7 @@ "IncludeFilePath": "../ReadmeFiles/ReadmeScenario.md", "Image": "./ReadmeFiles/topology.png", /* put additional notes, will be displayed right after image*/ - "AdditionalNotes": "" + "AdditionalNotesIncludeFilePath": "" }, "ReadmePrerequirements": { @@ -30,7 +30,6 @@ "ReadmeSetup": { "FreeText": "", "IncludeFilePath": "", - "UseNewSetup": "1", /* when set to 0, only legacy setup will be used */ "CertificateOption": "1", /* 1 when a certificate can be used instead of secret*/ "CreateProjectIncludeFilePath": "", "AppRegistrationIncludeFilePath": "", @@ -66,12 +65,12 @@ /* It either can be a text or link to another readme file */ "ReadmeAzureDeploy": { - "IncludeFilePath": "../ReadmeFiles/ReadmeAzureDeploy.md" + "IncludeFilePath": "" }, /* It either can be a text or link to another readme file */ - "ReadmeUsingTheSample": { - "IncludeFilePath": "../ReadmeFiles/ReadmeUsingTheSample.md" + "ReadmeExploreTheSample": { + "IncludeFilePath": "../ReadmeFiles/ReadmeExploreTheSample.md" }, /* It either can be a text or link to another readme file */ @@ -94,25 +93,41 @@ "SampleSubPath": "4-WebApp-Your-API\\4-1-MyOrg", "ProjectDirectory": "\\TodoListService" }, - "AppScopes": [ "ToDoList.Read", "ToDoList.Write" ] - + "OptionalClaims": { + "AccessTokenClaims": [ "idtyp" ] + }, + "Scopes": [ "ToDoList.Read", "ToDoList.ReadWrite" ], + "AppRoles": [ + { + "AllowedMemberTypes": [ "Application" ], + "Name": "ToDoList.Read.All", + "Description": "Allow application to read all ToDo list items" + }, + { + "AllowedMemberTypes": [ "Application" ], + "Name": "ToDoList.ReadWrite.All", + "Description": "Allow application to read and write into ToDo list" + } + ] }, { "Id": "client", "Name": "TodoListClient-aspnetcore-webapi", "Kind": "WebApp", "HomePage": "https://localhost:44321/", - "ReplyUrls": "https://localhost:44321/,https://localhost:44321/signin-oidc", + "ReplyUrls": "https://localhost:44321/signin-oidc", "LogoutUrl": "https://localhost:44321/signout-oidc", "Audience": "AzureADMyOrg", - "PasswordCredentials": "Auto", "RequiredResourcesAccess": [ { "Resource": "service", - "DelegatedPermissions": [ "ToDoList.Read", "ToDoList.Write" ] + "DelegatedPermissions": [ "ToDoList.Read", "ToDoList.ReadWrite" ] } ], - "Certificate": "the certificate will be named by application name", + "OptionalClaims": { + "IdTokenClaims": [ "acct" ] + }, + "Certificate":"the name will be assigned automatically by PowerShell script and it will be equal to the Application name", "ManualSteps": [], "Sample": { "SampleSubPath": "4-WebApp-Your-API\\4-1-MyOrg", diff --git a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/AppCreationScripts.md b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/AppCreationScripts.md index b204c739..6e062df6 100644 --- a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/AppCreationScripts.md +++ b/4-WebApp-your-API/4-1-MyOrg/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,21 +65,21 @@ 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 (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 Graph 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 Microsoft.Graph.Applications ``` or if you cannot be administrator on your machine, run: - + ```PowerShell Install-Module Microsoft.Graph.Applications -Scope CurrentUser ``` @@ -106,44 +96,29 @@ The scripts install the required PowerShell module (Microsoft.Graph.Applications 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,32 +130,19 @@ $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: - AzureCloud - AzureChinaCloud - AzureUSGovernment -- AzureGermanyCloud Example: ```PowerShell - . .\Cleanup.ps1 -AzureEnvironmentName "AzureGermanyCloud" - . .\Configure.ps1 -AzureEnvironmentName "AzureGermanyCloud" + . .\Cleanup.ps1 -AzureEnvironmentName "AzureUSGovernment" + . .\Configure.ps1 -AzureEnvironmentName "AzureUSGovernment" ``` diff --git a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/Cleanup.ps1 b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/Cleanup.ps1 index 870cfe3c..29be7ccf 100644 --- a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/Cleanup.ps1 +++ b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/Cleanup.ps1 @@ -1,3 +1,4 @@ + [CmdletBinding()] param( [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')] @@ -41,7 +42,7 @@ Function Cleanup } catch { - Write-Host "Unable to remove the application 'TodoListService-aspnetcore-webapi' . Try deleting manually." -ForegroundColor White -BackgroundColor Red + Write-Host "Unable to remove the application 'TodoListService-aspnetcore-webapi' . Try deleting manually." -ForegroundColor White -BackgroundColor Red } Write-Host "Making sure there are no more (TodoListService-aspnetcore-webapi) applications found, will remove if needed..." @@ -65,7 +66,7 @@ Function Cleanup } catch { - Write-Host "Unable to remove ServicePrincipal 'TodoListService-aspnetcore-webapi' . Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red + Write-Host "Unable to remove ServicePrincipal 'TodoListService-aspnetcore-webapi' . Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red } Write-Host "Removing 'client' (TodoListClient-aspnetcore-webapi) if needed" try @@ -74,7 +75,7 @@ Function Cleanup } catch { - Write-Host "Unable to remove the application 'TodoListClient-aspnetcore-webapi' . Try deleting manually." -ForegroundColor White -BackgroundColor Red + Write-Host "Unable to remove the application 'TodoListClient-aspnetcore-webapi' . Try deleting manually." -ForegroundColor White -BackgroundColor Red } Write-Host "Making sure there are no more (TodoListClient-aspnetcore-webapi) applications found, will remove if needed..." @@ -98,7 +99,7 @@ Function Cleanup } catch { - Write-Host "Unable to remove ServicePrincipal 'TodoListClient-aspnetcore-webapi' . Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red + Write-Host "Unable to remove ServicePrincipal 'TodoListClient-aspnetcore-webapi' . Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red } } @@ -113,4 +114,3 @@ Cleanup -tenantId $tenantId -environment $azureEnvironmentName Write-Host "Disconnecting from tenant" Disconnect-MgGraph - diff --git a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/Configure.ps1 b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/Configure.ps1 index c0627194..0ceffcd3 100644 --- a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/Configure.ps1 +++ b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/Configure.ps1 @@ -1,4 +1,4 @@ - + [CmdletBinding()] param( [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')] @@ -154,9 +154,24 @@ Function CreateAppRole([string] $types, [string] $name, [string] $description) $appRole.Value = $name; return $appRole } +Function CreateOptionalClaim([string] $name) +{ + <#.Description + This function creates a new Azure AD optional claims with default and provided values + #> + + $appClaim = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim + $appClaim.AdditionalProperties = New-Object System.Collections.Generic.List[string] + $appClaim.Source = $null + $appClaim.Essential = $false + $appClaim.Name = $name + return $appClaim +} Function ConfigureApplications { + $isOpenSSl = 'N' #temporary disable open certificate creation + <#.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) @@ -188,6 +203,10 @@ Function ConfigureApplications @{ ` HomePageUrl = "https://localhost:44351"; ` } ` + -Api ` + @{ ` + RequestedAccessTokenVersion = 2 ` + } ` -SignInAudience AzureADMyOrg ` #end of command $serviceIdentifierUri = 'api://'+$serviceAadApplication.AppId @@ -204,6 +223,28 @@ Function ConfigureApplications New-MgApplicationOwnerByRef -ApplicationId $serviceAadApplication.Id -BodyParameter = @{"@odata.id" = "htps://graph.microsoft.com/v1.0/directoryObjects/$user.ObjectId"} Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($serviceServicePrincipal.DisplayName)'" } + + # Add Claims + + $optionalClaims = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaims + $optionalClaims.AccessToken = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + $optionalClaims.IdToken = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + $optionalClaims.Saml2Token = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + + + # Add Optional Claims + + $newClaim = CreateOptionalClaim -name "idtyp" + $optionalClaims.AccessToken += ($newClaim) + Update-MgApplication -ApplicationId $serviceAadApplication.Id -OptionalClaims $optionalClaims + + # Add application Roles + $appRoles = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphAppRole] + $newRole = CreateAppRole -types "Application" -name "ToDoList.Read.All" -description "Allow application to read all ToDo list items" + $appRoles.Add($newRole) + $newRole = CreateAppRole -types "Application" -name "ToDoList.ReadWrite.All" -description "Allow application to read and write into ToDo list" + $appRoles.Add($newRole) + Update-MgApplication -ApplicationId $serviceAadApplication.Id -AppRoles $appRoles # rename the user_impersonation scope if it exists to match the readme steps or add a new scope @@ -228,14 +269,14 @@ Function ConfigureApplications -userConsentDisplayName "Access TodoListService-aspnetcore-webapi" ` -userConsentDescription "Allow the application to access TodoListService-aspnetcore-webapi on your behalf." ` -adminConsentDisplayName "Access TodoListService-aspnetcore-webapi" ` - -adminConsentDescription "Allows the app to have the same access to information in the directory on behalf of the signed-in user." + -adminConsentDescription "Allows the app to have the same access to information in the directory on behalf of an admin." $scopes.Add($scope) - $scope = CreateScope -value ToDoList.Write ` + $scope = CreateScope -value ToDoList.ReadWrite ` -userConsentDisplayName "Access TodoListService-aspnetcore-webapi" ` -userConsentDescription "Allow the application to access TodoListService-aspnetcore-webapi on your behalf." ` -adminConsentDisplayName "Access TodoListService-aspnetcore-webapi" ` - -adminConsentDescription "Allows the app to have the same access to information in the directory on behalf of the signed-in user." + -adminConsentDescription "Allows the app to have the same access to information in the directory on behalf of an admin." $scopes.Add($scope) @@ -259,7 +300,7 @@ Function ConfigureApplications $clientAadApplication = New-MgApplication -DisplayName "TodoListClient-aspnetcore-webapi" ` -Web ` @{ ` - RedirectUris = "https://localhost:44321/", "https://localhost:44321/signin-oidc"; ` + RedirectUris = "https://localhost:44321/signin-oidc"; ` HomePageUrl = "https://localhost:44321/"; ` LogoutUrl = "https://localhost:44321/signout-oidc"; ` } ` @@ -268,6 +309,7 @@ Function ConfigureApplications #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-aspnetcore-webapi") @@ -282,6 +324,20 @@ Function ConfigureApplications 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)'" } + + # Add Claims + + $optionalClaims = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaims + $optionalClaims.AccessToken = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + $optionalClaims.IdToken = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + $optionalClaims.Saml2Token = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + + + # Add Optional Claims + + $newClaim = CreateOptionalClaim -name "acct" + $optionalClaims.IdToken += ($newClaim) + Update-MgApplication -ApplicationId $clientAadApplication.Id -OptionalClaims $optionalClaims Write-Host "Done creating the client application (TodoListClient-aspnetcore-webapi)" # URL of the AAD application in the Azure portal @@ -289,12 +345,11 @@ Function ConfigureApplications $clientPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.Id+"/isMSAApp/" Add-Content -Value "client$currentAppIdTodoListClient-aspnetcore-webapi" -Path createdApps.html $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] - # Add Required Resources Access (from 'client' to 'service') Write-Host "Getting access from 'client' to 'service'" $requiredPermissions = GetRequiredPermissions -applicationDisplayName "TodoListService-aspnetcore-webapi" ` - -requiredDelegatedPermissions "ToDoList.Read|ToDoList.Write" ` + -requiredDelegatedPermissions "ToDoList.Read|ToDoList.ReadWrite" ` $requiredResourcesAccess.Add($requiredPermissions) @@ -311,12 +366,18 @@ Function ConfigureApplications # Update config file for 'client' $configFile = $pwd.Path + "\..\Client\appsettings.json" - $dictionary = @{ "Domain" = $tenantName;"TenantId" = $tenantId;"ClientId" = $clientAadApplication.AppId;"ClientSecret" = $pwdCredential.SecretText;"TodoListScopes" = "api://$($serviceAadApplication.AppId)/ToDoList.Read api://$($serviceAadApplication.AppId)/ToDoList.Write";"TodoListBaseAddress" = $serviceAadApplication.Web.HomePageUrl }; + $dictionary = @{ "Domain" = $tenantName;"TenantId" = $tenantId;"ClientId" = $clientAadApplication.AppId;"ClientSecret" = $pwdCredential.SecretText;"TodoListScopes" = "api://$($serviceAadApplication.AppId)/ToDoList.Read api://$($serviceAadApplication.AppId)/ToDoList.ReadWrite";"TodoListBaseAddress" = $serviceAadApplication.Web.HomePageUrl }; Write-Host "Updating the sample code ($configFile)" UpdateTextFile -configFilePath $configFile -dictionary $dictionary - if($isOpenSSL -eq 'Y') + 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 service" + Write-Host " - Navigate to $servicePortalUrl" + Write-Host " - Application 'service' publishes application permissions. Do remember to navigate to any client app(s) registration in the app portal and consent for those, if required" -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: " @@ -324,7 +385,7 @@ Function ConfigureApplications Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" } Add-Content -Value "" -Path createdApps.html -} +} # end of ConfigureApplications function # Pre-requisites if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Applications")) { diff --git a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/sample.json b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/sample.json index cfbe9304..ab55bbf2 100644 --- a/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/sample.json +++ b/4-WebApp-your-API/4-1-MyOrg/AppCreationScripts/sample.json @@ -7,19 +7,20 @@ "Service": "ASP.NET Core Web API", "RepositoryUrl": "active-directory-aspnetcore-webapp-openidconnect-v2", "Endpoint": "AAD v2.0", - "Description": "This sample demonstrates an ASP.NET Core client Web App calling an ASP.NET Core Web API that is secured using Azure AD.", + "Description": "This sample demonstrates an ASP.NET Core Web App signing-in a user and calling an ASP.NET Core Web API that is secured with Azure AD.", "Languages": [ "csharp" ], - "Products": [ "aspnet-core", "azure-active-directory" ] + "Products": [ "aspnet-core", "azure-active-directory" ], + "Library":["Microsoft.Identity.Web"] }, /* This section describes different regions of readme file */ "ReadmeScenario": { - "IncludeFilePath": "../ReadmeFiles/ReadmeScenario.md", + "IncludeFilePath": "", "Image": "./ReadmeFiles/topology.png", /* put additional notes, will be displayed right after image*/ - "AdditionalNotes": "" + "AdditionalNotesIncludeFilePath": "" }, "ReadmePrerequirements": { @@ -30,7 +31,6 @@ "ReadmeSetup": { "FreeText": "", "IncludeFilePath": "", - "UseNewSetup": "1", /* when set to 0, only legacy setup will be used */ "CertificateOption": "1", /* 1 when a certificate can be used instead of secret*/ "CreateProjectIncludeFilePath": "", "AppRegistrationIncludeFilePath": "", @@ -66,12 +66,12 @@ /* It either can be a text or link to another readme file */ "ReadmeAzureDeploy": { - "IncludeFilePath": "../ReadmeFiles/ReadmeAzureDeploy.md" + "IncludeFilePath": "" }, /* It either can be a text or link to another readme file */ - "ReadmeUsingTheSample": { - "IncludeFilePath": "../ReadmeFiles/ReadmeUsingTheSample.md" + "ReadmeExploreTheSample": { + "IncludeFilePath": "../ReadmeFiles/ReadmeExploreTheSample.md" }, /* It either can be a text or link to another readme file */ @@ -94,23 +94,41 @@ "SampleSubPath": "4-WebApp-Your-API\\4-1-MyOrg", "ProjectDirectory": "\\TodoListService" }, - "Scopes": [ "ToDoList.Read", "ToDoList.Write" ] + "OptionalClaims": { + "AccessTokenClaims": [ "idtyp" ] + }, + "Scopes": [ "ToDoList.Read", "ToDoList.ReadWrite" ], + "AppRoles": [ + { + "AllowedMemberTypes": [ "Application" ], + "Name": "ToDoList.Read.All", + "Description": "Allow application to read all ToDo list items" + }, + { + "AllowedMemberTypes": [ "Application" ], + "Name": "ToDoList.ReadWrite.All", + "Description": "Allow application to read and write into ToDo list" + } + ] }, { "Id": "client", "Name": "TodoListClient-aspnetcore-webapi", "Kind": "WebApp", "HomePage": "https://localhost:44321/", - "ReplyUrls": "https://localhost:44321/,https://localhost:44321/signin-oidc", + "ReplyUrls": "https://localhost:44321/signin-oidc", "LogoutUrl": "https://localhost:44321/signout-oidc", "Audience": "AzureADMyOrg", "PasswordCredentials": "Auto", "RequiredResourcesAccess": [ { "Resource": "service", - "DelegatedPermissions": [ "ToDoList.Read", "ToDoList.Write" ] + "DelegatedPermissions": [ "ToDoList.Read", "ToDoList.ReadWrite" ] } ], + "OptionalClaims": { + "IdTokenClaims": [ "acct" ] + }, "ManualSteps": [], "Sample": { "SampleSubPath": "4-WebApp-Your-API\\4-1-MyOrg", @@ -123,7 +141,7 @@ 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 + with respect to the root of the sample, and the mapping (which string in the config file is mapped to which value */ "CodeConfiguration": [ { diff --git a/4-WebApp-your-API/4-1-MyOrg/Client/Controllers/TodoListController.cs b/4-WebApp-your-API/4-1-MyOrg/Client/Controllers/TodoListController.cs index 8b82f94e..e0b2e445 100644 --- a/4-WebApp-your-API/4-1-MyOrg/Client/Controllers/TodoListController.cs +++ b/4-WebApp-your-API/4-1-MyOrg/Client/Controllers/TodoListController.cs @@ -2,8 +2,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Identity.Web; using System.Threading.Tasks; -using TodoListClient.Services; using TodoListService.Models; +using TodoListClient.Services; namespace TodoListClient.Controllers { @@ -16,11 +16,10 @@ public TodoListController(ITodoListService todoListService) _todoListService = todoListService; } - // GET: TodoList - [AuthorizeForScopes(ScopeKeySection = "TodoList:TodoListScopes")] public async Task Index() { - return View(await _todoListService.GetAsync()); + var result = await _todoListService.GetAsync(); + return View(result); } // GET: TodoList/Details/5 @@ -68,7 +67,7 @@ public async Task Edit(int id, [Bind("Id,Title,Owner")] Todo todo) } // GET: TodoList/Delete/5 - public async Task Delete(int id) + public async Task DeleteItem(int id) { Todo todo = await this._todoListService.GetAsync(id); @@ -83,7 +82,7 @@ public async Task Delete(int id) // POST: TodoList/Delete/5 [HttpPost] [ValidateAntiForgeryToken] - public async Task Delete(int id, [Bind("Id,Title,Owner")] Todo todo) + public async Task DeleteItem(int id, [Bind("Id,Title,Owner")] Todo todo) { await _todoListService.DeleteAsync(id); return RedirectToAction("Index"); diff --git a/4-WebApp-your-API/4-1-MyOrg/Client/Services/TodoListService.cs b/4-WebApp-your-API/4-1-MyOrg/Client/Services/TodoListService.cs index 1038d4e9..341cb7f7 100644 --- a/4-WebApp-your-API/4-1-MyOrg/Client/Services/TodoListService.cs +++ b/4-WebApp-your-API/4-1-MyOrg/Client/Services/TodoListService.cs @@ -58,7 +58,7 @@ public async Task AddAsync(Todo todo) return todo; } - throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}."); + return null; } public async Task DeleteAsync(int id) @@ -71,8 +71,6 @@ public async Task DeleteAsync(int id) { return; } - - throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}."); } public async Task EditAsync(Todo todo) @@ -91,7 +89,7 @@ public async Task EditAsync(Todo todo) return todo; } - throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}."); + return null; } public async Task> GetAsync() @@ -106,7 +104,7 @@ public async Task> GetAsync() return todolist; } - throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}."); + return null; } public async Task GetAsync(int id) @@ -121,9 +119,10 @@ public async Task GetAsync(int id) return todo; } - throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}."); + return null; } + //Acquire a token and add it as Bearer to Authorization header private async Task PrepareAuthenticatedClient() { //You would specify the scopes (delegated permissions) here for which you desire an Access token of this API from Azure AD. diff --git a/4-WebApp-your-API/4-1-MyOrg/Client/Startup.cs b/4-WebApp-your-API/4-1-MyOrg/Client/Startup.cs index 2e192f97..1ed11661 100644 --- a/4-WebApp-your-API/4-1-MyOrg/Client/Startup.cs +++ b/4-WebApp-your-API/4-1-MyOrg/Client/Startup.cs @@ -12,6 +12,10 @@ using TodoListClient.Services; using Microsoft.Extensions.Hosting; using Microsoft.Identity.Web.UI; +using Microsoft.IdentityModel.Logging; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using System.Linq; +using System.Diagnostics; namespace WebApp_OpenIDConnect_DotNet { @@ -40,15 +44,40 @@ public void ConfigureServices(IServiceCollection services) services.AddOptions(); + // The following lines of code adds the ability to authenticate users of this web app. + // Refer to https://github.com/AzureAD/microsoft-identity-web/wiki/web-apps to learn more services.AddMicrosoftIdentityWebAppAuthentication(Configuration) .EnableTokenAcquisitionToCallDownstreamApi( Configuration.GetSection("TodoList:TodoListScopes").Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries) ) .AddInMemoryTokenCaches(); + // This is how we configure certificates in startup - see README-use-certificate.md for more details on how to use this section + // Also read more at - https://github.com/AzureAD/microsoft-identity-web/wiki/Certificates + //services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + // .AddMicrosoftIdentityWebApp(microsoftIdentityOptions => + // { + // Configuration.Bind("AzureAd", microsoftIdentityOptions); + // microsoftIdentityOptions.ClientCertificates = new CertificateDescription[] { + // CertificateDescription.FromKeyVault("[Enter URL for you Key Vault]", + // "TodoListClient-aspnetcore-webapi")}; + // }) + // .EnableTokenAcquisitionToCallDownstreamApi(confidentialClientApplicationOptions => + // { + // Configuration.GetSection("TodoList:TodoListScopes").Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries); + // Configuration.Bind("AzureAd", confidentialClientApplicationOptions); + // }) + // .AddInMemoryTokenCaches(); + + // Add APIs services.AddTodoListService(Configuration); + // The following flag can be used to get more descriptive errors in development environments + // Enable diagnostic logging to help with troubleshooting. For more details, see https://aka.ms/IdentityModel/PII. + // You might not want to keep this following flag on for production + IdentityModelEventSource.ShowPII = true; + services.AddControllersWithViews(options => { var policy = new AuthorizationPolicyBuilder() @@ -80,6 +109,23 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); app.UseAuthentication(); + + app.Use(async (context, next) => { + if (context.User != null && context.User.Identity.IsAuthenticated) + { + // you can conduct any conditional processing for guest/homes user by inspecting the value of the 'acct' claim + // Read more about the 'acct' claim at aka.ms/optionalclaims + if (context.User.Claims.Any(x => x.Type == "acct")) + { + string claimvalue = context.User.Claims.FirstOrDefault(x => x.Type == "acct").Value; + string userType = claimvalue == "0" ? "Member" : "Guest"; + Debug.WriteLine($"The type of the user account from this Azure AD tenant is-{userType}"); + } + } + await next(); + }); + + app.UseAuthorization(); app.UseEndpoints(endpoints => diff --git a/4-WebApp-your-API/4-1-MyOrg/Client/TodoListClient.csproj b/4-WebApp-your-API/4-1-MyOrg/Client/TodoListClient.csproj index 22e11586..214c2faa 100644 --- a/4-WebApp-your-API/4-1-MyOrg/Client/TodoListClient.csproj +++ b/4-WebApp-your-API/4-1-MyOrg/Client/TodoListClient.csproj @@ -17,14 +17,18 @@ - - - - + + + + + + false + + diff --git a/4-WebApp-your-API/4-1-MyOrg/Client/Views/TodoList/Delete.cshtml b/4-WebApp-your-API/4-1-MyOrg/Client/Views/TodoList/DeleteItem.cshtml similarity index 95% rename from 4-WebApp-your-API/4-1-MyOrg/Client/Views/TodoList/Delete.cshtml rename to 4-WebApp-your-API/4-1-MyOrg/Client/Views/TodoList/DeleteItem.cshtml index 59cd8eec..079cf205 100644 --- a/4-WebApp-your-API/4-1-MyOrg/Client/Views/TodoList/Delete.cshtml +++ b/4-WebApp-your-API/4-1-MyOrg/Client/Views/TodoList/DeleteItem.cshtml @@ -25,7 +25,7 @@ -
+ | Back to List diff --git a/4-WebApp-your-API/4-1-MyOrg/Client/Views/TodoList/Index.cshtml b/4-WebApp-your-API/4-1-MyOrg/Client/Views/TodoList/Index.cshtml index 43edce73..e79b3a7d 100644 --- a/4-WebApp-your-API/4-1-MyOrg/Client/Views/TodoList/Index.cshtml +++ b/4-WebApp-your-API/4-1-MyOrg/Client/Views/TodoList/Index.cshtml @@ -41,7 +41,7 @@ @Html.ActionLink("Edit", "Edit", new { id = item.Id }) | @Html.ActionLink("Details", "Details", new { id = item.Id }) | - @Html.ActionLink("Delete", "Delete", new { id = item.Id }) + @Html.ActionLink("Delete", "DeleteItem", new { id = item.Id }) } diff --git a/4-WebApp-your-API/4-1-MyOrg/Client/appsettings.json b/4-WebApp-your-API/4-1-MyOrg/Client/appsettings.json index 50622592..69cb8974 100644 --- a/4-WebApp-your-API/4-1-MyOrg/Client/appsettings.json +++ b/4-WebApp-your-API/4-1-MyOrg/Client/appsettings.json @@ -5,26 +5,43 @@ "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", "CallbackPath": "/signin-oidc", - "SignedOutCallbackPath ": "/signout-callback-oidc", + "SignedOutCallbackPath ": "/signout-oidc", "ClientCapabilities": [ "cp1" ], // To call an API + + //comment the next line when working with certificates, the setting is only used when working with Client Secret in App Registration + //Important - this approach is not secure to use on production. Use a certificate with KeyVault on production "ClientSecret": "[Copy the client secret added to the app from the Azure portal]" + + ////************** Uncomment this part when working with KeyVault certificate //"ClientCertificates": [ // { // "SourceType": "KeyVault", // "KeyVaultUrl": "[Enter URL for you Key Vault]", - // "KeyVaultCertificateName": "[Enter name of the certificate]" + // "KeyVaultCertificateName": "" + // } + //] + ////********************************************************************************** + + //// ************* Uncomment this part when working with local certificate + //"ClientCertificates": [ + // { + // "SourceType": "StoreWithDistinguishedName", + // "CertificateStorePath": "CurrentUser/My", + // "CertificateDistinguishedName": "CN=TodoListClient-aspnetcore-webapi" // } //] + ////********************************************************************************** + }, "TodoList": { - /* - TodoListScope is the scope of the Web API you want to call. This is usually in the form of: "api://fc3ef71c-43ab-497d-89f0-332787e09c7c/ToDoList.Read", - - a list of scopes for a V2 application (for instance "api://b3682cc7-8b30-4bd2-aaba-080c6bf0fd31/ToDoList.Read api://b3682cc7-8b30-4bd2-aaba-080c6bf0fd31/ToDoList.Write") - - a scope corresponding to a V1 application (for instance /user_impersonation, where is the - clientId of a V1 application, created in the https://portal.azure.com portal. - */ + + // TodoListScope is the scope of the Web API you want to call. This is usually in the form of: "api://fc3ef71c-43ab-497d-89f0-332787e09c7c/ToDoList.Read", + // - a list of scopes for a V2 application (for instance "api://b3682cc7-8b30-4bd2-aaba-080c6bf0fd31/ToDoList.Read api://b3682cc7-8b30-4bd2-aaba-080c6bf0fd31/ToDoList.ReadWrite") + // - a scope corresponding to a V1 application (for instance /user_impersonation, where is the + // clientId of a V1 application, created in the https://portal.azure.com portal. + "TodoListScopes": "list of required scopes separated by space \"api://[Enter_client_ID_service]/ToDoList.Read api://[Enter_client_ID_service]/ToDoList.Write\"", "TodoListBaseAddress": "https://localhost:44351" }, diff --git a/4-WebApp-your-API/4-1-MyOrg/README-use-certificate.md b/4-WebApp-your-API/4-1-MyOrg/README-use-certificate.md index 89304a60..7f33fcdb 100644 --- a/4-WebApp-your-API/4-1-MyOrg/README-use-certificate.md +++ b/4-WebApp-your-API/4-1-MyOrg/README-use-certificate.md @@ -1,115 +1,113 @@ -# Web app using client certificates with Microsoft.Identity.Web +# How to use certificates instead of secrets in your client applications -There are different approaches for [using certificates with Microsoft.Identity.Web](https://github.com/AzureAD/microsoft-identity-web/wiki/Certificates). In this Readme, we will explore use of certificate from a path on the disk. +We recommend you familiarize yourself with [Using certificates with Microsoft\.Identity\.Web](https://github.com/AzureAD/microsoft-identity-web/wiki/Certificates#getting-certificates-from-key-vault) as it provides various ways for a developer to use a certificate instead of a client secret to authenticate their apps with Azure AD. +> Note: Please carefully go through [Getting certificates from Key Vault](https://github.com/AzureAD/microsoft-identity-web/wiki/Certificates#getting-certificates-from-key-vault) when deploying your app to production. -## Use Self Signed Certificate from a path +## Using a Client certificate with KeyVault -To use certificates instead of an application secret you will need to make some changes to what you have done so far: +This sample was configured to use a client secret, but have an option to use a certificate instead. -- generate a certificate and export it, if you don't have one already -- register the certificate with your application in the Azure AD portal -- Update appsettings.json +### To be able to use a certificate, please make the following changes: -### (Optional) use the automation script +1. Open Client/appsettings.json file +1. **Comment out** the next line: -If you want to use the automation script: - -1. On Windows run PowerShell and navigate to the root of the cloned directory. -1. Run the script to create your Azure AD application and configure the code of the sample application accordingly. - - ```PowerShell - .\AppCreationScripts-WtihCert\Configure.ps1 - ``` +```json +"ClientSecret": "[Copy the client secret added to the app from the Azure portal]" +``` - > Other ways of running the scripts are described in [App Creation Scripts](./AppCreationScripts-WithCert/AppCreationScripts.md) +1. **Un-comment** the following lines: -### Generate a Self Signed certificate +```json +"ClientCertificates": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "[Enter URL for you KeyVault]", + "KeyVaultCertificateName": "TodoListClient-aspnetcore-webapi" + } +] +``` -Since we'd be using certificate instead of a client secret in this sample, we'd first create a new self signed certificate. If you have an actual valid certificate available, then skip the following step. +1. While inside the sample folder, open a Powershell terminal -
-Click here to use Powershell +1. Set next execution policy -To generate a new self-signed certificate, we will use the [New-SelfSignedCertificate](https://docs.microsoft.com/powershell/module/pkiclient/new-selfsignedcertificate) Powershell command. +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force +``` -1. Open PowerShell and run `New-SelfSignedCertificate` command with the following parameters to create a new self-signed certificate that will be stored in the **current user** certificate store on your computer: +1. Run the Cleanup.ps1 script to delete any existing old App Registration for the sample -```PowerShell -$cert=New-SelfSignedCertificate -Subject "/CN=webapp" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature +```powershell +AppCreationScripts-withCert/Cleanup.ps1 ``` -1. Export this certificate using the "Manage User Certificate" MMC snap-in accessible from the Windows Control Panel. You can also add other options to generate the certificate in a different store such as the **Computer** or **service** store (See [How to: View Certificates with the MMC Snap-in](https://docs.microsoft.com/dotnet/framework/wcf/feature-details/how-to-view-certificates-with-the-mmc-snap-in)) for more details. +1. Run the AppCreationScripts-withCert/Configure.ps1 script to re-create the App Registration. The script will also create a [application name].pfx file that will be **manually** uploaded into Key Vault. When asked about a password, remember it - you will need the password when uploading the certificate. -Export one with private key as webapp.pfx and another as webapp.cer without private key. -
+```powershell +AppCreationScripts-withCert/Configure.ps1 +``` -
-Click here to use OpenSSL +1. To use KeyVault, sign in to the [Azure portal](https://portal.azure.com) and [create an Azure Key Vault](https://docs.microsoft.com/azure/key-vault/general/quick-create-portal) +1. Inside Client/appsettings.json file - update "KeyVaultUrl" key to have URL of your Key Vault, like https://[your Key Vault name here].vault.azure.net +1. [Upload](https://docs.microsoft.com/azure/key-vault/certificates/tutorial-import-certificate#import-a-certificate-to-key-vault) the generated AppCreationScripts-withCert\.PFX file into the Key Vault +1. Run the sample as indicated in [README.md](README.md) +1. Use the account you used to upload the certificate to key vault to sign-into the web app. +1. In production environments, you'd give access to your deployed web app or Virtual machine to read this certificate's Key Vault entry. -Type the following in a terminal. +## Using a local Client certificate -```PowerShell -openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -keyout webapp.key -out webapp.cer -nodes -batch +1. Open Client/appsettings.json file +2. **Comment out** the next line: -Generating a RSA private key -...........................................................................................................................................................................................................................................................++++ -......................................................................................................++++ -writing new private key to 'webapp.key' ------ +```json +"ClientSecret": "[Copy the client secret added to the app from the Azure portal]" ``` -Generate the webapp.pfx certificate with below command: +1. **Un-comment** the following lines: -```console -openssl pkcs12 -export -out webapp.pfx -inkey webapp.key -in webapp.cer +```json +"ClientCertificates": [ + { + "SourceType": "StoreWithDistinguishedName", + "CertificateStorePath": "CurrentUser/My", + "CertificateDistinguishedName": "CN=" + } +] ``` -Enter an export password when prompted and make a note of it. +1. While inside the sample folder, open a Powershell terminal -The following files should be generated: `webapp.key`, `webapp.cer` and `webapp.pfx`. -
+1. Set next execution policy -### Add the certificate for the TodoListClient-aspnetcore-webapi application in the application's registration page - -1. Navigate back to the [Azure portal](https://portal.azure.com). -In the left-hand navigation pane, select the **Azure Active Directory** service, and then select **App registrations**. -1. In the resultant screen, select the `TodoListClient-aspnetcore-webapi` application. -1. In the **Certificates & secrets** tab, go to **Certificates** section: -1. Select **Upload certificate** and, in select the browse button on the right to select the certificate you just exported, webapp.cer (or your existing certificate). -1. Select **Add**. - -### Configure the Visual Studio project +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force +``` -To change the visual studio project to enable certificates you need to: +1. Run the *Cleanup.ps1* script to delete any existing old App Registration for the sample -1. Open the `Client\appsettings.json` file -1. Find the app key `ClientCertificates` and add the keys as displayed below: +```powershell +AppCreationScripts-withCert/Cleanup.ps1 -TenantId "[Optional] - your tenant id" -AzureEnvironmentName "[Optional] - Azure environment, defaults to 'Global'" +``` - ```json - "ClientCertificates": [ - { - "SourceType": "", - "CertificateDiskPath": "", - "CertificatePassword": "" - } - ] - ``` +1. Run the AppCreationScripts-withCert/Configure.ps1 script to re-create the App Registration. The script will also create a [application name].pfx file that will be **manually** uploaded into Key Vault. When asked about a password, remember it - you will need the password when uploading the certificate. - Update values of the keys: +```powershell +AppCreationScripts-withCert/Configure.ps1 -TenantId "[Optional] - your tenant id" -AzureEnvironmentName "[Optional] - Azure environment, defaults to 'Global'" +``` - `SourceType` to `Path`. +1. Run the sample as indicated in [README.md](README.md) - `CertificateDiskPath` to the path where certificate exported with private key (webapp.pfx) is stored. For example, `C:\\active-directory-aspnetcore-webapp-openidconnect-v2\\4-WebApp-your-API\\4-1-MyOrg\\AppCreationScripts-withCert\\webapp.pfx` +## More information - `CertificatePassword` add the password used while exporting the certificate. -1. If you had set `ClientSecret` previously, set its value to empty string, `""`. +### Using Azure KeyVault -## (Alternate) Key Vault and Managed Service Identity (MSI) +Cloud applications and services use cryptographic keys and secrets to help keep information secure. [Azure KeyVault](https://azure.microsoft.com/services/key-vault/) safeguards these keys and secrets. When you use Key Vault, you can encrypt authentication keys, storage account keys, data encryption keys, .pfx files, and passwords by using keys that are protected by hardware security modules (HSMs). -[Azure Key Vault certificates](https://docs.microsoft.com/azure/key-vault/certificates/) enables Microsoft Azure applications and users to store and use certificates. Microsoft.Identity.Web leverages Managed Service Identity to retrieve these certificates. For details see [https://aka.ms/ms-id-web-certificates](https://aka.ms/ms-id-web-certificates). +### About Managed Identities for Azure Resources -To generate the certificate from KeyVault see [Get started with Key Vault certificates](https://docs.microsoft.com/azure/key-vault/certificates/certificate-scenarios) documentation. +[Azure KeyVault](https://azure.microsoft.com/services/key-vault/#product-overview) -## Run the sample +[Managed Identities for Azure Resources](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/) -Follow [Step 4: Run the sample](Readme.md#step-4-run-the-sample) in [Readme.md](Readme.md). +[Managed Identities for Azure App Services](https://docs.microsoft.com/azure/app-service/overview-managed-identity?tabs=dotnet) diff --git a/4-WebApp-your-API/4-1-MyOrg/README-use-keyvault-certificate.md b/4-WebApp-your-API/4-1-MyOrg/README-use-keyvault-certificate.md deleted file mode 100644 index 91e2cfe3..00000000 --- a/4-WebApp-your-API/4-1-MyOrg/README-use-keyvault-certificate.md +++ /dev/null @@ -1,65 +0,0 @@ -# How to use certificates instead of secrets in your client applications - -[Microsoft.Identity.Web](https://github.com/AzureAD/microsoft-identity-web/wiki/Certificates) provides various ways for a developer to use a certificate instead of a client secret to authenticate their apps with Azure AD. - -## Using Client certificate with Key Vault - -This sample was configured to use a client secret, but have an option to use a certificate instead. - -### To be able to use a certificate, please make the following changes: - -1. Open Client/appsettings.json file -2. **Comment out** the next line: - -```json -"ClientSecret": "[Copy the client secret added to the app from the Azure portal]" -``` - -3. **Un-comment** the following lines: - -```json -"ClientCertificates": [ - { - "SourceType": "KeyVault", - "KeyVaultUrl": "[Enter URL for you Key Vault]", - "KeyVaultCertificateName": "[Enter name of the certificate]" - } -] -``` - -4. Set execution policy - -```powershell -Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force -``` - -5. Run the cleanup script to delete old App Registration for the sample - -```powershell -AppCreationScripts-withCert/Cleanup.ps1 -``` - -6. Run the configuration script to re-create the App Registration. The script will also create a [application name].pfx file that will be **manually** uploaded into Key Vault. When asked about a password, remember it - you will need the password when uploading the certificate. - -```powershell -AppCreationScripts-withCert/Configuration.ps1 -``` - -1. To use Key Vault, sign in to the [Azure portal](https://portal.azure.com) and [create an Azure Key Vault](https://docs.microsoft.com/azure/key-vault/general/quick-create-portal) -2. Inside Client/appsettings.json file - update "KeyVaultUrl" key to have URL of your Key Vault, like https://[your Key Vault name here].vault.azure.net -3. [Upload](https://docs.microsoft.com/azure/key-vault/certificates/tutorial-import-certificate#import-a-certificate-to-key-vault) the generated AppCreationScripts-withCert\.PFX file into the Key Vault -4. Run the sample as indicated in [README.md](README.md) - -## More information - -### About Azure Key Vault - -Cloud applications and services use cryptographic keys and secrets to help keep information secure. [Azure Key Vault](https://azure.microsoft.com/services/key-vault/) safeguards these keys and secrets. When you use Key Vault, you can encrypt authentication keys, storage account keys, data encryption keys, .pfx files, and passwords by using keys that are protected by hardware security modules (HSMs). - -### About Managed Identities for Azure Resources - -[Azure Key Vault](https://azure.microsoft.com/services/key-vault/#product-overview) - -[Managed Identities for Azure Resources](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/) - -[Managed Identities for Azure App Services](https://docs.microsoft.com/azure/app-service/overview-managed-identity?tabs=dotnet) diff --git a/4-WebApp-your-API/4-1-MyOrg/README.md b/4-WebApp-your-API/4-1-MyOrg/README.md index 7bf2b707..7c32acd0 100644 --- a/4-WebApp-your-API/4-1-MyOrg/README.md +++ b/4-WebApp-your-API/4-1-MyOrg/README.md @@ -1,41 +1,44 @@ --- page_type: sample name: How to secure an ASP.NET Core Web API with the Microsoft identity platform -services: active-directory -platforms: dotnet -urlFragment: active-directory-aspnetcore-webapp-openidconnect-v2 -description: This sample demonstrates an ASP.NET Core client Web App calling an ASP.NET Core Web API that is secured using Azure AD. +services: ms-identity +platform: DotNet languages: - csharp products: - aspnet-core - azure-active-directory +urlFragment: active-directory-aspnetcore-webapp-openidconnect-v2 +description: This sample demonstrates an ASP.NET Core Web App signing-in a user and calling an ASP.NET Core Web API that is secured with Azure AD. --- # How to secure an ASP.NET Core Web API with the Microsoft identity platform -[![Build status](https://identitydivision.visualstudio.com/IDDP/_apis/build/status/AAD%20Samples/.NET%20client%20samples/ASP.NET%20Core%20Web%20App%20tutorial)](https://identitydivision.visualstudio.com/IDDP/_build/latest?definitionId=819) +[![Build status](https://identitydivision.visualstudio.com/IDDP/_apis/build/status/AAD%20Samples/.NET%20client%20samples/ASP.NET%20Core%20Web%20App%20tutorial)](https://identitydivision.visualstudio.com/IDDP/_build/latest?definitionId=XXX) + +* [Overview](#overview) +* [Scenario](#scenario) +* [Prerequisites](#prerequisites) +* [Setup the sample](#setup-the-sample) +* [Explore the sample](#explore-the-sample) +* [Troubleshooting](#troubleshooting) +* [About the code](#about-the-code) +* [How the code was created](#how-the-code-was-created) +* [Deploy the sample](#deploy-the-sample) +* [Next Steps](#next-steps) +* [Contributing](#contributing) +* [Learn More](#learn-more) -Table Of Contents +## Overview -* [Scenario](#Scenario) -* [Prerequisites](#Prerequisites) -* [Setup the sample](#Setup-the-sample) -* [Troubleshooting](#Troubleshooting) -* [Using the sample](#Using-the-sample) -* [About the code](#About-the-code) -* [How the code was created](#How-the-code-was-created) -* [How to deploy this sample to Azure](#How-to-deploy-this-sample-to-Azure) -* [Next Steps](#Next-Steps) -* [Contributing](#Contributing) -* [Learn More](#Learn-More) +This sample demonstrates a ASP.NET Core Web App calling a ASP.NET Core Web API that is secured using Azure AD. ## Scenario This sample demonstrates an ASP.NET Core client Web App calling an ASP.NET Core Web API that is secured using Azure AD. - 1. The client ASP.NET Core Web App uses the [Microsoft.Identity.Web](https://aka.ms/microsoft-identity-web) to sign-in and obtain a JWT [Access Tokens](https://docs.microsoft.com/azure/active-directory/develop/access-tokens) from **Azure AD**. - 1. The [Access Tokens](https://docs.microsoft.com/azure/active-directory/develop/access-tokens) is used as a bearer token to authorize the user to call the ASP.NET Core Web API protected by **Azure AD**. + 1. The client ASP.NET Core Web App uses the [Microsoft.Identity.Web](https://aka.ms/microsoft-identity-web) to sign-in a user and obtain a JWT [Access Token](https://aka.ms/access-tokens) from **Azure AD**. + 2. The service uses the [Microsoft.Identity.Web](https://aka.ms/microsoft-identity-web) to protect the Web api, check permissions and validate tokens. ![Scenario Image](./ReadmeFiles/topology.png) @@ -43,7 +46,7 @@ Table Of Contents * Either [Visual Studio](https://visualstudio.microsoft.com/downloads/) or [Visual Studio Code](https://code.visualstudio.com/download) and [.NET Core SDK](https://www.microsoft.com/net/learn/get-started) * An **Azure AD** tenant. For more information, see: [How to get an Azure AD tenant](https://docs.microsoft.com/azure/active-directory/develop/test-setup-environment#get-a-test-tenant) -* A user account in your **Azure AD** tenant. This sample will not work with a **personal Microsoft account**. If you're signed in to the [Azure portal](https://portal.azure.com) with a personal Microsoft account and have not created a user account in your directory before, you will need to create one before proceeding. +* A user account in your **Azure AD** tenant. This sample will not work with a **personal Microsoft account**. If you're signed in to the [Azure portal](https://portal.azure.com) with a personal Microsoft account and have not created a user account in your directory before, you will need to create one before proceeding. ## Setup the sample @@ -55,9 +58,9 @@ From your shell or command line: git clone https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2.git ``` -or download and extract the repository .zip file. +or download and extract the repository *.zip* file. ->:warning: To avoid path length limitations on Windows, we recommend cloning into a directory near the root of your drive. +> :warning: To avoid path length limitations on Windows, we recommend cloning into a directory near the root of your drive. ### Step 2: Navigate to project folder @@ -65,24 +68,19 @@ or download and extract the repository .zip file. cd 4-WebApp-Your-API\4-1-MyOrg ``` -### Step 3: Application Registration - -There are two projects in this sample. Each needs to be separately registered in your Azure AD tenant. To register these projects: - -You can follow the [manual steps](#Manual-steps) - -**OR** +### Step 3: Register the sample application(s) in your tenant -### Run automation scripts +There are two projects in this sample. Each needs to be separately registered in your Azure AD tenant. To register these projects, you can: -* use PowerShell scripts that: - * **automatically** creates the Azure AD applications and related objects (passwords, permissions, dependencies) for you. - * modify the projects' configuration files. +- follow the steps below for manually register your apps +- or use PowerShell scripts that: + - **automatically** creates the Azure AD applications and related objects (passwords, permissions, dependencies) for you. + - modify the projects' configuration files.
Expand this section if you want to use this automation: - > **WARNING**: If you have never used **Azure AD Powershell** before, we recommend you go through the [App Creation Scripts guide](./AppCreationScripts/AppCreationScripts.md) once to ensure that your environment is prepared correctly for this step. + > :warning: If you have never used **Microsoft Graph PowerShell** before, we recommend you go through the [App Creation Scripts Guide](./AppCreationScripts/AppCreationScripts.md) once to ensure that your environment is prepared correctly for this step. 1. On Windows, run PowerShell as **Administrator** and navigate to the root of the cloned directory 1. In PowerShell run: @@ -92,152 +90,163 @@ You can follow the [manual steps](#Manual-steps) ``` 1. Run the script to create your Azure AD application and configure the code of the sample application accordingly. - 1. For interactive process - in PowerShell run: + 1. For interactive process -in PowerShell, run: ```PowerShell cd .\AppCreationScripts\ - .\Configure.ps1 -TenantId "[Optional] - your tenant id" -Environment "[Optional] - Azure environment, defaults to 'Global'" + .\Configure.ps1 -TenantId "[Optional] - your tenant id" -AzureEnvironmentName "[Optional] - Azure environment, defaults to 'Global'" ``` - > Other ways of running the scripts are described in [App Creation Scripts guide](./AppCreationScripts/AppCreationScripts.md) - > The scripts also provide a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios. + > Other ways of running the scripts are described in [App Creation Scripts guide](./AppCreationScripts/AppCreationScripts.md). The scripts also provide a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios.
-### Manual Steps +#### Choose the Azure AD tenant where you want to create your applications - > Note: skip this part if you've just used Automation steps +To manually register the apps, as a first step you'll need to: -Follow the steps below for manually register and configure your apps - -
- Expand this section if you want to use this automation: - 1. Sign in to the [Azure portal](https://portal.azure.com). - 1. If your account is present in more than one Azure AD tenant, select your profile at the top right corner in the menu on top of the page, and then **switch directory** to change your portal session to the desired Azure AD tenant. +1. Sign in to the [Azure portal](https://portal.azure.com). +1. If your account is present in more than one Azure AD tenant, select your profile at the top right corner in the menu on top of the page, and then **switch directory** to change your portal session to the desired Azure AD tenant. #### Register the service app (TodoListService-aspnetcore-webapi) - 1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure Active Directory** service. - 1. Select the **App Registrations** blade on the left, then select **New registration**. - 1. In the **Register an application page** that appears, enter your application's registration information: - * In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `TodoListService-aspnetcore-webapi`. - 1. Under **Supported account types**, select **Accounts in this organizational directory only** - 1. Click **Register** to create the application. - 1. In the app's registration screen, find and note the **Application (client) ID**. You use this value in your app's configuration file(s) later in your code. - 1. Click **Save** to save your changes. - - 1. In the app's registration screen, select the **Expose an API** blade to the left to open the page where you can declare the parameters to expose this app as an API for which client applications can obtain [access tokens](https://aka.ms/access-tokens) for. The first thing that we need to do is to declare the unique [resource](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) URI that the clients will be using to obtain access tokens for this API. To declare an resource URI(Application ID URI), follow the following steps: - * Select `Set` next to the **Application ID URI** to generate a URI that is unique for this app. - * For this sample, accept the proposed Application ID URI (`api://{clientId}`) by selecting **Save**. Read more about Application ID URI at [Validation differences by supported account types \(signInAudience\)](https://docs.microsoft.com/azure/active-directory/develop/supported-accounts-validation). - 1. All APIs have to publish a minimum of two [scopes](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code), also called [Delegated Permissions](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#permission-types), for the client's to obtain an access token successfully. To publish a scope, follow these steps: - * Select **Add a scope** button open the **Add a scope** screen and Enter the values as indicated below: - * For **Scope name**, use `ToDoList.Read`. - * Select **Admins and users** options for **Who can consent?**. - - For **Admin consent display name** type `Allow the app TodoListService-aspnetcore-webapi to [ex, read ToDo list items] as the signed-in user`. - - For **Admin consent description** type `Allow the application to [ex, read ToDo list items] as the signed-in user.` - - For **User consent display name** type `[ex, Read ToDo list items] as you`. - - For **User consent description** type `Allow the application to [ex, Read ToDo list items] as the signed-in user on your behalf.` - * Keep **State** as **Enabled**. - * Select the **Add scope** button on the bottom to save this scope. - > Repeat the steps above for scope **ToDoList.Write** - - 1. Select the `Manifest` blade on the left. - * Set `accessTokenAcceptedVersion` property to **2**. - * Click on **Save**. +1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure Active Directory** service. +1. Select the **App Registrations** blade on the left, then select **New registration**. +1. In the **Register an application page** that appears, enter your application's registration information: + 1. In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `TodoListService-aspnetcore-webapi`. + 1. Under **Supported account types**, select **Accounts in this organizational directory only** + 1. Select **Register** to create the application. +1. In the **Overview** blade, find and note the **Application (client) ID**. You use this value in your app's configuration file(s) later in your code. + +1. In the app's registration screen, select the **Expose an API** blade to the left to open the page where you can publish the permission as an API for which client applications can obtain [access tokens](https://aka.ms/access-tokens) for. The first thing that we need to do is to declare the unique [resource](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) URI that the clients will be using to obtain access tokens for this API. To declare an resource URI(Application ID URI), follow the following steps: + 1. Select **Set** next to the **Application ID URI** to generate a URI that is unique for this app. + 1. For this sample, accept the proposed Application ID URI (`api://{clientId}`) by selecting **Save**. Read more about Application ID URI at [Validation differences by supported account types \(signInAudience\)](https://docs.microsoft.com/azure/active-directory/develop/supported-accounts-validation). + +##### Publish Delegated Permissions + +1. All APIs must publish a minimum of one [scope](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code), also called [Delegated Permission](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#permission-types), for the client apps to obtain an access token for a *user* successfully. To publish a scope, follow these steps: +1. Select **Add a scope** button open the **Add a scope** screen and Enter the values as indicated below: + 1. For **Scope name**, use `ToDoList.Read`. + 1. Select **Admins and users** options for **Who can consent?**. + 1. For **Admin consent display name** type in the details, `e.g. Allow the users of the app TodoListService-aspnetcore-webapi to read ToDo list items`. + 1. For **Admin consent description** type in the details `e.g. Allows the app TodoListService-aspnetcore-webapi to read the signed-in users ToDo list items.` + 1. For **User consent display name** type in the details `e.g. Read ToDo list items as yourself`. + 1. For **User consent description** type in the details `e.g. Allow the app TodoListService-aspnetcore-webapi to read ToDo list items on your behalf.` + 1. Keep **State** as **Enabled**. + 1. Select the **Add scope** button on the bottom to save this scope. + > Repeat the steps above for another scope named **ToDoList.ReadWrite** +1. Select the **Manifest** blade on the left. + 1. Set `accessTokenAcceptedVersion` property to **2**. + 1. Select on **Save**. + + > :information_source: Follow [the principle of least privilege](https://docs.microsoft.com/azure/active-directory/develop/secure-least-privileged-access) whenever you are publishing permissions for a web API. + +##### Publish Application Permissions + +1. All APIs should publish a minimum of one [App role](https://docs.microsoft.com/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps#assign-app-roles-to-applications), also called [Application Permission](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#permission-types), for the client apps to obtain an access token as *themselves*, i.e. when they are not signing-in a user. **Application permissions** are the type of permissions that APIs should publish when they want to enable client applications to successfully authenticate as themselves and not need to sign-in users. To publish an application permission, follow these steps: +1. Still on the same app registration, select the **App roles** blade to the left. +1. Select **Create app role**: + 1. For **Display name**, enter a suitable name for your application permission, for instance **ToDoList.Read.All**. + 1. For **Allowed member types**, choose **Application** to ensure other applications can be granted this permission. + 1. For **Value**, enter **ToDoList.Read.All**. + 1. For **Description**, enter **Allow application to read all ToDo list items**. + 1. Select **Apply** to save your changes. + > Repeat the steps above for another app permission named **ToDoList.ReadWrite.All** + +##### Configure Optional Claims + +1. Still on the same app registration, select the **Token configuration** blade to the left. +1. Select **Add optional claim**: + 1. Select **optional claim type**, then choose **Access**. + 1. Select the optional claim **idtyp**. Indicates token type.This claim is the most accurate way for an API to determine if a token is an app token or an app+user token + 1. Select **Add** to save your changes. ##### Configure the service app (TodoListService-aspnetcore-webapi) to use your app registration - Open the project in your IDE (like Visual Studio or Visual Studio Code) to configure the code. +Open the project in your IDE (like Visual Studio or Visual Studio Code) to configure the code. - > In the steps below, "ClientID" is the same as "Application ID" or "AppId". +> In the steps below, "ClientID" is the same as "Application ID" or "AppId". - 1. Open the `TodoListService\appsettings.json` file. - 1. Find the key `Domain` and replace the existing value with your Azure AD tenant name. - 1. Find the key `TenantId` and replace the existing value with your Azure AD tenant ID. - 1. Find the key `ClientId` and replace the existing value with the application ID (clientId) of `TodoListService-aspnetcore-webapi` app copied from the Azure portal. +1. Open the `TodoListService\appsettings.json` file. +1. Find the key `Domain` and replace the existing value with your Azure AD tenant domain, ex. `contoso.onmicrosoft.com`. +1. Find the key `TenantId` and replace the existing value with your Azure AD tenant/directory ID. +1. Find the key `ClientId` and replace the existing value with the application ID (clientId) of `TodoListService-aspnetcore-webapi` app copied from the Azure portal. #### Register the client app (TodoListClient-aspnetcore-webapi) - 1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure Active Directory** service. - 1. Select the **App Registrations** blade on the left, then select **New registration**. - 1. In the **Register an application page** that appears, enter your application's registration information: - * In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `TodoListClient-aspnetcore-webapi`. - 1. Under **Supported account types**, select **Accounts in this organizational directory only** - 1. Click **Register** to create the application. - 1. In the app's registration screen, find and note the **Application (client) ID**. You use this value in your app's configuration file(s) later in your code. - 1. In the app's registration screen, select **Authentication** in the menu. - * If you don't have a platform added, select **Add a platform** and select the **Web** option. - 1. In the **Redirect URI** section enter the following redirect URIs: - 1. `https://localhost:44321/` - 1. `https://localhost:44321/signin-oidc` - - 1. In the **Front-channel logout URL** section, set it to `https://localhost:44321/signout-oidc`. - 1. Click **Save** to save your changes. - 1. In the app's registration screen, select the **Certificates & secrets** blade in the left to open the page where you can generate secrets and upload certificates. - 1. In the **Client secrets** section, select **New client secret**: - * Type a key description (for instance `app secret`). - * Select one of the available key durations (**6 months**, **12 months** or **Custom**) as per your security posture. - * The generated key value will be displayed when you select the **Add** button. Copy and save the generated value for use in later steps. - * You'll need this key later in your code's configuration files. This key value will not be displayed again, and is not retrievable by any other means, so make sure to note it from the Azure portal before navigating to any other screen or blade. - -> :bulb: For enhanced security, consider [using certificates](./README-use-certificate.md) instead of client secrets. - -1. In the app's registration screen, select the **API permissions** blade in the left to open the page where we add access to the APIs that your application needs. - * Select the **Add a permission** button and then, - * Ensure that the **My APIs** tab is selected. - * In the list of APIs, select the API `TodoListService-aspnetcore-webapi`. - * In the **Delegated permissions** section, select the **ToDoList.Read**, **ToDoList.Write** in the list. Use the search box if necessary. - * Select the **Add permissions** button at the bottom. +1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure Active Directory** service. +1. Select the **App Registrations** blade on the left, then select **New registration**. +1. In the **Register an application page** that appears, enter your application's registration information: + 1. In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `TodoListClient-aspnetcore-webapi`. + 1. Under **Supported account types**, select **Accounts in this organizational directory only** + 1. Select **Register** to create the application. +1. In the **Overview** blade, find and note the **Application (client) ID**. You use this value in your app's configuration file(s) later in your code. +1. In the app's registration screen, select the **Authentication** blade to the left. +1. If you don't have a platform added, select **Add a platform** and select the **Web** option. + 1. In the **Redirect URI** section enter the following redirect URI: + 1. `https://localhost:44321/signin-oidc` + 1. In the **Front-channel logout URL** section, set it to `https://localhost:44321/signout-oidc`. + 1. Click **Save** to save your changes. +1. In the app's registration screen, select the **Certificates & secrets** blade in the left to open the page where you can generate secrets and upload certificates. +1. In the **Client secrets** section, select **New client secret**: + 1. Type a key description (for instance `app secret`). + 1. Select one of the available key durations (**6 months**, **12 months** or **Custom**) as per your security posture. + 1. The generated key value will be displayed when you select the **Add** button. Copy and save the generated value for use in later steps. + 1. You'll need this key later in your code's configuration files. This key value will not be displayed again, and is not retrievable by any other means, so make sure to note it from the Azure portal before navigating to any other screen or blade. + > :bulb: For enhanced security, instead of using client secrets, consider [using certificates](./README-use-certificate.md) and [Azure KeyVault](https://azure.microsoft.com/services/key-vault/#product-overview). + +1. Since this app signs-in users, we will now proceed to select **delegated permissions**, which is is required by apps signing-in users. + 1. In the app's registration screen, select the **API permissions** blade in the left to open the page where we add access to the APIs that your application needs: + 1. Select the **Add a permission** button and then, + 1. Ensure that the **My APIs** tab is selected. + 1. In the list of APIs, select the API `TodoListService-aspnetcore-webapi`. + * Since this app signs-in users, we will now proceed to select **delegated permissions**, which is is requested by apps when signing-in users. + 1. In the **Delegated permissions** section, select the **ToDoList.Read**, **ToDoList.ReadWrite** in the list. Use the search box if necessary. + 1. Select the **Add permissions** button at the bottom. + +##### Configure Optional Claims + +1. Still on the same app registration, select the **Token configuration** blade to the left. +1. Select **Add optional claim**: + 1. Select **optional claim type**, then choose **ID**. + 1. Select the optional claim **acct**. Provides user's account status in tenant.If the user is a member of the tenant, the value is 0. If they're a guest, the value is 1. + 1. Select **Add** to save your changes. ##### Configure the client app (TodoListClient-aspnetcore-webapi) to use your app registration - Open the project in your IDE (like Visual Studio or Visual Studio Code) to configure the code. +Open the project in your IDE (like Visual Studio or Visual Studio Code) to configure the code. - > In the steps below, "ClientID" is the same as "Application ID" or "AppId". - - 1. Open the `Client\appsettings.json` file. - 1. Find the key `Domain` and replace the existing value with your Azure AD tenant name. - 1. Find the key `TenantId` and replace the existing value with your Azure AD tenant ID. - 1. Find the key `ClientId` and replace the existing value with the application ID (clientId) of `TodoListClient-aspnetcore-webapi` 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-aspnetcore-webapi` copied from the Azure portal. - 1. Find the key `TodoListScopes` and replace the existing value with **"api:///ToDoList.Read api:///ToDoList.Write"**. - 1. Find the key `TodoListBaseAddress` and replace the existing value with the base address of `TodoListService-aspnetcore-webapi` (by default `https://localhost:44351`). -
+> In the steps below, "ClientID" is the same as "Application ID" or "AppId". +1. Open the `Client\appsettings.json` file. +1. Find the key `Domain` and replace the existing value with your Azure AD tenant domain, ex. `contoso.onmicrosoft.com`. +1. Find the key `TenantId` and replace the existing value with your Azure AD tenant/directory ID. +1. Find the key `ClientId` and replace the existing value with the application ID (clientId) of `TodoListClient-aspnetcore-webapi` 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-aspnetcore-webapi` copied from the Azure portal. +1. Find the key `TodoListScopes` and replace the existing value with **"api:///ToDoList.Read api:///ToDoList.ReadWrite"**. +1. Find the key `TodoListBaseAddress` and replace the existing value with the base address of `TodoListService-aspnetcore-webapi` (by default `https://localhost:44351`). ### Variation: web app using client certificates -Follow [README-use-keyvault-certificate.md](README-use-keyvault-certificate.md) to know how to use this option. +Follow [README-use-certificate.md](README-use-certificate.md) to know how to use this option. ### Step 4: Running the sample - To run the samlple, run the next commands: + +From your shell or command line, execute the following commands: + ```console cd 4-WebApp-Your-API\4-1-MyOrg\TodoListService dotnet run ``` -Then open a separate command line terminal and run +Then, open a separate command line and run: ```console cd 4-WebApp-Your-API\4-1-MyOrg\Client dotnet run ``` -## Troubleshooting - -
- Expand for troubleshooting info - -Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. -Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. -Make sure that your questions or comments are tagged with [`azure-active-directory` `adal` `msal` `dotnet`]. - -If you find a bug in the sample, please raise the issue on [GitHub Issues](../../issues). - -To provide a recommendation, visit the following [User Voice page](https://feedback.azure.com/forums/169401-azure-active-directory). -
- -## Using the sample +## Explore the sample
Expand to see how to use the sample @@ -253,16 +262,30 @@ Did the sample not work for you as expected? Did you encounter issues trying thi [Consider taking a moment to share your experience with us.](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbRz0h_jLR5HNJlvkZAewyoWxUNEFCQ0FSMFlPQTJURkJZMTRZWVJRNkdRMC4u)
+## Troubleshooting + +
+ Expand for troubleshooting info + +Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. +Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. +Make sure that your questions or comments are tagged with [`azure-active-directory` `adal` `msal` `dotnet`]. + +If you find a bug in the sample, please raise the issue on [GitHub Issues](../../issues). + +To provide a recommendation, visit the following [User Voice page](https://feedback.azure.com/forums/169401-azure-active-directory). +
+ ## About the code
Expand the section -1. In the `TodoListService` project, first the package `Microsoft.Identity.Web`is added from NuGet. +1. In the `TodoListService` project, which represents the web api, first the package `Microsoft.Identity.Web`is added from NuGet. 1. Starting with the **Startup.cs** file : - * at the top of the file, the following two using directives were added: + * at the top of the file, the following using directory was added: ```CSharp using Microsoft.Identity.Web; @@ -274,47 +297,128 @@ Did the sample not work for you as expected? Did you encounter issues trying thi services.AddMicrosoftIdentityWebApiAuthentication(Configuration); ``` - * `AddMicrosoftIdentityWebApiAuthentication()` protects the Web API by validating Access tokens sent tho this API. Check out [Protected web API: Code configuration](https://docs.microsoft.com/azure/active-directory/develop/scenario-protected-web-api-app-configuration) which explains the inner workings of this method in more detail. + * `AddMicrosoftIdentityWebApiAuthentication()` protects the Web API by [validating Access tokens](https://docs.microsoft.com/azure/active-directory/develop/access-tokens#validating-tokens) sent tho this API. Check out [Protected web API: Code configuration](https://docs.microsoft.com/azure/active-directory/develop/scenario-protected-web-api-app-configuration) which explains the inner workings of this method in more detail. + + * There is a bit of code (commented) provided under this method that can be used to used do **extended token validation** and do checks based on additional claims, such as: + * check if the client app's appid (azp) is in some sort of an allowed list via the 'azp' claim, in case you wanted to restrict the API to a list of client apps. + * check if the caller's account is homed or guest via the 'acct' optional claim + * check if the caller belongs to right roles or groups via the 'roles' or 'groups' claim, respectively - * Then in the controllers `TodoListController.cs`, the `[Authorize]` added on top of the class to protect this route. - * Further in the controller, the `RequiredScope` is used to list the scopes ([Delegated permissions](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent)), that the user should consent for, before the method can be called. - * The delegated permissions are checked inside `TodoListService\Controllers\ToDoListController.cs` in the following way: +1. Then in the controllers `TodoListController.cs`, the `[Authorize]` added on top of the class to protect this route. + * Further in the controller, the [RequiredScopeOrAppPermission](https://github.com/AzureAD/microsoft-identity-web/wiki/web-apis#checking-for-scopes-or-app-permissions=) is used to list the ([Delegated permissions](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent)), that the user should consent for, before the method can be called. + * The delegated permissions are checked inside `TodoListService\Controllers\ToDoListController.cs` in the following manner: ```CSharp [HttpGet] - [RequiredScope(new string[] { "ToDoList.Read", "ToDoList.Write" }) - + [RequiredScopeOrAppPermission( + AcceptedScope = new string[] { "ToDoList.Read", "ToDoList.ReadWrite" }, + AcceptedAppPermission = new string[] { "ToDoList.Read.All", "ToDoList.ReadWrite.All" } + )] public IEnumerable Get() { - string owner = User.Identity.Name; - return TodoStore.Values.Where(x => x.Owner == owner); + if (!IsAppOnlyToken()) + { + // this is a request for all ToDo list items of a certain user. + return TodoStore.Values.Where(x => x.Owner == _currentLoggedUser); + } + else + { + // Its an app calling with app permissions, so return all items across all users + return TodoStore.Values; + } } ``` - The code above demonstrates that to be able to reach a GET REST operation, the access token should contain AT LEAST ONE of the scopes listed inside parameter of [RequiredScope attribute](https://github.com/AzureAD/microsoft-identity-web/blob/master/src/Microsoft.Identity.Web/Policy/RequiredScopeAttribute.cs) + The code above demonstrates that to be able to reach a GET REST operation, the access token should contain AT LEAST ONE of the scopes (delegated permissions) listed inside parameter of [RequiredScopeOrAppPermission](https://github.com/AzureAD/microsoft-identity-web/wiki/web-apis#checking-for-scopes-or-app-permissions=) attribute + Please note that while in this sample, the client app only works with *Delegated Permissions*, the API's controller is designed to work with both *Delegated* and *Application* permissions. + + The **ToDoList.<*>.All** permissions are **Application Permissions**. + + Here is another example from the same controller: ``` CSharp [HttpDelete("{id}")] - [RequiredScope("ToDoList.Write")] + [RequiredScopeOrAppPermission( + AcceptedScope = new string[] { "ToDoList.ReadWrite" }, + AcceptedAppPermission = new string[] { "ToDoList.ReadWrite.All" })] public void Delete(int id) { - TodoStore.Remove(id); + if (!IsAppOnlyToken()) + { + // only delete if the ToDo list item belonged to this user + if (TodoStore.Values.Any(x => x.Id == id && x.Owner == _currentLoggedUser)) + { + TodoStore.Remove(id); + } + } + else + { + TodoStore.Remove(id); + } } ``` - The above code demonstrates that to be able to execute the DELETE REST operation, the access token MUST contain the `ToDoList.Write` scope. Note that the called is not allowed to access this operation with just `ToDoList.Read` scope only. + The above code demonstrates that to be able to execute the DELETE REST operation, the access token MUST contain the `ToDoList.ReadWrite` scope. Note that the called is not allowed to access this operation with just `ToDoList.Read` scope only. + Also note of how we distinguish the **what** a user can delete. When there is a **ToDoList.ReadWrite.All** permission available, the user can delete **ANY** entity from the database, + but with **ToDoList.ReadWrite**, the user can delete only their own entries. + + * The method *IsAppOnlyToken()* is used by controller method to detect presence of an app only token, i.e a token that was issued to an app using the [Client credentials](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) flow, i.e no users were signed-in by this client app. -### Initial scopes + ```csharp + private bool IsAppOnlyToken() + { + // Add in the optional 'idtyp' claim to check if the access token is coming from an application or user. + // + // See: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims + return HttpContext.User.Claims.Any(c => c.Type == "idtyp" && c.Value == "app"); + } + ``` -Client [appsettings.json](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/master/4-WebApp-your-API/4-1-MyOrg/Client/appsettings.json) file contains `ToDoListScopes` key that is used in [startup.cs](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/2607df1338a9f7c06fe228c87644b8b456ca708b/4-WebApp-your-API/4-1-MyOrg/Client/Startup.cs#L46) to specify which initial scopes should be requested from Web API when refreshing the token: +1. In the `TodoListClient` project, which represents the client app that signs-in a user and makes calls to the web api, first the package `Microsoft.Identity.Web`is added from NuGet. + +* The following lines in *Startup.cs* adds the ability to authenticate a user using Azure AD. ```csharp -services.AddMicrosoftIdentityWebAppAuthentication(Configuration) - .EnableTokenAcquisitionToCallDownstreamApi(Configuration.GetSection("TodoList:TodoListScopes") - .Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries)) - .AddInMemoryTokenCaches(); + services.AddMicrosoftIdentityWebAppAuthentication(Configuration) + .EnableTokenAcquisitionToCallDownstreamApi( + Configuration.GetSection("TodoList:TodoListScopes").Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries) + ) + .AddInMemoryTokenCaches(); ``` +* Specifying Initial scopes (delegated permissions) + +The ToDoListClient's *appsettings.json* file contains `ToDoListScopes` key that is used in *startup.cs* to specify which initial scopes (delegated permissions) should be requested for the Access Token when a user is being signed-in: + +```csharp + services.AddMicrosoftIdentityWebAppAuthentication(Configuration) + .EnableTokenAcquisitionToCallDownstreamApi(Configuration.GetSection("TodoList:TodoListScopes") + .Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries)) + .AddInMemoryTokenCaches(); +``` + +* Detecting *Guest* users of a tenant signing-in. This section of code in *Startup.cs* shows you how to detect if the user signing-in is a *member* or *guest*. + + ```CSharp + app.Use(async (context, next) => { + if (context.User != null && context.User.Identity.IsAuthenticated) + { + // you can conduct any conditional processing for guest/homes user by inspecting the value of the 'acct' claim + // Read more about the 'acct' claim at aka.ms/optionalclaims + if (context.User.Claims.Any(x => x.Type == "acct")) + { + string claimvalue = context.User.Claims.FirstOrDefault(x => x.Type == "acct").Value; + string userType = claimvalue == "0" ? "Member" : "Guest"; + Debug.WriteLine($"The type of the user account from this Azure AD tenant is-{userType}"); + } + } + await next(); + }); + ``` + +1. There is some commented code in *Startup.cs* that also shows how to user certificates and KeyVault in place, see [README-use-certificate](README-use-certificate.md) for more details on how to use code in this section. +1. Also consider adding [MSAL.NET Logging](https://docs.microsoft.com/azure/active-directory/develop/msal-logging-dotnet) to you project +
## How the code was created @@ -322,6 +426,47 @@ services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
Expand the section +### Creating the Web API project (TodoListService) + +The code for the TodoListService was created in the following way: + +#### Step 1: Create the web api using the ASP.NET Core templates + +```Text +md TodoListService +cd TodoListService +dotnet new webapi -au=SingleOrg +``` + +1. Open the generated project (.csproj) in Visual Studio, and save the solution. + +#### Add a model (TodoListItem) and modify the controller + +In the TodoListService project, add a folder named `Models` and then create a new file named `TodoItem.cs`. Copy the contents of the TodoListService\Models\TodoItem.cs in this file. + +### Modify the Program.cs file to validate bearer access tokens received by the Web API + +Update `Program.cs` file : + + *replace the following code: + + ```CSharp +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + ``` + + with + + ```Csharp + services.AddMicrosoftIdentityWebApiAuthentication(Configuration); + ``` + +### Create the TodoListController and associated Models + +1. Add a reference to the ToDoListService. +1. Create a new Controller named `TodoListController` and copy and paste the code from the sample (TodoListService\Controllers\TodoListController.cs) to this controller. +1. Open the `appsettings.json` file and copy the keys from the sample's corresponding file under the `AzureAd` and `TodoList` sections. + ### Creating the client web app (TodoListClient) #### Step 1: the sample from the command line @@ -339,34 +484,27 @@ services.AddMicrosoftIdentityWebAppAuthentication(Configuration) #### Step 2: Modify the generated code 1. Open the generated project (.csproj) in Visual Studio, and save the solution. -1. Add the `Microsoft.Identity.Web.csproj` project which is located at the root of this sample repo, to your solution (**Add Existing Project ...**). It's used to simplify signing-in and, in the next tutorial phases, to get a token. -1. Add a reference from your newly generated project to `Microsoft.Identity.Web` (right click on the **Dependencies** node under your new project, and choose **Add Reference ...**, and then in the projects tab find the `Microsoft.Identity.Web` project) - -1. Open the **Startup.cs** file and: +1. Add the `Microsoft.Identity.Web` via Nuget. +1. Open the **Program.cs** file and: - * at the top of the file, add the following using directive were added: + * Replace the two following lines: - ```CSharp - using Microsoft.Identity.Web; - ``` - - * in the `ConfigureServices` method, replace the two following lines: - - ```CSharp - services.AddAuthentication(AzureADDefaults.AuthenticationScheme) - .AddAzureAD(options => Configuration.Bind("AzureAd", options)); - ``` +```CSharp +services.AddAuthentication(AzureADDefaults.AuthenticationScheme) + .AddAzureAD(options => Configuration.Bind("AzureAd", options)); +``` - with these lines: +with these lines: - ```CSharp - services.AddMicrosoftIdentityWebAppAuthentication(Configuration) - .EnableTokenAcquisitionToCallDownstreamApi( - Configuration.GetSection("TodoList:TodoListScopes").Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries)) - .AddInMemoryTokenCaches(); - ``` +```CSharp +services.AddMicrosoftIdentityWebAppAuthentication(Configuration) + .EnableTokenAcquisitionToCallDownstreamApi( + Configuration.GetSection("TodoList:TodoListScopes").Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries) + ) + .AddInMemoryTokenCaches(); +``` - This enables your application to use the Microsoft identity platform endpoint. This endpoint is capable of signing-in users both with their Work and School and Microsoft Personal accounts. +* This enables your application to use the Microsoft identity platform endpoint. This endpoint is capable of signing-in users both with their Work and School and Microsoft Personal accounts. 1. Change the `Properties\launchSettings.json` file to ensure that you start your web app from as registered. For this: * update the `sslPort` of the `iisSettings` section to be `44321` @@ -378,7 +516,7 @@ services.AddMicrosoftIdentityWebAppAuthentication(Configuration) services.AddTodoListService(Configuration); ``` -1. Open the `appsettings.json` file and copy the keys from the sample's corresponding file under the `AzureAd` and `TodoList` sections. +2. Open the `appsettings.json` file and copy the keys from the sample's corresponding file under the `AzureAd` and `TodoList` sections. #### Add a model (TodoListItem) and add the controller and views @@ -391,152 +529,74 @@ services.AddMicrosoftIdentityWebAppAuthentication(Configuration) 1. Update the `configureServices` method in `startup.cs` to add the MSAL library and a token cache. ```CSharp - services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) - .AddMicrosoftIdentityWebApp(Configuration) - .EnableTokenAcquisitionToCallDownstreamApi(new string[] { Configuration["TodoList:TodoListScopes"] }) - .AddInMemoryTokenCaches(); + services.AddMicrosoftIdentityWebAppAuthentication(Configuration) + .EnableTokenAcquisitionToCallDownstreamApi( + Configuration.GetSection("TodoList:TodoListScopes").Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries) + ) + .AddInMemoryTokenCaches(); + + // Add APIs + services.AddTodoListService(Configuration); ``` -1. Update the `Configure` method to include **app.UseAuthentication();** before **app.UseAuthorization();** - - ```Csharp - app.UseAuthentication(); - app.AddAuthorization(); - ``` - -### Creating the Web API project (TodoListService) - -The code for the TodoListService was created in the following way: - -#### Step 1: Create the web api using the ASP.NET Core templates - -```Text -md TodoListService -cd TodoListService -dotnet new webapi -au=SingleOrg -``` - -1. Open the generated project (.csproj) in Visual Studio, and save the solution. - -#### Add a model (TodoListItem) and modify the controller - -In the TodoListService project, add a folder named `Models` and then create a new file named `TodoItem.cs`. Copy the contents of the TodoListService\Models\TodoItem.cs in this file. - -### Modify the Startup.cs file to validate bearer access tokens received by the Web API - -1. Add the `Microsoft.Identity.Web.csproj` project which is located at the root of this sample repo, to your solution (**Add Existing Project ...**). -1. Add a reference from your newly generated project to `Microsoft.Identity.Web` (right click on the **Dependencies** node under your new project, and choose **Add Reference ...**, and then in the projects tab find the `Microsoft.Identity.Web` project) -Update `Startup.cs` file : - - *Add the following two using statements - -```CSharp -using Microsoft.Identity.Web; -using Microsoft.Identity.Web.Client.TokenCacheProviders; -``` - - *In the `ConfigureServices` method, replace the following code: - - ```CSharp - services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme) - .AddAzureADBearer(options => Configuration.Bind("AzureAd", options)); - ``` +
- with +## How to deploy this sample to Azure - ```Csharp - services.AddMicrosoftIdentityWebApiAuthentication(Configuration); - ``` +### Deploying web API to Azure App Services - *Add the method **app.UseAuthentication()** before **app.UseAuthorization()** in the `Configure` method +There is one web API in this sample. To deploy it to **Azure App Services**, you'll need to: - ```Csharp - app.UseAuthentication(); - app.UseAuthorization(); - ``` +- create an **Azure App Service** +- publish the projects to the **App Services** -### Create the TodoListController.cs file +#### Publish your files (TodoListService-aspnetcore-webapi) -1. Add a folder named `Models` and then create a new file named `TodoItem.cs`. Copy the contents of the TodoListClient\Models\TodoItem.cs in this file. -1. Create a new Controller named `TodoListController` and copy and paste the code from the sample (TodoListService\Controllers\TodoListController.cs) to this controller. +##### Publish using Visual Studio - +Follow the link to [Publish with Visual Studio](https://docs.microsoft.com/visualstudio/deployment/quickstart-deploy-to-azure). -## How to deploy this sample to Azure +##### Publish using Visual Studio Code -
- Expand the section +1. Install the Visual Studio Code extension [Azure App Service](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azureappservice). +1. Follow the link to [Publish with Visual Studio Code](https://docs.microsoft.com/aspnet/core/tutorials/publish-to-azure-webapp-using-vscode) -This project has two WebApp / Web API projects. To deploy them to Azure Web Sites, you'll need, for each one, to: +### Deploying web app to Azure App Services - *create an Azure Web Site - *publish the Web App / Web APIs to the web site, and - *update its client(s) to call the web site instead of IIS Express. +There is one web app in this sample. To deploy it to **Azure App Services**, you'll need to: -### Create and publish the `TodoListService-aspnetcore-webapi` to an Azure Web Site +- create an **Azure App Service** +- publish the projects to the **App Services**, and +- update its client(s) to call the website instead of the local environment. -1. Sign in to the [Azure portal](https://portal.azure.com). -1. Click `Create a resource` in the top left-hand corner, select **Web** --> **Web App**, and give your web site a name, for example, `TodoListService-aspnetcore-webapi-contoso.azurewebsites.net`. -1. Thereafter select the `Subscription`, `Resource Group`, `App service plan and Location`. `OS` will be **Windows** and `Publish` will be **Code**. -1. Click `Create` and wait for the App Service to be created. -1. Once you get the `Deployment succeeded` notification, then click on `Go to resource` to navigate to the newly created App service. -1. Once the web site is created, locate it it in the **Dashboard** and click it to open **App Services** **Overview** screen. -1. From the **Overview** tab of the App Service, download the publish profile by clicking the **Get publish profile** link and save it. Other deployment mechanisms, such as from source control, can also be used. -1. Switch to Visual Studio and go to the TodoListService-aspnetcore-webapi project. Right click on the project in the Solution Explorer and select **Publish**. Click **Import Profile** on the bottom bar, and import the publish profile that you downloaded earlier. -1. Click on **Configure** and in the `Connection tab`, update the Destination URL so that it is a `https` in the home page url, for example [https://TodoListService-aspnetcore-webapi-contoso.azurewebsites.net](https://TodoListService-aspnetcore-webapi-contoso.azurewebsites.net). Click **Next**. -1. On the Settings tab, make sure `Enable Organizational Authentication` is NOT selected. Click **Save**. Click on **Publish** on the main screen. -1. Visual Studio will publish the project and automatically open a browser to the URL of the project. If you see the default web page of the project, the publication was successful. - -### Update the Active Directory tenant application registration for `TodoListService-aspnetcore-webapi` +#### Publish your files (TodoListClient-aspnetcore-webapi) -1. Navigate back to to the [Azure portal](https://portal.azure.com). -In the left-hand navigation pane, select the **Azure Active Directory** service, and then select **App registrations (Preview)**. -1. In the resultant screen, select the `TodoListService-aspnetcore-webapi` application. -1. From the *Branding* menu, update the **Home page URL**, to the address of your service, for example [https://TodoListService-aspnetcore-webapi-contoso.azurewebsites.net](https://TodoListService-aspnetcore-webapi-contoso.azurewebsites.net). Save the configuration. -1. Add the same URL in the list of values of the *Authentication -> Redirect URIs* menu. If you have multiple redirect urls, make sure that there a new entry using the App service's Uri for each redirect url. +##### Publish using Visual Studio -### Update the `TodoListClient-aspnetcore-webapi` to call the `TodoListService-aspnetcore-webapi` Running in Azure Web Sites +Follow the link to [Publish with Visual Studio](https://docs.microsoft.com/visualstudio/deployment/quickstart-deploy-to-azure). -1. In Visual Studio, go to the `TodoListClient-aspnetcore-webapi` project. -2. Open `Client\appsettings.json`. Only one change is needed - update the `todo:TodoListBaseAddress` key value to be the address of the website you published, - for example, [https://TodoListService-aspnetcore-webapi-contoso.azurewebsites.net](https://TodoListService-aspnetcore-webapi-contoso.azurewebsites.net). -3. Run the client! If you are trying multiple different client types (for example, .Net, Windows Store, Android, iOS) you can have them all call this one published web API. +##### Publish using Visual Studio Code -### Create and publish the `TodoListClient-aspnetcore-webapi` to an Azure Web Site +1. Install the Visual Studio Code extension [Azure App Service](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azureappservice). +1. Follow the link to [Publish with Visual Studio Code](https://docs.microsoft.com/aspnet/core/tutorials/publish-to-azure-webapp-using-vscode) -1. Sign in to the [Azure portal](https://portal.azure.com). -1. Click `Create a resource` in the top left-hand corner, select **Web** --> **Web App**, and give your web site a name, for example, `TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net`. -1. Thereafter select the `Subscription`, `Resource Group`, `App service plan and Location`. `OS` will be **Windows** and `Publish` will be **Code**. -1. Click `Create` and wait for the App Service to be created. -1. Once you get the `Deployment succeeded` notification, then click on `Go to resource` to navigate to the newly created App service. -1. Once the web site is created, locate it in the **Dashboard** and click it to open **App Services** **Overview** screen. -1. From the **Overview** tab of the App Service, download the publish profile by clicking the **Get publish profile** link and save it. Other deployment mechanisms, such as from source control, can also be used. -1. Switch to Visual Studio and go to the TodoListClient-aspnetcore-webapi project. Right click on the project in the Solution Explorer and select **Publish**. Click **Import Profile** on the bottom bar, and import the publish profile that you downloaded earlier. -1. Click on **Configure** and in the `Connection tab`, update the Destination URL so that it is a `https` in the home page url, for example [https://TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net](https://TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net). Click **Next**. -1. On the Settings tab, make sure `Enable Organizational Authentication` is NOT selected. Click **Save**. Click on **Publish** on the main screen. -1. Visual Studio will publish the project and automatically open a browser to the URL of the project. If you see the default web page of the project, the publication was successful. - -### Update the Active Directory tenant application registration for `TodoListClient-aspnetcore-webapi` +#### Update the Azure AD app registration (TodoListClient-aspnetcore-webapi) 1. Navigate back to to the [Azure portal](https://portal.azure.com). In the left-hand navigation pane, select the **Azure Active Directory** service, and then select **App registrations (Preview)**. -1. In the resultant screen, select the `TodoListClient-aspnetcore-webapi` application. -1. In the **Authentication** | page for your application, update the Logout URL fields with the address of your service, for example [https://TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net](https://TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net) -1. From the *Branding* menu, update the **Home page URL**, to the address of your service, for example [https://TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net](https://TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net). Save the configuration. -1. Add the same URL in the list of values of the *Authentication -> Redirect URIs* menu. If you have multiple redirect urls, make sure that there a new entry using the App service's Uri for each redirect url. - -> NOTE: Remember, the To Do list is stored in memory in this TodoListService sample. Azure Web Sites will spin down your web site if it is inactive, and your To Do list will get emptied. -Also, if you increase the instance count of the web site, requests will be distributed among the instances. To Do will, therefore, not be the same on each instance. - -
+1. In the resulting screen, select the `TodoListClient-aspnetcore-webapi` application. +1. In the app's registration screen, select **Authentication** in the menu. + - In the **Redirect URIs** section, update the reply URLs to match the site URL of your Azure deployment. For example: + - `https://TodoListClient-aspnetcore-webapi.azurewebsites.net/signin-oidc` + - Update the **Front-channel logout URL** fields with the address of your service, for example [https://TodoListClient-aspnetcore-webapi.azurewebsites.net](https://TodoListClient-aspnetcore-webapi.azurewebsites.net) -## Next Steps +#### Update authentication configuration parameters (TodoListClient-aspnetcore-webapi) -Learn how to: +1. In your IDE, locate the `TodoListClient-aspnetcore-webapi` project. Then, open `Client\appsettings.json`. +2. Find the key for **redirect URI** and replace its value with the address of the web app you published, for example, [https://TodoListClient-aspnetcore-webapi.azurewebsites.net/redirect](https://TodoListClient-aspnetcore-webapi.azurewebsites.net/redirect). +3. Find the key for **web API endpoint** and replace its value with the address of the web API you published, for example, [https://TodoListService-aspnetcore-webapi.azurewebsites.net/api](https://TodoListService-aspnetcore-webapi.azurewebsites.net/api). -* [Change your app to sign-in users from any organization or any Microsoft accounts](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/1-WebApp-OIDC/1-3-AnyOrgOrPersonal) -* [Enable users from National clouds to sign-in to your application](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/1-WebApp-OIDC/1-4-Sovereign) -* [Enable your Web App to call a Web API on behalf of the signed-in user](https://github.com/Azure-Samples/ms-identity-dotnetcore-ca-auth-context-app) +> :warning: If your app is using an *in-memory* storage, **Azure App Services** will spin down your web site if it is inactive, and any records that your app was keeping will emptied. In addition, if you increase the instance count of your website, requests will be distributed among the instances. Your app's records, therefore, will not be the same on each instance. ## Contributing @@ -546,13 +606,6 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope ## Learn More -* Microsoft identity platform (Azure Active Directory for developers)](https://docs.microsoft.com/azure/active-directory/develop/) -* Overview of Microsoft Authentication Library (MSAL)](https://docs.microsoft.com/azure/active-directory/develop/msal-overview) -* Authentication Scenarios for Azure AD](https://docs.microsoft.com/azure/active-directory/develop/authentication-flows-app-scenarios) -* Azure AD code samples](https://docs.microsoft.com/azure/active-directory/develop/sample-v2-code) -* Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) -* Building Zero Trust ready apps](https://aka.ms/ztdevsession) - For more information, visit the following links: *To lean more about the application registration, visit: @@ -569,6 +622,4 @@ For more information, visit the following links: *[Introduction to Identity on ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/authentication/identity) *[AuthenticationBuilder](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder) *[Azure Active Directory with ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/authentication/azure-active-directory) - - - + *[Protected web API: Verify scopes and app roles](https://docs.microsoft.com/azure/active-directory/develop/scenario-protected-web-api-verification-scope-app-roles?tabs=aspnetcore) diff --git a/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeAboutTheCode.md b/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeAboutTheCode.md index 0c283ee8..fb8516da 100644 --- a/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeAboutTheCode.md +++ b/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeAboutTheCode.md @@ -3,11 +3,11 @@
Expand the section -1. In the `TodoListService` project, first the package `Microsoft.Identity.Web`is added from NuGet. +1. In the `TodoListService` project, which represents the web api, first the package `Microsoft.Identity.Web`is added from NuGet. 1. Starting with the **Startup.cs** file : - * at the top of the file, the following two using directives were added: + * at the top of the file, the following using directory was added: ```CSharp using Microsoft.Identity.Web; @@ -19,45 +19,126 @@ services.AddMicrosoftIdentityWebApiAuthentication(Configuration); ``` - * `AddMicrosoftIdentityWebApiAuthentication()` protects the Web API by validating Access tokens sent tho this API. Check out [Protected web API: Code configuration](https://docs.microsoft.com/azure/active-directory/develop/scenario-protected-web-api-app-configuration) which explains the inner workings of this method in more detail. + * `AddMicrosoftIdentityWebApiAuthentication()` protects the Web API by [validating Access tokens](https://docs.microsoft.com/azure/active-directory/develop/access-tokens#validating-tokens) sent tho this API. Check out [Protected web API: Code configuration](https://docs.microsoft.com/azure/active-directory/develop/scenario-protected-web-api-app-configuration) which explains the inner workings of this method in more detail. - * Then in the controllers `TodoListController.cs`, the `[Authorize]` added on top of the class to protect this route. - * Further in the controller, the `RequiredScope` is used to list the scopes ([Delegated permissions](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent)), that the user should consent for, before the method can be called. - * The delegated permissions are checked inside `TodoListService\Controllers\ToDoListController.cs` in the following way: + * There is a bit of code (commented) provided under this method that can be used to used do **extended token validation** and do checks based on additional claims, such as: + * check if the client app's appid (azp) is in some sort of an allowed list via the 'azp' claim, in case you wanted to restrict the API to a list of client apps. + * check if the caller's account is homed or guest via the 'acct' optional claim + * check if the caller belongs to right roles or groups via the 'roles' or 'groups' claim, respectively + +1. Then in the controllers `TodoListController.cs`, the `[Authorize]` added on top of the class to protect this route. + * Further in the controller, the [RequiredScopeOrAppPermission](https://github.com/AzureAD/microsoft-identity-web/wiki/web-apis#checking-for-scopes-or-app-permissions=) is used to list the ([Delegated permissions](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent)), that the user should consent for, before the method can be called. + * The delegated permissions are checked inside `TodoListService\Controllers\ToDoListController.cs` in the following manner: ```CSharp [HttpGet] - [RequiredScope(new string[] { "ToDoList.Read", "ToDoList.Write" }) - + [RequiredScopeOrAppPermission( + AcceptedScope = new string[] { "ToDoList.Read", "ToDoList.ReadWrite" }, + AcceptedAppPermission = new string[] { "ToDoList.Read.All", "ToDoList.ReadWrite.All" } + )] public IEnumerable Get() { - string owner = User.Identity.Name; - return TodoStore.Values.Where(x => x.Owner == owner); + if (!IsAppOnlyToken()) + { + // this is a request for all ToDo list items of a certain user. + return TodoStore.Values.Where(x => x.Owner == _currentLoggedUser); + } + else + { + // Its an app calling with app permissions, so return all items across all users + return TodoStore.Values; + } } ``` - The code above demonstrates that to be able to reach a GET REST operation, the access token should contain AT LEAST ONE of the scopes listed inside parameter of [RequiredScope attribute](https://github.com/AzureAD/microsoft-identity-web/blob/master/src/Microsoft.Identity.Web/Policy/RequiredScopeAttribute.cs) + The code above demonstrates that to be able to reach a GET REST operation, the access token should contain AT LEAST ONE of the scopes (delegated permissions) listed inside parameter of [RequiredScopeOrAppPermission](https://github.com/AzureAD/microsoft-identity-web/wiki/web-apis#checking-for-scopes-or-app-permissions=) attribute + Please note that while in this sample, the client app only works with *Delegated Permissions*, the API's controller is designed to work with both *Delegated* and *Application* permissions. + + The **ToDoList.<*>.All** permissions are **Application Permissions**. + + Here is another example from the same controller: ``` CSharp [HttpDelete("{id}")] - [RequiredScope("ToDoList.Write")] + [RequiredScopeOrAppPermission( + AcceptedScope = new string[] { "ToDoList.ReadWrite" }, + AcceptedAppPermission = new string[] { "ToDoList.ReadWrite.All" })] public void Delete(int id) { - TodoStore.Remove(id); + if (!IsAppOnlyToken()) + { + // only delete if the ToDo list item belonged to this user + if (TodoStore.Values.Any(x => x.Id == id && x.Owner == _currentLoggedUser)) + { + TodoStore.Remove(id); + } + } + else + { + TodoStore.Remove(id); + } } ``` - The above code demonstrates that to be able to execute the DELETE REST operation, the access token MUST contain the `ToDoList.Write` scope. Note that the called is not allowed to access this operation with just `ToDoList.Read` scope only. + The above code demonstrates that to be able to execute the DELETE REST operation, the access token MUST contain the `ToDoList.ReadWrite` scope. Note that the called is not allowed to access this operation with just `ToDoList.Read` scope only. + Also note of how we distinguish the **what** a user can delete. When there is a **ToDoList.ReadWrite.All** permission available, the user can delete **ANY** entity from the database, + but with **ToDoList.ReadWrite**, the user can delete only their own entries. + + * The method *IsAppOnlyToken()* is used by controller method to detect presence of an app only token, i.e a token that was issued to an app using the [Client credentials](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) flow, i.e no users were signed-in by this client app. + + ```csharp + private bool IsAppOnlyToken() + { + // Add in the optional 'idtyp' claim to check if the access token is coming from an application or user. + // + // See: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims + return HttpContext.User.Claims.Any(c => c.Type == "idtyp" && c.Value == "app"); + } + ``` + +1. In the `TodoListClient` project, which represents the client app that signs-in a user and makes calls to the web api, first the package `Microsoft.Identity.Web`is added from NuGet. + +* The following lines in *Startup.cs* adds the ability to authenticate a user using Azure AD. + +```csharp + services.AddMicrosoftIdentityWebAppAuthentication(Configuration) + .EnableTokenAcquisitionToCallDownstreamApi( + Configuration.GetSection("TodoList:TodoListScopes").Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries) + ) + .AddInMemoryTokenCaches(); +``` -### Initial scopes +* Specifying Initial scopes (delegated permissions) -Client [appsettings.json](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/master/4-WebApp-your-API/4-1-MyOrg/Client/appsettings.json) file contains `ToDoListScopes` key that is used in [startup.cs](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/2607df1338a9f7c06fe228c87644b8b456ca708b/4-WebApp-your-API/4-1-MyOrg/Client/Startup.cs#L46) to specify which initial scopes should be requested from Web API when refreshing the token: +The ToDoListClient's *appsettings.json* file contains `ToDoListScopes` key that is used in *startup.cs* to specify which initial scopes (delegated permissions) should be requested for the Access Token when a user is being signed-in: ```csharp -services.AddMicrosoftIdentityWebAppAuthentication(Configuration) - .EnableTokenAcquisitionToCallDownstreamApi(Configuration.GetSection("TodoList:TodoListScopes") - .Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries)) - .AddInMemoryTokenCaches(); + services.AddMicrosoftIdentityWebAppAuthentication(Configuration) + .EnableTokenAcquisitionToCallDownstreamApi(Configuration.GetSection("TodoList:TodoListScopes") + .Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries)) + .AddInMemoryTokenCaches(); ``` +* Detecting *Guest* users of a tenant signing-in. This section of code in *Startup.cs* shows you how to detect if the user signing-in is a *member* or *guest*. + + ```CSharp + app.Use(async (context, next) => { + if (context.User != null && context.User.Identity.IsAuthenticated) + { + // you can conduct any conditional processing for guest/homes user by inspecting the value of the 'acct' claim + // Read more about the 'acct' claim at aka.ms/optionalclaims + if (context.User.Claims.Any(x => x.Type == "acct")) + { + string claimvalue = context.User.Claims.FirstOrDefault(x => x.Type == "acct").Value; + string userType = claimvalue == "0" ? "Member" : "Guest"; + Debug.WriteLine($"The type of the user account from this Azure AD tenant is-{userType}"); + } + } + await next(); + }); + ``` + +1. There is some commented code in *Startup.cs* that also shows how to user certificates and KeyVault in place, see [README-use-certificate](README-use-certificate.md) for more details on how to use code in this section. +1. Also consider adding [MSAL.NET Logging](https://docs.microsoft.com/azure/active-directory/develop/msal-logging-dotnet) to you project +
\ No newline at end of file diff --git a/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeAzureDeploy.md b/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeAzureDeploy.md deleted file mode 100644 index 34ad9156..00000000 --- a/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeAzureDeploy.md +++ /dev/null @@ -1,67 +0,0 @@ -## How to deploy this sample to Azure - -
- Expand the section - -This project has two WebApp / Web API projects. To deploy them to Azure Web Sites, you'll need, for each one, to: - - *create an Azure Web Site - *publish the Web App / Web APIs to the web site, and - *update its client(s) to call the web site instead of IIS Express. - -### Create and publish the `TodoListService-aspnetcore-webapi` to an Azure Web Site - -1. Sign in to the [Azure portal](https://portal.azure.com). -1. Click `Create a resource` in the top left-hand corner, select **Web** --> **Web App**, and give your web site a name, for example, `TodoListService-aspnetcore-webapi-contoso.azurewebsites.net`. -1. Thereafter select the `Subscription`, `Resource Group`, `App service plan and Location`. `OS` will be **Windows** and `Publish` will be **Code**. -1. Click `Create` and wait for the App Service to be created. -1. Once you get the `Deployment succeeded` notification, then click on `Go to resource` to navigate to the newly created App service. -1. Once the web site is created, locate it it in the **Dashboard** and click it to open **App Services** **Overview** screen. -1. From the **Overview** tab of the App Service, download the publish profile by clicking the **Get publish profile** link and save it. Other deployment mechanisms, such as from source control, can also be used. -1. Switch to Visual Studio and go to the TodoListService-aspnetcore-webapi project. Right click on the project in the Solution Explorer and select **Publish**. Click **Import Profile** on the bottom bar, and import the publish profile that you downloaded earlier. -1. Click on **Configure** and in the `Connection tab`, update the Destination URL so that it is a `https` in the home page url, for example [https://TodoListService-aspnetcore-webapi-contoso.azurewebsites.net](https://TodoListService-aspnetcore-webapi-contoso.azurewebsites.net). Click **Next**. -1. On the Settings tab, make sure `Enable Organizational Authentication` is NOT selected. Click **Save**. Click on **Publish** on the main screen. -1. Visual Studio will publish the project and automatically open a browser to the URL of the project. If you see the default web page of the project, the publication was successful. - -### Update the Active Directory tenant application registration for `TodoListService-aspnetcore-webapi` - -1. Navigate back to to the [Azure portal](https://portal.azure.com). -In the left-hand navigation pane, select the **Azure Active Directory** service, and then select **App registrations (Preview)**. -1. In the resultant screen, select the `TodoListService-aspnetcore-webapi` application. -1. From the *Branding* menu, update the **Home page URL**, to the address of your service, for example [https://TodoListService-aspnetcore-webapi-contoso.azurewebsites.net](https://TodoListService-aspnetcore-webapi-contoso.azurewebsites.net). Save the configuration. -1. Add the same URL in the list of values of the *Authentication -> Redirect URIs* menu. If you have multiple redirect urls, make sure that there a new entry using the App service's Uri for each redirect url. - -### Update the `TodoListClient-aspnetcore-webapi` to call the `TodoListService-aspnetcore-webapi` Running in Azure Web Sites - -1. In Visual Studio, go to the `TodoListClient-aspnetcore-webapi` project. -2. Open `Client\appsettings.json`. Only one change is needed - update the `todo:TodoListBaseAddress` key value to be the address of the website you published, - for example, [https://TodoListService-aspnetcore-webapi-contoso.azurewebsites.net](https://TodoListService-aspnetcore-webapi-contoso.azurewebsites.net). -3. Run the client! If you are trying multiple different client types (for example, .Net, Windows Store, Android, iOS) you can have them all call this one published web API. - -### Create and publish the `TodoListClient-aspnetcore-webapi` to an Azure Web Site - -1. Sign in to the [Azure portal](https://portal.azure.com). -1. Click `Create a resource` in the top left-hand corner, select **Web** --> **Web App**, and give your web site a name, for example, `TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net`. -1. Thereafter select the `Subscription`, `Resource Group`, `App service plan and Location`. `OS` will be **Windows** and `Publish` will be **Code**. -1. Click `Create` and wait for the App Service to be created. -1. Once you get the `Deployment succeeded` notification, then click on `Go to resource` to navigate to the newly created App service. -1. Once the web site is created, locate it in the **Dashboard** and click it to open **App Services** **Overview** screen. -1. From the **Overview** tab of the App Service, download the publish profile by clicking the **Get publish profile** link and save it. Other deployment mechanisms, such as from source control, can also be used. -1. Switch to Visual Studio and go to the TodoListClient-aspnetcore-webapi project. Right click on the project in the Solution Explorer and select **Publish**. Click **Import Profile** on the bottom bar, and import the publish profile that you downloaded earlier. -1. Click on **Configure** and in the `Connection tab`, update the Destination URL so that it is a `https` in the home page url, for example [https://TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net](https://TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net). Click **Next**. -1. On the Settings tab, make sure `Enable Organizational Authentication` is NOT selected. Click **Save**. Click on **Publish** on the main screen. -1. Visual Studio will publish the project and automatically open a browser to the URL of the project. If you see the default web page of the project, the publication was successful. - -### Update the Active Directory tenant application registration for `TodoListClient-aspnetcore-webapi` - -1. Navigate back to to the [Azure portal](https://portal.azure.com). -In the left-hand navigation pane, select the **Azure Active Directory** service, and then select **App registrations (Preview)**. -1. In the resultant screen, select the `TodoListClient-aspnetcore-webapi` application. -1. In the **Authentication** | page for your application, update the Logout URL fields with the address of your service, for example [https://TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net](https://TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net) -1. From the *Branding* menu, update the **Home page URL**, to the address of your service, for example [https://TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net](https://TodoListClient-aspnetcore-webapi-contoso.azurewebsites.net). Save the configuration. -1. Add the same URL in the list of values of the *Authentication -> Redirect URIs* menu. If you have multiple redirect urls, make sure that there a new entry using the App service's Uri for each redirect url. - -> NOTE: Remember, the To Do list is stored in memory in this TodoListService sample. Azure Web Sites will spin down your web site if it is inactive, and your To Do list will get emptied. -Also, if you increase the instance count of the web site, requests will be distributed among the instances. To Do will, therefore, not be the same on each instance. - -
diff --git a/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeUsingTheSample.md b/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeExploreTheSample.md similarity index 98% rename from 4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeUsingTheSample.md rename to 4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeExploreTheSample.md index c504a4d0..01470038 100644 --- a/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeUsingTheSample.md +++ b/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeExploreTheSample.md @@ -1,4 +1,4 @@ -## Using the sample +## Explore the sample
Expand to see how to use the sample diff --git a/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeHowTheCodeWasCreated.md b/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeHowTheCodeWasCreated.md index 0893ee52..026519ed 100644 --- a/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeHowTheCodeWasCreated.md +++ b/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeHowTheCodeWasCreated.md @@ -3,6 +3,47 @@
Expand the section +### Creating the Web API project (TodoListService) + +The code for the TodoListService was created in the following way: + +#### Step 1: Create the web api using the ASP.NET Core templates + +```Text +md TodoListService +cd TodoListService +dotnet new webapi -au=SingleOrg +``` + +1. Open the generated project (.csproj) in Visual Studio, and save the solution. + +#### Add a model (TodoListItem) and modify the controller + +In the TodoListService project, add a folder named `Models` and then create a new file named `TodoItem.cs`. Copy the contents of the TodoListService\Models\TodoItem.cs in this file. + +### Modify the Program.cs file to validate bearer access tokens received by the Web API + +Update `Program.cs` file : + + *replace the following code: + + ```CSharp +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + ``` + + with + + ```Csharp + services.AddMicrosoftIdentityWebApiAuthentication(Configuration); + ``` + +### Create the TodoListController and associated Models + +1. Add a reference to the ToDoListService. +1. Create a new Controller named `TodoListController` and copy and paste the code from the sample (TodoListService\Controllers\TodoListController.cs) to this controller. +1. Open the `appsettings.json` file and copy the keys from the sample's corresponding file under the `AzureAd` and `TodoList` sections. + ### Creating the client web app (TodoListClient) #### Step 1: the sample from the command line @@ -20,34 +61,27 @@ #### Step 2: Modify the generated code 1. Open the generated project (.csproj) in Visual Studio, and save the solution. -1. Add the `Microsoft.Identity.Web.csproj` project which is located at the root of this sample repo, to your solution (**Add Existing Project ...**). It's used to simplify signing-in and, in the next tutorial phases, to get a token. -1. Add a reference from your newly generated project to `Microsoft.Identity.Web` (right click on the **Dependencies** node under your new project, and choose **Add Reference ...**, and then in the projects tab find the `Microsoft.Identity.Web` project) - -1. Open the **Startup.cs** file and: - - * at the top of the file, add the following using directive were added: +1. Add the `Microsoft.Identity.Web` via Nuget. +1. Open the **Program.cs** file and: - ```CSharp - using Microsoft.Identity.Web; - ``` + * Replace the two following lines: - * in the `ConfigureServices` method, replace the two following lines: - - ```CSharp - services.AddAuthentication(AzureADDefaults.AuthenticationScheme) - .AddAzureAD(options => Configuration.Bind("AzureAd", options)); - ``` +```CSharp +services.AddAuthentication(AzureADDefaults.AuthenticationScheme) + .AddAzureAD(options => Configuration.Bind("AzureAd", options)); +``` - with these lines: +with these lines: - ```CSharp - services.AddMicrosoftIdentityWebAppAuthentication(Configuration) - .EnableTokenAcquisitionToCallDownstreamApi( - Configuration.GetSection("TodoList:TodoListScopes").Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries)) - .AddInMemoryTokenCaches(); - ``` +```CSharp +services.AddMicrosoftIdentityWebAppAuthentication(Configuration) + .EnableTokenAcquisitionToCallDownstreamApi( + Configuration.GetSection("TodoList:TodoListScopes").Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries) + ) + .AddInMemoryTokenCaches(); +``` - This enables your application to use the Microsoft identity platform endpoint. This endpoint is capable of signing-in users both with their Work and School and Microsoft Personal accounts. +* This enables your application to use the Microsoft identity platform endpoint. This endpoint is capable of signing-in users both with their Work and School and Microsoft Personal accounts. 1. Change the `Properties\launchSettings.json` file to ensure that you start your web app from as registered. For this: * update the `sslPort` of the `iisSettings` section to be `44321` @@ -59,7 +93,7 @@ services.AddTodoListService(Configuration); ``` -1. Open the `appsettings.json` file and copy the keys from the sample's corresponding file under the `AzureAd` and `TodoList` sections. +2. Open the `appsettings.json` file and copy the keys from the sample's corresponding file under the `AzureAd` and `TodoList` sections. #### Add a model (TodoListItem) and add the controller and views @@ -72,73 +106,14 @@ 1. Update the `configureServices` method in `startup.cs` to add the MSAL library and a token cache. ```CSharp - services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) - .AddMicrosoftIdentityWebApp(Configuration) - .EnableTokenAcquisitionToCallDownstreamApi(new string[] { Configuration["TodoList:TodoListScopes"] }) - .AddInMemoryTokenCaches(); + services.AddMicrosoftIdentityWebAppAuthentication(Configuration) + .EnableTokenAcquisitionToCallDownstreamApi( + Configuration.GetSection("TodoList:TodoListScopes").Get().Split(" ", System.StringSplitOptions.RemoveEmptyEntries) + ) + .AddInMemoryTokenCaches(); + + // Add APIs + services.AddTodoListService(Configuration); ``` -1. Update the `Configure` method to include **app.UseAuthentication();** before **app.UseAuthorization();** - - ```Csharp - app.UseAuthentication(); - app.AddAuthorization(); - ``` - -### Creating the Web API project (TodoListService) - -The code for the TodoListService was created in the following way: - -#### Step 1: Create the web api using the ASP.NET Core templates - -```Text -md TodoListService -cd TodoListService -dotnet new webapi -au=SingleOrg -``` - -1. Open the generated project (.csproj) in Visual Studio, and save the solution. - -#### Add a model (TodoListItem) and modify the controller - -In the TodoListService project, add a folder named `Models` and then create a new file named `TodoItem.cs`. Copy the contents of the TodoListService\Models\TodoItem.cs in this file. - -### Modify the Startup.cs file to validate bearer access tokens received by the Web API - -1. Add the `Microsoft.Identity.Web.csproj` project which is located at the root of this sample repo, to your solution (**Add Existing Project ...**). -1. Add a reference from your newly generated project to `Microsoft.Identity.Web` (right click on the **Dependencies** node under your new project, and choose **Add Reference ...**, and then in the projects tab find the `Microsoft.Identity.Web` project) -Update `Startup.cs` file : - - *Add the following two using statements - -```CSharp -using Microsoft.Identity.Web; -using Microsoft.Identity.Web.Client.TokenCacheProviders; -``` - - *In the `ConfigureServices` method, replace the following code: - - ```CSharp - services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme) - .AddAzureADBearer(options => Configuration.Bind("AzureAd", options)); - ``` - - with - - ```Csharp - services.AddMicrosoftIdentityWebApiAuthentication(Configuration); - ``` - - *Add the method **app.UseAuthentication()** before **app.UseAuthorization()** in the `Configure` method - - ```Csharp - app.UseAuthentication(); - app.UseAuthorization(); - ``` - -### Create the TodoListController.cs file - -1. Add a folder named `Models` and then create a new file named `TodoItem.cs`. Copy the contents of the TodoListClient\Models\TodoItem.cs in this file. -1. Create a new Controller named `TodoListController` and copy and paste the code from the sample (TodoListService\Controllers\TodoListController.cs) to this controller. - -
\ No newline at end of file +
diff --git a/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeLearnMore.md b/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeLearnMore.md index a0e532f7..820cb249 100644 --- a/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeLearnMore.md +++ b/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeLearnMore.md @@ -14,3 +14,4 @@ For more information, visit the following links: *[Introduction to Identity on ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/authentication/identity) *[AuthenticationBuilder](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder) *[Azure Active Directory with ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/authentication/azure-active-directory) + *[Protected web API: Verify scopes and app roles](https://docs.microsoft.com/azure/active-directory/develop/scenario-protected-web-api-verification-scope-app-roles?tabs=aspnetcore) \ No newline at end of file diff --git a/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeScenario.md b/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeScenario.md index a768dedb..2d7114a5 100644 --- a/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeScenario.md +++ b/4-WebApp-your-API/4-1-MyOrg/ReadmeFiles/ReadmeScenario.md @@ -1,4 +1,4 @@  This sample demonstrates an ASP.NET Core client Web App calling an ASP.NET Core Web API that is secured using Azure AD. - 1. The client ASP.NET Core Web App uses the [Microsoft.Identity.Web](https://aka.ms/microsoft-identity-web) to sign-in and obtain a JWT [Access Tokens](https://docs.microsoft.com/azure/active-directory/develop/access-tokens) from **Azure AD**. - 1. The [Access Tokens](https://docs.microsoft.com/azure/active-directory/develop/access-tokens) is used as a bearer token to authorize the user to call the ASP.NET Core Web API protected by **Azure AD**. + 1. The client ASP.NET Core Web App uses the [Microsoft.Identity.Web](https://aka.ms/microsoft-identity-web) to sign-in a user and obtain a JWT [Access Token](https://aka.ms/access-tokens) from **Azure AD**. + 2. The service uses the [Microsoft.Identity.Web](https://aka.ms/microsoft-identity-web) to protect the Web api, check permissions and validate tokens. diff --git a/4-WebApp-your-API/4-1-MyOrg/TodoListService/Controllers/TodoListController.cs b/4-WebApp-your-API/4-1-MyOrg/TodoListService/Controllers/TodoListController.cs index 3a747391..7a5850c2 100644 --- a/4-WebApp-your-API/4-1-MyOrg/TodoListService/Controllers/TodoListController.cs +++ b/4-WebApp-your-API/4-1-MyOrg/TodoListService/Controllers/TodoListController.cs @@ -4,13 +4,19 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Identity.Web; using Microsoft.Identity.Web.Resource; using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Net.Http; using TodoListService.Models; namespace TodoListService.Controllers { + /// + /// This is an example of ToDo list controller that serves requests from client apps that sign-in users and as themselves (client credentials flow). + /// [Authorize] [Route("api/[controller]")] public class TodoListController : Controller @@ -18,74 +24,194 @@ public class TodoListController : Controller // In-memory TodoList private static readonly Dictionary TodoStore = new Dictionary(); + // This is needed to get access to the internal HttpContext.User, if available. private readonly IHttpContextAccessor _contextAccessor; + private const string _todoListReadScope = "ToDoList.Read"; + private const string _todoListReadWriteScope = "ToDoList.ReadWrite"; + private const string _todoListReadAllPermission = "ToDoList.Read.All"; + private const string _todoListReadWriteAllPermission = "ToDoList.ReadWrite.All"; + + /// + /// We store the object id of the user derived from the presented Access token + /// + private string _currentLoggedUser = string.Empty; + + /// + /// The API controller that manages an instance of ToDo list + /// + /// public TodoListController(IHttpContextAccessor contextAccessor) { _contextAccessor = contextAccessor; - // Pre-populate with sample data - if (TodoStore.Count == 0) + // We seek the details of the user represented by the access token presented to this API, can be empty + /** + * The 'oid' (object id) is the only claim that should be used to uniquely identify + * a user in an Azure AD tenant. The token might have one or more of the following claim, + * that might seem like a unique identifier, but is not and should not be used as such, + * especially for systems which act as system of record (SOR): + * + * - upn (user principal name): might be unique amongst the active set of users in a tenant but + * tend to get reassigned to new employees as employees leave the organization and + * others take their place or might change to reflect a personal change like marriage. + * + * - email: might be unique amongst the active set of users in a tenant but tend to get + * reassigned to new employees as employees leave the organization and others take their place. + **/ + + _currentLoggedUser = _contextAccessor?.HttpContext?.User?.GetObjectId(); + + if (!string.IsNullOrWhiteSpace(_currentLoggedUser)) { - TodoStore.Add(1, new Todo() { Id = 1, Owner = $"{_contextAccessor.HttpContext.User.Identity.Name}", Title = "Pick up groceries" }); - TodoStore.Add(2, new Todo() { Id = 2, Owner = $"{_contextAccessor.HttpContext.User.Identity.Name}", Title = "Finish invoice report" }); + // Pre-populate with sample data + if (TodoStore.Count == 0 && !string.IsNullOrEmpty(_currentLoggedUser)) + { + TodoStore.Add(1, new Todo() { Id = 1, Owner = $"{_currentLoggedUser}", Title = "Pick up groceries" }); + TodoStore.Add(2, new Todo() { Id = 2, Owner = $"{_currentLoggedUser}", Title = "Finish invoice report" }); + TodoStore.Add(3, new Todo() { Id = 3, Owner = "Fake id of another User", Title = "Rent a car" }); + TodoStore.Add(4, new Todo() { Id = 4, Owner = "made up id of another ", Title = "Get vaccinated" }); + } } } + /// + /// Indicates of the AT presented was for a app or not. + /// + /// + private bool IsAppOnlyToken() + { + // Add in the optional 'idtyp' claim to check if the access token is coming from an application or user. + // + // See: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims + return HttpContext.User.Claims.Any(c => c.Type == "idtyp" && c.Value == "app"); + } + // GET: api/values - [HttpGet] - [RequiredScope(new string[] { "ToDoList.Read", "ToDoList.Write" })] + /// + /// Returns todo list items in a list + /// + /// + [HttpGet()] + [RequiredScopeOrAppPermission( + AcceptedScope = new string[] { _todoListReadScope, _todoListReadWriteScope }, + AcceptedAppPermission = new string[] { _todoListReadAllPermission, _todoListReadWriteAllPermission } + )] public IEnumerable Get() { - string owner = User.Identity.Name; - return TodoStore.Values.Where(x => x.Owner == owner); + if (!IsAppOnlyToken()) + { + // this is a request for all ToDo list items of a certain user. + return TodoStore.Values.Where(x => x.Owner == _currentLoggedUser); + } + else + { + // Its an app calling with app permissions, so return all items across all users + return TodoStore.Values; + } } // GET: api/values [HttpGet("{id}", Name = "Get")] - [RequiredScope(new string[] { "ToDoList.Read", "ToDoList.Write" })] + [RequiredScopeOrAppPermission( + AcceptedScope = new string[] { _todoListReadScope, _todoListReadWriteScope }, + AcceptedAppPermission = new string[] { _todoListReadAllPermission, _todoListReadWriteAllPermission })] public Todo Get(int id) { - return TodoStore.Values.FirstOrDefault(t => t.Id == id); + //if it only has delegated permissions + //then it will be t.id==id && x.Owner == owner + //if it has app permissions the it will return t.id==id + + if (!IsAppOnlyToken()) + { + return TodoStore.Values.FirstOrDefault(t => t.Id == id && t.Owner == _currentLoggedUser); + } + else + { + return TodoStore.Values.FirstOrDefault(t => t.Id == id); + } } + // DELETE: TodoList/Delete/5 [HttpDelete("{id}")] - [RequiredScope("ToDoList.Write")] + [RequiredScopeOrAppPermission( + AcceptedScope = new string[] { _todoListReadWriteScope }, + AcceptedAppPermission = new string[] { _todoListReadWriteAllPermission })] public void Delete(int id) { - TodoStore.Remove(id); + if (!IsAppOnlyToken()) + { + // only delete if the ToDo list item belonged to this user + if (TodoStore.Values.Any(x => x.Id == id && x.Owner == _currentLoggedUser)) + { + TodoStore.Remove(id); + } + } + else + { + TodoStore.Remove(id); + } } - // POST api/values + // POST: TodoList/Create [HttpPost] - [RequiredScope("ToDoList.Write")] + [RequiredScopeOrAppPermission( + AcceptedScope = new string[] { _todoListReadWriteScope }, + AcceptedAppPermission = new string[] { _todoListReadWriteAllPermission })] public IActionResult Post([FromBody] Todo todo) { - int id = TodoStore.Values.OrderByDescending(x => x.Id).FirstOrDefault().Id + 1; - Todo todonew = new Todo() { Id = id, Owner = HttpContext.User.Identity.Name, Title = todo.Title }; - TodoStore.Add(id, todonew); + if (IsAppOnlyToken()) + { + if (string.IsNullOrEmpty(todo.Owner)) + { + var msg = new HttpResponseMessage(HttpStatusCode.BadRequest) + { + ReasonPhrase = "The owner's objectid was not provided in the ToDo list item payload" + }; - return Ok(todo); + return BadRequest(msg); + } + } + else + { + // The signed-in user becomes the owner + todo.Owner = _currentLoggedUser; + } + + int nextid = TodoStore.Values.OrderByDescending(x => x.Id).FirstOrDefault().Id + 1; + + todo.Id = nextid; + TodoStore.Add(nextid, todo); + return Created($"/todo/{nextid}", todo); } - // PATCH api/values [HttpPatch("{id}")] - [RequiredScope("ToDoList.Write")] + [RequiredScopeOrAppPermission( + AcceptedScope = new string[] { _todoListReadWriteScope }, + AcceptedAppPermission = new string[] { _todoListReadWriteAllPermission })] public IActionResult Patch(int id, [FromBody] Todo todo) { - if (id != todo.Id) + Todo existingToDo = TodoStore.Values.FirstOrDefault(x => x.Id == id); + + if (id != todo.Id || existingToDo == null) { return NotFound(); } - if (TodoStore.Values.FirstOrDefault(x => x.Id == id) == null) + if (!IsAppOnlyToken()) { - return NotFound(); + // a user can only modify their own ToDos + if (existingToDo.Owner != _currentLoggedUser) + { + return Unauthorized(); + } + + // Overwrite ownership, just in case + todo.Owner = _currentLoggedUser; } TodoStore.Remove(id); TodoStore.Add(id, todo); - return Ok(todo); } } diff --git a/4-WebApp-your-API/4-1-MyOrg/TodoListService/Filters/CustomExceptionFilter.cs b/4-WebApp-your-API/4-1-MyOrg/TodoListService/Filters/CustomExceptionFilter.cs new file mode 100644 index 00000000..05cdc87d --- /dev/null +++ b/4-WebApp-your-API/4-1-MyOrg/TodoListService/Filters/CustomExceptionFilter.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Net; +using System; + +namespace TodoListService.Filters +{ + /// + /// Custom exception filter to correctly sent Unauthorized Http response (and some more) + /// + /// + /// + public class CustomExceptionFilter : IExceptionFilter + { + public void OnException(ExceptionContext context) + { + HttpStatusCode status = HttpStatusCode.InternalServerError; + String message = String.Empty; + + var exceptionType = context.Exception.GetType(); + if (exceptionType == typeof(UnauthorizedAccessException)) + { + message = "Unauthorized Access"; + status = HttpStatusCode.Unauthorized; + } + else if (exceptionType == typeof(NotImplementedException)) + { + message = "A server error occurred."; + status = HttpStatusCode.NotImplemented; + } + else + { + message = context.Exception.Message; + status = HttpStatusCode.NotFound; + } + context.ExceptionHandled = true; + + HttpResponse response = context.HttpContext.Response; + response.StatusCode = (int)status; + response.ContentType = "application/json"; + var err = message + " " + context.Exception.StackTrace; + response.WriteAsync(err); + } + } +} diff --git a/4-WebApp-your-API/4-1-MyOrg/TodoListService/Models/TodoItem.cs b/4-WebApp-your-API/4-1-MyOrg/TodoListService/Models/TodoItem.cs index 8a40eb3e..96190e19 100644 --- a/4-WebApp-your-API/4-1-MyOrg/TodoListService/Models/TodoItem.cs +++ b/4-WebApp-your-API/4-1-MyOrg/TodoListService/Models/TodoItem.cs @@ -1,5 +1,8 @@ namespace TodoListService.Models { + /// + /// Data object to transfer information between client/server/database + /// public class Todo { public int Id { get; set; } diff --git a/4-WebApp-your-API/4-1-MyOrg/TodoListService/Startup.cs b/4-WebApp-your-API/4-1-MyOrg/TodoListService/Startup.cs index 2f1294a1..f29d743f 100644 --- a/4-WebApp-your-API/4-1-MyOrg/TodoListService/Startup.cs +++ b/4-WebApp-your-API/4-1-MyOrg/TodoListService/Startup.cs @@ -7,6 +7,15 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Identity.Web; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.Logging; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System.Linq; +using System.Net; +using System; +using Microsoft.AspNetCore.Mvc.Filters; +using TodoListService.Filters; +using System.Threading.Tasks; namespace TodoListService { @@ -26,11 +35,65 @@ 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 protect this Api services.AddMicrosoftIdentityWebApiAuthentication(Configuration); + + //// Comment the lines of code above and uncomment the following section if you would like to limit calls to this API to just a set of client apps + //// The following is an example of extended token validation + /** + * The example below can be used do extended token validation and check for additional claims, such as: + * -check if the caller's tenant is in the allowed tenants list via the 'tid' claim (for multi-tenant applications) + * -check if the caller's account is homed or guest via the 'acct' optional claim + * -check if the caller belongs to right roles or groups via the 'roles' or 'groups' claim, respectively + * + * For more information, visit: https://docs.microsoft.com/azure/active-directory/develop/access-tokens#validate-the-user-has-permission-to-access-this-data + **/ + + /** + * Also look up Policy-based authorization in ASP.NET Core(https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) + */ + + //services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + // .AddMicrosoftIdentityWebApi(options => + // { + // Configuration.Bind("AzureAd", options); + // options.Events = new JwtBearerEvents(); + // options.Events.OnTokenValidated = async context => + // { + // string[] allowedClientApps = + // { + // /* list of client ids to allow */ + // }; + // string clientappId = context?.Principal?.Claims + // .FirstOrDefault(x => x.Type == "azp" || x.Type == "appid")?.Value; + + // if (!allowedClientApps.Contains(clientappId)) + // { + // throw new UnauthorizedAccessException("The client app is not permitted to access this API"); + // } + + // await Task.CompletedTask; + // }; + + // }, options => + // { + // Configuration.Bind("AzureAd", options); + // }); + + + // The following flag can be used to get more descriptive errors in development environments + // Enable diagnostic logging to help with troubleshooting. For more details, see https://aka.ms/IdentityModel/PII. + // You might not want to keep this following flag on for production + IdentityModelEventSource.ShowPII = true; + + services.AddMvc(options => + { + options.Filters.Add(); + }); + services.AddControllers(); } @@ -51,7 +114,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) } app.UseHttpsRedirection(); - + app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); @@ -60,6 +123,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); }); + + } } } \ No newline at end of file