Skip to content

Commit 0b36d99

Browse files
author
Tiago Brenck
committed
Onboarding process for multi-tenant
1 parent 42a1379 commit 0b36d99

File tree

11 files changed

+287
-12
lines changed

11 files changed

+287
-12
lines changed
Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,40 @@
11
using Microsoft.AspNetCore.Authorization;
22
using Microsoft.AspNetCore.Mvc;
33
using System.Diagnostics;
4+
using System.Linq;
5+
using WebApp_OpenIDConnect_DotNet.DAL;
46
using WebApp_OpenIDConnect_DotNet.Models;
57

68
namespace WebApp_OpenIDConnect_DotNet.Controllers
79
{
810
[Authorize]
911
public class HomeController : Controller
1012
{
11-
public HomeController()
13+
private readonly SampleDbContext dbContext;
14+
15+
public HomeController(SampleDbContext dbContext)
1216
{
17+
this.dbContext = dbContext;
1318
}
1419

1520
public IActionResult Index()
21+
{
22+
var authorizedTenants = dbContext.AuthorizedTenants.Where(x => x.TenantId != null && x.AuthorizedOn != null).ToList();
23+
return View(authorizedTenants);
24+
}
25+
26+
public IActionResult DeleteTenant(string id)
27+
{
28+
var tenants = dbContext.AuthorizedTenants.Where(x => x.TenantId == id).ToList();
29+
dbContext.RemoveRange(tenants);
30+
dbContext.SaveChanges();
31+
32+
return RedirectToAction("Index");
33+
}
34+
35+
[AllowAnonymous]
36+
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
37+
public IActionResult UnauthorizedTenant()
1638
{
1739
return View();
1840
}
@@ -21,7 +43,7 @@ public IActionResult Index()
2143
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
2244
public IActionResult Error()
2345
{
24-
return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier});
46+
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
2547
}
2648
}
2749
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
6+
using Microsoft.AspNetCore.Authorization;
7+
using Microsoft.AspNetCore.Http.Extensions;
8+
using Microsoft.AspNetCore.Mvc;
9+
using Microsoft.Extensions.Options;
10+
using WebApp_OpenIDConnect_DotNet.DAL;
11+
using WebApp_OpenIDConnect_DotNet.Models;
12+
13+
namespace WebApp_OpenIDConnect_DotNet.Controllers
14+
{
15+
[AllowAnonymous]
16+
public class OnboardingController : Controller
17+
{
18+
private readonly SampleDbContext dbContext;
19+
private readonly AzureADOptions azureADOptions;
20+
21+
public OnboardingController(SampleDbContext dbContext, IOptions<AzureADOptions> azureADOptions)
22+
{
23+
this.dbContext = dbContext;
24+
this.azureADOptions = azureADOptions.Value;
25+
}
26+
27+
[HttpGet]
28+
public ActionResult SignUp()
29+
{
30+
return View();
31+
}
32+
33+
[HttpPost]
34+
[ValidateAntiForgeryToken]
35+
public ActionResult Onboard()
36+
{
37+
// Generate a random value to identify the request
38+
string stateMarker = Guid.NewGuid().ToString();
39+
40+
AuthorizedTenant authorizedTenant = new AuthorizedTenant
41+
{
42+
CreatedOn = DateTime.Now,
43+
TempAuthorizationCode = stateMarker //Use the stateMarker as a tempCode, so we can find this entity on the ProcessCode method
44+
};
45+
46+
dbContext.AuthorizedTenants.Add(authorizedTenant);
47+
dbContext.SaveChanges();
48+
49+
string currentUri = UriHelper.BuildAbsolute(
50+
this.Request.Scheme,
51+
this.Request.Host,
52+
this.Request.PathBase);
53+
54+
// 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.
55+
string authorizationRequest = string.Format(
56+
"{0}common/adminconsent?client_id={1}&redirect_uri={2}&state={3}",
57+
azureADOptions.Instance,
58+
Uri.EscapeDataString(azureADOptions.ClientId),
59+
Uri.EscapeDataString(currentUri + "Onboarding/ProcessCode"),
60+
Uri.EscapeDataString(stateMarker));
61+
62+
63+
return new RedirectResult(authorizationRequest);
64+
}
65+
66+
// This is the redirect Uri for the admin consent authorization
67+
public async Task<ActionResult> ProcessCode(string tenant, string error, string error_description, string resource, string state)
68+
{
69+
if (error != null)
70+
{
71+
TempData["ErrorMessage"] = error_description;
72+
return RedirectToAction("Error", "Home");
73+
}
74+
75+
// Check if tenant is already authorized
76+
if(dbContext.AuthorizedTenants.FirstOrDefault(x => x.TenantId == tenant) != null)
77+
{
78+
return RedirectToAction("Index", "Home");
79+
}
80+
81+
// Find a tenant carrying a TempAuthorizationCode that we previously saved
82+
var preAuthorizedTenant = dbContext.AuthorizedTenants.FirstOrDefault(a => a.TempAuthorizationCode == state);
83+
84+
// If we don't find it, return an error because the state param was not generated from this app
85+
if (preAuthorizedTenant == null)
86+
{
87+
TempData["ErrorMessage"] = "State verification failed.";
88+
return RedirectToAction("Error", "Home");
89+
}
90+
else
91+
{
92+
// Update the authorized tenant with its Id
93+
preAuthorizedTenant.TenantId = tenant;
94+
preAuthorizedTenant.AuthorizedOn = DateTime.Now;
95+
96+
await dbContext.SaveChangesAsync();
97+
98+
return RedirectToAction("Index", "Home");
99+
}
100+
}
101+
}
102+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using WebApp_OpenIDConnect_DotNet.Models;
7+
8+
namespace WebApp_OpenIDConnect_DotNet.DAL
9+
{
10+
public class SampleDbContext : DbContext
11+
{
12+
public SampleDbContext(DbContextOptions<SampleDbContext> options) : base(options) { }
13+
14+
public DbSet<AuthorizedTenant> AuthorizedTenants { get; set; }
15+
}
16+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel.DataAnnotations;
4+
using System.ComponentModel.DataAnnotations.Schema;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
8+
namespace WebApp_OpenIDConnect_DotNet.Models
9+
{
10+
public class AuthorizedTenant
11+
{
12+
[Key]
13+
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
14+
public int Id { get; set; }
15+
public string TempAuthorizationCode { get; set; }
16+
public string TenantId { get; set; }
17+
public DateTime CreatedOn { get; set; }
18+
public DateTime? AuthorizedOn { get; set; }
19+
}
20+
}

1-WebApp-OIDC/1-2-AnyOrg/Startup.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
2+
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
23
using Microsoft.AspNetCore.Authorization;
34
using Microsoft.AspNetCore.Builder;
45
using Microsoft.AspNetCore.Hosting;
56
using Microsoft.AspNetCore.Http;
67
using Microsoft.AspNetCore.Mvc;
78
using Microsoft.AspNetCore.Mvc.Authorization;
9+
using Microsoft.EntityFrameworkCore;
810
using Microsoft.Extensions.Configuration;
911
using Microsoft.Extensions.DependencyInjection;
1012
using Microsoft.Identity.Web;
13+
using Microsoft.Identity.Web.Resource;
14+
using Microsoft.IdentityModel.Logging;
15+
using Microsoft.IdentityModel.Tokens;
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
using System.Threading.Tasks;
20+
using WebApp_OpenIDConnect_DotNet.DAL;
21+
using WebApp_OpenIDConnect_DotNet.Utils;
1122

1223
namespace WebApp_OpenIDConnect_DotNet
1324
{
@@ -30,8 +41,42 @@ public void ConfigureServices(IServiceCollection services)
3041
options.MinimumSameSitePolicy = SameSiteMode.None;
3142
});
3243

44+
services.AddOptions();
45+
46+
services.AddDbContext<SampleDbContext>(options => options.UseInMemoryDatabase(databaseName: "MultiTenantOnboarding"));
47+
3348
// Sign-in users with the Microsoft identity platform
3449
services.AddMicrosoftIdentityPlatformAuthentication(Configuration);
50+
51+
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
52+
{
53+
//options.TokenValidationParameters = BuildTokenValidationParameters(options);
54+
options.Events.OnTokenValidated = async context =>
55+
{
56+
string tenantId = context.SecurityToken.Claims.FirstOrDefault(x => x.Type == "tid" || x.Type == "http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
57+
58+
if (string.IsNullOrWhiteSpace(tenantId))
59+
throw new UnauthorizedAccessException("Unable to get tenantId from token.");
60+
61+
var dbContext = context.HttpContext.RequestServices.GetRequiredService<SampleDbContext>();
62+
63+
var authorizedTenant = await dbContext.AuthorizedTenants.FirstOrDefaultAsync(t => t.TenantId == tenantId);
64+
65+
if (authorizedTenant == null)
66+
throw new UnauthorizedTenantException("This tenant is not authorized");
67+
68+
};
69+
options.Events.OnAuthenticationFailed = (context) =>
70+
{
71+
if (context.Exception != null && context.Exception is UnauthorizedTenantException)
72+
{
73+
context.Response.Redirect("/Home/UnauthorizedTenant");
74+
context.HandleResponse(); // Suppress the exception
75+
}
76+
77+
return Task.FromResult(0);
78+
};
79+
});
3580

3681
services.AddMvc(options =>
3782
{
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
6+
namespace WebApp_OpenIDConnect_DotNet.Utils
7+
{
8+
public class UnauthorizedTenantException : UnauthorizedAccessException
9+
{
10+
public UnauthorizedTenantException():base() { }
11+
public UnauthorizedTenantException(string message):base(message) { }
12+
}
13+
}
Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,36 @@
1-
@{
1+
@model IEnumerable<AuthorizedTenant>
2+
@{
23
ViewData["Title"] = "Home Page";
34
}
45

56
<h1>
67
ASP.NET Core web app signing-in users in any Azure AD organization
78
</h1>
89
<p>
9-
This sample shows how to build a .NET Core 2.2 MVC Web app that uses OpenID Connect to sign in users. Users can use work and school accounts from any company or organization that has integrated with Azure Active Directory. It leverages the ASP.NET Core OpenID Connect middleware.
10+
This sample shows how to build a .NET Core MVC Web app that uses OpenID Connect to sign in users. Users can use work and school accounts from any company or organization that has integrated with Azure Active Directory. It leverages the ASP.NET Core OpenID Connect middleware.
1011
</p>
11-
<img src="https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/raw/master/1-WebApp-OIDC/1-2-AnyOrg/ReadmeFiles/sign-in.png"/>
12+
<img src="https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/raw/master/1-WebApp-OIDC/1-2-AnyOrg/ReadmeFiles/sign-in.png"/>
13+
<hr />
14+
15+
<h2>Authorized Tenants</h2>
16+
<p>
17+
These are the authorized tenants. If you want to repeat the onboarding process, deleted the desired tenants and <b>sign-out.</b>
18+
</p>
19+
20+
<table class="table">
21+
<thead>
22+
<tr>
23+
<th>Tenant Id</th>
24+
<th></th>
25+
</tr>
26+
</thead>
27+
<tbody>
28+
@foreach (var tenant in Model)
29+
{
30+
<tr>
31+
<td>@tenant.TenantId</td>
32+
<td><a asp-controller="Home" asp-action="DeleteTenant" asp-route-id=@tenant.TenantId class="btn btn-danger">Delete</a></td>
33+
</tr>
34+
}
35+
</tbody>
36+
</table>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
@{
3+
ViewData["Title"] = "UnauthorizedTenant";
4+
}
5+
6+
<h2>Unauthorized Tenant</h2>
7+
<p>You have signed-in with a user from a Tenant that haven't been authorized by this application yet.</p>
8+
<p>The authorization can be done via the onboarding proccess.</p>
9+
<a class="btn btn-info" asp-controller="Onboarding" asp-action="SignUp">Take me to the onboarding process</a>
10+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
@{
3+
ViewData["Title"] = "SignUp";
4+
}
5+
6+
<h2>Tenant Onboarding</h2>
7+
<div class="panel panel-info">
8+
<div class="panel-heading"><h4>Instructions</h4></div>
9+
<div class="panel-body">
10+
<p>In this multi-tenant sample, only tenants that are registered on the database will have their tokens validated.</p>
11+
<p>This step represents an onboarding process, where we will provide this application's service principle into the new client tenant, using the admin consent endpoint. Then, we save the tenantId on the database to accept tokens issued by them.</p>
12+
<p>Click on the button below to prompt the admin consent screen and register your tenant. You must use an <b>admin account</b> on this step.</p>
13+
<form asp-controller="Onboarding" asp-action="Onboard" method="post" class="form-horizontal" role="form">
14+
@Html.AntiForgeryToken()
15+
<div>
16+
<button type="submit" class="btn btn-info">Register my tenant</button>
17+
</div>
18+
</form>
19+
</div>
20+
</div>
21+
22+

1-WebApp-OIDC/1-2-AnyOrg/Views/Shared/Error.cshtml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@
1313
</p>
1414
}
1515

16-
<h3>Development Mode</h3>
17-
<p>
18-
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
19-
</p>
20-
<p>
21-
<strong>Development environment should not be enabled in deployed applications</strong>, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>, and restarting the application.
22-
</p>
16+
@if (TempData["ErrorMessage"] != null)
17+
{
18+
<p class="text-danger">@TempData["ErrorMessage"]</p>
19+
}
20+

0 commit comments

Comments
 (0)