diff --git a/Todo.Api.Tests/UserApiTests.cs b/Todo.Api.Tests/UserApiTests.cs index 5f6721a..0b279f2 100644 --- a/Todo.Api.Tests/UserApiTests.cs +++ b/Todo.Api.Tests/UserApiTests.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using Microsoft.AspNetCore.DataProtection; namespace TodoApi.Tests; @@ -116,7 +117,12 @@ public async Task CanGetATokenForExternalUser() await using var db = application.CreateTodoDbContext(); var client = application.CreateClient(); - var response = await client.PostAsJsonAsync("/users/token/Google", new ExternalUserInfo { Username = "todouser", ProviderKey = "1003" }); + + var encryptedId = application.Services.GetRequiredService() + .CreateProtector("Google") + .Protect("1003"); + + var response = await client.PostAsJsonAsync("/users/token/Google", new ExternalUserInfo { Username = "todouser", ProviderKey = encryptedId }); Assert.True(response.IsSuccessStatusCode); diff --git a/Todo.Api/Program.cs b/Todo.Api/Program.cs index 5a7d195..7c3402f 100644 --- a/Todo.Api/Program.cs +++ b/Todo.Api/Program.cs @@ -6,6 +6,10 @@ builder.AddServiceDefaults(); +// Configure data protection, setup the application discriminator +// so that the data protection keys can be shared between the BFF and this API +builder.Services.AddDataProtection(o => o.ApplicationDiscriminator = "TodoApp"); + // Configure auth builder.Services.AddAuthentication().AddBearerToken(IdentityConstants.BearerScheme); builder.Services.AddAuthorizationBuilder().AddCurrentUserHandler(); diff --git a/Todo.Api/Users/UsersApi.cs b/Todo.Api/Users/UsersApi.cs index 3151229..26c17cb 100644 --- a/Todo.Api/Users/UsersApi.cs +++ b/Todo.Api/Users/UsersApi.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Authentication.BearerToken; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Identity; @@ -20,9 +21,13 @@ public static RouteGroupBuilder MapUsers(this IEndpointRouteBuilder routes) // The MapIdentityApi doesn't expose an external login endpoint so we write this custom endpoint that follows // a similar pattern - group.MapPost("/token/{provider}", async Task, SignInHttpResult, ValidationProblem>> (string provider, ExternalUserInfo userInfo, UserManager userManager, SignInManager signInManager) => + group.MapPost("/token/{provider}", async Task, SignInHttpResult, ValidationProblem>> (string provider, ExternalUserInfo userInfo, UserManager userManager, SignInManager signInManager, IDataProtectionProvider dataProtectionProvider) => { - var user = await userManager.FindByLoginAsync(provider, userInfo.ProviderKey); + var protector = dataProtectionProvider.CreateProtector(provider); + + var providerKey = protector.Unprotect(userInfo.ProviderKey); + + var user = await userManager.FindByLoginAsync(provider, providerKey); var result = IdentityResult.Success; @@ -34,7 +39,7 @@ public static RouteGroupBuilder MapUsers(this IEndpointRouteBuilder routes) if (result.Succeeded) { - result = await userManager.AddLoginAsync(user, new UserLoginInfo(provider, userInfo.ProviderKey, displayName: null)); + result = await userManager.AddLoginAsync(user, new UserLoginInfo(provider, providerKey, displayName: null)); } } diff --git a/Todo.Web/Server/AuthApi.cs b/Todo.Web/Server/AuthApi.cs index fb1d36d..ca7cd69 100644 --- a/Todo.Web/Server/AuthApi.cs +++ b/Todo.Web/Server/AuthApi.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.DataProtection; namespace Todo.Web.Server; @@ -60,7 +61,7 @@ public static RouteGroupBuilder MapAuth(this IEndpointRouteBuilder routes) authenticationSchemes: [provider]); }); - group.MapGet("signin/{provider}", async (string provider, AuthClient client, HttpContext context) => + group.MapGet("signin/{provider}", async (string provider, AuthClient client, HttpContext context, IDataProtectionProvider dataProtectionProvider) => { // Grab the login information from the external login dance var result = await context.AuthenticateAsync(AuthenticationSchemes.ExternalScheme); @@ -75,7 +76,10 @@ public static RouteGroupBuilder MapAuth(this IEndpointRouteBuilder routes) // for now we'll prefer the email address var name = (principal.FindFirstValue(ClaimTypes.Email) ?? principal.Identity?.Name)!; - var token = await client.GetOrCreateUserAsync(provider, new() { Username = name, ProviderKey = id }); + // Protect the user id so it for transport + var protector = dataProtectionProvider.CreateProtector(provider); + + var token = await client.GetOrCreateUserAsync(provider, new() { Username = name, ProviderKey = protector.Protect(id) }); if (token is not null) { diff --git a/Todo.Web/Server/Program.cs b/Todo.Web/Server/Program.cs index c28a1bb..d5a5a14 100644 --- a/Todo.Web/Server/Program.cs +++ b/Todo.Web/Server/Program.cs @@ -9,6 +9,9 @@ builder.AddAuthentication(); builder.Services.AddAuthorizationBuilder(); +// Configure data protection, setup the application discriminator so that the data protection keys can be shared between the BFF and this API +builder.Services.AddDataProtection(o => o.ApplicationDiscriminator = "TodoApp"); + // Must add client services builder.Services.AddScoped();