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+ }
0 commit comments