Skip to content

Commit df985c7

Browse files
author
Kalyan Krishna
authored
Merge pull request #602 from Azure-Samples/aremo-ms/Task-1924572-4-1-basher-update
Task 1924572 Sample 4-1 basher update
2 parents 5881ab6 + e03d1a7 commit df985c7

39 files changed

+1856
-935
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,4 @@ packages
136136
/4-WebApp-your-API/4-1-MyOrg/.vscode/settings.json
137137
/5-WebApp-AuthZ/5-1-Roles/.vscode/settings.json
138138
/.vscode
139+
/2-WebApp-graph-user/2-1-Call-MSGraph/Properties/PublishProfiles

2-WebApp-graph-user/2-1-Call-MSGraph/ReadmeFiles/AboutTheCode.md

Whitespace-only changes.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
### Process the CAE challenge from Microsoft Graph
2+
3+
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:
4+
5+
1. Inject and instance of `MicrosoftIdentityConsentAndConditionalAccessHandler` in the controller constructor. The beginning of the HomeController becomes:
6+
7+
```CSharp
8+
public class HomeController : Controller
9+
{
10+
private readonly ILogger<HomeController> _logger;
11+
private readonly GraphServiceClient _graphServiceClient;
12+
private readonly MicrosoftIdentityConsentAndConditionalAccessHandler _consentHandler;
13+
private string[] _graphScopes = new[] { "user.read" };
14+
public HomeController(ILogger<HomeController> logger,
15+
IConfiguration configuration,
16+
GraphServiceClient graphServiceClient,
17+
MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler)
18+
{
19+
_logger = logger;
20+
_graphServiceClient = graphServiceClient;
21+
this._consentHandler = consentHandler;
22+
// Capture the Scopes for Graph that were used in the original request for an Access token (AT) for MS Graph as
23+
// they'd be needed again when requesting a fresh AT for Graph during claims challenge processing
24+
_graphScopes = configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
25+
}
26+
27+
// more code here
28+
```
29+
1. The process to handle CAE challenges from MS Graph comprises of the following steps:
30+
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:
31+
```CSharp
32+
currentUser = await _graphServiceClient.Me.Request().GetAsync();
33+
```
34+
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.
35+
```CSharp
36+
try
37+
{
38+
currentUser = await _graphServiceClient.Me.Request().GetAsync();
39+
}
40+
// Catch CAE exception from Graph SDK
41+
catch (ServiceException svcex) when (svcex.Message.Contains("Continuous access evaluation resulted in claims challenge"))
42+
{
43+
try
44+
{
45+
Console.WriteLine($"{svcex}");
46+
string claimChallenge = WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(svcex.ResponseHeaders);
47+
_consentHandler.ChallengeUser(_graphScopes, claimChallenge);
48+
return new EmptyResult();
49+
}
50+
catch (Exception ex2)
51+
{
52+
_consentHandler.HandleException(ex2);
53+
}
54+
}
55+
```
56+
57+
The `AuthenticationHeaderHelper` class is available from the `Helpers\AuthenticationHeaderHelper.cs file`.
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
## Deployment
2+
3+
### Deploying web app to Azure App Services
4+
5+
There is one web app in this sample. To deploy it to **Azure App Services**, you'll need to:
6+
7+
- create an **Azure App Service**
8+
- publish the projects to the **App Services**, and
9+
- update its client(s) to call the website instead of the local environment.
10+
11+
#### Publish your files
12+
13+
##### Publish using Visual Studio
14+
15+
Follow the link to [Publish with Visual Studio](https://docs.microsoft.com/visualstudio/deployment/quickstart-deploy-to-azure).
16+
17+
##### Publish using Visual Studio Code
18+
19+
1. Open an instance of Visual Studio code set to the `WebApp-OpenIDConnect-DotNet-graph-v2` project folder.
20+
1. Install the VS Code extension [Azure App Service](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azureappservice).
21+
1. Using the extension you just installed, sign in to **Azure App Service** using your Azure AD account.
22+
1. Choose `Terminal > New Terminal` from the VS Code menu to open a new terminal window in the project directory.
23+
1. Run the following command
24+
25+
```console
26+
dotnet publish WebApp-OpenIDConnect-DotNet-graph.csproj --configuration Release
27+
```
28+
29+
1. A `publish` folder is created within the following folder: `bin/Release/netcoreapp3.1/`.
30+
1. From the VS Code file explorer, right-click on the **publish** folder and select **Deploy to Web App**.
31+
1. Select **Create New Web App**.
32+
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`.
33+
1. Select **Windows** as the OS. Press Enter.
34+
1. Select **.NET Core 3.1 (LTS)** as runtime stack.
35+
1. Select `Free` or any other option for your pricing tier.
36+
37+
#### Update the Azure AD app registration (WebApp-OpenIDConnect-DotNet-graph-v2)
38+
39+
1. Navigate back to to the [Azure portal](https://portal.azure.com).
40+
1. Go to the **Azure Active Directory** section, and then select **App registrations**.
41+
1. In the resulting screen, select the `WebApp-OpenIDConnect-DotNet-graph-v2` application.
42+
1. In the app's registration screen, select **Authentication** in the menu.
43+
- 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:
44+
- `https://example-domain.azurewebsites.net/`
45+
- `https://example-domain.azurewebsites.net/signin-oidc`
46+
1. Update the **Front-channel logout URL** fields with the address of your service, for example `https://example-domain.azurewebsites.net`.
47+
48+
> :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.
49+
50+
### Enabling your code to get secrets from Key Vault using Managed Identity
51+
52+
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.
53+
54+
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).
55+
56+
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)
57+
58+
#### Set up your Managed Identity
59+
60+
1. Navigate to [Azure portal](https://portal.azure.com) and select the **Azure App Service**.
61+
1. Find and select the App Service you've created previously.
62+
1. On App Service portal, select **Identity**.
63+
1. Within the **System assigned** tab, switch **Status** to **On**. Click **Save**.
64+
1. Record the **Object Id** that will appear, as you will need it in the next step.
65+
66+
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)
67+
68+
#### Set up your Key vault
69+
70+
Before starting here, make sure:
71+
72+
- You have an [Azure Subscription](https://azure.microsoft.com/free/).
73+
- 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.
74+
- Follow the guide to [create an Azure Key Vault](https://docs.microsoft.com/azure/key-vault/general/quick-create-portal).
75+
76+
##### Upload your secret to KeyVault
77+
78+
1. Navigate to your new key vault in the Azure portal.
79+
1. On the Key Vault settings pages, select **Secrets**.
80+
1. Click on **Generate/Import**.
81+
1. On the **Create a secret** screen choose the following values:
82+
- **Upload options**: Manual.
83+
- **Name**: Type a name for the secret. The secret name must be unique within a Key Vault. For example, `myClientSecret`
84+
- **Value**: Copy and paste the value for the `ClientSecret` property (without quotes!) from your `appsettings.json` file.
85+
- Leave the other values to their defaults. Click **Create**.
86+
87+
##### Provide the managed identity access to Key Vault
88+
89+
1. Navigate to your Key Vault in the portal.
90+
1. Select **Overview** > **Access policies**.
91+
1. Click on **Add Access Policy**.
92+
1. In the input box for **Secret permissions**, select **Get**.
93+
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.
94+
1. Click on **OK** to add the new Access Policy, then click **Save** to save the Access Policy.
95+
96+
#### Modify your code to connect to Key Vault
97+
98+
1. In the `appsettings.json` file, find and delete the `ClientSecret` property and its value.
99+
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/`
100+
1. Add the `Azure.Identity` NuGet package to the solution. This sample project has already added this package.
101+
1. Add the following directives to your `startup.cs`.
102+
</p>
103+
:information_source: this has been already added in the sample project.
104+
105+
```CSharp
106+
using Azure;
107+
using Azure.Identity;
108+
using Azure.Security.KeyVault.Secrets;
109+
```
110+
111+
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.
112+
</p>
113+
:information_source: this has already been added in the sample project.
114+
115+
```CSharp
116+
private string GetSecretFromKeyVault(string tenantId, string secretName)
117+
{
118+
// this should point to your vault's URI, like https://<yourkeyvault>.vault.azure.net/
119+
string uri = Environment.GetEnvironmentVariable("KEY_VAULT_URI");
120+
DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions();
121+
122+
// Specify the tenant ID to use the dev credentials when running the app locally
123+
options.VisualStudioTenantId = tenantId;
124+
options.SharedTokenCacheTenantId = tenantId;
125+
SecretClient client = new SecretClient(new Uri(uri), new DefaultAzureCredential(options));
126+
127+
// The secret name, for example if the full url to the secret is https://<yourkeyvault>.vault.azure.net/secrets/Graph-App-Secret
128+
Response<KeyVaultSecret> secret = client.GetSecretAsync(secretName).Result;
129+
130+
return secret.Value.Value;
131+
}
132+
```
133+
134+
6. In your `Startup.cs` file, find the `ConfigureServices` method. Add the following code to call the GetSecretFromKeyVault method, right after `services.AddAuthentication`.
135+
</p>
136+
:information_source: In the sample project, this code is present but commented out by default. Uncomment it.
137+
</p>
138+
: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`.
139+
140+
```CSharp
141+
// uncomment the following 3 lines to get ClientSecret from KeyVault
142+
string tenantId = Configuration.GetValue<string>("AzureAd:TenantId");
143+
services.Configure<MicrosoftIdentityOptions>(
144+
options => { options.ClientSecret = GetSecretFromKeyVault(tenantId, "ENTER_YOUR_SECRET_NAME_HERE"); });
145+
```
146+
147+
7. Your `ConfigureServices` method should now look like the following snippet:
148+
149+
```CSharp
150+
public void ConfigureServices(IServiceCollection services)
151+
{
152+
string[] initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
153+
154+
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
155+
.AddMicrosoftIdentityWebApp(Configuration)
156+
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
157+
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
158+
.AddInMemoryTokenCaches();
159+
160+
// uncomment the following 3 lines to get ClientSecret from KeyVault
161+
string tenantId = Configuration.GetValue<string>("AzureAd:TenantId");
162+
services.Configure<MicrosoftIdentityOptions>(
163+
options => { options.ClientSecret = GetSecretFromKeyVault(tenantId, "myClientSecret"); });
164+
165+
// ... more method code continues below
166+
}
167+
```
168+
169+
8. Add an environment variable to your App Service so your web app can find its key vault.
170+
171+
1. Go to the [Azure portal](https://portal.azure.com). Search for and select **App Service**, and then select your app.
172+
1. Select **Configuration** blade on the left, then select **New Application Settings**.
173+
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/`
174+
175+
1. Re-deploy your project to Azure App Service.
176+
177+
1. Run the following command:
178+
179+
```console
180+
dotnet publish WebApp-OpenIDConnect-DotNet-graph.csproj --configuration Release
181+
```
182+
183+
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.
184+
185+
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.
186+
187+
## Optional - Handle Continuous Access Evaluation (CAE) challenge from Microsoft Graph
188+
189+
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).
190+
191+
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:
192+
193+
1. Declaring that the client app is capable of handling claims challenges from the web API.
194+
2. Processing these challenges when thrown.
195+
196+
### Declare the CAE capability in the configuration
197+
198+
This sample declares that it's CAE-capable by adding a `ClientCapabilities` property in the configuration, whose value is `[ "cp1" ]`.
199+
200+
```Json
201+
{
202+
"AzureAd": {
203+
// ...
204+
// the following is required to handle Continuous Access Evaluation challenges
205+
"ClientCapabilities": [ "cp1" ],
206+
// ...
207+
},
208+
// ...
209+
}
210+
```
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
## Explore the sample
2+
3+
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.
4+
2. Provide consent to the screen presented.
5+
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.
6+
7+
> 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.
8+
9+
> [Consider taking a moment to share your experience with us.](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbRz0h_jLR5HNJlvkZAewyoWxUNEFCQ0FSMFlPQTJURkJZMTRZWVJRNkdRMC4u)
10+
11+
## About The code
12+
13+
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.
14+
15+
2. Starting with the **Startup.cs** file :
16+
17+
- at the top of the file, the following two using directives were added:
18+
19+
```CSharp
20+
using Microsoft.Identity.Web;
21+
using Microsoft.Identity.Web.UI;
22+
```
23+
24+
- in the `ConfigureServices` method, the following code was added, replacing any existing `AddAuthentication()` code:
25+
26+
```CSharp
27+
28+
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
29+
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
30+
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
31+
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
32+
.AddInMemoryTokenCaches();
33+
34+
```
35+
36+
`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).
37+
38+
`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.
39+
40+
3. In the `Controllers\HomeController.cs` file, the following code is added to allow calling MS Graph:
41+
42+
```CSharp
43+
private readonly ILogger<HomeController> _logger;
44+
private readonly GraphServiceClient _graphServiceClient;
45+
46+
private readonly GraphServiceClient _graphServiceClient;
47+
public HomeController(ILogger<HomeController> logger,
48+
IConfiguration configuration,
49+
GraphServiceClient graphServiceClient)
50+
{
51+
_logger = logger;
52+
_graphServiceClient = graphServiceClient;
53+
this._consentHandler = consentHandler;
54+
}
55+
```
56+
57+
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.
58+
59+
```CSharp
60+
[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
61+
public async Task<IActionResult> Profile()
62+
{
63+
var me = await _graphServiceClient.Me.Request().GetAsync();
64+
ViewData["Me"] = me;
65+
66+
try
67+
{
68+
// Get user photo
69+
using (var photoStream = await _graphServiceClient.Me.Photo.Content.Request().GetAsync())
70+
{
71+
byte[] photoByte = ((MemoryStream)photoStream).ToArray();
72+
ViewData["Photo"] = Convert.ToBase64String(photoByte);
73+
}
74+
}
75+
catch (System.Exception)
76+
{
77+
ViewData["Photo"] = null;
78+
}
79+
80+
return View();
81+
}
82+
```
83+
84+
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`:
85+
- update the `sslPort` of the `iisSettings` section to be `44321`
86+
- update the `applicationUrl` property to `https://localhost:44321`

0 commit comments

Comments
 (0)