Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add configuration for Identity API endpoints #55529

Open
akordowski opened this issue May 4, 2024 · 3 comments · May be fixed by #59716
Open

Add configuration for Identity API endpoints #55529

akordowski opened this issue May 4, 2024 · 3 comments · May be fixed by #59716
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-identity Includes: Identity and providers
Milestone

Comments

@akordowski
Copy link

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.

@akordowski akordowski added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label May 4, 2024
@dotnet-issue-labeler dotnet-issue-labeler bot added the area-identity Includes: Identity and providers label May 4, 2024
@halter73 halter73 added api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels May 7, 2024
@halter73 halter73 added this to the Backlog milestone May 7, 2024
@halter73 halter73 removed their assignment May 7, 2024
@tcortega
Copy link
Contributor

tcortega commented May 9, 2024

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:

var result = await signInManager.PasswordSignInAsync(login.Email, login.Password, isPersistent, lockoutOnFailure: true);

var user = await UserManager.FindByNameAsync(userName);

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?

@graknar
Copy link

graknar commented Aug 4, 2024

Same here, I encounter an issue with the generic call to add the Endpoint, only on the registration.
I use a custom IdentityUser and some new properties are mandatory.
But since I cannot overrite RegisterRequest and change part of the API, I need to copy the endpoint mapping to manage my own delegate and change the RegisterRequest.

Maybe this part can be improve to offer more customisation.

@halter73 halter73 added api-suggestion Early API idea and discussion, it is NOT ready for implementation and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels Oct 10, 2024
@TomatoDotDev
Copy link

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:

aspnetcore/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs

Line 99 in d6c1619

var result = await signInManager.PasswordSignInAsync(login.Email, login.Password, isPersistent, lockoutOnFailure: true);
aspnetcore/src/Identity/Core/src/SignInManager.cs

Line 356 in d6c1619

var user = await UserManager.FindByNameAsync(userName);
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?

had this exact same issue when trying to seed a default user and wondering for ages why it was not signing in - this gotcha with the username and email being forced to be the same is a strange behaviour.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-identity Includes: Identity and providers
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants