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

Protect the user id to make sure only the BFF can call the external auth API #100

Merged
merged 2 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading