|
| 1 | +## About the code |
| 2 | + |
| 3 | +<details> |
| 4 | + <summary>Expand the section</summary> |
| 5 | + |
| 6 | +1. In the `TodoListService` project, which represents the web api, first the package `Microsoft.Identity.Web`is added from NuGet. |
| 7 | + |
| 8 | +1. Starting with the **Startup.cs** file : |
| 9 | + |
| 10 | + * at the top of the file, the following using directory was added: |
| 11 | + |
| 12 | + ```CSharp |
| 13 | + using Microsoft.Identity.Web; |
| 14 | + ``` |
| 15 | + |
| 16 | + * in the `ConfigureServices` method, the following code was added, replacing any existing `AddAuthentication()` code: |
| 17 | + |
| 18 | + ```CSharp |
| 19 | + services.AddMicrosoftIdentityWebApiAuthentication(Configuration); |
| 20 | + ``` |
| 21 | + |
| 22 | + * `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. |
| 23 | +
|
| 24 | + * 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: |
| 25 | + * 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. |
| 26 | + * check if the caller's account is homed or guest via the `acct` optional claim |
| 27 | + * check if the caller belongs to right roles or groups via the `roles` or `groups` claim, respectively |
| 28 | + |
| 29 | + See [How to manually validate a JWT access token using the Microsoft identity platform](https://aka.ms/extendtokenvalidation) for more details on to further verify the caller using this method. |
| 30 | +
|
| 31 | +1. Then in the controllers `TodoListController.cs`, the `[Authorize]` added on top of the class to protect this route. |
| 32 | + * 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. |
| 33 | + * The delegated permissions are checked inside `TodoListService\Controllers\ToDoListController.cs` in the following manner: |
| 34 | + |
| 35 | + ```CSharp |
| 36 | + [HttpGet] |
| 37 | + [RequiredScopeOrAppPermission( |
| 38 | + AcceptedScope = new string[] { "ToDoList.Read", "ToDoList.ReadWrite" }, |
| 39 | + AcceptedAppPermission = new string[] { "ToDoList.Read.All", "ToDoList.ReadWrite.All" } |
| 40 | + )] |
| 41 | + public IEnumerable<Todo> Get() |
| 42 | + { |
| 43 | + if (!IsAppOnlyToken()) |
| 44 | + { |
| 45 | + // this is a request for all ToDo list items of a certain user. |
| 46 | + return TodoStore.Values.Where(x => x.Owner == _currentLoggedUser); |
| 47 | + } |
| 48 | + else |
| 49 | + { |
| 50 | + // Its an app calling with app permissions, so return all items across all users |
| 51 | + return TodoStore.Values; |
| 52 | + } |
| 53 | + } |
| 54 | + ``` |
| 55 | + |
| 56 | + 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 |
| 57 | + 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. |
| 58 | + |
| 59 | + The **ToDoList.<*>.All** permissions are **Application Permissions**. |
| 60 | + |
| 61 | + Here is another example from the same controller: |
| 62 | + |
| 63 | + ``` CSharp |
| 64 | + [HttpDelete("{id}")] |
| 65 | + [RequiredScopeOrAppPermission( |
| 66 | + AcceptedScope = new string[] { "ToDoList.ReadWrite" }, |
| 67 | + AcceptedAppPermission = new string[] { "ToDoList.ReadWrite.All" })] |
| 68 | + public void Delete(int id) |
| 69 | + { |
| 70 | + if (!IsAppOnlyToken()) |
| 71 | + { |
| 72 | + // only delete if the ToDo list item belonged to this user |
| 73 | + if (TodoStore.Values.Any(x => x.Id == id && x.Owner == _currentLoggedUser)) |
| 74 | + { |
| 75 | + TodoStore.Remove(id); |
| 76 | + } |
| 77 | + } |
| 78 | + else |
| 79 | + { |
| 80 | + TodoStore.Remove(id); |
| 81 | + } |
| 82 | + } |
| 83 | + ``` |
| 84 | + |
| 85 | + 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. |
| 86 | + 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, |
| 87 | + but with **ToDoList.ReadWrite**, the user can delete only their own entries. |
| 88 | + |
| 89 | + * 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. |
| 90 | +
|
| 91 | + ```csharp |
| 92 | + private bool IsAppOnlyToken() |
| 93 | + { |
| 94 | + // Add in the optional 'idtyp' claim to check if the access token is coming from an application or user. |
| 95 | + // |
| 96 | + // See: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims |
| 97 | + return HttpContext.User.Claims.Any(c => c.Type == "idtyp" && c.Value == "app"); |
| 98 | + } |
| 99 | + ``` |
| 100 | + |
| 101 | +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. |
| 102 | + |
| 103 | +* The following lines in *Startup.cs* adds the ability to authenticate a user using Azure AD. |
| 104 | + |
| 105 | +```csharp |
| 106 | + services.AddMicrosoftIdentityWebAppAuthentication(Configuration) |
| 107 | + .EnableTokenAcquisitionToCallDownstreamApi( |
| 108 | + Configuration.GetSection("TodoList:TodoListScopes").Get<string>().Split(" ", System.StringSplitOptions.RemoveEmptyEntries) |
| 109 | + ) |
| 110 | + .AddInMemoryTokenCaches(); |
| 111 | +``` |
| 112 | + |
| 113 | +* Specifying Initial scopes (delegated permissions) |
| 114 | + |
| 115 | +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: |
| 116 | + |
| 117 | +```csharp |
| 118 | + services.AddMicrosoftIdentityWebAppAuthentication(Configuration) |
| 119 | + .EnableTokenAcquisitionToCallDownstreamApi(Configuration.GetSection("TodoList:TodoListScopes") |
| 120 | + .Get<string>().Split(" ", System.StringSplitOptions.RemoveEmptyEntries)) |
| 121 | + .AddInMemoryTokenCaches(); |
| 122 | +``` |
| 123 | + |
| 124 | +* 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*. |
| 125 | + |
| 126 | + ```CSharp |
| 127 | + app.Use(async (context, next) => { |
| 128 | + if (context.User != null && context.User.Identity.IsAuthenticated) |
| 129 | + { |
| 130 | + // you can conduct any conditional processing for guest/homes user by inspecting the value of the 'acct' claim |
| 131 | + // Read more about the 'acct' claim at aka.ms/optionalclaims |
| 132 | + if (context.User.Claims.Any(x => x.Type == "acct")) |
| 133 | + { |
| 134 | + string claimvalue = context.User.Claims.FirstOrDefault(x => x.Type == "acct").Value; |
| 135 | + string userType = claimvalue == "0" ? "Member" : "Guest"; |
| 136 | + Debug.WriteLine($"The type of the user account from this Azure AD tenant is-{userType}"); |
| 137 | + } |
| 138 | + } |
| 139 | + await next(); |
| 140 | + }); |
| 141 | + ``` |
| 142 | + |
| 143 | +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. |
| 144 | +1. Also consider adding [MSAL.NET Logging](https://docs.microsoft.com/azure/active-directory/develop/msal-logging-dotnet) to you project |
| 145 | +
|
| 146 | +</details> |
0 commit comments