Skip to content

Commit

Permalink
Multi-tenancy example (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonashendrickx authored Aug 5, 2024
1 parent b52d808 commit 540def1
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 0 deletions.
7 changes: 7 additions & 0 deletions Passwordless.sln
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Passwordless.Tests.Infra",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Passwordless.AspNetCore.Tests.Dummy", "tests\Passwordless.AspNetCore.Tests.Dummy\Passwordless.AspNetCore.Tests.Dummy.csproj", "{9E15DF1C-BD60-4373-9905-C86C16A06553}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Passwordless.MultiTenancy.Example", "examples\Passwordless.MultiTenancy.Example\Passwordless.MultiTenancy.Example.csproj", "{964D1EDD-31B9-46A6-9159-21E2DD4A90D6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -74,6 +76,10 @@ Global
{9E15DF1C-BD60-4373-9905-C86C16A06553}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E15DF1C-BD60-4373-9905-C86C16A06553}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E15DF1C-BD60-4373-9905-C86C16A06553}.Release|Any CPU.Build.0 = Release|Any CPU
{964D1EDD-31B9-46A6-9159-21E2DD4A90D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{964D1EDD-31B9-46A6-9159-21E2DD4A90D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{964D1EDD-31B9-46A6-9159-21E2DD4A90D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{964D1EDD-31B9-46A6-9159-21E2DD4A90D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -87,5 +93,6 @@ Global
{F9487727-715D-442F-BE2F-7FB9931606C2} = {6EFECBD2-2BF5-473D-A7C3-A1F3A3F1816A}
{92D2ED44-AC6F-4E10-8300-7E5BFC4890EE} = {8FC08940-3E9D-4AE5-AB1D-940B4D5DC0E6}
{9E15DF1C-BD60-4373-9905-C86C16A06553} = {8FC08940-3E9D-4AE5-AB1D-940B4D5DC0E6}
{964D1EDD-31B9-46A6-9159-21E2DD4A90D6} = {6EFECBD2-2BF5-473D-A7C3-A1F3A3F1816A}
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Examples:

- [Passwordless.Example](examples/Passwordless.Example) — basic Passwordless.dev integration inside an ASP.NET app
- [Passwordless.AspNetIdentity.Example](examples/Passwordless.AspNetIdentity.Example) — Passwordless.dev integration using ASP.NET Identity.
- [Passwordless.MultiTenancy.Example](examples/Passwordless.AspNetIdentity.Example) — Passwordless.dev integration for multi-tenant applications.

## Usage

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Mvc;

namespace Passwordless.MultiTenancy.Example.Controllers;

[ApiController]
[Route("[controller]")]
public class MultiTenancyController : ControllerBase
{
[HttpGet("Users")]
public async Task<IActionResult> Get([FromServices] IPasswordlessClient client)
{
var users = await client.ListUsersAsync();

return Ok(users);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Passwordless\Passwordless.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Passwordless.MultiTenancy.Example.Passwordless;

public interface IPasswordlessClientBuilder
{
PasswordlessClientBuilder WithApiUrl(string apiUrl);

PasswordlessClientBuilder WithTenant(string tenant);

PasswordlessClient Build();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.Extensions.Options;

namespace Passwordless.MultiTenancy.Example.Passwordless;

public class PasswordlessClientBuilder : IPasswordlessClientBuilder
{
private readonly PasswordlessOptions _options = new()
{
ApiSecret = null!
};
private readonly PasswordlessMultiTenancyConfiguration _multiTenancyOptions;

public PasswordlessClientBuilder(IOptions<PasswordlessMultiTenancyConfiguration> multiTenancyOptions)
{
_multiTenancyOptions = multiTenancyOptions.Value ?? throw new ArgumentNullException(nameof(multiTenancyOptions));
}

public PasswordlessClientBuilder WithApiUrl(string apiUrl)
{
_options.ApiUrl = apiUrl;
return this;
}

public PasswordlessClientBuilder WithTenant(string tenant)
{
var tenantConfiguration = _multiTenancyOptions.Tenants[tenant];
_options.ApiSecret = tenantConfiguration.ApiSecret;
return this;
}

public PasswordlessClient Build()
{
return new PasswordlessClient(_options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Passwordless.MultiTenancy.Example.Passwordless;

public class PasswordlessMultiTenancyConfiguration
{
public Dictionary<string, TenantConfiguration> Tenants { get; set; } = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Passwordless.MultiTenancy.Example.Passwordless;

public class TenantConfiguration
{
public required string ApiSecret { get; set; }
}
55 changes: 55 additions & 0 deletions examples/Passwordless.MultiTenancy.Example/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Passwordless;
using Passwordless.MultiTenancy.Example.Passwordless;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Multi-tenancy: For accessing the current HTTP context
builder.Services.AddHttpContextAccessor();

// Multi-tenancy: To build the PasswordlessClient
builder.Services.AddSingleton<IPasswordlessClientBuilder, PasswordlessClientBuilder>();

// Multi-tenancy: To get the multi-tenant api secrets from our configuration
builder.Services.AddOptions<PasswordlessMultiTenancyConfiguration>().BindConfiguration("Passwordless");

// Multi-tenancy: Integrate the multi-tenant PasswordlessClient with the HttpClient
builder.Services.AddHttpClient<IPasswordlessClient, PasswordlessClient>((http, sp) =>
{

var httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
var host = httpContextAccessor.HttpContext!.Request.Host;

// gameofthrones or thewalkingdead tenant
var tenant = host.Host.Split('.')[0];

var clientBuilder = sp.GetRequiredService<IPasswordlessClientBuilder>();
clientBuilder.WithTenant(tenant);

var passwordlessClient = clientBuilder.Build();
return passwordlessClient;
});

builder.Services.AddScoped(sp => (PasswordlessClient)sp.GetRequiredService<IPasswordlessClient>());


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:62502",
"sslPort": 0
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5265",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
50 changes: 50 additions & 0 deletions examples/Passwordless.MultiTenancy.Example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Requirements:
- .NET 8.0 or later
- Passwordless.dev Pro or Enterprise

# Getting started
This example demonstrates how to use the Passwordless library in a multi-tenant environment. Where the subdomain is used
to identify the tenant. For example:

- `gameofthrones.passwordless.local`
- `thewalkingdead.passwordless.local`

We can achieve this by bootstrapping the Passwordless SDK ourselves in `Program.cs` and providing the necessary
configuration.

For a tenant `thewalkingdead` or `gameofthrones` respectively. You would find the configuration then in your appsettings.json
file. Similarly, you can use a database or any other configuration source.

```json
{
"Passwordless": {
"Tenants": {
"gameofthrones": {
"ApiSecret": "gameofthrones:secret:00000000000000000000000000000000"
},
"thewalkingdead": {
"ApiSecret": "thewalkingdead:secret:00000000000000000000000000000000"
}
}
}
}
```

1. Create entries in the hosts file:

127.0.0.1 gameofthrones.passwordless.local
127.0.0.1 thewalkingdead.passwordless.local

These are the tenants of your own backend as an example named 'gameofthrones' and 'thewalkingdead'

2. Modify any tenants and their related `ApiSecret` setting in the `appsettings.json` file.

3. Run the sample locally with .NET 8, debug if you have to step through. And visit:

- http://gameofthrones.passwordless.local/swagger/index.html
- http://thewalkingdead.passwordless.local/swagger/index.html

4. Call the 'Users' endpoint from Swagger to test using your own API secrets obtained.

You can refer to the `Passwordless.Example` project how to create your own complete backend with the Passwordless
library, as this example is a stripped-down version of it to demonstrate multi-tenancy in a simple way.
19 changes: 19 additions & 0 deletions examples/Passwordless.MultiTenancy.Example/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Passwordless": {
"Tenants": {
"gameofthrones": {
"ApiSecret": "app1:secret:a06f1e0e1b3149a385f7fc50553d21f8"
},
"thewalkingdead": {
"ApiSecret": "app2:secret:a06f1e0e1b3149a385f7fc50553d21f8"
}
}
}
}

0 comments on commit 540def1

Please sign in to comment.