Skip to content

Commit ee8bdfc

Browse files
authored
Add "user info" endpoint for the Delivery API (#17719)
* Add "user info" endpoint for the Delivery API * Add comment about public and unsealed class
1 parent b55d484 commit ee8bdfc

File tree

6 files changed

+91
-2
lines changed

6 files changed

+91
-2
lines changed

src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ private static void ConfigureOpenIddict(IUmbracoBuilder builder)
4040
.SetLogoutEndpointUris(
4141
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
4242
.SetRevocationEndpointUris(
43-
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
43+
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
44+
.SetUserinfoEndpointUris(
45+
Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
4446

4547
// Enable authorization code flow with PKCE
4648
options
@@ -52,7 +54,8 @@ private static void ConfigureOpenIddict(IUmbracoBuilder builder)
5254
options
5355
.UseAspNetCore()
5456
.EnableAuthorizationEndpointPassthrough()
55-
.EnableLogoutEndpointPassthrough();
57+
.EnableLogoutEndpointPassthrough()
58+
.EnableUserinfoEndpointPassthrough();
5659

5760
// Enable reference tokens
5861
// - see https://documentation.openiddict.com/configuration/token-storage.html

src/Umbraco.Cms.Api.Common/Security/Paths.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public static class MemberApi
1414

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

17+
public static readonly string UserinfoEndpoint = EndpointPath($"{EndpointTemplate}/userinfo");
18+
1719
// NOTE: we're NOT using /api/v1.0/ here because it will clash with the Delivery API docs
1820
private static string EndpointPath(string relativePath) => $"/umbraco/delivery/api/v1/{relativePath}";
1921
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Asp.Versioning;
2+
using Microsoft.AspNetCore.Authorization;
3+
using Microsoft.AspNetCore.Mvc;
4+
using OpenIddict.Server.AspNetCore;
5+
using Umbraco.Cms.Api.Delivery.Routing;
6+
using Umbraco.Cms.Api.Delivery.Services;
7+
8+
namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
9+
10+
[ApiVersion("1.0")]
11+
[ApiController]
12+
[VersionedDeliveryApiRoute(Common.Security.Paths.MemberApi.EndpointTemplate)]
13+
[ApiExplorerSettings(IgnoreApi = true)]
14+
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
15+
public class CurrentMemberController : DeliveryApiControllerBase
16+
{
17+
private readonly ICurrentMemberClaimsProvider _currentMemberClaimsProvider;
18+
19+
public CurrentMemberController(ICurrentMemberClaimsProvider currentMemberClaimsProvider)
20+
=> _currentMemberClaimsProvider = currentMemberClaimsProvider;
21+
22+
[HttpGet("userinfo")]
23+
public async Task<IActionResult> Userinfo()
24+
{
25+
Dictionary<string, object> claims = await _currentMemberClaimsProvider.GetClaimsAsync();
26+
return Ok(claims);
27+
}
28+
}

src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public static IUmbracoBuilder AddDeliveryApi(this IUmbracoBuilder builder)
6060
builder.Services.AddSingleton<IApiMediaQueryService, ApiMediaQueryService>();
6161
builder.Services.AddTransient<IMemberApplicationManager, MemberApplicationManager>();
6262
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
63+
builder.Services.AddTransient<ICurrentMemberClaimsProvider, CurrentMemberClaimsProvider>();
6364

6465
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
6566
builder.AddUmbracoApiOpenApiUI();
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using OpenIddict.Abstractions;
2+
using Umbraco.Cms.Core.Security;
3+
4+
namespace Umbraco.Cms.Api.Delivery.Services;
5+
6+
// NOTE: this is public and unsealed to allow overriding the default claims with minimal effort.
7+
public class CurrentMemberClaimsProvider : ICurrentMemberClaimsProvider
8+
{
9+
private readonly IMemberManager _memberManager;
10+
11+
public CurrentMemberClaimsProvider(IMemberManager memberManager)
12+
=> _memberManager = memberManager;
13+
14+
public virtual async Task<Dictionary<string, object>> GetClaimsAsync()
15+
{
16+
MemberIdentityUser? memberIdentityUser = await _memberManager.GetCurrentMemberAsync();
17+
return memberIdentityUser is not null
18+
? await GetClaimsForMemberIdentityAsync(memberIdentityUser)
19+
: throw new InvalidOperationException("Could not retrieve the current member. This method should only ever be invoked when a member has been authorized.");
20+
}
21+
22+
protected virtual async Task<Dictionary<string, object>> GetClaimsForMemberIdentityAsync(MemberIdentityUser memberIdentityUser)
23+
{
24+
var claims = new Dictionary<string, object>
25+
{
26+
[OpenIddictConstants.Claims.Subject] = memberIdentityUser.Key
27+
};
28+
29+
if (memberIdentityUser.Name is not null)
30+
{
31+
claims[OpenIddictConstants.Claims.Name] = memberIdentityUser.Name;
32+
}
33+
34+
if (memberIdentityUser.Email is not null)
35+
{
36+
claims[OpenIddictConstants.Claims.Email] = memberIdentityUser.Email;
37+
}
38+
39+
claims[OpenIddictConstants.Claims.Role] = await _memberManager.GetRolesAsync(memberIdentityUser);
40+
41+
return claims;
42+
}
43+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Umbraco.Cms.Api.Delivery.Services;
2+
3+
public interface ICurrentMemberClaimsProvider
4+
{
5+
/// <summary>
6+
/// Retrieves the claims for the currently logged in member.
7+
/// </summary>
8+
/// <remarks>
9+
/// This is used by the OIDC user info endpoint to supply "current user" info.
10+
/// </remarks>
11+
Task<Dictionary<string, object>> GetClaimsAsync();
12+
}

0 commit comments

Comments
 (0)