Skip to content

Commit 905bbee

Browse files
committed
Add authorization to App API
1 parent a9a31a7 commit 905bbee

File tree

4 files changed

+212
-5
lines changed

4 files changed

+212
-5
lines changed

articles/active-directory/develop/TOC.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@
139139
href: scenario-protected-web-api-app-registration.md
140140
- name: Code configuration
141141
href: scenario-protected-web-api-app-configuration.md
142+
name: Verification of scopes or app roles
143+
href: scenario-protected-web-api-verification-scope-app-roles.md
142144
- name: Move to production
143145
href: scenario-protected-web-api-production.md
144146
- name: Web API that calls web APIs

articles/active-directory/develop/scenario-protected-web-api-app-configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,4 @@ The validators are all associated with properties of the `TokenValidationParamet
156156
## Next steps
157157

158158
> [!div class="nextstepaction"]
159-
> [Move to production](scenario-protected-web-api-production.md)
159+
> [Move to production](scenario-protected-web-api-verification-scope-app-roles.md)

articles/active-directory/develop/scenario-protected-web-api-app-registration.md

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,19 @@ Scopes are usually of the form `resourceURI/scopeName`. For Microsoft Graph, the
5757
During app registration, you'll need to define the following parameters:
5858

5959
- One resource URI - By default the application registration portal recommends that you to use `api://{clientId}`. This resource URI is unique, but it's not human readable. You can change it, but make sure that it's unique.
60-
- One or several scopes
60+
- One or several **scopes** (that client applications will refer to as **delegated permissions** for your Web API)
61+
- One or several **app roles** (that client applications will refer to as **application permissions** for your Web API)
6162

62-
The scopes are also displayed on the consent screen that's presented to end-users who use your application. Therefore, you'll need to provide the corresponding strings that describe the scope:
63+
The scopes are also displayed on the consent screen that's presented to end users who use your application. Therefore, you'll need to provide the corresponding strings that describe the scope:
6364

6465
- As seen by the end user
6566
- As seen by the tenant admin, who can grant admin consent
6667

67-
### How to expose the API
68+
### How to expose delegated permissions (scopes)
6869

6970
1. Select the **Expose an API** section in the application registration, and:
7071
1. Select **Add a scope**.
71-
1. Accept the proposed Application ID URI (api://{clientId}) by selecting **Save and Continue**.
72+
1. If requested, accept the proposed Application ID URI (api://{clientId}) by selecting **Save and Continue**.
7273
1. Enter the following parameters:
7374
- For **Scope name**, use `access_as_user`.
7475
- For **Who can consent**, make sure the **Admins and users** option is selected.
@@ -79,6 +80,59 @@ The scopes are also displayed on the consent screen that's presented to end-user
7980
- Keep **State** set to **Enabled**.
8081
- Select **Add scope**.
8182

83+
### Case where your Web API is called by daemon application
84+
85+
In this paragraph, you'll learn how to register your protected Web API so that it can be called securely by daemon applications:
86+
87+
- you'll need to expose application permissions
88+
- tenant admins may require AAD to acquire tokens for your Web App only for registered applications;
89+
90+
#### How to expose application permissions (app roles)
91+
92+
To Expose application permissions, you'll need to edit the manifest.
93+
94+
1. In the application registration for your application, click **Manifest**.
95+
1. Edit the manifest by locating the `appRoles` setting and adding one or several application roles. The role definition is provided in the JSON block below. Leave the `allowedMemberTypes` to "Application" only.
96+
1. Save the manifest.
97+
98+
The content of `appRoles` should be the following (the `id` can be any unique GUID)
99+
100+
```JSon
101+
"appRoles": [
102+
{
103+
"allowedMemberTypes": [ "Application" ],
104+
"description": "Accesses the TodoListService-Cert as an application.",
105+
"displayName": "access_as_application",
106+
"id": "ccf784a6-fd0c-45f2-9c08-2f9d162a0628",
107+
"isEnabled": true,
108+
"lang": null,
109+
"origin": "Application",
110+
"value": "access_as_application"
111+
}
112+
],
113+
```
114+
115+
#### How to ensure that Azure AD issues tokens for your Web API only to allowed clients
116+
117+
The Web API tests for the app role (that's the developer way of doing it). But you can even ask Azure Active Directory to issue a token for your Web API only to applications that were approved by the tenant admin. To add this additional security:
118+
119+
1. On the app **Overview** page for your app registration, select the hyperlink with the name of your application in **Managed application in local directory**. The title for this field can be truncated. You could, for instance, read: `Managed application in ...`)
120+
121+
> !INFO
122+
>
123+
> When you select this link you will navigate to the **Enterprise Application Overview** page associated with the service principal for your application in the tenant where you created it. You can navigate back to the app registration page by using the back button of your browser.
124+
125+
1. Select the **Properties** page in the **Manage** section of the Enterprise application pages
126+
1. If you want AAD to enforce access to your Web API from only certain clients, set **User assignment required?** to **Yes**.
127+
128+
> ! IMPORTANT
129+
>
130+
> By setting **User assignment required?** to **Yes**, AAD will check the app role assignments of the clients when they request an access token for the Web API. If the client was not be assigned to any AppRoles, AAD would just return `invalid_client: AADSTS501051: Application xxxx is not assigned to a role for the xxxx`
131+
>
132+
> If you keep **User assignment required?** to **No**, <span style='background-color:yellow; display:inline'>Azure AD won’t check the app role assignments when a client requests an access token to your Web API</span>. Therefore, any daemon client (that is any client using client credentials flow) would still be able to obtain the access token for the Web API just by specifying its audience. Any application, would be able to access the API without having to request permissions for it. Now this is not then end of it, as your Web API can always, as is done in this sample, verify that the application has the right role (which was authorized by the tenant admin), by validating that the access token has a `roles` claim, and the right value for this claim (in our case `access_as_application`).
133+
134+
1. Select **Save**
135+
82136
## Next steps
83137

84138
> [!div class="nextstepaction"]
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
---
2+
title: Protected web API - app code configuration | Azure
3+
description: Learn how to build a protected Web API and configure your application's code.
4+
services: active-directory
5+
documentationcenter: dev-center-name
6+
author: jmprieur
7+
manager: CelesteDG
8+
editor: ''
9+
10+
ms.service: active-directory
11+
ms.subservice: develop
12+
ms.devlang: na
13+
ms.topic: conceptual
14+
ms.tgt_pltfrm: na
15+
ms.workload: identity
16+
ms.date: 05/07/2019
17+
ms.author: jmprieur
18+
ms.custom: aaddev
19+
#Customer intent: As an application developer, I want to know how to write a protected Web API using the Microsoft identity platform for developers.
20+
ms.collection: M365-identity-device-management
21+
---
22+
23+
# Protected web API - adding authorization to your API.
24+
25+
This article describes how you can add authorization to your Web API. This protection ensures that it's only called by:
26+
27+
- applications on behalf of users with the right scopes
28+
- or by daemon apps with the right application roles.
29+
30+
For an ASP.NET / ASP.NET Core Web API to be protected, you'll need to add the `[Authorize]` attribute on:
31+
32+
- the Controller itself if you want all the actions of the controller to be protected
33+
- the individual controller action for your API, otherwise.
34+
35+
```CSharp
36+
[Authorize]
37+
public class TodoListController : Controller
38+
{
39+
...
40+
}
41+
```
42+
43+
But this protection isn't enough. It only guaranties that ASP.NET / ASP.NET Core will validate the token. Your API needs to verify that the token used to call your Web API was requested with the claims it expects, in particular:
44+
45+
- the **scopes** if the API is called on behalf of a user
46+
- the **app roles** if the API can be called from a daemon app.
47+
48+
## Verifying scopes in APIs called on behalf of users
49+
50+
If your API is called by a client app on behalf of a user, then it needs to request a bearer token with specific scopes for the API (see [Code configuration | Bearer token](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-protected-web-api-app-configuration#bearer-token))
51+
52+
```CSharp
53+
[Authorize]
54+
public class TodoListController : Controller
55+
{
56+
/// <summary>
57+
/// The Web API will only accept tokens 1) for users, 2) having the `access_as_user` scope for
58+
/// this API
59+
/// </summary>
60+
const string scopeRequiredByAPI = "access_as_user";
61+
62+
// GET: api/values
63+
[HttpGet]
64+
public IEnumerable<TodoItem> Get()
65+
{
66+
VerifyUserHasAnyAcceptedScope(scopeRequiredByAPI);
67+
// Do the work and return the result
68+
...
69+
}
70+
...
71+
}
72+
```
73+
74+
The `VerifyUserHasAnyAcceptedScope` method would do something like the following:
75+
76+
- verify that there's a claims named `scope`
77+
- verify that the claim has a value containing the scope expected by the API.
78+
79+
```CSharp
80+
/// <summary>
81+
/// When applied to an <see cref="HttpContext"/>, verifies that the user authenticated in the
82+
/// Web API has any of the accepted scopes. *
83+
/// If the authentication user does not have any of these <paramref name="acceptedScopes"/>, the
84+
/// method throws an HTTP Unauthorized with the message telling which scopes are expected in the token
85+
/// </summary>
86+
/// <param name="acceptedScopes">Scopes accepted by this API</param>
87+
/// <exception cref="HttpRequestException"/> with a <see cref="HttpResponse.StatusCode"/> set to
88+
/// <see cref="HttpStatusCode.Unauthorized"/>
89+
public static void VerifyUserHasAnyAcceptedScope(this HttpContext context,
90+
params string[] acceptedScopes)
91+
{
92+
if (acceptedScopes == null)
93+
{
94+
throw new ArgumentNullException(nameof(acceptedScopes));
95+
}
96+
Claim scopeClaim = HttpContext?.User
97+
?.FindFirst("http://schemas.microsoft.com/identity/claims/scope");
98+
if (scopeClaim == null || !scopeClaim.Value.Split(' ').Intersect(acceptedScopes).Any())
99+
{
100+
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
101+
string message = $"The 'scope' claim does not contain scopes '{string.Join(",", acceptedScopes)}' or was not found";
102+
throw new HttpRequestException(message);
103+
}
104+
}
105+
```
106+
107+
This sample code is for ASP.NET Core. For ASP.NET just replace `HttpContext.User` by `ClaimsPrincipal.Current`, and the claim type `"http://schemas.microsoft.com/identity/claims/scope"` by `"scope"` (See also the code snippet below)
108+
109+
110+
## Verifying app roles in APIs called by daemon apps
111+
112+
If you Web API is called by a [Daemon application](scenario-daemon-overview.md), then that application should require an application permission
113+
to your Web API. We've seen in [scenario-protected-web-api-app-registration.md#how-to-expose-application-permissions--app-roles-] that your API
114+
exposes such permissions (for instance as the `access_as_application` app role). You now need to have your APIs verify that the token it received contains the `roles` claims and
115+
that this claim has the value it expects. To do this, the code is similar to the code that verifies delegated permissions, except that, instead of testing for `scopes`, your controller action will test for `roles`:
116+
117+
```CSharp
118+
[Authorize]
119+
public class TodoListController : ApiController
120+
{
121+
public IEnumerable<TodoItem> Get()
122+
{
123+
ValidateAppRole("access_as_application");
124+
...
125+
}
126+
```
127+
128+
The ValidateAppRole() method can be something like this:
129+
130+
```CSharp
131+
private void ValidateAppRole(string appRole)
132+
{
133+
//
134+
// The `role` claim tells you what permissions the client application has in the service.
135+
// In this case we look for a `role` value of `access_as_application`
136+
//
137+
Claim scopeClaim = ClaimsPrincipal.Current.FindFirst("roles");
138+
if (scopeClaim == null || (scopeClaim.Value != appRole))
139+
{
140+
throw new HttpResponseException(new HttpResponseMessage { StatusCode = HttpStatusCode.Unauthorized, ReasonPhrase = $"The 'roles' claim does not contain '{appRole}' or was not found" });
141+
}
142+
}
143+
}
144+
```
145+
146+
This sample code is for ASP.NET. For ASP.NET Core, just replace `ClaimsPrincipal.Current` by `HttpContext.User` and the `"roles"` claim name by `"http://schemas.microsoft.com/identity/claims/roles"` (see also the code snippet above)
147+
148+
## Next steps
149+
150+
> [!div class="nextstepaction"]
151+
> [Move to production](scenario-protected-web-api-production.md)

0 commit comments

Comments
 (0)