From 71b7b75856f967869cdaa4b41fe4887bcf148533 Mon Sep 17 00:00:00 2001 From: Steve Temple Date: Tue, 6 Aug 2024 08:15:25 +0100 Subject: [PATCH] Add support for multiple profiles (#33) * Add support for multiple profiles * Tidy up and update JSON schema * Tidy up and configuration validation * Added extra validation healthcheck and tidy up * Todos for config validation and NotImplmentedException for unused options method --- README.md | 92 ++++++++++++++++--- .../AzureSSOConfiguration.cs | 79 ++++++++++++++-- .../HealthChecks/AzureSSOHealthCheck.cs | 52 +++++++++++ ...icrosoftAccountAuthenticationExtensions.cs | 70 ++++++++++---- ...tBackOfficeExternalLoginProviderOptions.cs | 48 +++++----- .../Settings/AzureSSOSettings.cs | 47 +++++++--- .../Umbraco.Community.AzureSSO.csproj | 45 ++++----- ...tings-schema.UmbracoCommunityAzureSSO.json | 21 ++++- 8 files changed, 357 insertions(+), 97 deletions(-) create mode 100644 src/Umbraco.Community.AzureSSO/HealthChecks/AzureSSOHealthCheck.cs diff --git a/README.md b/README.md index c8463b2..671c7e8 100644 --- a/README.md +++ b/README.md @@ -13,26 +13,26 @@ You'll need to configure the package by adding the following section to the root ``` "AzureSSO": { "Credentials": { - "Instance": "https://login.microsoftonline.com/", - "Domain": "", - "TenantId": "", - "ClientId": "", - "CallbackPath": "/umbraco-microsoft-signin/", - "SignedOutCallbackPath ": "/umbraco-microsoft-signout/", - "ClientSecret": "" + "Instance": "https://login.microsoftonline.com/", + "Domain": "", + "TenantId": "", + "ClientId": "", + "CallbackPath": "/umbraco-microsoft-signin/", + "SignedOutCallbackPath ": "/umbraco-microsoft-signout/", + "ClientSecret": "" }, "DisplayName": "Azure AD", "DenyLocalLogin": true, "AutoRedirectLoginToExternalProvider": true, "TokenCacheType": "InMemory", "GroupBindings": { - "": "", - "": "" + "": "", + "": "" }, "SetGroupsOnLogin": true, "DefaultGroups": [ - "editor" - ], + "editor" + ], "Icon": "fa fa-lock", "ButtonStyle": "btn-microsoft", }, @@ -82,7 +82,77 @@ If you are having problems with NET BIOS group names, you can set the groups cla You can now use the guid format for the Group Id like: `"xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx": "admin", "44a38651-xxxx-4c92-b1b6-51cf26ff9bab": "editor"` +# Advanced usage + +## Multiple tenants + +If you'd like to use more than one tenant, or app registration then you can change the configuration to use profiles, see below. +This could be used for having one SSO option for agency users and another for client users. + +``` +"AzureSSO": { + "Profiles": [ + { + "Name": "InternalAccount", + "Credentials": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "", + "TenantId": "", + "ClientId": "", + "CallbackPath": "/umbraco-microsoft-signin/", + "SignedOutCallbackPath ": "/umbraco-microsoft-signout/", + "ClientSecret": "" + }, + "DisplayName": "My AD", + "DenyLocalLogin": true, + "AutoRedirectLoginToExternalProvider": false, + "TokenCacheType": "InMemory", + "GroupBindings": { + "": "", + "": "" + }, + "SetGroupsOnLogin": true, + "DefaultGroups": [ + "editor" + ], + "Icon": "fa fa-lock", + "ButtonStyle": "btn-microsoft", + }, + { + "Name": "AlternateAccount", + "Credentials": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "", + "TenantId": "", + "ClientId": "", + "CallbackPath": "/umbraco-microsoft-alt-signin/", + "SignedOutCallbackPath ": "/umbraco-microsoft-alt-signout/", + "ClientSecret": "" + }, + "DisplayName": "My Client AD", + "DenyLocalLogin": true, + "AutoRedirectLoginToExternalProvider": false, + "TokenCacheType": "InMemory", + "GroupBindings": { + "": "", + "": "" + }, + "SetGroupsOnLogin": true, + "DefaultGroups": [ + "editor" + ], + "Icon": "fa fa-lock", + "ButtonStyle": "btn-microsoft", + }, + ] +}, +``` + +Each ClientId and ClientSecret should be different, also TentantId and domain should be different if using a different tenant. + +Please ensure that the CallbackPath and SignedOutCallbackPath are different for each profile. +Note you cannot use AutoRedirectLoginToExternalProvider if you'd like 2 profiles. diff --git a/src/Umbraco.Community.AzureSSO/AzureSSOConfiguration.cs b/src/Umbraco.Community.AzureSSO/AzureSSOConfiguration.cs index afbae22..223ac71 100644 --- a/src/Umbraco.Community.AzureSSO/AzureSSOConfiguration.cs +++ b/src/Umbraco.Community.AzureSSO/AzureSSOConfiguration.cs @@ -1,16 +1,15 @@ +using System; using System.Collections.Generic; +using System.Linq; +using Microsoft.IdentityModel.Tokens; namespace Umbraco.Community.AzureSSO { public class AzureSSOConfiguration { public const string AzureSsoSectionName = "AzureSSO"; - public const string AzureSsoCredentialSectionName = "AzureSSO:Credentials"; - public AzureSSOConfiguration() - { - GroupBindings = new Dictionary(); - } + public string? Name { get; set; } public string? DisplayName { get; set; } @@ -18,7 +17,7 @@ public AzureSSOConfiguration() public string? Icon { get; set; } - public Dictionary GroupBindings { get; set; } + public Dictionary GroupBindings { get; set; } = new(); public bool? SetGroupsOnLogin { get; set; } @@ -27,7 +26,69 @@ public AzureSSOConfiguration() public bool? DenyLocalLogin { get; set; } public TokenCacheType TokenCacheType { get; set; } = TokenCacheType.InMemory; - - public bool? AutoRedirectLoginToExternalProvider { get; set; } - } + + public bool? AutoRedirectLoginToExternalProvider { get; set; } + + public AzureSSOCredentials? Credentials { get; set; } + + public AzureSSOConfiguration[]? Profiles { get; set; } + + public bool IsValid() + { + // TODO : Make this give or log specific feedback before we do anything like prevent booting if misconfigured + // and to make it more useful for the HealthCheck + return (Profiles != null && Profiles.Any() && AllValuesEmpty() && AllProfilesUnique() && AllProfilesHaveName()) || + (Profiles.IsNullOrEmpty() && Credentials != null && Credentials.IsValid()); + } + + public bool AllValuesEmpty() + { + return string.IsNullOrEmpty(Name) && + string.IsNullOrEmpty(DisplayName) && + string.IsNullOrEmpty(ButtonStyle) && + string.IsNullOrEmpty(Icon) && + !GroupBindings.Any() && + SetGroupsOnLogin == null && + (DefaultGroups == null || !DefaultGroups.Any()) && + DenyLocalLogin == null && + AutoRedirectLoginToExternalProvider == null && + Credentials == null; + } + + public bool AllProfilesHaveName() + { + return Profiles != null && Profiles.All(x => !string.IsNullOrEmpty(x.Name)); + } + + public bool AllProfilesUnique() + { + return Profiles != null && + Profiles.Select(x => x.Name).Distinct().Count() == Profiles.Count() && + Profiles.Select(x => x.Credentials?.CallbackPath).Distinct().Count() == Profiles.Count() && + Profiles.Select(x => x.Credentials?.SignedOutCallbackPath).Distinct().Count() == Profiles.Count() && + Profiles.Select(x => x.DisplayName).Distinct().Count() == Profiles.Count(); + } + } + + public class AzureSSOCredentials + { + public string Instance { get; set; } = ""; + public string Domain { get; set; } = ""; + public string TenantId { get; set; } = ""; + public string ClientId { get; set; } = ""; + public string ClientSecret { get; set; } = ""; + public string CallbackPath { get; set; } = ""; + public string SignedOutCallbackPath { get; set; } = ""; + + public bool IsValid() + { + return !string.IsNullOrEmpty(Instance) && + !string.IsNullOrEmpty(Domain) && + !string.IsNullOrEmpty(TenantId) && + !string.IsNullOrEmpty(ClientId) && + !string.IsNullOrEmpty(ClientSecret) && + !string.IsNullOrEmpty(CallbackPath) && + !string.IsNullOrEmpty(SignedOutCallbackPath); + } + } } diff --git a/src/Umbraco.Community.AzureSSO/HealthChecks/AzureSSOHealthCheck.cs b/src/Umbraco.Community.AzureSSO/HealthChecks/AzureSSOHealthCheck.cs new file mode 100644 index 0000000..f63cd5e --- /dev/null +++ b/src/Umbraco.Community.AzureSSO/HealthChecks/AzureSSOHealthCheck.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Umbraco.Cms.Core.HealthChecks; + +namespace Umbraco.Community.AzureSSO.HealthChecks +{ + [HealthCheck(HealthCheckId, HealthCheckName, Description = "Checks the Azure SSO config to ensure it is valid.", Group = "Configuration")] + public class AzureSSOHealthCheck : HealthCheck + { + private const string HealthCheckId = "07F7DA0A-D351-4347-92B3-9B607E1D38BB"; + private const string HealthCheckName = "Azure SSO"; + + private AzureSSOConfiguration _configuration; + + public AzureSSOHealthCheck(AzureSSOConfiguration configuration) + { + _configuration = configuration; + } + + public override async Task> GetStatus() + { + var statuses = new List(); + + if (!_configuration.IsValid()) + { + // TODO : We really need specific feedback for this to be useful + statuses.Add(new HealthCheckStatus("Configuration Invalid.") + { + Description = "Check AzureSSO configuration", + ResultType = StatusResultType.Error + }); + } + + if(!statuses.Any()) + { + statuses.Add(new HealthCheckStatus("Configuration valid.") + { + ResultType = StatusResultType.Success + }); + } + + return statuses; + } + + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => new("How did you get here?") + { + ResultType = StatusResultType.Info + }; + } +} diff --git a/src/Umbraco.Community.AzureSSO/MicrosoftAccountAuthenticationExtensions.cs b/src/Umbraco.Community.AzureSSO/MicrosoftAccountAuthenticationExtensions.cs index 253f16c..ce10409 100644 --- a/src/Umbraco.Community.AzureSSO/MicrosoftAccountAuthenticationExtensions.cs +++ b/src/Umbraco.Community.AzureSSO/MicrosoftAccountAuthenticationExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Client; using Microsoft.Identity.Web; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Community.AzureSSO.Settings; @@ -15,32 +16,63 @@ internal static IUmbracoBuilder AddMicrosoftAccountAuthentication(this IUmbracoB { var azureSsoConfiguration = new AzureSSOConfiguration(); builder.Config.Bind(AzureSSOConfiguration.AzureSsoSectionName, azureSsoConfiguration); - - builder.Services.AddSingleton(conf => new AzureSsoSettings(azureSsoConfiguration)); - builder.Services.ConfigureOptions(); + builder.Services.AddSingleton(conf => azureSsoConfiguration); + var settings = new AzureSsoSettings(azureSsoConfiguration); + builder.Services.AddSingleton(conf => settings); + builder.Services.ConfigureOptions(); + var initialScopes = Array.Empty(); builder.AddBackOfficeExternalLogins(logins => - { - logins.AddBackOfficeLogin( - backOfficeAuthenticationBuilder => + { + foreach (var profile in settings.Profiles) { - backOfficeAuthenticationBuilder.AddMicrosoftIdentityWebApp(options => - { - builder.Config.Bind(AzureSSOConfiguration.AzureSsoCredentialSectionName, options); - options.SignInScheme = backOfficeAuthenticationBuilder.SchemeForBackOffice(MicrosoftAccountBackOfficeExternalLoginProviderOptions.SchemeName); - options.Events = new OpenIdConnectEvents(); - }, - options => { builder.Config.Bind(AzureSSOConfiguration.AzureSsoCredentialSectionName, options); }, - displayName: azureSsoConfiguration.DisplayName ?? "Azure Active Directory", - openIdConnectScheme: backOfficeAuthenticationBuilder.SchemeForBackOffice(MicrosoftAccountBackOfficeExternalLoginProviderOptions.SchemeName) ?? String.Empty) - .EnableTokenAcquisitionToCallDownstreamApi(options => builder.Config.Bind(AzureSSOConfiguration.AzureSsoCredentialSectionName, options), initialScopes) - .AddTokenCaches(azureSsoConfiguration.TokenCacheType); - }); - }); + logins.AddBackOfficeLogin( + backOfficeAuthenticationBuilder => + { + backOfficeAuthenticationBuilder.AddMicrosoftIdentityWebApp(options => + { + CopyCredentials(options, profile.Credentials); + options.SignInScheme = backOfficeAuthenticationBuilder.SchemeForBackOffice(profile.Name); + options.Events = new OpenIdConnectEvents(); + + }, + displayName: profile.DisplayName ?? "Microsoft Entra ID", + cookieScheme: $"{profile.Name}Cookies", + openIdConnectScheme: backOfficeAuthenticationBuilder.SchemeForBackOffice(profile.Name) ?? + String.Empty) + .EnableTokenAcquisitionToCallDownstreamApi( + options => CopyCredentials(options, profile.Credentials), + initialScopes) + .AddTokenCaches(profile.TokenCacheType); + }); + } + } + + ); + return builder; } + private static void CopyCredentials(MicrosoftIdentityOptions options, AzureSsoCredentialSettings settings) + { + options.Instance = settings.Instance; + options.Domain = settings.Domain; + options.TenantId = settings.TenantId; + options.ClientId = settings.ClientId; + options.ClientSecret = settings.ClientSecret; + options.SignedOutCallbackPath = settings.SignedOutCallbackPath; + options.CallbackPath = settings.CallbackPath; + } + + private static void CopyCredentials(ConfidentialClientApplicationOptions options, AzureSsoCredentialSettings settings) + { + options.Instance = settings.Instance; + options.TenantId = settings.TenantId; + options.ClientId = settings.ClientId; + options.ClientSecret = settings.ClientSecret; + } + private static MicrosoftIdentityAppCallsWebApiAuthenticationBuilder AddTokenCaches(this MicrosoftIdentityAppCallsWebApiAuthenticationBuilder builder, TokenCacheType tokenCacheType) { switch (tokenCacheType) diff --git a/src/Umbraco.Community.AzureSSO/MicrosoftAccountBackOfficeExternalLoginProviderOptions.cs b/src/Umbraco.Community.AzureSSO/MicrosoftAccountBackOfficeExternalLoginProviderOptions.cs index 9c51d81..4968278 100644 --- a/src/Umbraco.Community.AzureSSO/MicrosoftAccountBackOfficeExternalLoginProviderOptions.cs +++ b/src/Umbraco.Community.AzureSSO/MicrosoftAccountBackOfficeExternalLoginProviderOptions.cs @@ -1,39 +1,35 @@ +using System; using System.Linq; using System.Security.Claims; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Community.AzureSSO.Settings; namespace Umbraco.Community.AzureSSO { - public class MicrosoftAccountBackOfficeExternalLoginProviderOptions : IConfigureNamedOptions + public class MicrosoftAccountBackOfficeExternalLoginProviderOptions(AzureSsoSettings settings) + : IConfigureNamedOptions { public const string SchemeName = "MicrosoftAccount"; - private readonly AzureSsoSettings _settings; - - public MicrosoftAccountBackOfficeExternalLoginProviderOptions(AzureSsoSettings settings) - { - _settings = settings; - } - public void Configure(string? name, BackOfficeExternalLoginProviderOptions options) { - if (name != $"{Constants.Security.BackOfficeExternalAuthenticationTypePrefix}{SchemeName}") + var profile = settings.Profiles + .FirstOrDefault(x => x.Name == name); + if (profile == null) { return; } - Configure(options); + Configure(options, profile); } - public void Configure(BackOfficeExternalLoginProviderOptions options) + public void Configure(BackOfficeExternalLoginProviderOptions options, AzureSsoProfileSettings profileSettings) { - options.ButtonStyle = _settings.ButtonStyle; - options.Icon = _settings.Icon; + options.ButtonStyle = profileSettings.ButtonStyle; + options.Icon = profileSettings.Icon; options.AutoLinkOptions = new ExternalSignInAutoLinkOptions( // must be true for auto-linking to be enabled autoLinkExternalAccount: true, @@ -62,15 +58,15 @@ public void Configure(BackOfficeExternalLoginProviderOptions options) { if (!autoLoginUser.IsApproved) { - SetGroups(autoLoginUser, loginInfo); + SetGroups(autoLoginUser, loginInfo, profileSettings); SetName(autoLoginUser, loginInfo); } }, OnExternalLogin = (user, loginInfo) => { - if (_settings.SetGroupsOnLogin) + if (profileSettings.SetGroupsOnLogin) { - SetGroups(user, loginInfo); + SetGroups(user, loginInfo, profileSettings); } SetName(user, loginInfo); @@ -82,29 +78,29 @@ public void Configure(BackOfficeExternalLoginProviderOptions options) // to login with a username/password. If this is set // to true, it will disable username/password login // even if there are other external login providers installed. - options.DenyLocalLogin = _settings.DenyLocalLogin; + options.DenyLocalLogin = profileSettings.DenyLocalLogin; // Optionally choose to automatically redirect to the // external login provider so the user doesn't have // to click the login button. - options.AutoRedirectLoginToExternalProvider = _settings.AutoRedirectLoginToExternalProvider; + options.AutoRedirectLoginToExternalProvider = profileSettings.AutoRedirectLoginToExternalProvider; } - private void SetGroups(BackOfficeIdentityUser user, ExternalLoginInfo loginInfo) + private void SetGroups(BackOfficeIdentityUser user, ExternalLoginInfo loginInfo, AzureSsoProfileSettings settings) { user.Roles.Clear(); - var groups = loginInfo.Principal.Claims.Where(c => _settings.GroupLookup.ContainsKey(c.Value)); + var groups = loginInfo.Principal.Claims.Where(c => settings.GroupLookup.ContainsKey(c.Value)); foreach (var group in groups) { - var umbracoGroups = _settings.GroupLookup[group.Value].Split(','); + var umbracoGroups = settings.GroupLookup[group.Value].Split(','); foreach (var umbracoGroupAlias in umbracoGroups) { user.AddRole(umbracoGroupAlias); } } - foreach (var group in _settings.DefaultGroups) + foreach (var group in settings.DefaultGroups) { user.AddRole(group); } @@ -126,5 +122,11 @@ private string DisplayName(ClaimsPrincipal claimsPrincipal, string defaultValue) return !string.IsNullOrWhiteSpace(displayName) ? displayName : defaultValue; } + + public void Configure(BackOfficeExternalLoginProviderOptions options) + { + throw new NotImplementedException( + "Use Configure(BackOfficeExternalLoginProviderOptions, AzureSsoProfileSettings) instead"); + } } } diff --git a/src/Umbraco.Community.AzureSSO/Settings/AzureSSOSettings.cs b/src/Umbraco.Community.AzureSSO/Settings/AzureSSOSettings.cs index 6ca7a0f..333e140 100644 --- a/src/Umbraco.Community.AzureSSO/Settings/AzureSSOSettings.cs +++ b/src/Umbraco.Community.AzureSSO/Settings/AzureSSOSettings.cs @@ -1,23 +1,48 @@ using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; namespace Umbraco.Community.AzureSSO.Settings { public class AzureSsoSettings { - private readonly AzureSSOConfiguration _configuration; - public AzureSsoSettings(AzureSSOConfiguration configuration) { - _configuration = configuration; + if (configuration.Profiles == null) + { + Profiles = new[] { new AzureSsoProfileSettings(configuration) }; + return; + } + + Profiles = configuration.Profiles.Select(x => new AzureSsoProfileSettings(x)).ToArray(); } - public string ButtonStyle => _configuration.ButtonStyle ?? "btn-microsoft"; - public string Icon => _configuration.Icon ?? "fa fa-lock"; - public Dictionary GroupLookup => _configuration.GroupBindings; - public bool SetGroupsOnLogin => _configuration.SetGroupsOnLogin ?? true; - public string[] DefaultGroups => _configuration.DefaultGroups ?? System.Array.Empty(); - public bool DenyLocalLogin => _configuration.DenyLocalLogin ?? false; - public TokenCacheType TokenCacheType => _configuration.TokenCacheType; - public bool AutoRedirectLoginToExternalProvider => _configuration.AutoRedirectLoginToExternalProvider ?? false; + public AzureSsoProfileSettings[] Profiles { get; } + } + + public class AzureSsoProfileSettings(AzureSSOConfiguration configuration) + { + public string Name => $"{Constants.Security.BackOfficeExternalAuthenticationTypePrefix}{configuration.Name}" ?? MicrosoftAccountBackOfficeExternalLoginProviderOptions.SchemeName; + public string? DisplayName => configuration.DisplayName; + public string ButtonStyle => configuration.ButtonStyle ?? "btn-microsoft"; + public string Icon => configuration.Icon ?? "fa fa-lock"; + public Dictionary GroupLookup => configuration.GroupBindings; + public bool SetGroupsOnLogin => configuration.SetGroupsOnLogin ?? true; + public string[] DefaultGroups => configuration.DefaultGroups ?? System.Array.Empty(); + public bool DenyLocalLogin => configuration.DenyLocalLogin ?? false; + public TokenCacheType TokenCacheType => configuration.TokenCacheType; + public bool AutoRedirectLoginToExternalProvider => configuration.AutoRedirectLoginToExternalProvider ?? false; + public AzureSsoCredentialSettings Credentials => new AzureSsoCredentialSettings(configuration.Credentials); + } + + public class AzureSsoCredentialSettings(AzureSSOCredentials credentials) + { + public string Instance => credentials.Instance; + public string Domain => credentials.Domain; + public string TenantId => credentials.TenantId; + public string ClientId => credentials.ClientId; + public string ClientSecret => credentials.ClientSecret; + public string CallbackPath => credentials.CallbackPath; + public string SignedOutCallbackPath => credentials.SignedOutCallbackPath; } } diff --git a/src/Umbraco.Community.AzureSSO/Umbraco.Community.AzureSSO.csproj b/src/Umbraco.Community.AzureSSO/Umbraco.Community.AzureSSO.csproj index 2c1e96f..b8ed65f 100644 --- a/src/Umbraco.Community.AzureSSO/Umbraco.Community.AzureSSO.csproj +++ b/src/Umbraco.Community.AzureSSO/Umbraco.Community.AzureSSO.csproj @@ -1,12 +1,12 @@ - - net8.0;net7.0;net6.0 - enable - Umbraco.Community.AzureSSO - 2.0.0 - Steve Temple - Gibe Digital - Azure SSO for Umbraco + + net8.0;net7.0;net6.0 + enable + Umbraco.Community.AzureSSO + 2.0.0 + Steve Temple + Gibe Digital + Azure SSO for Umbraco https://github.com/Gibe/Umbraco.Community.AzureSSO umbraco-marketplace;sso;azure-ad;aad Copyright (c) 2024 Gibe Digital Ltd @@ -17,17 +17,18 @@ README.md https://github.com/Gibe/Umbraco.Community.AzureSSO git - - - - True - \ - - - True - \ - - + 12.0 + + + + True + \ + + + True + \ + + @@ -38,9 +39,9 @@ - - 11.0.0 - + + 11.0.0 + diff --git a/src/Umbraco.Community.AzureSSO/appsettings-schema.UmbracoCommunityAzureSSO.json b/src/Umbraco.Community.AzureSSO/appsettings-schema.UmbracoCommunityAzureSSO.json index a301ada..f82d9a8 100644 --- a/src/Umbraco.Community.AzureSSO/appsettings-schema.UmbracoCommunityAzureSSO.json +++ b/src/Umbraco.Community.AzureSSO/appsettings-schema.UmbracoCommunityAzureSSO.json @@ -4,10 +4,27 @@ "type": "object", "properties": { "AzureSSO": { - "$ref": "#/definitions/UmbracoCommunityAzureSSODefinition" + "$ref": "#/definitions/umbracoCommunityAzure" } }, "definitions": { + "umbracoCommunityAzure": { + "oneOf": [ + { "$ref": "#/definitions/UmbracoCommunityAzureSSODefinition" }, + { "$ref": "#/definitions/umbracoCommunityAzureSSOProfiles" } + ] + }, + "umbracoCommunityAzureSSOProfiles": { + "Profiles": { + "type": "array", + "description": "Alternative to a single profile, allows for multiple tenants", + "items": [ + { + "type": { "$ref": "#/definitions/UmbracoCommunityAzureSSODefinition"} + } + ] + } + }, "UmbracoCommunityAzureSSODefinition": { "type": "object", "description": "Configuration of Umbraco.Community.AzureSSO settings", @@ -86,7 +103,7 @@ "description": "The groups to assign to users regardless of any AD groups assigned (defaults to none)", "items": [ { - "type": "string" + "$ref": "#definitions/UmbracoCommunityAzureSSODefinition" } ] },