Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ private static void ConfigureOpenIddict(IUmbracoBuilder builder)
.SetLogoutEndpointUris(
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetRevocationEndpointUris(
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetUserinfoEndpointUris(
Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));

// Enable authorization code flow with PKCE
options
Expand All @@ -52,7 +54,8 @@ private static void ConfigureOpenIddict(IUmbracoBuilder builder)
options
.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough();
.EnableLogoutEndpointPassthrough()
.EnableUserinfoEndpointPassthrough();

// Enable reference tokens
// - see https://documentation.openiddict.com/configuration/token-storage.html
Expand Down
2 changes: 2 additions & 0 deletions src/Umbraco.Cms.Api.Common/Security/Paths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public static class MemberApi

public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");

public static readonly string UserinfoEndpoint = EndpointPath($"{EndpointTemplate}/userinfo");

// NOTE: we're NOT using /api/v1.0/ here because it will clash with the Delivery API docs
private static string EndpointPath(string relativePath) => $"/umbraco/delivery/api/v1/{relativePath}";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Server.AspNetCore;
using Umbraco.Cms.Api.Delivery.Routing;
using Umbraco.Cms.Api.Delivery.Services;

namespace Umbraco.Cms.Api.Delivery.Controllers.Security;

[ApiVersion("1.0")]
[ApiController]
[VersionedDeliveryApiRoute(Common.Security.Paths.MemberApi.EndpointTemplate)]
[ApiExplorerSettings(IgnoreApi = true)]
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
public class CurrentMemberController : DeliveryApiControllerBase
{
private readonly ICurrentMemberClaimsProvider _currentMemberClaimsProvider;

public CurrentMemberController(ICurrentMemberClaimsProvider currentMemberClaimsProvider)
=> _currentMemberClaimsProvider = currentMemberClaimsProvider;

[HttpGet("userinfo")]
public async Task<IActionResult> Userinfo()
{
Dictionary<string, object> claims = await _currentMemberClaimsProvider.GetClaimsAsync();
return Ok(claims);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public static IUmbracoBuilder AddDeliveryApi(this IUmbracoBuilder builder)
builder.Services.AddSingleton<IApiMediaQueryService, ApiMediaQueryService>();
builder.Services.AddTransient<IMemberApplicationManager, MemberApplicationManager>();
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
builder.Services.AddTransient<ICurrentMemberClaimsProvider, CurrentMemberClaimsProvider>();

builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
builder.AddUmbracoApiOpenApiUI();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using OpenIddict.Abstractions;
using Umbraco.Cms.Core.Security;

namespace Umbraco.Cms.Api.Delivery.Services;

// NOTE: this is public and unsealed to allow overriding the default claims with minimal effort.
public class CurrentMemberClaimsProvider : ICurrentMemberClaimsProvider
{
private readonly IMemberManager _memberManager;

public CurrentMemberClaimsProvider(IMemberManager memberManager)
=> _memberManager = memberManager;

public virtual async Task<Dictionary<string, object>> GetClaimsAsync()
{
MemberIdentityUser? memberIdentityUser = await _memberManager.GetCurrentMemberAsync();
return memberIdentityUser is not null
? await GetClaimsForMemberIdentityAsync(memberIdentityUser)
: throw new InvalidOperationException("Could not retrieve the current member. This method should only ever be invoked when a member has been authorized.");
}

protected virtual async Task<Dictionary<string, object>> GetClaimsForMemberIdentityAsync(MemberIdentityUser memberIdentityUser)
{
var claims = new Dictionary<string, object>
{
[OpenIddictConstants.Claims.Subject] = memberIdentityUser.Key
};

if (memberIdentityUser.Name is not null)
{
claims[OpenIddictConstants.Claims.Name] = memberIdentityUser.Name;
}

if (memberIdentityUser.Email is not null)
{
claims[OpenIddictConstants.Claims.Email] = memberIdentityUser.Email;
}

claims[OpenIddictConstants.Claims.Role] = await _memberManager.GetRolesAsync(memberIdentityUser);

return claims;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Api.Delivery.Services;

public interface ICurrentMemberClaimsProvider
{
/// <summary>
/// Retrieves the claims for the currently logged in member.
/// </summary>
/// <remarks>
/// This is used by the OIDC user info endpoint to supply "current user" info.
/// </remarks>
Task<Dictionary<string, object>> GetClaimsAsync();
}