diff --git a/Passwordless.sln b/Passwordless.sln index 1877828..56125c8 100644 --- a/Passwordless.sln +++ b/Passwordless.sln @@ -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 @@ -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 @@ -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 diff --git a/README.md b/README.md index 8e922be..0caed0c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/Passwordless.MultiTenancy.Example/Controllers/MultiTenancyController.cs b/examples/Passwordless.MultiTenancy.Example/Controllers/MultiTenancyController.cs new file mode 100644 index 0000000..efa5b55 --- /dev/null +++ b/examples/Passwordless.MultiTenancy.Example/Controllers/MultiTenancyController.cs @@ -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 Get([FromServices] IPasswordlessClient client) + { + var users = await client.ListUsersAsync(); + + return Ok(users); + } +} \ No newline at end of file diff --git a/examples/Passwordless.MultiTenancy.Example/Passwordless.MultiTenancy.Example.csproj b/examples/Passwordless.MultiTenancy.Example/Passwordless.MultiTenancy.Example.csproj new file mode 100644 index 0000000..a1f4c77 --- /dev/null +++ b/examples/Passwordless.MultiTenancy.Example/Passwordless.MultiTenancy.Example.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/examples/Passwordless.MultiTenancy.Example/Passwordless/IPasswordlessClientBuilder.cs b/examples/Passwordless.MultiTenancy.Example/Passwordless/IPasswordlessClientBuilder.cs new file mode 100644 index 0000000..4ebcfc3 --- /dev/null +++ b/examples/Passwordless.MultiTenancy.Example/Passwordless/IPasswordlessClientBuilder.cs @@ -0,0 +1,10 @@ +namespace Passwordless.MultiTenancy.Example.Passwordless; + +public interface IPasswordlessClientBuilder +{ + PasswordlessClientBuilder WithApiUrl(string apiUrl); + + PasswordlessClientBuilder WithTenant(string tenant); + + PasswordlessClient Build(); +} \ No newline at end of file diff --git a/examples/Passwordless.MultiTenancy.Example/Passwordless/PasswordlessClientBuilder.cs b/examples/Passwordless.MultiTenancy.Example/Passwordless/PasswordlessClientBuilder.cs new file mode 100644 index 0000000..82d671c --- /dev/null +++ b/examples/Passwordless.MultiTenancy.Example/Passwordless/PasswordlessClientBuilder.cs @@ -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 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); + } +} \ No newline at end of file diff --git a/examples/Passwordless.MultiTenancy.Example/Passwordless/PasswordlessMultiTenancyConfiguration.cs b/examples/Passwordless.MultiTenancy.Example/Passwordless/PasswordlessMultiTenancyConfiguration.cs new file mode 100644 index 0000000..a667fee --- /dev/null +++ b/examples/Passwordless.MultiTenancy.Example/Passwordless/PasswordlessMultiTenancyConfiguration.cs @@ -0,0 +1,6 @@ +namespace Passwordless.MultiTenancy.Example.Passwordless; + +public class PasswordlessMultiTenancyConfiguration +{ + public Dictionary Tenants { get; set; } = new(); +} \ No newline at end of file diff --git a/examples/Passwordless.MultiTenancy.Example/Passwordless/TenantConfiguration.cs b/examples/Passwordless.MultiTenancy.Example/Passwordless/TenantConfiguration.cs new file mode 100644 index 0000000..232ea26 --- /dev/null +++ b/examples/Passwordless.MultiTenancy.Example/Passwordless/TenantConfiguration.cs @@ -0,0 +1,6 @@ +namespace Passwordless.MultiTenancy.Example.Passwordless; + +public class TenantConfiguration +{ + public required string ApiSecret { get; set; } +} \ No newline at end of file diff --git a/examples/Passwordless.MultiTenancy.Example/Program.cs b/examples/Passwordless.MultiTenancy.Example/Program.cs new file mode 100644 index 0000000..38e98b2 --- /dev/null +++ b/examples/Passwordless.MultiTenancy.Example/Program.cs @@ -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(); + +// Multi-tenancy: To get the multi-tenant api secrets from our configuration +builder.Services.AddOptions().BindConfiguration("Passwordless"); + +// Multi-tenancy: Integrate the multi-tenant PasswordlessClient with the HttpClient +builder.Services.AddHttpClient((http, sp) => +{ + + var httpContextAccessor = sp.GetRequiredService(); + var host = httpContextAccessor.HttpContext!.Request.Host; + + // gameofthrones or thewalkingdead tenant + var tenant = host.Host.Split('.')[0]; + + var clientBuilder = sp.GetRequiredService(); + clientBuilder.WithTenant(tenant); + + var passwordlessClient = clientBuilder.Build(); + return passwordlessClient; +}); + +builder.Services.AddScoped(sp => (PasswordlessClient)sp.GetRequiredService()); + + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/examples/Passwordless.MultiTenancy.Example/Properties/launchSettings.json b/examples/Passwordless.MultiTenancy.Example/Properties/launchSettings.json new file mode 100644 index 0000000..fe93c57 --- /dev/null +++ b/examples/Passwordless.MultiTenancy.Example/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/examples/Passwordless.MultiTenancy.Example/README.md b/examples/Passwordless.MultiTenancy.Example/README.md new file mode 100644 index 0000000..6939108 --- /dev/null +++ b/examples/Passwordless.MultiTenancy.Example/README.md @@ -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. diff --git a/examples/Passwordless.MultiTenancy.Example/appsettings.json b/examples/Passwordless.MultiTenancy.Example/appsettings.json new file mode 100644 index 0000000..c854328 --- /dev/null +++ b/examples/Passwordless.MultiTenancy.Example/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Passwordless": { + "Tenants": { + "gameofthrones": { + "ApiSecret": "app1:secret:a06f1e0e1b3149a385f7fc50553d21f8" + }, + "thewalkingdead": { + "ApiSecret": "app2:secret:a06f1e0e1b3149a385f7fc50553d21f8" + } + } + } +}