Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
davidfowl committed Sep 16, 2023
1 parent 6d9f7ae commit 84cd69d
Show file tree
Hide file tree
Showing 33 changed files with 322 additions and 1,599 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Expand Up @@ -3,6 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="8.0.0-preview.6.23329.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0-preview.6.23329.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0-preview.6.23329.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.0-preview.23280.1" />
Expand Down
6 changes: 5 additions & 1 deletion Todo.Web/Server/Program.cs
Expand Up @@ -20,10 +20,14 @@
builder.Configuration["TodoApiUrl"] ??
throw new InvalidOperationException("Todo API URL is not configured");

var identityUrl = builder.Configuration.GetServiceUri("identityapi")?.ToString() ??
builder.Configuration["IdentityApiUrl"] ??
throw new InvalidOperationException("Todo API URL is not configured");

// Configure the HttpClient for the backend API
builder.Services.AddHttpClient<AuthClient>(client =>
{
client.BaseAddress = new(todoUrl);
client.BaseAddress = new(identityUrl);
});

var app = builder.Build();
Expand Down
3 changes: 2 additions & 1 deletion Todo.Web/Server/appsettings.json
Expand Up @@ -7,5 +7,6 @@
}
},
"AllowedHosts": "*",
"TodoApiUrl": "https://localhost:5001"
"TodoApiUrl": "https://localhost:5001",
"IdentityApiUrl": "https://localhost:7104"
}
10 changes: 10 additions & 0 deletions TodoApi.Identity.Tests/GlobalUsings.cs
@@ -0,0 +1,10 @@
global using System.Net.Http.Headers;
global using Microsoft.AspNetCore.Identity;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore.Mvc.Testing;
global using Microsoft.Data.Sqlite;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.DependencyInjection.Extensions;
global using Microsoft.Extensions.Hosting;
global using UsersDbContext = Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityDbContext<TodoApi.TodoUser>;
36 changes: 36 additions & 0 deletions TodoApi.Identity.Tests/TodoApi.Identity.Tests.csproj
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\TodoApi.Tests\DbContextExtensions.cs" Link="DbContextExtensions.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
<PackageReference Include="coverlet.collector" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\TodoApi.Identity\TodoApi.Identity.csproj" />
</ItemGroup>


<ItemGroup>
<Using Include="System.Net.Http.Json" />
<Using Include="System.Net.Http" />
<Using Include="System.Net" />
<Using Include="Xunit" />
</ItemGroup>

</Project>
62 changes: 62 additions & 0 deletions TodoApi.Identity.Tests/TodoApplication.cs
@@ -0,0 +1,62 @@
using Microsoft.AspNetCore.DataProtection;

namespace TodoApi.Tests;

internal class TodoApplication : WebApplicationFactory<Program>
{
private readonly SqliteConnection _sqliteConnection = new("Filename=:memory:");

public UsersDbContext CreateTodoDbContext()
{
var db = Services.GetRequiredService<IDbContextFactory<UsersDbContext>>().CreateDbContext();
db.Database.EnsureCreated();
return db;
}

public async Task CreateUserAsync(string username, string? password = null)
{
using var scope = Services.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TodoUser>>();
var newUser = new TodoUser { Id = username };
var result = await userManager.CreateAsync(newUser, password ?? Guid.NewGuid().ToString());
Assert.True(result.Succeeded);
}

protected override IHost CreateHost(IHostBuilder builder)
{
// Open the connection, this creates the SQLite in-memory database, which will persist until the connection is closed
_sqliteConnection.Open();

builder.ConfigureServices(services =>
{
// We're going to use the factory from our tests
services.AddDbContextFactory<UsersDbContext>();
// We need to replace the configuration for the DbContext to use a different configured database
services.AddDbContextOptions<UsersDbContext>(o => o.UseSqlite(_sqliteConnection));
// Lower the requirements for the tests
services.Configure<IdentityOptions>(o =>
{
o.Password.RequireNonAlphanumeric = false;
o.Password.RequireDigit = false;
o.Password.RequiredUniqueChars = 0;
o.Password.RequiredLength = 1;
o.Password.RequireLowercase = false;
o.Password.RequireUppercase = false;
});
// Since tests run in parallel, it's possible multiple servers will startup,
// we use an ephemeral key provider and repository to avoid filesystem contention issues
services.AddSingleton<IDataProtectionProvider, EphemeralDataProtectionProvider>();
});

return base.CreateHost(builder);
}

protected override void Dispose(bool disposing)
{
_sqliteConnection?.Dispose();
base.Dispose(disposing);
}
}
File renamed without changes.
25 changes: 25 additions & 0 deletions TodoApi.Identity/Extensions/DataProtectionExtensions.cs
@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace TodoApi;

public static class DataProtectionExtensions
{
public static IServiceCollection AddSharedKeys(this IServiceCollection services, IConfiguration configuration)
{
var keysConnectionString = configuration.GetConnectionString("Keys") ?? "Data Source=../Keys.db";
services.AddSqlite<SharedKeysDb>(keysConnectionString);

services.AddDataProtection()
.SetApplicationName("TodoApp")
.PersistKeysToDbContext<SharedKeysDb>();

return services;
}
}

public class SharedKeysDb(DbContextOptions<SharedKeysDb> options) : DbContext(options), IDataProtectionKeyContext
{
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
}
49 changes: 49 additions & 0 deletions TodoApi.Identity/Program.cs
@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using TodoApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication().AddIdentityBearerToken<TodoUser>();

Check failure on line 8 in TodoApi.Identity/Program.cs

View workflow job for this annotation

GitHub Actions / build

'AuthenticationBuilder' does not contain a definition for 'AddIdentityBearerToken' and no accessible extension method 'AddIdentityBearerToken' accepting a first argument of type 'AuthenticationBuilder' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 8 in TodoApi.Identity/Program.cs

View workflow job for this annotation

GitHub Actions / build

'AuthenticationBuilder' does not contain a definition for 'AddIdentityBearerToken' and no accessible extension method 'AddIdentityBearerToken' accepting a first argument of type 'AuthenticationBuilder' could be found (are you missing a using directive or an assembly reference?)

builder.Services.AddSharedKeys(builder.Configuration);

var connectionString = builder.Configuration.GetConnectionString("Users") ?? "Data Source=.db/Users.db";
builder.Services.AddSqlite<IdentityDbContext<TodoUser>>(connectionString);

// Configure Open API
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(o => o.AddOpenApiSecurity());

// Configure identity
builder.Services.AddIdentityCore<TodoUser>()
.AddEntityFrameworkStores<IdentityDbContext<TodoUser>>()
.AddApiEndpoints();

var app = builder.Build();

await MakeDb(app.Services);

async Task MakeDb(IServiceProvider sp)
{
await using var scope = sp.CreateAsyncScope();

var db0 = scope.ServiceProvider.GetRequiredService<SharedKeysDb>();
var db1 = scope.ServiceProvider.GetRequiredService<IdentityDbContext<TodoUser>>();

await db0.Database.EnsureCreatedAsync();
await db1.Database.EnsureCreatedAsync();
}

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.MapUsers();

app.Map("/", () => Results.Redirect("/swagger"));

app.Run();
38 changes: 38 additions & 0 deletions TodoApi.Identity/Properties/launchSettings.json
@@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:6521",
"sslPort": 44327
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5210",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7104;http://localhost:5210",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
31 changes: 31 additions & 0 deletions TodoApi.Identity/TodoApi.Identity.csproj
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\TodoApi\Extensions\OpenApiExtensions.cs" Link="Extensions\OpenApiExtensions.cs" />
<Compile Include="..\TodoApi\Extensions\RateLimitExtensions.cs" Link="Extensions\RateLimitExtensions.cs" />
<Compile Include="..\TodoApi\Filters\ValidationFilter.cs" Link="Filters\ValidationFilter.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.OpenApi" />
<PackageReference Include="MiniValidation" />
<PackageReference Include="Swashbuckle.AspNetCore" />
</ItemGroup>

<ItemGroup>
<Folder Include="Filters\" />
<InternalsVisibleTo Include="TodoApi.Identity.Tests" />
</ItemGroup>

</Project>
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions TodoApi.Identity/appsettings.Development.json
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions TodoApi.Identity/appsettings.json
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
1 change: 0 additions & 1 deletion TodoApi.Tests/GlobalUsings.cs
Expand Up @@ -7,4 +7,3 @@
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.DependencyInjection.Extensions;
global using Microsoft.Extensions.Hosting;

1 change: 0 additions & 1 deletion TodoApi.Tests/TodoApi.Tests.csproj
@@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
Expand Down
2 changes: 1 addition & 1 deletion TodoApi.Tests/TodoApplication.cs
Expand Up @@ -17,7 +17,7 @@ public async Task CreateUserAsync(string username, string? password = null)
{
using var scope = Services.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TodoUser>>();
var newUser = new TodoUser { Id = username, UserName = username };
var newUser = new TodoUser { Id = username };
var result = await userManager.CreateAsync(newUser, password ?? Guid.NewGuid().ToString());
Assert.True(result.Succeeded);
}
Expand Down
2 changes: 1 addition & 1 deletion TodoApi.Tests/TokenService.cs
Expand Up @@ -11,7 +11,7 @@ public sealed class TokenService(SignInManager<TodoUser> signInManager, IOptions

public async Task<string> GenerateTokenAsync(string username, bool isAdmin = false)
{
var claimsPrincipal = await signInManager.CreateUserPrincipalAsync(new TodoUser { Id = username, UserName = username });
var claimsPrincipal = await signInManager.CreateUserPrincipalAsync(new TodoUser { Id = username });

if (isAdmin)
{
Expand Down
12 changes: 12 additions & 0 deletions TodoApi.sln
Expand Up @@ -22,6 +22,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Todo.Web.Client", "Todo.Web
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Todo.Web.Shared", "Todo.Web\Shared\Todo.Web.Shared.csproj", "{272942F6-94E8-4D6B-8AD8-C4CCA305836D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApi.Identity", "TodoApi.Identity\TodoApi.Identity.csproj", "{1780E3C2-741A-4B38-A9F9-6E874E75BA0A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApi.Identity.Tests", "TodoApi.Identity.Tests\TodoApi.Identity.Tests.csproj", "{BBAC6FFC-3A32-4171-B35C-75C6EE5E79B4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -48,6 +52,14 @@ Global
{272942F6-94E8-4D6B-8AD8-C4CCA305836D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{272942F6-94E8-4D6B-8AD8-C4CCA305836D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{272942F6-94E8-4D6B-8AD8-C4CCA305836D}.Release|Any CPU.Build.0 = Release|Any CPU
{1780E3C2-741A-4B38-A9F9-6E874E75BA0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1780E3C2-741A-4B38-A9F9-6E874E75BA0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1780E3C2-741A-4B38-A9F9-6E874E75BA0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1780E3C2-741A-4B38-A9F9-6E874E75BA0A}.Release|Any CPU.Build.0 = Release|Any CPU
{BBAC6FFC-3A32-4171-B35C-75C6EE5E79B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BBAC6FFC-3A32-4171-B35C-75C6EE5E79B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBAC6FFC-3A32-4171-B35C-75C6EE5E79B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BBAC6FFC-3A32-4171-B35C-75C6EE5E79B4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
5 changes: 2 additions & 3 deletions TodoApi/Authorization/CurrentUserExtensions.cs
@@ -1,6 +1,5 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;

namespace TodoApi;

Expand All @@ -13,7 +12,7 @@ public static IServiceCollection AddCurrentUser(this IServiceCollection services
return services;
}

private sealed class ClaimsTransformation(CurrentUser currentUser, UserManager<TodoUser> userManager) : IClaimsTransformation
private sealed class ClaimsTransformation(CurrentUser currentUser, TodoDbContext db) : IClaimsTransformation
{
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
Expand All @@ -25,7 +24,7 @@ public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
// Resolve the user manager and see if the current user is a valid user in the database
// we do this once and store it on the current user.
currentUser.User = await userManager.FindByIdAsync(id);
currentUser.User = await db.Users.FindAsync(id);
}

return principal;
Expand Down

0 comments on commit 84cd69d

Please sign in to comment.