diff --git a/src/Identity/Core/src/Data/InfoResponse.cs b/src/Identity/Core/src/Data/InfoResponse.cs index a58c195aba55..05e4483ce253 100644 --- a/src/Identity/Core/src/Data/InfoResponse.cs +++ b/src/Identity/Core/src/Data/InfoResponse.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Security.Claims; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Identity.Data; @@ -21,9 +19,4 @@ public sealed class InfoResponse /// Indicates whether or not the has been confirmed yet. /// public required bool IsEmailConfirmed { get; init; } - - /// - /// The from the authenticated . - /// - public required IDictionary Claims { get; init; } } diff --git a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs index 8a9ed5ae1431..115d151bdf56 100644 --- a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs +++ b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs @@ -340,7 +340,7 @@ await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not T return TypedResults.NotFound(); } - return TypedResults.Ok(await CreateInfoResponseAsync(user, claimsPrincipal, userManager)); + return TypedResults.Ok(await CreateInfoResponseAsync(user, userManager)); }); accountGroup.MapPost("/info", async Task, ValidationProblem, NotFound>> @@ -382,7 +382,7 @@ await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not T } } - return TypedResults.Ok(await CreateInfoResponseAsync(user, claimsPrincipal, userManager)); + return TypedResults.Ok(await CreateInfoResponseAsync(user, userManager)); }); async Task SendConfirmationEmailAsync(TUser user, UserManager userManager, HttpContext context, string email, bool isChange = false) @@ -452,14 +452,13 @@ private static ValidationProblem CreateValidationProblem(IdentityResult result) return TypedResults.ValidationProblem(errorDictionary); } - private static async Task CreateInfoResponseAsync(TUser user, ClaimsPrincipal claimsPrincipal, UserManager userManager) + private static async Task CreateInfoResponseAsync(TUser user, UserManager userManager) where TUser : class { return new() { Email = await userManager.GetEmailAsync(user) ?? throw new NotSupportedException("Users must have an email."), IsEmailConfirmed = await userManager.IsEmailConfirmedAsync(user), - Claims = claimsPrincipal.Claims.ToDictionary(c => c.Type, c => c.Value), }; } diff --git a/src/Identity/Core/src/PublicAPI.Unshipped.txt b/src/Identity/Core/src/PublicAPI.Unshipped.txt index 8b9f4af02588..83d4283afbdd 100644 --- a/src/Identity/Core/src/PublicAPI.Unshipped.txt +++ b/src/Identity/Core/src/PublicAPI.Unshipped.txt @@ -12,8 +12,6 @@ Microsoft.AspNetCore.Identity.Data.InfoRequest.NewPassword.init -> void Microsoft.AspNetCore.Identity.Data.InfoRequest.OldPassword.get -> string? Microsoft.AspNetCore.Identity.Data.InfoRequest.OldPassword.init -> void Microsoft.AspNetCore.Identity.Data.InfoResponse -Microsoft.AspNetCore.Identity.Data.InfoResponse.Claims.get -> System.Collections.Generic.IDictionary! -Microsoft.AspNetCore.Identity.Data.InfoResponse.Claims.init -> void Microsoft.AspNetCore.Identity.Data.InfoResponse.Email.get -> string! Microsoft.AspNetCore.Identity.Data.InfoResponse.Email.init -> void Microsoft.AspNetCore.Identity.Data.InfoResponse.InfoResponse() -> void diff --git a/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs b/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs index 57ac19d7fccf..a531c7162767 100644 --- a/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs +++ b/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs @@ -13,7 +13,6 @@ using Identity.DefaultUI.WebSite; using Identity.DefaultUI.WebSite.Data; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.UI.Services; @@ -862,7 +861,7 @@ public async Task CanResetRecoveryCodes() client.DefaultRequestHeaders.Authorization = new("Bearer", recoveryAccessToken); var updated2faResponse = await client.PostAsJsonAsync("/identity/manage/2fa", new object()); - var updated2faContent = await updated2faResponse.Content.ReadFromJsonAsync();; + var updated2faContent = await updated2faResponse.Content.ReadFromJsonAsync(); Assert.Equal(8, updated2faContent.GetProperty("recoveryCodesLeft").GetInt32()); Assert.Null(updated2faContent.GetProperty("recoveryCodes").GetString()); @@ -1013,25 +1012,6 @@ public async Task CanResetPassword() AssertOk(await client.PostAsJsonAsync("/identity/login", new { Email = confirmedEmail, Password = newPassword })); } - [Fact] - public async Task CanGetClaims() - { - await using var app = await CreateAppAsync(); - using var client = app.GetTestClient(); - - await RegisterAsync(client); - await LoginAsync(client); - - var infoResponse = await client.GetFromJsonAsync("/identity/manage/info"); - Assert.Equal(Email, infoResponse.GetProperty("email").GetString()); - - var claims = infoResponse.GetProperty("claims"); - Assert.Equal(Email, claims.GetProperty(ClaimTypes.Name).GetString()); - Assert.Equal(Email, claims.GetProperty(ClaimTypes.Email).GetString()); - Assert.Equal("pwd", claims.GetProperty("amr").GetString()); - Assert.NotNull(claims.GetProperty(ClaimTypes.NameIdentifier).GetString()); - } - [Theory] [MemberData(nameof(AddIdentityModes))] public async Task CanChangeEmail(string addIdentityModes) @@ -1058,12 +1038,12 @@ public async Task CanChangeEmail(string addIdentityModes) Assert.Equal(Email, infoResponse.GetProperty("email").GetString()); Assert.True(infoResponse.GetProperty("isEmailConfirmed").GetBoolean()); - var infoClaims = infoResponse.GetProperty("claims"); - Assert.Equal("pwd", infoClaims.GetProperty("amr").GetString()); - Assert.Equal(Email, infoClaims.GetProperty(ClaimTypes.Name).GetString()); - Assert.Equal(Email, infoClaims.GetProperty(ClaimTypes.Email).GetString()); + var infoClaims = await client.GetFromJsonAsync("/auth/claims"); + Assert.Equal("pwd", GetSingleClaim(infoClaims, "amr")); + Assert.Equal(Email, GetSingleClaim(infoClaims, ClaimTypes.Name)); + Assert.Equal(Email, GetSingleClaim(infoClaims, ClaimTypes.Email)); - var originalNameIdentifier = infoResponse.GetProperty("claims").GetProperty(ClaimTypes.NameIdentifier).GetString(); + var originalNameIdentifier = GetSingleClaim(infoClaims, ClaimTypes.NameIdentifier); var newEmail = $"New-{Email}"; // The email must pass DataAnnotations validation by EmailAddressAttribute. @@ -1077,10 +1057,10 @@ public async Task CanChangeEmail(string addIdentityModes) Assert.True(infoPostContent.GetProperty("isEmailConfirmed").GetBoolean()); // And none of the claims have yet been updated. - var infoPostClaims = infoPostContent.GetProperty("claims"); - Assert.Equal(Email, infoPostClaims.GetProperty(ClaimTypes.Name).GetString()); - Assert.Equal(Email, infoPostClaims.GetProperty(ClaimTypes.Email).GetString()); - Assert.Equal(originalNameIdentifier, infoClaims.GetProperty(ClaimTypes.NameIdentifier).GetString()); + var infoPostClaims = await client.GetFromJsonAsync("/auth/claims"); + Assert.Equal(Email, GetSingleClaim(infoPostClaims, ClaimTypes.Name)); + Assert.Equal(Email, GetSingleClaim(infoPostClaims, ClaimTypes.Email)); + Assert.Equal(originalNameIdentifier, GetSingleClaim(infoPostClaims, ClaimTypes.NameIdentifier)); // We cannot log in with the new email until we confirm the email change. await AssertProblemAsync(await client.PostAsJsonAsync("/identity/login", new { Email = newEmail, Password }), @@ -1103,10 +1083,10 @@ public async Task CanChangeEmail(string addIdentityModes) Assert.Equal(newEmail, infoAfterEmailChange.GetProperty("email").GetString()); // The email still won't be available as a claim until we get a new token. - var claimsAfterEmailChange = infoAfterEmailChange.GetProperty("claims"); - Assert.Equal(Email, claimsAfterEmailChange.GetProperty(ClaimTypes.Name).GetString()); - Assert.Equal(Email, claimsAfterEmailChange.GetProperty(ClaimTypes.Email).GetString()); - Assert.Equal(originalNameIdentifier, infoClaims.GetProperty(ClaimTypes.NameIdentifier).GetString()); + var claimsAfterEmailChange = await client.GetFromJsonAsync("/auth/claims"); + Assert.Equal(Email, GetSingleClaim(claimsAfterEmailChange, ClaimTypes.Name)); + Assert.Equal(Email, GetSingleClaim(claimsAfterEmailChange, ClaimTypes.Email)); + Assert.Equal(originalNameIdentifier, GetSingleClaim(claimsAfterEmailChange, ClaimTypes.NameIdentifier)); // And now the email has changed, the refresh token is invalidated by the security stamp. AssertUnauthorizedAndEmpty(await client.PostAsJsonAsync("/identity/refresh", new { RefreshToken = originalRefreshToken })); @@ -1118,10 +1098,10 @@ public async Task CanChangeEmail(string addIdentityModes) Assert.Equal(newEmail, infoAfterFinalLogin.GetProperty("email").GetString()); Assert.True(infoAfterFinalLogin.GetProperty("isEmailConfirmed").GetBoolean()); - var claimsAfterFinalLogin = infoAfterFinalLogin.GetProperty("claims"); - Assert.Equal(newEmail, claimsAfterFinalLogin.GetProperty(ClaimTypes.Name).GetString()); - Assert.Equal(newEmail, claimsAfterFinalLogin.GetProperty(ClaimTypes.Email).GetString()); - Assert.Equal(originalNameIdentifier, infoClaims.GetProperty(ClaimTypes.NameIdentifier).GetString()); + var claimsAfterFinalLogin = await client.GetFromJsonAsync("/auth/claims"); + Assert.Equal(newEmail, GetSingleClaim(claimsAfterFinalLogin, ClaimTypes.Name)); + Assert.Equal(newEmail, GetSingleClaim(claimsAfterFinalLogin, ClaimTypes.Email)); + Assert.Equal(originalNameIdentifier, GetSingleClaim(claimsAfterFinalLogin, ClaimTypes.NameIdentifier)); } [Fact] @@ -1152,12 +1132,13 @@ public async Task CannotUpdateClaimsDuringInfoPostWithCookies() var infoResponse = await client.GetFromJsonAsync("/identity/manage/info"); Assert.Equal(Email, infoResponse.GetProperty("email").GetString()); - var infoClaims = infoResponse.GetProperty("claims"); - Assert.Equal("pwd", infoClaims.GetProperty("amr").GetString()); - Assert.Equal(Email, infoClaims.GetProperty(ClaimTypes.Name).GetString()); - Assert.Equal(Email, infoClaims.GetProperty(ClaimTypes.Email).GetString()); - var originalNameIdentifier = infoResponse.GetProperty("claims").GetProperty(ClaimTypes.NameIdentifier).GetString(); + var infoClaims = await client.GetFromJsonAsync("/auth/claims"); + Assert.Equal("pwd", GetSingleClaim(infoClaims, "amr")); + Assert.Equal(Email, GetSingleClaim(infoClaims, ClaimTypes.Name)); + Assert.Equal(Email, GetSingleClaim(infoClaims, ClaimTypes.Email)); + + var originalNameIdentifier = GetSingleClaim(infoClaims, ClaimTypes.NameIdentifier); var newEmail = $"NewEmailPrefix-{Email}"; var infoPostResponse = await client.PostAsJsonAsync("/identity/manage/info", new { newEmail }); @@ -1169,9 +1150,9 @@ public async Task CannotUpdateClaimsDuringInfoPostWithCookies() Assert.Equal(Email, infoPostContent.GetProperty("email").GetString()); // The claims have not been updated to match. - var infoPostClaims = infoPostContent.GetProperty("claims"); - Assert.Equal(Email, infoPostClaims.GetProperty(ClaimTypes.Email).GetString()); - Assert.Equal(originalNameIdentifier, infoClaims.GetProperty(ClaimTypes.NameIdentifier).GetString()); + var infoPostClaims = await client.GetFromJsonAsync("/auth/claims"); + Assert.Equal(Email, GetSingleClaim(infoPostClaims, ClaimTypes.Email)); + Assert.Equal(originalNameIdentifier, GetSingleClaim(infoPostClaims, ClaimTypes.NameIdentifier)); // Two emails have now been sent. The first was sent during registration. And the second for the email change. Assert.Equal(2, emailSender.Emails.Count); @@ -1191,9 +1172,9 @@ public async Task CannotUpdateClaimsDuringInfoPostWithCookies() Assert.Equal(newEmail, infoAfterEmailChange.GetProperty("email").GetString()); // The email still won't be available as a claim until we get a new cookie. - var claimsAfterEmailChange = infoAfterEmailChange.GetProperty("claims"); - Assert.Equal(Email, claimsAfterEmailChange.GetProperty(ClaimTypes.Email).GetString()); - Assert.Equal(originalNameIdentifier, infoClaims.GetProperty(ClaimTypes.NameIdentifier).GetString()); + var claimsAfterEmailChange = await client.GetFromJsonAsync("/auth/claims"); + Assert.Equal(Email, GetSingleClaim(claimsAfterEmailChange, ClaimTypes.Email)); + Assert.Equal(originalNameIdentifier, GetSingleClaim(claimsAfterEmailChange, ClaimTypes.NameIdentifier)); // We will finally see all the claims updated after logging in again. var secondLoginResponse = await client.PostAsJsonAsync("/identity/login?useCookies=true", new { Email = newEmail, Password }); @@ -1202,10 +1183,10 @@ public async Task CannotUpdateClaimsDuringInfoPostWithCookies() var infoAfterFinalLogin = await client.GetFromJsonAsync("/identity/manage/info"); Assert.Equal(newEmail, infoAfterFinalLogin.GetProperty("email").GetString()); - var claimsAfterFinalLogin = infoAfterFinalLogin.GetProperty("claims"); - Assert.Equal(newEmail, claimsAfterFinalLogin.GetProperty(ClaimTypes.Name).GetString()); - Assert.Equal(newEmail, claimsAfterFinalLogin.GetProperty(ClaimTypes.Email).GetString()); - Assert.Equal(originalNameIdentifier, infoClaims.GetProperty(ClaimTypes.NameIdentifier).GetString()); + var claimsAfterFinalLogin = await client.GetFromJsonAsync("/auth/claims"); + Assert.Equal(newEmail, GetSingleClaim(claimsAfterFinalLogin, ClaimTypes.Name)); + Assert.Equal(newEmail, GetSingleClaim(claimsAfterFinalLogin, ClaimTypes.Email)); + Assert.Equal(originalNameIdentifier, GetSingleClaim(claimsAfterFinalLogin, ClaimTypes.NameIdentifier)); } [Fact] @@ -1321,6 +1302,8 @@ private async Task CreateAppAsync(Action $"Hello, {user.Identity?.Name}!"); + authGroup.MapGet("/claims", (ClaimsPrincipal user) => user.Claims.Select(c => new { c.Type, c.Value })); + await dbConnection.OpenAsync(); await app.Services.GetRequiredService().Database.EnsureCreatedAsync(); @@ -1367,6 +1350,9 @@ private Task CreateAppAsync(Action? configur public static object[][] AddIdentityModes => AddIdentityActions.Keys.Select(key => new object[] { key }).ToArray(); + private static string? GetSingleClaim(JsonElement claims, string name) + => claims.EnumerateArray().Single(e => e.GetProperty("type").GetString() == name).GetProperty("value").GetString(); + private static string GetEmailConfirmationLink(TestEmail email) { // Update if we add more links to the email.