dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
34.51k stars 9.75k forks source link

Add configuration for Identity API endpoints #55529

Open akordowski opened 2 weeks ago

akordowski commented 2 weeks ago

Background and Motivation

Although the Identity API is highly customizable one feature I am missing is the possibility to customize the Identity API endpoints. I copied the exisitng code and changed it to my needs. Below you can find the changes. If you find this feature usefull I would appreciate it if it be added to the code base.

Proposed API

  src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs   | 37 ++++++++++++++--------
 1 file changed, 24 insertions(+), 13 deletions(-)

diff --git a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs
index 115d151bdf..788dad545f 100644
--- a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs
+++ b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs
@@ -36,8 +36,11 @@ public static class IdentityApiEndpointRouteBuilderExtensions
     /// The <see cref="IEndpointRouteBuilder"/> to add the identity endpoints to.
     /// Call <see cref="EndpointRouteBuilderExtensions.MapGroup(IEndpointRouteBuilder, string)"/> to add a prefix to all the endpoints.
     /// </param>
+    /// <param name="identityApiOptionsAction">
+    /// An optional action to configure the <see cref="IdentityApiOptions" /> for the endpoints.
+    /// </param>
     /// <returns>An <see cref="IEndpointConventionBuilder"/> to further customize the added endpoints.</returns>
-    public static IEndpointConventionBuilder MapIdentityApi<TUser>(this IEndpointRouteBuilder endpoints)
+    public static IEndpointConventionBuilder MapIdentityApi<TUser>(this IEndpointRouteBuilder endpoints, Action<IdentityApiOptions>? identityApiOptionsAction = null)
         where TUser : class, new()
     {
         ArgumentNullException.ThrowIfNull(endpoints);
@@ -50,11 +53,19 @@ public static class IdentityApiEndpointRouteBuilderExtensions
         // We'll figure out a unique endpoint name based on the final route pattern during endpoint generation.
         string? confirmEmailEndpointName = null;

-        var routeGroup = endpoints.MapGroup("");
+        var identityApiOptions = new IdentityApiOptions();
+        identityApiOptionsAction?.Invoke(identityApiOptions);
+
+        var routeGroup = endpoints.MapGroup(identityApiOptions.RouteGroup);
+
+        if (!string.IsNullOrWhiteSpace(identityApiOptions.RouteTag))
+        {
+            routeGroup = routeGroup.WithTags(identityApiOptions.RouteTag);
+        }

         // NOTE: We cannot inject UserManager<TUser> directly because the TUser generic parameter is currently unsupported by RDG.
         // https://github.com/dotnet/aspnetcore/issues/47338
-        routeGroup.MapPost("/register", async Task<Results<Ok, ValidationProblem>>
+        routeGroup.MapPost(identityApiOptions.RegisterEndpoint, async Task<Results<Ok, ValidationProblem>>
             ([FromBody] RegisterRequest registration, HttpContext context, [FromServices] IServiceProvider sp) =>
         {
             var userManager = sp.GetRequiredService<UserManager<TUser>>();
@@ -87,7 +98,7 @@ public static class IdentityApiEndpointRouteBuilderExtensions
             return TypedResults.Ok();
         });

-        routeGroup.MapPost("/login", async Task<Results<Ok<AccessTokenResponse>, EmptyHttpResult, ProblemHttpResult>>
+        routeGroup.MapPost(identityApiOptions.LoginEndpoint, async Task<Results<Ok<AccessTokenResponse>, EmptyHttpResult, ProblemHttpResult>>
             ([FromBody] LoginRequest login, [FromQuery] bool? useCookies, [FromQuery] bool? useSessionCookies, [FromServices] IServiceProvider sp) =>
         {
             var signInManager = sp.GetRequiredService<SignInManager<TUser>>();
@@ -119,7 +130,7 @@ public static class IdentityApiEndpointRouteBuilderExtensions
             return TypedResults.Empty;
         });

-        routeGroup.MapPost("/refresh", async Task<Results<Ok<AccessTokenResponse>, UnauthorizedHttpResult, SignInHttpResult, ChallengeHttpResult>>
+        routeGroup.MapPost(identityApiOptions.RefreshEndpoint, async Task<Results<Ok<AccessTokenResponse>, UnauthorizedHttpResult, SignInHttpResult, ChallengeHttpResult>>
             ([FromBody] RefreshRequest refreshRequest, [FromServices] IServiceProvider sp) =>
         {
             var signInManager = sp.GetRequiredService<SignInManager<TUser>>();
@@ -139,7 +150,7 @@ public static class IdentityApiEndpointRouteBuilderExtensions
             return TypedResults.SignIn(newPrincipal, authenticationScheme: IdentityConstants.BearerScheme);
         });

-        routeGroup.MapGet("/confirmEmail", async Task<Results<ContentHttpResult, UnauthorizedHttpResult>>
+        routeGroup.MapGet(identityApiOptions.ConfirmEmailEndpoint, async Task<Results<ContentHttpResult, UnauthorizedHttpResult>>
             ([FromQuery] string userId, [FromQuery] string code, [FromQuery] string? changedEmail, [FromServices] IServiceProvider sp) =>
         {
             var userManager = sp.GetRequiredService<UserManager<TUser>>();
@@ -190,7 +201,7 @@ public static class IdentityApiEndpointRouteBuilderExtensions
             endpointBuilder.Metadata.Add(new EndpointNameMetadata(confirmEmailEndpointName));
         });

-        routeGroup.MapPost("/resendConfirmationEmail", async Task<Ok>
+        routeGroup.MapPost(identityApiOptions.ResendConfirmationEmailEndpoint, async Task<Ok>
             ([FromBody] ResendConfirmationEmailRequest resendRequest, HttpContext context, [FromServices] IServiceProvider sp) =>
         {
             var userManager = sp.GetRequiredService<UserManager<TUser>>();
@@ -203,7 +214,7 @@ public static class IdentityApiEndpointRouteBuilderExtensions
             return TypedResults.Ok();
         });

-        routeGroup.MapPost("/forgotPassword", async Task<Results<Ok, ValidationProblem>>
+        routeGroup.MapPost(identityApiOptions.ForgotPasswordEndpoint, async Task<Results<Ok, ValidationProblem>>
             ([FromBody] ForgotPasswordRequest resetRequest, [FromServices] IServiceProvider sp) =>
         {
             var userManager = sp.GetRequiredService<UserManager<TUser>>();
@@ -222,7 +233,7 @@ public static class IdentityApiEndpointRouteBuilderExtensions
             return TypedResults.Ok();
         });

-        routeGroup.MapPost("/resetPassword", async Task<Results<Ok, ValidationProblem>>
+        routeGroup.MapPost(identityApiOptions.ResetPasswordEndpoint, async Task<Results<Ok, ValidationProblem>>
             ([FromBody] ResetPasswordRequest resetRequest, [FromServices] IServiceProvider sp) =>
         {
             var userManager = sp.GetRequiredService<UserManager<TUser>>();
@@ -255,9 +266,9 @@ public static class IdentityApiEndpointRouteBuilderExtensions
             return TypedResults.Ok();
         });

-        var accountGroup = routeGroup.MapGroup("/manage").RequireAuthorization();
+        var accountGroup = routeGroup.MapGroup(identityApiOptions.ManageRouteGroup).RequireAuthorization();

-        accountGroup.MapPost("/2fa", async Task<Results<Ok<TwoFactorResponse>, ValidationProblem, NotFound>>
+        accountGroup.MapPost(identityApiOptions.MfaEndpoint, async Task<Results<Ok<TwoFactorResponse>, ValidationProblem, NotFound>>
             (ClaimsPrincipal claimsPrincipal, [FromBody] TwoFactorRequest tfaRequest, [FromServices] IServiceProvider sp) =>
         {
             var signInManager = sp.GetRequiredService<SignInManager<TUser>>();
@@ -331,7 +342,7 @@ public static class IdentityApiEndpointRouteBuilderExtensions
             });
         });

-        accountGroup.MapGet("/info", async Task<Results<Ok<InfoResponse>, ValidationProblem, NotFound>>
+        accountGroup.MapGet(identityApiOptions.InfoEndpoint, async Task<Results<Ok<InfoResponse>, ValidationProblem, NotFound>>
             (ClaimsPrincipal claimsPrincipal, [FromServices] IServiceProvider sp) =>
         {
             var userManager = sp.GetRequiredService<UserManager<TUser>>();
@@ -343,7 +354,7 @@ public static class IdentityApiEndpointRouteBuilderExtensions
             return TypedResults.Ok(await CreateInfoResponseAsync(user, userManager));
         });

-        accountGroup.MapPost("/info", async Task<Results<Ok<InfoResponse>, ValidationProblem, NotFound>>
+        accountGroup.MapPost(identityApiOptions.InfoEndpoint, async Task<Results<Ok<InfoResponse>, ValidationProblem, NotFound>>
             (ClaimsPrincipal claimsPrincipal, [FromBody] InfoRequest infoRequest, HttpContext context, [FromServices] IServiceProvider sp) =>
         {
             var userManager = sp.GetRequiredService<UserManager<TUser>>();
 src/Identity/Core/src/IdentityApiOptions.cs | 70 +++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/src/Identity/Core/src/IdentityApiOptions.cs b/src/Identity/Core/src/IdentityApiOptions.cs
new file mode 100644
index 0000000000..2ccbfb2269
--- /dev/null
+++ b/src/Identity/Core/src/IdentityApiOptions.cs
@@ -0,0 +1,70 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Identity;
+
+/// <summary>
+/// Represents all the options you can use to configure the identity api endpoints.
+/// </summary>
+public class IdentityApiOptions
+{
+    /// <summary>
+    /// The value for the route tag.
+    /// </summary>
+    public string? RouteTag { get; set; }
+
+    /// <summary>
+    /// The value for the route group.
+    /// </summary>
+    public string RouteGroup { get; set; } = "";
+
+    /// <summary>
+    /// The value for the register endpoint.
+    /// </summary>
+    public string RegisterEndpoint { get; set; } = "/register";
+
+    /// <summary>
+    /// The value for the login endpoint.
+    /// </summary>
+    public string LoginEndpoint { get; set; } = "/login";
+
+    /// <summary>
+    /// The value for the refresh endpoint.
+    /// </summary>
+    public string RefreshEndpoint { get; set; } = "/refresh";
+
+    /// <summary>
+    /// The value for the confirm email endpoint.
+    /// </summary>
+    public string ConfirmEmailEndpoint { get; set; } = "/confirmEmail";
+
+    /// <summary>
+    /// The value for the resend confirmation email endpoint.
+    /// </summary>
+    public string ResendConfirmationEmailEndpoint { get; set; } = "/resendConfirmationEmail";
+
+    /// <summary>
+    /// The value for the forgot password endpoint.
+    /// </summary>
+    public string ForgotPasswordEndpoint { get; set; } = "/forgotPassword";
+
+    /// <summary>
+    /// The value for the reset password endpoint.
+    /// </summary>
+    public string ResetPasswordEndpoint { get; set; } = "/resetPassword";
+
+    /// <summary>
+    /// The value for the manage route group.
+    /// </summary>
+    public string ManageRouteGroup { get; set; } = "manage";
+
+    /// <summary>
+    /// The value for the 2fa endpoint.
+    /// </summary>
+    public string MfaEndpoint { get; set; } = "/2fa";
+
+    /// <summary>
+    /// The value for the info endpoint.
+    /// </summary>
+    public string InfoEndpoint { get; set; } = "/info";
+}

Usage Examples

With the propsed changes the API endpoints can be configured like followed:

app.MapIdentityApi<User>(options =>
{
    options.RouteTag = "auth";
    options.RouteGroup = "/auth";
    options.ConfirmEmailEndpoint = "/confirm-email";
    options.ResendConfirmationEmailEndpoint = "/resend-confirmation-email";
    options.ForgotPasswordEndpoint = "/forgot-password";
    options.ResetPasswordEndpoint = "/reset-password";
});

screenshot

Risks

I don't see any risks as the changes are implemented as an optional parameter.

Looking forward to your feedback. Thank you for the consideration.

dotnet-policy-service[bot] commented 1 week ago

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

tcortega commented 1 week ago

It would be beneficial to have an option to either disable endpoints or fully customize them via delegates. My primary issue with the API was the inability to implement login via email. This limitation stems from the current design, which searches for users by username as seen here:

https://github.com/dotnet/aspnetcore/blob/d6c161969965bfeff71110ea5b3462afacfa9f24/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs#L99

https://github.com/dotnet/aspnetcore/blob/d6c161969965bfeff71110ea5b3462afacfa9f24/src/Identity/Core/src/SignInManager.cs#L356

And honestly it's also really confusing that the api asks for an email but searches for the username. Maybe this should be an issue after all?