Skip to content

Commit 13185fd

Browse files
authored
Added configurable roles to a user (#17)
1 parent edc3fa2 commit 13185fd

6 files changed

Lines changed: 346 additions & 8 deletions

File tree

DevOidcToolkit.Documentation/docs/configuration.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,25 @@ This is a list of all of the environment variables that can be used to configure
7171
<td>false</td>
7272
</tr>
7373
<tr>
74-
<td>DevOidcToolkit__Https_File_CertificatePath</td>
74+
<td>DevOidcToolkit__Https__File__CertificatePath</td>
7575
<td>The path to the certificate file.</td>
7676
<td>/app/cert.pem</td>
7777
<td>None</td>
7878
</tr>
7979
<tr>
80-
<td>DevOidcToolkit__Https_File_PrivateKeyPath</td>
80+
<td>DevOidcToolkit__Https__File__PrivateKeyPath</td>
8181
<td>The path to the private key file.</td>
8282
<td>/app/key.pem</td>
8383
<td>None</td>
8484
</tr>
8585
<tr>
86-
<td>DevOidcToolkit__Https_Inline_Certificate</td>
86+
<td>DevOidcToolkit__Https__Inline__Certificate</td>
8787
<td>The certificate as a string.</td>
8888
<td>Raw PEM certificate</td>
8989
<td>None</td>
9090
</tr>
9191
<tr>
92-
<td>DevOidcToolkit__Https_Inline_PrivateKey</td>
92+
<td>DevOidcToolkit__Https__Inline__PrivateKey</td>
9393
<td>The private key as a string.</td>
9494
<td>Raw PEM private key</td>
9595
<td>None</td>
@@ -112,6 +112,12 @@ This is a list of all of the environment variables that can be used to configure
112112
<td>Doe</td>
113113
<td>None</td>
114114
</tr>
115+
<tr>
116+
<td>DevOidcToolkit__Users__INDEX__Roles__INDEX</td>
117+
<td>The roles of the user</td>
118+
<td>user</td>
119+
<td>None</td>
120+
</tr>
115121
<tr>
116122
<td>DevOidcToolkit__Clients__INDEX__Id</td>
117123
<td>The ID of the client.</td>

DevOidcToolkit.UnitTests/Controllers/ConnectControllerTests.cs

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,114 @@ public async Task Authorize_WhenConsentNotRequired_ReturnsSignInResult()
466466
// Assert
467467
Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
468468
}
469+
470+
[Fact]
471+
public async Task Authorize_WhenUserHasRoles_IncludesRoleClaimsInSignInResult()
472+
{
473+
// Arrange
474+
var testApp = new object();
475+
var oidcAppManager = new Mock<IOpenIddictApplicationManager>();
476+
var userManager = MockUserManager.CreateMockUserManager<DevOidcToolkitUser>();
477+
var signInManager = MockSignInManager.CreateMockSignInManager<DevOidcToolkitUser>();
478+
479+
oidcAppManager.Setup(x => x.FindByClientIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
480+
.ReturnsAsync(testApp);
481+
oidcAppManager.Setup(x => x.GetConsentTypeAsync(testApp, It.IsAny<CancellationToken>()))
482+
.ReturnsAsync(ConsentTypes.Implicit);
483+
484+
var testUser = new DevOidcToolkitUser
485+
{
486+
Id = "user123",
487+
UserName = "testuser",
488+
Email = "test@example.com",
489+
FirstName = "Test",
490+
LastName = "User"
491+
};
492+
493+
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>()))
494+
.ReturnsAsync(testUser);
495+
userManager.Setup(x => x.GetRolesAsync(testUser))
496+
.ReturnsAsync(new List<string> { "admin", "editor" });
497+
498+
var claimsIdentity = new ClaimsIdentity();
499+
var principal = new ClaimsPrincipal(claimsIdentity);
500+
signInManager.Setup(x => x.CreateUserPrincipalAsync(testUser))
501+
.ReturnsAsync(principal);
502+
503+
var controller = CreateController(
504+
oidcAppManager.Object,
505+
userManager.Object,
506+
signInManager.Object,
507+
isAuthenticated: true,
508+
userId: testUser.Id,
509+
userName: testUser.UserName);
510+
511+
var request = new OpenIddictRequest { Scope = "openid profile" };
512+
var feature = new OpenIddictServerAspNetCoreFeature { Transaction = new() { Request = request } };
513+
controller.HttpContext.Features.Set(feature);
514+
515+
// Act
516+
var result = await controller.Authorize();
517+
518+
// Assert
519+
var signInResult = Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
520+
var roleClaims = signInResult.Principal.FindAll(Claims.Role).Select(c => c.Value).ToList();
521+
Assert.Contains("admin", roleClaims);
522+
Assert.Contains("editor", roleClaims);
523+
}
524+
525+
[Fact]
526+
public async Task Authorize_WhenUserHasNoRoles_NoRoleClaimsInSignInResult()
527+
{
528+
// Arrange
529+
var testApp = new object();
530+
var oidcAppManager = new Mock<IOpenIddictApplicationManager>();
531+
var userManager = MockUserManager.CreateMockUserManager<DevOidcToolkitUser>();
532+
var signInManager = MockSignInManager.CreateMockSignInManager<DevOidcToolkitUser>();
533+
534+
oidcAppManager.Setup(x => x.FindByClientIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
535+
.ReturnsAsync(testApp);
536+
oidcAppManager.Setup(x => x.GetConsentTypeAsync(testApp, It.IsAny<CancellationToken>()))
537+
.ReturnsAsync(ConsentTypes.Implicit);
538+
539+
var testUser = new DevOidcToolkitUser
540+
{
541+
Id = "user123",
542+
UserName = "testuser",
543+
Email = "test@example.com",
544+
FirstName = "Test",
545+
LastName = "User"
546+
};
547+
548+
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>()))
549+
.ReturnsAsync(testUser);
550+
userManager.Setup(x => x.GetRolesAsync(testUser))
551+
.ReturnsAsync(new List<string>());
552+
553+
var claimsIdentity = new ClaimsIdentity();
554+
var principal = new ClaimsPrincipal(claimsIdentity);
555+
signInManager.Setup(x => x.CreateUserPrincipalAsync(testUser))
556+
.ReturnsAsync(principal);
557+
558+
var controller = CreateController(
559+
oidcAppManager.Object,
560+
userManager.Object,
561+
signInManager.Object,
562+
isAuthenticated: true,
563+
userId: testUser.Id,
564+
userName: testUser.UserName);
565+
566+
var request = new OpenIddictRequest { Scope = "openid profile" };
567+
var feature = new OpenIddictServerAspNetCoreFeature { Transaction = new() { Request = request } };
568+
controller.HttpContext.Features.Set(feature);
569+
570+
// Act
571+
var result = await controller.Authorize();
572+
573+
// Assert
574+
var signInResult = Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
575+
Assert.Empty(signInResult.Principal.FindAll(Claims.Role));
576+
}
469577
}
470578

471579

@@ -885,6 +993,124 @@ public async Task AuthorizePost_WhenConsentNotRequired_ReturnsSignInResult()
885993
// Assert
886994
Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
887995
}
996+
997+
[Fact]
998+
public async Task AuthorizePost_WhenUserHasRoles_IncludesRoleClaimsInSignInResult()
999+
{
1000+
// Arrange
1001+
var testApp = new object();
1002+
var oidcAppManager = new Mock<IOpenIddictApplicationManager>();
1003+
var userManager = MockUserManager.CreateMockUserManager<DevOidcToolkitUser>();
1004+
var signInManager = MockSignInManager.CreateMockSignInManager<DevOidcToolkitUser>();
1005+
1006+
oidcAppManager.Setup(x => x.FindByClientIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
1007+
.ReturnsAsync(testApp);
1008+
1009+
var testUser = new DevOidcToolkitUser
1010+
{
1011+
Id = "user123",
1012+
UserName = "testuser",
1013+
Email = "test@example.com",
1014+
FirstName = "Test",
1015+
LastName = "User"
1016+
};
1017+
1018+
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>()))
1019+
.ReturnsAsync(testUser);
1020+
userManager.Setup(x => x.GetRolesAsync(testUser))
1021+
.ReturnsAsync(new List<string> { "admin", "editor" });
1022+
1023+
var claimsIdentity = new ClaimsIdentity();
1024+
var principal = new ClaimsPrincipal(claimsIdentity);
1025+
signInManager.Setup(x => x.CreateUserPrincipalAsync(testUser))
1026+
.ReturnsAsync(principal);
1027+
1028+
var controller = CreateController(
1029+
oidcAppManager.Object,
1030+
userManager.Object,
1031+
signInManager.Object,
1032+
isAuthenticated: true,
1033+
userId: testUser.Id,
1034+
userName: testUser.UserName);
1035+
1036+
controller.ControllerContext.HttpContext.Request.Method = "POST";
1037+
controller.ControllerContext.HttpContext.Request.ContentType = "application/x-www-form-urlencoded";
1038+
controller.ControllerContext.HttpContext.Request.Form = new FormCollection(new Dictionary<string, Microsoft.Extensions.Primitives.StringValues>
1039+
{
1040+
["consent"] = "yes"
1041+
});
1042+
1043+
var request = new OpenIddictRequest { Scope = "openid profile" };
1044+
var feature = new OpenIddictServerAspNetCoreFeature { Transaction = new() { Request = request } };
1045+
controller.HttpContext.Features.Set(feature);
1046+
1047+
// Act
1048+
var result = await controller.AuthorizePost();
1049+
1050+
// Assert
1051+
var signInResult = Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
1052+
var roleClaims = signInResult.Principal.FindAll(Claims.Role).Select(c => c.Value).ToList();
1053+
Assert.Contains("admin", roleClaims);
1054+
Assert.Contains("editor", roleClaims);
1055+
}
1056+
1057+
[Fact]
1058+
public async Task AuthorizePost_WhenUserHasNoRoles_NoRoleClaimsInSignInResult()
1059+
{
1060+
// Arrange
1061+
var testApp = new object();
1062+
var oidcAppManager = new Mock<IOpenIddictApplicationManager>();
1063+
var userManager = MockUserManager.CreateMockUserManager<DevOidcToolkitUser>();
1064+
var signInManager = MockSignInManager.CreateMockSignInManager<DevOidcToolkitUser>();
1065+
1066+
oidcAppManager.Setup(x => x.FindByClientIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
1067+
.ReturnsAsync(testApp);
1068+
1069+
var testUser = new DevOidcToolkitUser
1070+
{
1071+
Id = "user123",
1072+
UserName = "testuser",
1073+
Email = "test@example.com",
1074+
FirstName = "Test",
1075+
LastName = "User"
1076+
};
1077+
1078+
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>()))
1079+
.ReturnsAsync(testUser);
1080+
userManager.Setup(x => x.GetRolesAsync(testUser))
1081+
.ReturnsAsync(new List<string>());
1082+
1083+
var claimsIdentity = new ClaimsIdentity();
1084+
var principal = new ClaimsPrincipal(claimsIdentity);
1085+
signInManager.Setup(x => x.CreateUserPrincipalAsync(testUser))
1086+
.ReturnsAsync(principal);
1087+
1088+
var controller = CreateController(
1089+
oidcAppManager.Object,
1090+
userManager.Object,
1091+
signInManager.Object,
1092+
isAuthenticated: true,
1093+
userId: testUser.Id,
1094+
userName: testUser.UserName);
1095+
1096+
controller.ControllerContext.HttpContext.Request.Method = "POST";
1097+
controller.ControllerContext.HttpContext.Request.ContentType = "application/x-www-form-urlencoded";
1098+
controller.ControllerContext.HttpContext.Request.Form = new FormCollection(new Dictionary<string, Microsoft.Extensions.Primitives.StringValues>
1099+
{
1100+
["consent"] = "yes"
1101+
});
1102+
1103+
var request = new OpenIddictRequest { Scope = "openid profile" };
1104+
var feature = new OpenIddictServerAspNetCoreFeature { Transaction = new() { Request = request } };
1105+
controller.HttpContext.Features.Set(feature);
1106+
1107+
// Act
1108+
var result = await controller.AuthorizePost();
1109+
1110+
// Assert
1111+
var signInResult = Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
1112+
Assert.Empty(signInResult.Principal.FindAll(Claims.Role));
1113+
}
8881114
}
8891115

8901116
public class ConnectControllerExchangeTests
@@ -1094,6 +1320,74 @@ public async Task Exchange_WithUnsupportedGrantType_ReturnsBadRequest()
10941320
Assert.Equal(Errors.UnsupportedGrantType, errorResponse.Error);
10951321
Assert.Equal("The specified grant type is not supported.", errorResponse.ErrorDescription);
10961322
}
1323+
1324+
[Fact]
1325+
public async Task Exchange_WithAuthorizationCodeGrant_RoleClaimsIncludedInIdentityToken()
1326+
{
1327+
// Arrange
1328+
var oidcAppManager = new Mock<IOpenIddictApplicationManager>();
1329+
var userManager = MockUserManager.CreateMockUserManager<DevOidcToolkitUser>();
1330+
var signInManager = MockSignInManager.CreateMockSignInManager<DevOidcToolkitUser>();
1331+
1332+
var controller = CreateController(
1333+
oidcAppManager.Object,
1334+
userManager.Object,
1335+
signInManager.Object);
1336+
1337+
var request = new OpenIddictRequest
1338+
{
1339+
GrantType = GrantTypes.AuthorizationCode,
1340+
Code = "test-code"
1341+
};
1342+
1343+
// Simulate the principal stored in the authorization code, which includes role claims
1344+
// set by ProcessAuthorizationRequest
1345+
var identity = new ClaimsIdentity(
1346+
authenticationType: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
1347+
nameType: Claims.Name,
1348+
roleType: Claims.Role);
1349+
1350+
identity.AddClaim(Claims.Subject, "user123");
1351+
identity.AddClaim(new Claim(Claims.Role, "admin"));
1352+
identity.AddClaim(new Claim(Claims.Role, "editor"));
1353+
1354+
var principal = new ClaimsPrincipal(identity);
1355+
var ticket = new AuthenticationTicket(principal, null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
1356+
1357+
var authServiceMock = new Mock<IAuthenticationService>();
1358+
authServiceMock
1359+
.Setup(x => x.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
1360+
.ReturnsAsync(AuthenticateResult.Success(ticket));
1361+
1362+
var serviceProvider = new ServiceCollection()
1363+
.AddSingleton(authServiceMock.Object)
1364+
.BuildServiceProvider();
1365+
1366+
controller.ControllerContext.HttpContext.RequestServices = serviceProvider;
1367+
1368+
var feature = new OpenIddictServerAspNetCoreFeature
1369+
{
1370+
Transaction = new()
1371+
{
1372+
Request = request,
1373+
EndpointType = OpenIddictServerEndpointType.Token
1374+
}
1375+
};
1376+
controller.HttpContext.Features.Set(feature);
1377+
1378+
// Act
1379+
var result = await controller.Exchange();
1380+
1381+
// Assert
1382+
var signInResult = Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
1383+
var roleClaims = signInResult.Principal.FindAll(Claims.Role).ToList();
1384+
Assert.NotEmpty(roleClaims);
1385+
foreach (var roleClaim in roleClaims)
1386+
{
1387+
Assert.Contains(Destinations.AccessToken, roleClaim.GetDestinations());
1388+
Assert.Contains(Destinations.IdentityToken, roleClaim.GetDestinations());
1389+
}
1390+
}
10971391
}
10981392

10991393

DevOidcToolkit/Controllers/ConnectController.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ private async Task<IActionResult> ProcessAuthorizationRequest(OpenIddictRequest
143143
principal.SetClaim(Claims.GivenName, user.FirstName);
144144
principal.SetClaim(Claims.FamilyName, user.LastName);
145145

146+
var roles = await _userManager.GetRolesAsync(user) ?? [];
147+
if (roles.Count > 0)
148+
{
149+
principal.SetClaims(Claims.Role, [.. roles]);
150+
}
151+
146152
principal.SetScopes(request.GetScopes());
147153
principal.SetResources("resource_server");
148154

@@ -156,6 +162,7 @@ private async Task<IActionResult> ProcessAuthorizationRequest(OpenIddictRequest
156162
Claims.Email => [Destinations.AccessToken, Destinations.IdentityToken],
157163
Claims.GivenName => [Destinations.AccessToken, Destinations.IdentityToken],
158164
Claims.FamilyName => [Destinations.AccessToken, Destinations.IdentityToken],
165+
Claims.Role => [Destinations.AccessToken, Destinations.IdentityToken],
159166
_ => [Destinations.AccessToken],
160167
});
161168
}
@@ -234,6 +241,7 @@ public async Task<IActionResult> Exchange()
234241
Claims.Email => [Destinations.AccessToken, Destinations.IdentityToken],
235242
Claims.GivenName => [Destinations.AccessToken, Destinations.IdentityToken],
236243
Claims.FamilyName => [Destinations.AccessToken, Destinations.IdentityToken],
244+
Claims.Role => [Destinations.AccessToken, Destinations.IdentityToken],
237245
_ => [Destinations.AccessToken]
238246
});
239247
}

DevOidcToolkit/Infrastructure/Configuration/Configuration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class UserConfiguration
2525
[Required] public required string Email { get; set; }
2626
[Required] public required string FirstName { get; set; }
2727
[Required] public required string LastName { get; set; }
28+
public List<string> Roles { get; set; } = [];
2829
}
2930

3031
public class ClientConfiguration

0 commit comments

Comments
 (0)