Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README-uptov12.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ You can also customise the configuration by setting these settings:
| DenyLocalLogin | Allow users to login via Umbraco's standard login |
| GroupBindings | The bindings for Entry ID group to Umbraco group |
| SetGroupsOnLogin | Whether or not to reset the users assigned groups on each login |
| SetProfileImageOnLogin | Fetch the user's Entra ID profile photo on each login and set it as their Umbraco avatar |
| TokenCacheType | Token Cache Type to use: InMemory, Session, Distributed (defaults to InMemory) |
| DefaultGroups | The groups to assign to users regardless of any Entry ID groups assigned (defaults to none) |
| LogUnmappedRolesAsWarning | Log any roles in Entra ID that aren't mapped as warnings for debugging purposes |
Expand Down Expand Up @@ -81,6 +82,10 @@ You can now use the guid format for the Group Id like: `"xxxxxxxxx-xxxx-xxxx-xxx

You can bind one Entra ID group to multiple Umbraco groups by including a comma separated list of groups. For example: `"GIBE\Producers" : "editors, writers"` will bind the `GIBE\Producers` group to both the `editors` and `writers` Umbraco groups.

## Profile Photo

Setting `SetProfileImageOnLogin` to `true` requests the `User.Read` Microsoft Graph scope and, on every login, fetches the user's photo from `https://graph.microsoft.com/v1.0/me/photo/$value` and sets it as their Umbraco avatar. This is opt-in and off by default, since it adds an extra Graph API call to every login and requests an additional scope. If the user has no photo set in Entra ID, or the Graph call fails for any reason, the login proceeds as normal and the failure is logged as a warning.
Comment thread
stevetemple marked this conversation as resolved.
Outdated

# Advanced usage

## Multiple tenants
Expand Down
5 changes: 5 additions & 0 deletions README-v13.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ You can also customise the configuration by setting these settings:
| DenyLocalLogin | Allow users to login via Umbraco's standard login |
| GroupBindings | The bindings for Entry ID group to Umbraco group |
| SetGroupsOnLogin | Whether or not to reset the users assigned groups on each login |
| SetProfileImageOnLogin | Fetch the user's Entra ID profile photo on each login and set it as their Umbraco avatar |
| TokenCacheType | Token Cache Type to use: InMemory, Session, Distributed (defaults to InMemory) |
| DefaultGroups | The groups to assign to users regardless of any Entry ID groups assigned (defaults to none) |
| LogUnmappedRolesAsWarning | Log any roles in Entra ID that aren't mapped as warnings for debugging purposes |
Expand Down Expand Up @@ -81,6 +82,10 @@ You can now use the guid format for the Group Id like: `"xxxxxxxxx-xxxx-xxxx-xxx

You can bind one Entra ID group to multiple Umbraco groups by including a comma separated list of groups. For example: `"GIBE\Producers" : "editors, writers"` will bind the `GIBE\Producers` group to both the `editors` and `writers` Umbraco groups.

## Profile Photo

Setting `SetProfileImageOnLogin` to `true` requests the `User.Read` Microsoft Graph scope and, on every login, fetches the user's photo from `https://graph.microsoft.com/v1.0/me/photo/$value` and sets it as their Umbraco avatar. This is opt-in and off by default, since it adds an extra Graph API call to every login and requests an additional scope. If the user has no photo set in Entra ID, or the Graph call fails for any reason, the login proceeds as normal and the failure is logged as a warning.
Comment thread
stevetemple marked this conversation as resolved.
Outdated

# Advanced usage

## Multiple tenants
Expand Down
5 changes: 5 additions & 0 deletions README-v14plus.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ You can also customise the configuration by setting these settings:
| DenyLocalLogin | Allow users to login via Umbraco's standard login |
| GroupBindings | The bindings for Entry ID group to Umbraco group |
| SetGroupsOnLogin | Whether or not to reset the users assigned groups on each login |
| SetProfileImageOnLogin | Fetch the user's Entra ID profile photo on each login and set it as their Umbraco avatar |
| TokenCacheType | Token Cache Type to use: InMemory, Session, Distributed (defaults to InMemory) |
| DefaultGroups | The groups to assign to users regardless of any Entry ID groups assigned (defaults to none) |
| LogUnmappedRolesAsWarning | Log any roles in Entra ID that aren't mapped as warnings for debugging purposes |
Expand All @@ -68,6 +69,10 @@ 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"`

## Profile Photo

Setting `SetProfileImageOnLogin` to `true` requests the `User.Read` Microsoft Graph scope and, on every login, fetches the user's photo from `https://graph.microsoft.com/v1.0/me/photo/$value` and sets it as their Umbraco avatar. This is opt-in and off by default, since it adds an extra Graph API call to every login and requests an additional scope. If the user has no photo set in Entra ID, or the Graph call fails for any reason, the login proceeds as normal and the failure is logged as a warning.
Comment thread
stevetemple marked this conversation as resolved.
Outdated

# Advanced usage

## Multiple tenants
Expand Down
5 changes: 5 additions & 0 deletions README-v15plus.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ You can also customise the configuration by setting these settings:
| DenyLocalLogin | Allow users to login via Umbraco's standard login |
| GroupBindings | The bindings for Entry ID group to Umbraco group |
| SetGroupsOnLogin | Whether or not to reset the users assigned groups on each login |
| SetProfileImageOnLogin | Fetch the user's Entra ID profile photo on each login and set it as their Umbraco avatar |
| TokenCacheType | Token Cache Type to use: InMemory, Session, Distributed (defaults to InMemory) |
| DefaultGroups | The groups to assign to users regardless of any Entry ID groups assigned (defaults to none) |
| LogUnmappedRolesAsWarning | Log any roles in Entra ID that aren't mapped as warnings for debugging purposes |
Expand All @@ -74,6 +75,10 @@ You can now use the guid format for the Group Id like: `"xxxxxxxxx-xxxx-xxxx-xxx

You can bind one Entra ID group to multiple Umbraco groups by including a comma separated list of groups. For example: `"GIBE\Producers" : "editors, writers"` will bind the `GIBE\Producers` group to both the `editors` and `writers` Umbraco groups.

## Profile Photo

Setting `SetProfileImageOnLogin` to `true` requests the `User.Read` Microsoft Graph scope and, on every login, fetches the user's photo from `https://graph.microsoft.com/v1.0/me/photo/$value` and sets it as their Umbraco avatar. This is opt-in and off by default, since it adds an extra Graph API call to every login and requests an additional scope. If the user has no photo set in Entra ID, or the Graph call fails for any reason, the login proceeds as normal and the failure is logged as a warning.
Comment thread
stevetemple marked this conversation as resolved.
Outdated

# Advanced usage

## Multiple tenants
Expand Down
3 changes: 1 addition & 2 deletions src/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ root = true
###############################################################################
# Set default behavior to:
# a UTF-8 encoding,
# Unix-style line endings,
# Windows-style line endings,
# a newline ending the file,
# 2 space indentation, and
# trimming of trailing whitespace
Expand Down Expand Up @@ -80,7 +80,6 @@ dotnet_naming_rule.type_parameters_should_be_pascal_case_prefixed_with_t.symbols


[*.cs]

# Define the 'private_fields' symbol group:
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
Expand Down
3 changes: 3 additions & 0 deletions src/Umbraco.Community.AzureSSO/AzureSSOConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class AzureSSOConfiguration

public bool? SetGroupsOnLogin { get; set; }

public bool? SetProfileImageOnLogin { get; set; }

public bool? LogUnmappedRolesAsWarning { get; set; }

public string[]? DefaultGroups { get; set; }
Expand Down Expand Up @@ -53,6 +55,7 @@ public bool AllValuesEmpty()
string.IsNullOrEmpty(Icon) &&
!GroupBindings.Any() &&
SetGroupsOnLogin == null &&
SetProfileImageOnLogin == null &&
(DefaultGroups == null || !DefaultGroups.Any()) &&
DenyLocalLogin == null &&
AutoRedirectLoginToExternalProvider == null &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,16 @@ internal static IUmbracoBuilder AddMicrosoftAccountAuthenticationInternal(this I
builder.Services.AddSingleton<IPackageManifestReader, AzureSsoManifestReader>();
#endif

var initialScopes = Array.Empty<string>();
builder.AddBackOfficeExternalLogins(logins =>
{
foreach (var profile in settings.Profiles)
{
if (profile.Enabled)
{
var initialScopes = profile.SetProfileImageOnLogin
? new[] { "User.Read" }
: Array.Empty<string>();
Comment thread
stevetemple marked this conversation as resolved.

logins.AddBackOfficeLogin(
backOfficeAuthenticationBuilder =>
{
Expand All @@ -60,6 +63,7 @@ internal static IUmbracoBuilder AddMicrosoftAccountAuthenticationInternal(this I
CopyCredentials(options, profile.Credentials);
options.SignInScheme = SchemeForBackOffice(profile.Name, backOfficeAuthenticationBuilder);
options.Events = new OpenIdConnectEvents();
options.SaveTokens = profile.SetProfileImageOnLogin;

},
displayName: profile.DisplayName ?? "Microsoft Entra ID",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Community.AzureSSO.Settings;
using Umbraco.Extensions;
using Microsoft.Extensions.Logging;

#if NEW_BACKOFFICE
Expand All @@ -18,7 +24,10 @@
namespace Umbraco.Community.AzureSSO
{
public class MicrosoftAccountBackOfficeExternalLoginProviderOptions(AzureSsoSettings settings,
ILogger<MicrosoftAccountBackOfficeExternalLoginProviderOptions> logger)
ILogger<MicrosoftAccountBackOfficeExternalLoginProviderOptions> logger,
IHttpClientFactory httpClientFactory,
IUserService userService,
MediaFileManager mediaFileManager)
: IConfigureNamedOptions<BackOfficeExternalLoginProviderOptions>
{
public const string SchemeName = "MicrosoftAccount";
Expand Down Expand Up @@ -78,12 +87,19 @@ public void Configure(BackOfficeExternalLoginProviderOptions options, AzureSsoPr
},
OnExternalLogin = (user, loginInfo) =>
{
var existingUsername = user.UserName;

if (profileSettings.SetGroupsOnLogin)
{
SetGroups(user, loginInfo, profileSettings);
}
SetName(user, loginInfo);

if (profileSettings.SetProfileImageOnLogin)
{
SetProfileImage(existingUsername, loginInfo);
}

if (user.Roles.Any())
{
return true;
Expand Down Expand Up @@ -153,6 +169,57 @@ private string DisplayName(ClaimsPrincipal claimsPrincipal, string defaultValue)
return !string.IsNullOrWhiteSpace(displayName) ? displayName : defaultValue;
}

private void SetProfileImage(string? username, ExternalLoginInfo loginInfo)
{
if (string.IsNullOrEmpty(username))
{
return;
}

try
{
var accessToken = loginInfo.AuthenticationTokens?
.FirstOrDefault(t => t.Name == "access_token")?.Value;

if (string.IsNullOrEmpty(accessToken))
{
return;
}

if (userService.GetByUsername(username) is not { } umbracoUser)
{
return;
}

using var httpClient = httpClientFactory.CreateClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

using var response = httpClient
.GetAsync("https://graph.microsoft.com/v1.0/me/photo/$value")
.GetAwaiter()
.GetResult();

if (!response.IsSuccessStatusCode)
{
return;
}

var avatarPath = $"UserAvatars/{$"{umbracoUser.Key}profile.jpg".GenerateHash<SHA1>()}.jpg";

using (var photoStream = response.Content.ReadAsStream())
{
mediaFileManager.FileSystem.AddFile(avatarPath, photoStream, true);
}
Comment thread
stevetemple marked this conversation as resolved.
Outdated

umbracoUser.Avatar = avatarPath;
userService.Save(umbracoUser);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to fetch and set the Entra ID profile picture as the avatar for user {Username}", username);
}
}

public void Configure(BackOfficeExternalLoginProviderOptions options)
{
throw new NotImplementedException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class AzureSsoProfileSettings(AzureSSOConfiguration configuration)
public string Icon => configuration.Icon ?? "fa fa-lock";
public Dictionary<string, string> GroupLookup => configuration.GroupBindings;
public bool SetGroupsOnLogin => configuration.SetGroupsOnLogin ?? true;
public bool SetProfileImageOnLogin => configuration.SetProfileImageOnLogin ?? false;
public bool LogUnmappedRolesAsWarning => configuration.LogUnmappedRolesAsWarning ?? false;
public string[] DefaultGroups => configuration.DefaultGroups ?? System.Array.Empty<string>();
public bool DenyLocalLogin => configuration.DenyLocalLogin ?? false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@
"type": "boolean",
"description": "The bindings for AD group to Umbraco group."
},
"SetProfileImageOnLogin": {
"type": "boolean",
"description": "Fetch the user's Entra ID profile photo on each login and set it as their Umbraco avatar."
},
"Icon": {
"type": "string",
"description": "The icon to use on the login button"
Expand Down
Loading