|
| 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