Skip to content
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
14 changes: 10 additions & 4 deletions DevOidcToolkit.Documentation/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,25 @@ This is a list of all of the environment variables that can be used to configure
<td>false</td>
</tr>
<tr>
<td>DevOidcToolkit__Https_File_CertificatePath</td>
<td>DevOidcToolkit__Https__File__CertificatePath</td>
<td>The path to the certificate file.</td>
<td>/app/cert.pem</td>
<td>None</td>
</tr>
<tr>
<td>DevOidcToolkit__Https_File_PrivateKeyPath</td>
<td>DevOidcToolkit__Https__File__PrivateKeyPath</td>
<td>The path to the private key file.</td>
<td>/app/key.pem</td>
<td>None</td>
</tr>
<tr>
<td>DevOidcToolkit__Https_Inline_Certificate</td>
<td>DevOidcToolkit__Https__Inline__Certificate</td>
<td>The certificate as a string.</td>
<td>Raw PEM certificate</td>
<td>None</td>
</tr>
<tr>
<td>DevOidcToolkit__Https_Inline_PrivateKey</td>
<td>DevOidcToolkit__Https__Inline__PrivateKey</td>
<td>The private key as a string.</td>
<td>Raw PEM private key</td>
<td>None</td>
Expand All @@ -112,6 +112,12 @@ This is a list of all of the environment variables that can be used to configure
<td>Doe</td>
<td>None</td>
</tr>
<tr>
<td>DevOidcToolkit__Users__INDEX__Roles__INDEX</td>
<td>The roles of the user</td>
<td>user</td>
<td>None</td>
</tr>
<tr>
<td>DevOidcToolkit__Clients__INDEX__Id</td>
<td>The ID of the client.</td>
Expand Down
294 changes: 294 additions & 0 deletions DevOidcToolkit.UnitTests/Controllers/ConnectControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,114 @@ public async Task Authorize_WhenConsentNotRequired_ReturnsSignInResult()
// Assert
Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
}

[Fact]
public async Task Authorize_WhenUserHasRoles_IncludesRoleClaimsInSignInResult()
{
// Arrange
var testApp = new object();
var oidcAppManager = new Mock<IOpenIddictApplicationManager>();
var userManager = MockUserManager.CreateMockUserManager<DevOidcToolkitUser>();
var signInManager = MockSignInManager.CreateMockSignInManager<DevOidcToolkitUser>();

oidcAppManager.Setup(x => x.FindByClientIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(testApp);
oidcAppManager.Setup(x => x.GetConsentTypeAsync(testApp, It.IsAny<CancellationToken>()))
.ReturnsAsync(ConsentTypes.Implicit);

var testUser = new DevOidcToolkitUser
{
Id = "user123",
UserName = "testuser",
Email = "test@example.com",
FirstName = "Test",
LastName = "User"
};

userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>()))
.ReturnsAsync(testUser);
userManager.Setup(x => x.GetRolesAsync(testUser))
.ReturnsAsync(new List<string> { "admin", "editor" });

var claimsIdentity = new ClaimsIdentity();
var principal = new ClaimsPrincipal(claimsIdentity);
signInManager.Setup(x => x.CreateUserPrincipalAsync(testUser))
.ReturnsAsync(principal);

var controller = CreateController(
oidcAppManager.Object,
userManager.Object,
signInManager.Object,
isAuthenticated: true,
userId: testUser.Id,
userName: testUser.UserName);

var request = new OpenIddictRequest { Scope = "openid profile" };
var feature = new OpenIddictServerAspNetCoreFeature { Transaction = new() { Request = request } };
controller.HttpContext.Features.Set(feature);

// Act
var result = await controller.Authorize();

// Assert
var signInResult = Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
var roleClaims = signInResult.Principal.FindAll(Claims.Role).Select(c => c.Value).ToList();
Assert.Contains("admin", roleClaims);
Assert.Contains("editor", roleClaims);
}

[Fact]
public async Task Authorize_WhenUserHasNoRoles_NoRoleClaimsInSignInResult()
{
// Arrange
var testApp = new object();
var oidcAppManager = new Mock<IOpenIddictApplicationManager>();
var userManager = MockUserManager.CreateMockUserManager<DevOidcToolkitUser>();
var signInManager = MockSignInManager.CreateMockSignInManager<DevOidcToolkitUser>();

oidcAppManager.Setup(x => x.FindByClientIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(testApp);
oidcAppManager.Setup(x => x.GetConsentTypeAsync(testApp, It.IsAny<CancellationToken>()))
.ReturnsAsync(ConsentTypes.Implicit);

var testUser = new DevOidcToolkitUser
{
Id = "user123",
UserName = "testuser",
Email = "test@example.com",
FirstName = "Test",
LastName = "User"
};

userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>()))
.ReturnsAsync(testUser);
userManager.Setup(x => x.GetRolesAsync(testUser))
.ReturnsAsync(new List<string>());

var claimsIdentity = new ClaimsIdentity();
var principal = new ClaimsPrincipal(claimsIdentity);
signInManager.Setup(x => x.CreateUserPrincipalAsync(testUser))
.ReturnsAsync(principal);

var controller = CreateController(
oidcAppManager.Object,
userManager.Object,
signInManager.Object,
isAuthenticated: true,
userId: testUser.Id,
userName: testUser.UserName);

var request = new OpenIddictRequest { Scope = "openid profile" };
var feature = new OpenIddictServerAspNetCoreFeature { Transaction = new() { Request = request } };
controller.HttpContext.Features.Set(feature);

// Act
var result = await controller.Authorize();

// Assert
var signInResult = Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
Assert.Empty(signInResult.Principal.FindAll(Claims.Role));
}
}


Expand Down Expand Up @@ -885,6 +993,124 @@ public async Task AuthorizePost_WhenConsentNotRequired_ReturnsSignInResult()
// Assert
Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
}

[Fact]
public async Task AuthorizePost_WhenUserHasRoles_IncludesRoleClaimsInSignInResult()
{
// Arrange
var testApp = new object();
var oidcAppManager = new Mock<IOpenIddictApplicationManager>();
var userManager = MockUserManager.CreateMockUserManager<DevOidcToolkitUser>();
var signInManager = MockSignInManager.CreateMockSignInManager<DevOidcToolkitUser>();

oidcAppManager.Setup(x => x.FindByClientIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(testApp);

var testUser = new DevOidcToolkitUser
{
Id = "user123",
UserName = "testuser",
Email = "test@example.com",
FirstName = "Test",
LastName = "User"
};

userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>()))
.ReturnsAsync(testUser);
userManager.Setup(x => x.GetRolesAsync(testUser))
.ReturnsAsync(new List<string> { "admin", "editor" });

var claimsIdentity = new ClaimsIdentity();
var principal = new ClaimsPrincipal(claimsIdentity);
signInManager.Setup(x => x.CreateUserPrincipalAsync(testUser))
.ReturnsAsync(principal);

var controller = CreateController(
oidcAppManager.Object,
userManager.Object,
signInManager.Object,
isAuthenticated: true,
userId: testUser.Id,
userName: testUser.UserName);

controller.ControllerContext.HttpContext.Request.Method = "POST";
controller.ControllerContext.HttpContext.Request.ContentType = "application/x-www-form-urlencoded";
controller.ControllerContext.HttpContext.Request.Form = new FormCollection(new Dictionary<string, Microsoft.Extensions.Primitives.StringValues>
{
["consent"] = "yes"
});

var request = new OpenIddictRequest { Scope = "openid profile" };
var feature = new OpenIddictServerAspNetCoreFeature { Transaction = new() { Request = request } };
controller.HttpContext.Features.Set(feature);

// Act
var result = await controller.AuthorizePost();

// Assert
var signInResult = Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
var roleClaims = signInResult.Principal.FindAll(Claims.Role).Select(c => c.Value).ToList();
Assert.Contains("admin", roleClaims);
Assert.Contains("editor", roleClaims);
}

[Fact]
public async Task AuthorizePost_WhenUserHasNoRoles_NoRoleClaimsInSignInResult()
{
// Arrange
var testApp = new object();
var oidcAppManager = new Mock<IOpenIddictApplicationManager>();
var userManager = MockUserManager.CreateMockUserManager<DevOidcToolkitUser>();
var signInManager = MockSignInManager.CreateMockSignInManager<DevOidcToolkitUser>();

oidcAppManager.Setup(x => x.FindByClientIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(testApp);

var testUser = new DevOidcToolkitUser
{
Id = "user123",
UserName = "testuser",
Email = "test@example.com",
FirstName = "Test",
LastName = "User"
};

userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>()))
.ReturnsAsync(testUser);
userManager.Setup(x => x.GetRolesAsync(testUser))
.ReturnsAsync(new List<string>());

var claimsIdentity = new ClaimsIdentity();
var principal = new ClaimsPrincipal(claimsIdentity);
signInManager.Setup(x => x.CreateUserPrincipalAsync(testUser))
.ReturnsAsync(principal);

var controller = CreateController(
oidcAppManager.Object,
userManager.Object,
signInManager.Object,
isAuthenticated: true,
userId: testUser.Id,
userName: testUser.UserName);

controller.ControllerContext.HttpContext.Request.Method = "POST";
controller.ControllerContext.HttpContext.Request.ContentType = "application/x-www-form-urlencoded";
controller.ControllerContext.HttpContext.Request.Form = new FormCollection(new Dictionary<string, Microsoft.Extensions.Primitives.StringValues>
{
["consent"] = "yes"
});

var request = new OpenIddictRequest { Scope = "openid profile" };
var feature = new OpenIddictServerAspNetCoreFeature { Transaction = new() { Request = request } };
controller.HttpContext.Features.Set(feature);

// Act
var result = await controller.AuthorizePost();

// Assert
var signInResult = Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
Assert.Empty(signInResult.Principal.FindAll(Claims.Role));
}
}

public class ConnectControllerExchangeTests
Expand Down Expand Up @@ -1094,6 +1320,74 @@ public async Task Exchange_WithUnsupportedGrantType_ReturnsBadRequest()
Assert.Equal(Errors.UnsupportedGrantType, errorResponse.Error);
Assert.Equal("The specified grant type is not supported.", errorResponse.ErrorDescription);
}

[Fact]
public async Task Exchange_WithAuthorizationCodeGrant_RoleClaimsIncludedInIdentityToken()
{
// Arrange
var oidcAppManager = new Mock<IOpenIddictApplicationManager>();
var userManager = MockUserManager.CreateMockUserManager<DevOidcToolkitUser>();
var signInManager = MockSignInManager.CreateMockSignInManager<DevOidcToolkitUser>();

var controller = CreateController(
oidcAppManager.Object,
userManager.Object,
signInManager.Object);

var request = new OpenIddictRequest
{
GrantType = GrantTypes.AuthorizationCode,
Code = "test-code"
};

// Simulate the principal stored in the authorization code, which includes role claims
// set by ProcessAuthorizationRequest
var identity = new ClaimsIdentity(
authenticationType: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
nameType: Claims.Name,
roleType: Claims.Role);

identity.AddClaim(Claims.Subject, "user123");
identity.AddClaim(new Claim(Claims.Role, "admin"));
identity.AddClaim(new Claim(Claims.Role, "editor"));

var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

var authServiceMock = new Mock<IAuthenticationService>();
authServiceMock
.Setup(x => x.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
.ReturnsAsync(AuthenticateResult.Success(ticket));

var serviceProvider = new ServiceCollection()
.AddSingleton(authServiceMock.Object)
.BuildServiceProvider();

controller.ControllerContext.HttpContext.RequestServices = serviceProvider;

var feature = new OpenIddictServerAspNetCoreFeature
{
Transaction = new()
{
Request = request,
EndpointType = OpenIddictServerEndpointType.Token
}
};
controller.HttpContext.Features.Set(feature);

// Act
var result = await controller.Exchange();

// Assert
var signInResult = Assert.IsType<Microsoft.AspNetCore.Mvc.SignInResult>(result);
var roleClaims = signInResult.Principal.FindAll(Claims.Role).ToList();
Assert.NotEmpty(roleClaims);
foreach (var roleClaim in roleClaims)
{
Assert.Contains(Destinations.AccessToken, roleClaim.GetDestinations());
Assert.Contains(Destinations.IdentityToken, roleClaim.GetDestinations());
}
}
}


Expand Down
8 changes: 8 additions & 0 deletions DevOidcToolkit/Controllers/ConnectController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ private async Task<IActionResult> ProcessAuthorizationRequest(OpenIddictRequest
principal.SetClaim(Claims.GivenName, user.FirstName);
principal.SetClaim(Claims.FamilyName, user.LastName);

var roles = await _userManager.GetRolesAsync(user) ?? [];
if (roles.Count > 0)
{
principal.SetClaims(Claims.Role, [.. roles]);
}

principal.SetScopes(request.GetScopes());
principal.SetResources("resource_server");

Expand All @@ -156,6 +162,7 @@ private async Task<IActionResult> ProcessAuthorizationRequest(OpenIddictRequest
Claims.Email => [Destinations.AccessToken, Destinations.IdentityToken],
Claims.GivenName => [Destinations.AccessToken, Destinations.IdentityToken],
Claims.FamilyName => [Destinations.AccessToken, Destinations.IdentityToken],
Claims.Role => [Destinations.AccessToken, Destinations.IdentityToken],
_ => [Destinations.AccessToken],
});
}
Expand Down Expand Up @@ -234,6 +241,7 @@ public async Task<IActionResult> Exchange()
Claims.Email => [Destinations.AccessToken, Destinations.IdentityToken],
Claims.GivenName => [Destinations.AccessToken, Destinations.IdentityToken],
Claims.FamilyName => [Destinations.AccessToken, Destinations.IdentityToken],
Claims.Role => [Destinations.AccessToken, Destinations.IdentityToken],
_ => [Destinations.AccessToken]
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class UserConfiguration
[Required] public required string Email { get; set; }
[Required] public required string FirstName { get; set; }
[Required] public required string LastName { get; set; }
public List<string> Roles { get; set; } = [];
}

public class ClientConfiguration
Expand Down
Loading