Skip to content

Commit

Permalink
feat(auth): oidc support for groups field or username field which be …
Browse files Browse the repository at this point in the history
…inside other fields (#1320)
  • Loading branch information
dongockien92 authored and tchiotludo committed Apr 4, 2023
1 parent d7dae9f commit 6d05f5b
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 13 deletions.
39 changes: 26 additions & 13 deletions src/main/java/org/akhq/modules/OidcUserDetailsMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ private AuthenticationResponse createDirectClaimAuthenticationResponse(String oi
* @return The username to set in the {@link io.micronaut.security.authentication.Authentication}
*/
protected String getUsername(Oidc.Provider provider, OpenIdClaims openIdClaims) {
return Objects.toString(openIdClaims.get(provider.getUsernameField()));
final Object username = getClaimValue(openIdClaims, provider.getUsernameField());
return Objects.toString(username);
}

/**
Expand All @@ -114,19 +115,31 @@ protected String getUsername(Oidc.Provider provider, OpenIdClaims openIdClaims)
*/
protected List<String> getOidcGroups(Oidc.Provider provider, OpenIdClaims openIdClaims) {
List<String> groups = new ArrayList<>();
if (openIdClaims.contains(provider.getGroupsField())) {
Object groupsField = openIdClaims.get(provider.getGroupsField());
// When the user belongs to only one group, groupsField can either be an array (with one item)
// or a string, depending on the IdP implementation.
if (groupsField instanceof Collection) {
groups = ((Collection<Object>) groupsField)
.stream()
.map(Objects::toString)
.collect(Collectors.toList());
} else if (groupsField instanceof String) {
groups.add((String) groupsField);
}
Object groupsField = getClaimValue(openIdClaims, provider.getGroupsField());
// When the user belongs to only one group, groupsField can either be an array (with one item)
// or a string, depending on the IdP implementation.
if (groupsField instanceof Collection) {
groups = ((Collection<Object>) groupsField)
.stream()
.map(Objects::toString)
.collect(Collectors.toList());
} else if (groupsField instanceof String) {
groups.add((String) groupsField);
}
return groups;
}

private Object getClaimValue(OpenIdClaims openIdClaims, String name) {
final String[] subFields = name.split("\\.");
Object claimValue = openIdClaims.get(subFields[0]);
for(int i = 1; i < subFields.length; i++) {
final String subField = subFields[i];
if (claimValue instanceof Map) {
claimValue = ((Map) claimValue).get(subField);
} else {
break;
}
}
return claimValue;
}
}
160 changes: 160 additions & 0 deletions src/test/java/org/akhq/modules/OidcUserDetailsMapperTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package org.akhq.modules;

import com.nimbusds.jwt.JWTClaimsSet;
import io.micronaut.security.authentication.AuthenticationMode;
import io.micronaut.security.config.AuthenticationModeConfiguration;
import io.micronaut.security.oauth2.configuration.OpenIdAdditionalClaimsConfiguration;
import io.micronaut.security.oauth2.endpoint.token.response.JWTOpenIdClaims;
import org.akhq.configs.Oidc;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.text.ParseException;
import java.util.List;

class OidcUserDetailsMapperTest {
private static final String NESTED_GROUPS_AND_USERNAME_PAYLOAD = "{\n" +
" \"exp\": 1672716500,\n" +
" \"iat\": 1672714700,\n" +
" \"auth_time\": 1672714691,\n" +
" \"jti\": \"771921ac-378c-4911-98c2-110482cbe62c\",\n" +
" \"iss\": \"http://localhost:8903/realms/ttcntt\",\n" +
" \"aud\": \"account\",\n" +
" \"sub\": \"36eb7ab9-6b1c-4350-b59b-dc0c16e288cb\",\n" +
" \"typ\": \"Bearer\",\n" +
" \"azp\": \"isohoa\",\n" +
" \"nonce\": \"d2033cqtyrs\",\n" +
" \"session_state\": \"246bbcf0-e301-4bb4-97e9-4ccc5b7e202d\",\n" +
" \"acr\": \"1\",\n" +
" \"realm_access\": {\n" +
" \"roles\": [\n" +
" \"default-roles-ttcntt\",\n" +
" \"offline_access\",\n" +
" \"uma_authorization\",\n" +
" \"clerical\"\n" +
" ]\n" +
" },\n" +
" \"resource_access\": {\n" +
" \"account\": {\n" +
" \"roles\": [\n" +
" \"manage-account\",\n" +
" \"manage-account-links\",\n" +
" \"view-profile\"\n" +
" ]\n" +
" }\n" +
" },\n" +
" \"scope\": \"email profile\",\n" +
" \"sid\": \"246bbcf0-e301-4bb4-97e9-4ccc5b7e202d\",\n" +
" \"email_verified\": true,\n" +
" \"name\": \"C 1\",\n" +
" \"username\": {\n" +
" \"preferred_username\": \"clerical1\"\n" +
" },\n" +
" \"given_name\": \"C\",\n" +
" \"family_name\": \"1\",\n" +
" \"email\": \"[email protected]\"\n" +
"}";

private static final String NON_NESTED_GROUPS_AND_USERNAME_PAYLOAD = "{\n" +
" \"exp\": 1672716500,\n" +
" \"iat\": 1672714700,\n" +
" \"auth_time\": 1672714691,\n" +
" \"jti\": \"771921ac-378c-4911-98c2-110482cbe62c\",\n" +
" \"iss\": \"http://localhost:8903/realms/ttcntt\",\n" +
" \"aud\": \"account\",\n" +
" \"sub\": \"36eb7ab9-6b1c-4350-b59b-dc0c16e288cb\",\n" +
" \"typ\": \"Bearer\",\n" +
" \"azp\": \"isohoa\",\n" +
" \"nonce\": \"d2033cqtyrs\",\n" +
" \"session_state\": \"246bbcf0-e301-4bb4-97e9-4ccc5b7e202d\",\n" +
" \"acr\": \"1\",\n" +
" \"roles\": [\n" +
" \"default-roles-ttcntt\",\n" +
" \"offline_access\",\n" +
" \"uma_authorization\",\n" +
" \"clerical\"\n" +
" ],\n" +
" \"resource_access\": {\n" +
" \"account\": {\n" +
" \"roles\": [\n" +
" \"manage-account\",\n" +
" \"manage-account-links\",\n" +
" \"view-profile\"\n" +
" ]\n" +
" }\n" +
" },\n" +
" \"scope\": \"email profile\",\n" +
" \"sid\": \"246bbcf0-e301-4bb4-97e9-4ccc5b7e202d\",\n" +
" \"email_verified\": true,\n" +
" \"name\": \"C 1\",\n" +
" \"preferred_username\": \"clerical1\",\n" +
" \"given_name\": \"C\",\n" +
" \"family_name\": \"1\",\n" +
" \"email\": \"[email protected]\"\n" +
"}";

@Test
void givenNestedGroupsAndUsername_whenGetGroupsAndUsername_thenSuccess() throws ParseException {
OidcUserDetailsMapper mapper = createMapper();
JWTClaimsSet jwtClaimsSet = JWTClaimsSet.parse(NESTED_GROUPS_AND_USERNAME_PAYLOAD);
JWTOpenIdClaims jwtOpenIdClaims = new JWTOpenIdClaims(jwtClaimsSet);

Oidc.Provider provider = new Oidc.Provider();
provider.setGroupsField("realm_access.roles");
provider.setUsernameField("username.preferred_username");

List<String> oidcGroups = mapper.getOidcGroups(provider, jwtOpenIdClaims);
Assertions.assertEquals(4, oidcGroups.size());
Assertions.assertEquals("default-roles-ttcntt", oidcGroups.get(0));
Assertions.assertEquals("offline_access", oidcGroups.get(1));
Assertions.assertEquals("uma_authorization", oidcGroups.get(2));
Assertions.assertEquals("clerical", oidcGroups.get(3));

String username = mapper.getUsername(provider, jwtOpenIdClaims);
Assertions.assertEquals("clerical1", username);
}

@Test
void givenNonNestedGroupsAndUsername_whenGetGroupsAndUsername_thenSuccess() throws ParseException {
OidcUserDetailsMapper mapper = createMapper();
JWTClaimsSet jwtClaimsSet = JWTClaimsSet.parse(NON_NESTED_GROUPS_AND_USERNAME_PAYLOAD);
JWTOpenIdClaims jwtOpenIdClaims = new JWTOpenIdClaims(jwtClaimsSet);

Oidc.Provider provider = new Oidc.Provider();

List<String> oidcGroups = mapper.getOidcGroups(provider, jwtOpenIdClaims);
Assertions.assertEquals(4, oidcGroups.size());
Assertions.assertEquals("default-roles-ttcntt", oidcGroups.get(0));
Assertions.assertEquals("offline_access", oidcGroups.get(1));
Assertions.assertEquals("uma_authorization", oidcGroups.get(2));
Assertions.assertEquals("clerical", oidcGroups.get(3));

String username = mapper.getUsername(provider, jwtOpenIdClaims);
Assertions.assertEquals("clerical1", username);
}


private OidcUserDetailsMapper createMapper() {
OpenIdAdditionalClaimsConfiguration openIdAdditionalClaimsConfiguration =
new OpenIdAdditionalClaimsConfiguration() {
@Override
public boolean isJwt() {
return true;
}

@Override
public boolean isAccessToken() {
return true;
}

@Override
public boolean isRefreshToken() {
return true;
}
};

AuthenticationModeConfiguration authenticationModeConfiguration = () -> AuthenticationMode.BEARER;

return new OidcUserDetailsMapper(openIdAdditionalClaimsConfiguration, authenticationModeConfiguration);
}
}

0 comments on commit 6d05f5b

Please sign in to comment.