Skip to content

Commit 0a3b0dd

Browse files
committed
feat: first implementation of identity server
1 parent f777e39 commit 0a3b0dd

63 files changed

Lines changed: 4673 additions & 1369 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

user-management/Stickerlandia.UserManagement.sln

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Driving", "Driving", "{9247
3333
EndProject
3434
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Driven", "Driven", "{86CA6782-DAF7-4130-9B10-02DE0680DF78}"
3535
EndProject
36-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stickerlandia.UserManagement.SharedSetup", "src\Stickerlandia.UserManagement.SharedSetup\Stickerlandia.UserManagement.SharedSetup.csproj", "{DE058729-25B8-4F03-BA72-0EF1BCF1A275}"
36+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stickerlandia.UserManagement.Auth", "src\Stickerlandia.UserManagement.Auth\Stickerlandia.UserManagement.Auth.csproj", "{408EFE63-AA9C-46BD-ADD5-AB917BB4E9E6}"
3737
EndProject
3838
Global
3939
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -84,10 +84,10 @@ Global
8484
{5C770F92-B6E7-4270-A041-A32F385DA6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
8585
{5C770F92-B6E7-4270-A041-A32F385DA6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
8686
{5C770F92-B6E7-4270-A041-A32F385DA6C4}.Release|Any CPU.Build.0 = Release|Any CPU
87-
{DE058729-25B8-4F03-BA72-0EF1BCF1A275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
88-
{DE058729-25B8-4F03-BA72-0EF1BCF1A275}.Debug|Any CPU.Build.0 = Debug|Any CPU
89-
{DE058729-25B8-4F03-BA72-0EF1BCF1A275}.Release|Any CPU.ActiveCfg = Release|Any CPU
90-
{DE058729-25B8-4F03-BA72-0EF1BCF1A275}.Release|Any CPU.Build.0 = Release|Any CPU
87+
{408EFE63-AA9C-46BD-ADD5-AB917BB4E9E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
88+
{408EFE63-AA9C-46BD-ADD5-AB917BB4E9E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
89+
{408EFE63-AA9C-46BD-ADD5-AB917BB4E9E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
90+
{408EFE63-AA9C-46BD-ADD5-AB917BB4E9E6}.Release|Any CPU.Build.0 = Release|Any CPU
9191
EndGlobalSection
9292
GlobalSection(NestedProjects) = preSolution
9393
{4DDC795B-2EA4-4123-B631-F0D8CAA99BA6} = {FE220876-2B87-4095-8BD2-039049503C72}
@@ -102,6 +102,6 @@ Global
102102
{295E8315-2CD2-4CD8-B7F4-21EF587B74A9} = {86CA6782-DAF7-4130-9B10-02DE0680DF78}
103103
{9A175F6D-120D-4AF2-B407-B1499B70BE9E} = {86CA6782-DAF7-4130-9B10-02DE0680DF78}
104104
{1F6841EA-97F0-4079-967A-42F816E4FD1F} = {86CA6782-DAF7-4130-9B10-02DE0680DF78}
105-
{DE058729-25B8-4F03-BA72-0EF1BCF1A275} = {924749FC-98D5-40A6-8F4B-BA9BFDEDDEC3}
105+
{408EFE63-AA9C-46BD-ADD5-AB917BB4E9E6} = {FC9DDF76-8DAC-4498-8926-8883F984709C}
106106
EndGlobalSection
107107
EndGlobal

user-management/infra/main.tf

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,6 @@ locals {
6464
app_settings = {
6565
"ConnectionStrings__messaging" = azurerm_servicebus_namespace.stickerlandia_users_service_bus.default_primary_connection_string
6666
"ConnectionStrings__database" = "AccountEndpoint=${azurerm_cosmosdb_account.user_management.endpoint};AccountKey=${azurerm_cosmosdb_account.user_management.primary_key};"
67-
"Auth__Issuer"= "https://stickerlandia.com"
68-
"Auth__Audience"= "https://stickerlandia.com"
69-
"Auth__Key"= "This is a super secret key that should not be used in production'"
7067
}
7168

7269
tags = {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
2+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
3+
// Copyright 2025 Datadog, Inc.
4+
5+
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Hosting;
8+
using OpenIddict.Abstractions;
9+
10+
namespace Stickerlandia.UserManagement.Agnostic;
11+
12+
public class AuthenticationWorker : IHostedService
13+
{
14+
private readonly IServiceProvider _serviceProvider;
15+
16+
public AuthenticationWorker(IServiceProvider serviceProvider)
17+
=> _serviceProvider = serviceProvider;
18+
19+
public async Task StartAsync(CancellationToken cancellationToken)
20+
{
21+
using var scope = _serviceProvider.CreateScope();
22+
var identityDbContext = scope.ServiceProvider.GetRequiredService<UserManagementDbContext>();
23+
await identityDbContext.Database.EnsureCreatedAsync(cancellationToken);
24+
25+
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
26+
27+
if (await manager.FindByClientIdAsync("user-authentication") is null)
28+
{
29+
await manager.CreateAsync(new OpenIddictApplicationDescriptor
30+
{
31+
ClientId = "user-authentication",
32+
ClientSecret = "388D45FA-B36B-4988-BA59-B187D329C207",
33+
Permissions =
34+
{
35+
OpenIddictConstants.Permissions.Endpoints.Token,
36+
OpenIddictConstants.Permissions.GrantTypes.Password,
37+
}
38+
});
39+
}
40+
41+
if (await manager.FindByClientIdAsync("internal-service") is null)
42+
{
43+
await manager.CreateAsync(new OpenIddictApplicationDescriptor
44+
{
45+
ClientId = "internal-service",
46+
ClientSecret = "8E1167EF-5C44-4209-A803-3A109155FDD3",
47+
Permissions =
48+
{
49+
OpenIddictConstants.Permissions.Endpoints.Token,
50+
OpenIddictConstants.Permissions.GrantTypes.Password,
51+
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
52+
}
53+
});
54+
}
55+
}
56+
57+
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
58+
}

user-management/src/Stickerlandia.UserManagement.Agnostic/DbContextFactory.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ public static IServiceCollection AddPostgresUserRepository(this IServiceCollecti
1818
});
1919

2020
// Register repositories
21-
services.AddScoped<IUsers, PostgresUserRepository>();
22-
services.AddScoped<IOutbox, PostgresUserRepository>();
21+
2322

2423
return services;
2524
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
2+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
3+
// Copyright 2025 Datadog, Inc.
4+
5+
using System.Collections.Immutable;
6+
using System.Security.Claims;
7+
using Microsoft.AspNetCore.Authentication;
8+
using Microsoft.AspNetCore.Identity;
9+
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
10+
using Microsoft.EntityFrameworkCore;
11+
using Microsoft.Extensions.Logging;
12+
using OpenIddict.Abstractions;
13+
using OpenIddict.Server.AspNetCore;
14+
using Stickerlandia.UserManagement.Core;
15+
16+
namespace Stickerlandia.UserManagement.Agnostic;
17+
18+
public class MicrosoftIdentityAuthService(
19+
ILogger<MicrosoftIdentityAuthService> logger,
20+
IOpenIddictApplicationManager applicationManager,
21+
IOpenIddictScopeManager scopeManager,
22+
UserManagementDbContext dbContext,
23+
SignInManager<PostgresUserAccount> signInManager,
24+
UserManager<PostgresUserAccount> userManager) : IAuthService
25+
{
26+
public async Task<ClaimsIdentity?> VerifyClient(string clientId)
27+
{
28+
var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
29+
OpenIddictConstants.Claims.Name, OpenIddictConstants.Claims.Role);
30+
31+
var application = await applicationManager.FindByClientIdAsync(clientId) ??
32+
throw new InvalidOperationException("The application cannot be found.");
33+
34+
// Use the client_id as the subject identifier.
35+
identity.SetClaim(OpenIddictConstants.Claims.Subject,
36+
await applicationManager.GetClientIdAsync(application));
37+
identity.SetClaim(OpenIddictConstants.Claims.Name,
38+
await applicationManager.GetDisplayNameAsync(application));
39+
40+
identity.SetDestinations(static claim => claim.Type switch
41+
{
42+
// Allow the "name" claim to be stored in both the access and identity tokens
43+
// when the "profile" scope was granted (by calling principal.SetScopes(...)).
44+
OpenIddictConstants.Claims.Name when claim.Subject.HasScope(OpenIddictConstants.Permissions.Scopes
45+
.Profile)
46+
=> [OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken],
47+
48+
// Otherwise, only store the claim in the access tokens.
49+
_ => [OpenIddictConstants.Destinations.AccessToken]
50+
});
51+
52+
return identity;
53+
}
54+
55+
public async Task<ClaimsIdentity?> VerifyPassword(string username, string password, ImmutableArray<string> scopes)
56+
{
57+
var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
58+
OpenIddictConstants.Claims.Name, OpenIddictConstants.Claims.Role);
59+
PostgresUserAccount? user = await userManager.FindByEmailAsync(username);
60+
AuthenticationProperties properties = new();
61+
62+
if (user == null)
63+
return null;
64+
65+
// Check that the user can sign in and is not locked out.
66+
// If two-factor authentication is supported, it would also be appropriate to check that 2FA is enabled for the user
67+
if (!await signInManager.CanSignInAsync(user) ||
68+
(userManager.SupportsUserLockout && await userManager.IsLockedOutAsync(user)))
69+
// Return bad request is the user can't sign in
70+
return null;
71+
72+
// Validate the username/password parameters and ensure the account is not locked out.
73+
var result = await signInManager.CheckPasswordSignInAsync(user, password, false);
74+
if (!result.Succeeded)
75+
{
76+
if (result.IsNotAllowed)
77+
return null;
78+
79+
if (result.RequiresTwoFactor)
80+
return null;
81+
82+
if (result.IsLockedOut)
83+
return null;
84+
else
85+
return null;
86+
}
87+
88+
// The user is now validated, so reset lockout counts, if necessary
89+
if (userManager.SupportsUserLockout) await userManager.ResetAccessFailedCountAsync(user);
90+
91+
//// Getting scopes from user parameters (TokenViewModel) and adding in Identity
92+
identity.SetScopes(scopes);
93+
94+
var resources = await scopeManager.ListResourcesAsync(scopes).ToListAsync();
95+
identity.SetResources(resources);
96+
97+
// Add Custom claims
98+
identity.AddClaim(new Claim(OpenIddictConstants.Claims.Subject, user.Id));
99+
identity.AddClaim(new Claim(OpenIddictConstants.Claims.Audience, "Resource"));
100+
identity.AddClaim(new Claim(OpenIddictConstants.Claims.Email, user.Email));
101+
identity.AddClaim(new Claim(OpenIddictConstants.Claims.Username, user.Id));
102+
103+
// Setting destinations of claims i.e. identity token or access token
104+
identity.SetDestinations(GetDestinations);
105+
106+
return identity;
107+
}
108+
109+
public async Task EnsureStoreCreatedAsync()
110+
{
111+
await dbContext.Database.MigrateAsync();
112+
}
113+
114+
private static IEnumerable<string> GetDestinations(Claim claim)
115+
{
116+
// Note: by default, claims are NOT automatically included in the access and identity tokens.
117+
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
118+
// whether they should be included in access tokens, in identity tokens or in both.
119+
120+
return claim.Type switch
121+
{
122+
OpenIddictConstants.Claims.Name or
123+
OpenIddictConstants.Claims.Subject
124+
=> new[]
125+
{
126+
OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken
127+
},
128+
129+
_ => new[] { OpenIddictConstants.Destinations.AccessToken }
130+
};
131+
}
132+
}

user-management/src/Stickerlandia.UserManagement.Agnostic/Migrations/20250502111315_InitialCreate.Designer.cs

Lines changed: 0 additions & 126 deletions
This file was deleted.

0 commit comments

Comments
 (0)