Skip to content

Commit 436192c

Browse files
author
Kalyan Krishna
committed
review complete
1 parent 7511453 commit 436192c

File tree

13 files changed

+114
-77
lines changed

13 files changed

+114
-77
lines changed

2-WebApp-graph-user/2-3-Multi-Tenant/Controllers/HomeController.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2323
*/
2424

2525
using Microsoft.AspNetCore.Authorization;
26-
using Microsoft.AspNetCore.Http.Authentication;
2726
using Microsoft.AspNetCore.Mvc;
2827
using Microsoft.Identity.Web;
2928
using System.Diagnostics;
3029
using System.Linq;
31-
using System.Threading.Tasks;
3230
using WebApp_OpenIDConnect_DotNet.DAL;
3331
using WebApp_OpenIDConnect_DotNet.Models;
3432

@@ -44,32 +42,41 @@ public HomeController(SampleDbContext dbContext)
4442
this.dbContext = dbContext;
4543
}
4644

45+
/// <summary>
46+
/// Retrieves a list all authorized tenants to be displayed in a table, for demonstration purpose only
47+
/// </summary>
48+
/// <returns></returns>
4749
public IActionResult Index()
4850
{
49-
//Getting all authorized tenants to be displayed on a table, for demonstration purpose
5051
var authorizedTenants = dbContext.AuthorizedTenants.Where(x => x.TenantId != null && x.AuthorizedOn != null).ToList();
5152
return View(authorizedTenants);
5253
}
5354

55+
/// <summary>Deletes the selected tenant from the app's own DB (off-boarding).</summary>
56+
/// <param name="id">The tenant Id.</param>
57+
/// <returns></returns>
5458
public IActionResult DeleteTenant(string id)
5559
{
5660
var tenants = dbContext.AuthorizedTenants.Where(x => x.TenantId == id).ToList();
5761
dbContext.RemoveRange(tenants);
5862
dbContext.SaveChanges();
5963

60-
var signedUserTenant = User.GetTenantId();
64+
var signedUsersTenant = User.GetTenantId();
6165

62-
// If the user deletes its own tenant from the list, they should be signed-out
63-
if (id == signedUserTenant)
66+
// If the user deletes its own tenant from the list, they would also be signed-out
67+
if (id == signedUsersTenant)
6468
return RedirectToAction("SignOut", "Account", new { area = "AzureAD" });
6569
else
6670
return RedirectToAction("Index");
6771
}
6872

73+
/// <summary>
74+
/// If you landed here, its because you tried to sign-in with a user account from a tenant that hasn't onboarded the application yet.
75+
/// </summary>
76+
/// <returns></returns>
6977
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
7078
public IActionResult UnauthorizedTenant()
7179
{
72-
//If you landed here, is because you tried to sign-in with a user from a tenant that hasnt been onboarded in the application yet.
7380
return View();
7481
}
7582

2-WebApp-graph-user/2-3-Multi-Tenant/Controllers/OnboardingController.cs

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public IActionResult SignUp()
5454
return View();
5555
}
5656

57+
/// <summary>This action builds the admin consent Url to let the tenant admin consent and provision a service principal of their app in their tenant.</summary>
58+
/// <returns></returns>
5759
[HttpPost]
5860
[ValidateAntiForgeryToken]
5961
public IActionResult Onboard()
@@ -64,7 +66,7 @@ public IActionResult Onboard()
6466
AuthorizedTenant authorizedTenant = new AuthorizedTenant
6567
{
6668
CreatedOn = DateTime.Now,
67-
TempAuthorizationCode = stateMarker //Use the stateMarker as a tempCode, so we can find this entity on the ProcessCode method
69+
TempAuthorizationCode = stateMarker //Use the stateMarker as a tempCode, so we can locate this entity in the ProcessCode method
6870
};
6971

7072
// Saving a temporary tenant to validate the stateMarker on the admin consent response
@@ -76,41 +78,57 @@ public IActionResult Onboard()
7678
this.Request.Host,
7779
this.Request.PathBase);
7880

79-
// Create an OAuth2 request, using the web app as the client.This will trigger a consent flow that will provision the app in the target tenant.
81+
// Create an OAuth2 request, using the web app as the client. This will trigger a consent flow that will provision the app in the target tenant.
82+
// Refer to https://docs.microsoft.com/azure/active-directory/develop/v2-admin-consent for details about the Url format being constructed below
8083
string authorizationRequest = string.Format(
8184
"{0}common/v2.0/adminconsent?client_id={1}&redirect_uri={2}&state={3}&scope={4}",
8285
azureADOptions.Instance,
83-
Uri.EscapeDataString(azureADOptions.ClientId), // The application Id on Azure Portal
84-
Uri.EscapeDataString(currentUri + "Onboarding/ProcessCode"), //Uri that will get redirected after the admin has consented
85-
Uri.EscapeDataString(stateMarker), // The state parameter is used to validate the response, preventing a man-in-the-middle attack, and it will also be used to identify the entity on ProcessCode action.
86-
Uri.EscapeDataString("https://graph.microsoft.com/.default")); // The scopes to be presented to the admin. Here we are using the static scope /.default.
86+
Uri.EscapeDataString(azureADOptions.ClientId), // The application Id as obtained from the Azure Portal
87+
Uri.EscapeDataString(currentUri + "Onboarding/ProcessCode"), // Uri that the admin will be redirected to after the consent
88+
Uri.EscapeDataString(stateMarker), // The state parameter is used to validate the response, preventing a man-in-the-middle attack, and it will also be used to identify this request in the ProcessCode action.
89+
Uri.EscapeDataString("https://graph.microsoft.com/.default")); // The scopes to be presented to the admin to consent. Here we are using the static scope '/.default' (https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope).
8790

8891
return Redirect(authorizationRequest);
8992
}
9093

91-
// This is the redirect Uri for the admin consent authorization
92-
public async Task<IActionResult> ProcessCode(string tenant, string error, string error_description, string resource, string state)
94+
/// <summary>
95+
/// This handler is used to process the response after the admin consent process is complete.
96+
/// </summary>
97+
/// <param name="tenant">The directory tenant that granted your application the permissions it requested, in GUID format..</param>
98+
/// <param name="error">An error code string that can be used to classify types of errors that occur, and can be used to react to errors..</param>
99+
/// <param name="error_description">A specific error message that can help a developer identify the root cause of an error..</param>
100+
/// <param name="admin_consent">Will be set to True to indicate that this response occurred on an admin consent flow..</param>
101+
/// <param name="state">A value included in the request that also will be returned in the token response. It can be a string of any content you want. The state is used to encode information about the user's state in the app before the authentication request occurred, such as the page or view they were on..</param>
102+
/// <remarks>Refer to https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-admin-consent for details on the response</remarks>
103+
/// <returns></returns>
104+
public async Task<IActionResult> ProcessCode(string tenant, string error, string error_description, string admin_consent, string state)
93105
{
94106
if (error != null)
95107
{
96108
TempData["ErrorMessage"] = error_description;
97109
return RedirectToAction("Error", "Home");
98110
}
99111

112+
if (admin_consent.ToUpper() != "TRUE")
113+
{
114+
TempData["ErrorMessage"] = "The admin consent operation failed.";
115+
return RedirectToAction("Error", "Home");
116+
}
117+
100118
var authenticationProperties = new AuthenticationProperties { RedirectUri = "Home/Index" };
101119

102120
// If tenant is already authorized, there is no need updated its record
103121
if (dbContext.AuthorizedTenants.FirstOrDefault(x => x.TenantId == tenant) != null)
104122
{
105-
// Challenge an authentication so dotnet can set the user identity claims.
123+
// Create a Sign-in challenge to re-authenticate the user again as we need claims from the user's id_token.
106124
// Since the user will have a session on AAD already, they wont need to select an account again.
107125
return Challenge(authenticationProperties, AzureADDefaults.OpenIdScheme);
108126
}
109127

110-
// Find a tenant carrying a TempAuthorizationCode that we previously saved
128+
// Find a tenant record matching the TempAuthorizationCode that we previously saved in the Onboard()
111129
var preAuthorizedTenant = dbContext.AuthorizedTenants.FirstOrDefault(a => a.TempAuthorizationCode == state);
112130

113-
// If we don't find it, return an error because the state param was not generated from this app
131+
// If we don't find it, return an error because the state param was not generated from this app and we do not wish to process this request
114132
if (preAuthorizedTenant == null)
115133
{
116134
TempData["ErrorMessage"] = "State verification failed.";
@@ -124,8 +142,8 @@ public async Task<IActionResult> ProcessCode(string tenant, string error, string
124142

125143
await dbContext.SaveChangesAsync();
126144

127-
// Challenge an authentication so dotnet can set the user identity claims.
128-
// Since the user will have a session on AAD already, they wont need to select an account again.
145+
// Create a Sign-in challenge to re-authenticate the user again as we need claims from the user's id_token.
146+
// Since the user will have a session on AAD already, they wont need to select an account again
129147
return Challenge(authenticationProperties, AzureADDefaults.OpenIdScheme);
130148
}
131149
}

2-WebApp-graph-user/2-3-Multi-Tenant/Controllers/TodoListController.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222
SOFTWARE.
2323
*/
2424

25-
using System;
26-
using System.Collections.Generic;
27-
using System.Linq;
28-
using System.Threading.Tasks;
2925
using Microsoft.AspNetCore.Authorization;
3026
using Microsoft.AspNetCore.Mvc;
3127
using Microsoft.AspNetCore.Mvc.Rendering;
3228
using Microsoft.Identity.Web;
33-
using WebApp_OpenIDConnect_DotNet.Services;
29+
using System.Linq;
30+
using System.Threading.Tasks;
3431
using WebApp_OpenIDConnect_DotNet.Models;
32+
using WebApp_OpenIDConnect_DotNet.Services;
3533
using WebApp_OpenIDConnect_DotNet.Utils;
3634

3735
namespace WebApp_OpenIDConnect_DotNet.Controllers
@@ -90,15 +88,15 @@ public async Task<IActionResult> Edit(int id)
9088
{
9189
TodoItem todoItem = await _todoItemService.Get(id, User);
9290

93-
if(todoItem == null)
91+
if (todoItem == null)
9492
{
9593
TempData["ErrorMessage"] = "Item not found";
9694
return RedirectToAction("Error", "Home");
9795
}
9896

9997
var userTenant = User.GetTenantId();
10098

101-
// Acquiring token for graph using the user's tenantId, so it can return all the users from their tenant
99+
// Acquiring token for graph in the signed-in users tenant, so it can be used to retrieve all the users from their tenant
102100
var graphAccessToken = await _tokenAcquisition.GetAccessTokenOnBehalfOfUserAsync(new string[] { GraphScope.UserReadAll }, userTenant);
103101

104102
TempData["UsersDropDown"] = (await _msGraphService.GetUsersAsync(graphAccessToken))

2-WebApp-graph-user/2-3-Multi-Tenant/README.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ If you try to sign-in with a tenant that hasn't been "onboarded" yet, you will l
178178

179179
![Unauthorized Tenant](ReadmeFiles/unauthorized-tenant.png)
180180

181-
> :warning: If you onboarded your tenant using this sample in the past already and getting the **AADSTS650051** error when onboarding again, please refer to the [Error AADSTS650051](#error-aadsts650051) section below.
181+
> :warning: If you had onboarded your tenant using this sample in the past and now getting the **AADSTS650051** error when onboarding again, please refer to the [Error AADSTS650051](#error-aadsts650051) section below to mitigate this error.
182182
183183
#### ToDo List
184184

@@ -215,9 +215,9 @@ services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
215215

216216
You can read about the various endpoints of the Microsoft Identity Platform [here](https://docs.microsoft.com/azure/active-directory/develop/active-directory-v2-protocols#endpoints).
217217

218-
### Service principle provision for new tenants (onboarding process)
218+
### Service principle provisioning for new tenants (onboarding process)
219219

220-
For a multi-tenant app, its service principle will be created on all the users' tenants that have signed-in at least once. Some might want that only tenant admins accept the service principle provisioning. For that, we are using the [admin consent endpoint](https://docs.microsoft.com/azure/active-directory/develop/v2-admin-consent) for the onboarding process on the `OnboardingController.cs`. The `Onboard` action and corresponding view, simulate an onboarding experience.
220+
For a multi-tenant app to work across tenants, its service principle will need to be provisioned in the users' tenant. It can either happen when the first user signs in, or most tenant admins only allow a tenant admin to carry out the service principle provisioning. For provisioning, we will be using the [admin consent endpoint](https://docs.microsoft.com/azure/active-directory/develop/v2-admin-consent) for the onboarding process. The code for this is provided in the `OnboardingController.cs`. The `Onboard` action and corresponding view, simulate the onboarding flow and experience.
221221

222222
```csharp
223223
[HttpPost]
@@ -236,13 +236,13 @@ public IActionResult Onboard()
236236
}
237237
```
238238

239-
This results in an OAuth2 code grant request that triggers the admin consent flow and creates the service principle in the admin's tenant. The `state` parameter is used to validate the response, preventing a man-in-the-middle attack. Then, the `ProcessCode` action receives the authorization code from Azure AD and, if they appear valid, it creates an entry in the application database for the new customer.
239+
This results in an OAuth2 code grant request that triggers the admin consent flow and creates the service principle in the admin's tenant. The `state` parameter is used to validate the response, preventing a man-in-the-middle attack. Then, the `ProcessCode` action receives the authorization code from Azure AD and, if they appear valid, we create an entry in the application database for the new customer.
240240

241-
The `https://graph.microsoft.com/.default` is a static scope. You can find more about static scope on [this link.](https://docs.microsoft.com/azure/active-directory/develop/v2-admin-consent#request-the-permissions-from-a-directory-admin)
241+
The `https://graph.microsoft.com/.default` is a static scope that allows the tenant admin to consent for all permissions in one go. You can find more about static scope on [this link.](https://docs.microsoft.com/azure/active-directory/develop/v2-admin-consent#request-the-permissions-from-a-directory-admin)
242242

243243
### Custom token validation allowing only registered tenants
244244

245-
On the `Startup.cs` we are calling `AddMicrosoftIdentityPlatformAuthentication` to configure the authentication, and it also validates that the token issuer is from AAD.
245+
On the `Startup.cs` we are calling `AddMicrosoftIdentityPlatformAuthentication` to configure the authentication, and within that method, we validates that the token issuer is from AAD.
246246

247247
```csharp
248248
options.TokenValidationParameters.IssuerValidator = AadIssuerValidator.GetIssuerValidator(options.Authority).Validate;
@@ -283,17 +283,17 @@ services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =
283283

284284
### Data partitioning by tenant
285285

286-
There are two common scenarios regarding data partition on a multi-tenant app. Having a separate database for each tenant or having a single database and using the **tenantId** to distinguish the data from each tenant. In this sample, we used the single database approach to save the todo items.
286+
There are two common scenarios regarding data partition on a multi-tenant app. Having a separate database for each tenant or having a single database and using the **tenantId** to separate the data of each tenant. In this sample, we have taken the single database approach to save the ToDo items for all users from all tenants.
287287

288-
`TodoListController.cs` have basic CRUD actions for `todoItem` and each operation takes into account the signed user's **tenantId** to separate data from each tenant. The tenantId can be found in the user' claims.
288+
`TodoListController.cs` has the basic CRUD actions for `ToDoItem` and each operation takes into account the signed user's **tenantId** to separate data from each tenant. The tenantId can be found in the user' claims.
289289

290-
### Microsoft Graph token by tenant
290+
### Acquiring Access token for Microsoft Graph for each tenant
291291

292-
If a multi-tenant app needs to acquire a token from Graph to read data from the signed user's tenant, the token must be issued with their tenantId authority and not where the application is registered. This feature is being showed on the **Edit** action result on `todoListController.cs`.
292+
If a multi-tenant app needs to acquire an access token for Microsoft Graph to be able to read data from the signed user's tenant, the token must be issued from their tenanted authority and not from the tenant where the SaaS application is registered. This feature is being showed on the **Edit** action result on `todoListController.cs`.
293293

294294
```csharp
295295
var userTenant = User.GetTenantId();
296-
// Acquiring token for graph using the user's tenantId, so it can return all the users from their tenant
296+
// Acquiring token for graph using the user's tenant, so it can return all the users from their tenant
297297
var graphAccessToken = await _tokenAcquisition.GetAccessTokenOnBehalfOfUserAsync(new string[] { GraphScope.UserReadAll }, userTenant);
298298
```
299299

2-WebApp-graph-user/2-3-Multi-Tenant/Services/IMSGraphService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ public interface IMSGraphService
3232
{
3333
Task<IEnumerable<User>> GetUsersAsync(string accessToken);
3434
}
35-
}
35+
}

2-WebApp-graph-user/2-3-Multi-Tenant/Services/MSGraphService.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222
SOFTWARE.
2323
*/
2424

25-
using Microsoft.AspNetCore.Mvc.Rendering;
2625
using Microsoft.Graph;
2726
using System;
2827
using System.Collections.Generic;
2928
using System.Diagnostics;
30-
using System.Linq;
3129
using System.Net.Http.Headers;
3230
using System.Threading.Tasks;
3331

3432
namespace WebApp_OpenIDConnect_DotNet.Services
3533
{
34+
/// <summary>Provides helper methods built over MS Graph SDK</summary>
35+
/// <seealso cref="WebApp_OpenIDConnect_DotNet.Services.IMSGraphService" />
3636
public class MSGraphService : IMSGraphService
3737
{
3838
// the Graph SDK's GraphServiceClient
@@ -58,7 +58,7 @@ public async Task<IEnumerable<User>> GetUsersAsync(string accessToken)
5858
.Filter($"accountEnabled eq true")
5959
.Select("id, userPrincipalName")
6060
.GetAsync();
61-
61+
6262
if (users?.CurrentPage.Count > 0)
6363
{
6464
return users;
@@ -81,6 +81,14 @@ private void PrepareAuthenticatedClient(string accessToken)
8181
{
8282
try
8383
{
84+
/***
85+
<!--Microsoft Azure AD Graph API endpoint,
86+
'https://graph.microsoft.com' Microsoft Graph global service
87+
'https://graph.microsoft.us' Microsoft Graph for US Government
88+
'https://graph.microsoft.de' Microsoft Graph Germany
89+
'https://microsoftgraph.chinacloudapi.cn' Microsoft Graph China
90+
-->
91+
* ***/
8492
graphServiceClient = new GraphServiceClient("https://graph.microsoft.com/beta",
8593
new DelegateAuthenticationProvider(
8694
async (requestMessage) =>
@@ -97,4 +105,4 @@ await Task.Run(() =>
97105
}
98106
}
99107
}
100-
}
108+
}

0 commit comments

Comments
 (0)