Skip to content

Commit

Permalink
Protect the user id to make sure only the BFF can call the external a…
Browse files Browse the repository at this point in the history
…uth API (#100)

* Protect the id for external auth with a shared sercret (using data protection).

* Fix the id
  • Loading branch information
davidfowl authored Nov 27, 2024
1 parent 69127f1 commit a519c10
Show file tree
Hide file tree
Showing 5 changed files with 28 additions and 6 deletions.
8 changes: 7 additions & 1 deletion Todo.Api.Tests/UserApiTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.DataProtection;

namespace TodoApi.Tests;

Expand Down Expand Up @@ -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<IDataProtectionProvider>()
.CreateProtector("Google")
.Protect("1003");

var response = await client.PostAsJsonAsync("/users/token/Google", new ExternalUserInfo { Username = "todouser", ProviderKey = encryptedId });

Assert.True(response.IsSuccessStatusCode);

Expand Down
4 changes: 4 additions & 0 deletions Todo.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
11 changes: 8 additions & 3 deletions Todo.Api/Users/UsersApi.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authentication.BearerToken;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Identity;

Expand All @@ -20,9 +21,13 @@ public static RouteGroupBuilder MapUsers(this IEndpointRouteBuilder routes)

// The MapIdentityApi<T> doesn't expose an external login endpoint so we write this custom endpoint that follows
// a similar pattern
group.MapPost("/token/{provider}", async Task<Results<Ok<AccessTokenResponse>, SignInHttpResult, ValidationProblem>> (string provider, ExternalUserInfo userInfo, UserManager<TodoUser> userManager, SignInManager<TodoUser> signInManager) =>
group.MapPost("/token/{provider}", async Task<Results<Ok<AccessTokenResponse>, SignInHttpResult, ValidationProblem>> (string provider, ExternalUserInfo userInfo, UserManager<TodoUser> userManager, SignInManager<TodoUser> 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;

Expand All @@ -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));
}
}

Expand Down
8 changes: 6 additions & 2 deletions Todo.Web/Server/AuthApi.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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)
{
Expand Down
3 changes: 3 additions & 0 deletions Todo.Web/Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TodoClient>();

Expand Down

0 comments on commit a519c10

Please sign in to comment.