Skip to content

Commit 8bce715

Browse files
authored
Merge pull request #79395 from jmprieur/master
Add authorization to App API
2 parents a97c295 + e118cb6 commit 8bce715

File tree

4 files changed

+228
-6
lines changed

4 files changed

+228
-6
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+
> [Verify scopes and app roles in your code](scenario-protected-web-api-verification-scope-app-roles.md)

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

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,20 @@ Scopes are usually of the form `resourceURI/scopeName`. For Microsoft Graph, the
5656

5757
During app registration, you'll need to define the following parameters:
5858

59-
- 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
59+
- The 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 more **scopes** (to client applications, they will show up as **delegated permissions** for your Web API)
61+
- One or more **app roles** (to client applications, they will show up 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**. You will only declare application permissions as daemon applications do not interact with users and therefore delegated permissions would not make sense.
88+
- tenant admins may require Azure AD to issue tokens for your Web App to only applications that have registered that they want to access one of the Web API apps permissions.
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 sample JSON block below. Leave the `allowedMemberTypes` to "Application" only. Please make sure that the **id** is a unique guid and **displayName** and **Value** don't contain any spaces.
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 checks for the app role (that's the developer way of doing it). But you can even configure Azure Active Directory to issue a token for your Web API only to applications that were approved by the tenant admin to access your API. 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+
> [!NOTE]
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 the following error: `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 for your Web API</span>. Therefore, any daemon client (that is any client using client credentials flow) would still be able to obtain an access token for the 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 explained in the next section, 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: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
---
2+
title: Protected web API - app code configuration | Azure Active Directory
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 learn 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+
- or the individual controller action for your API.
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](scenario-protected-web-api-app-configuration.md#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 `http://schemas.microsoft.com/identity/claims/scope` or `scp`
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 authenticated 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 `"scp"` (See also the code snippet below)
108+
109+
## Verifying app roles in APIs called by daemon apps
110+
111+
If your Web API is called by a [Daemon application](scenario-daemon-overview.md), then that application should require an application permission 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 exposes such permissions (for instance as the `access_as_application` app role).
112+
You now need to have your APIs verify that the token it received contains the `roles` claims and
113+
that this claim has the value it expects. The code doing this verification is similar to the code that verifies delegated permissions, except that, instead of testing for `scopes`, your controller action will test for `roles`:
114+
115+
```CSharp
116+
[Authorize]
117+
public class TodoListController : ApiController
118+
{
119+
public IEnumerable<TodoItem> Get()
120+
{
121+
ValidateAppRole("access_as_application");
122+
...
123+
}
124+
```
125+
126+
The `ValidateAppRole()` method can be something like this:
127+
128+
```CSharp
129+
private void ValidateAppRole(string appRole)
130+
{
131+
//
132+
// The `role` claim tells you what permissions the client application has in the service.
133+
// In this case we look for a `role` value of `access_as_application`
134+
//
135+
Claim roleClaim = ClaimsPrincipal.Current.FindFirst("roles");
136+
if (roleClaim == null || !roleClaim.Value.Split(' ').Contains(appRole))
137+
{
138+
throw new HttpResponseException(new HttpResponseMessage
139+
{ StatusCode = HttpStatusCode.Unauthorized,
140+
ReasonPhrase = $"The 'roles' claim does not contain '{appRole}' or was not found"
141+
});
142+
}
143+
}
144+
}
145+
```
146+
147+
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)
148+
149+
### Accepting app only tokens if the Web API should only be called by daemon apps
150+
151+
The `roles` claim is also used for users in user assignment patterns (See [How to: Add app roles in your application and receive them in the token](howto-add-app-roles-in-azure-ad-apps.md)). So just checking roles will allow apps to sign in as users and the other way around, if the roles are assignable to both. We recommend having different roles declared for users and apps to prevent this confusion.
152+
153+
If you want to only allow daemon applications to call your Web API, you'll want to add a condition, when you validate the app role, that the token is an app-only token:
154+
155+
```CSharp
156+
string oid = ClaimsPrincipal.Current.FindFirst("oid");
157+
string sub = ClaimsPrincipal.Current.FindFirst("sub");
158+
bool isAppOnlyToken = oid == sub;
159+
```
160+
161+
Checking the inverse condition will allow only apps that sign in a user, to call your API.
162+
163+
## Next steps
164+
165+
> [!div class="nextstepaction"]
166+
> [Move to production](scenario-protected-web-api-production.md)

0 commit comments

Comments
 (0)