Skip to content

Commit

Permalink
Add new JWT profile that rename 'groups' claim with 'roles' (#637)
Browse files Browse the repository at this point in the history
This PR adds a 'kc' profile which aim is allowing the integration with a Keycloak environment by having "roles" claim instead of "groups".
  • Loading branch information
enricovianello authored Sep 14, 2023
1 parent 952692a commit 283d791
Show file tree
Hide file tree
Showing 18 changed files with 901 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import static it.infn.mw.iam.core.oauth.profile.ScopeAwareProfileResolver.AARC_PROFILE_ID;
import static it.infn.mw.iam.core.oauth.profile.ScopeAwareProfileResolver.IAM_PROFILE_ID;
import static it.infn.mw.iam.core.oauth.profile.ScopeAwareProfileResolver.KC_PROFILE_ID;
import static it.infn.mw.iam.core.oauth.profile.ScopeAwareProfileResolver.WLCG_PROFILE_ID;

import java.time.Clock;
Expand Down Expand Up @@ -69,6 +70,12 @@
import it.infn.mw.iam.core.oauth.profile.iam.IamJWTProfileIdTokenCustomizer;
import it.infn.mw.iam.core.oauth.profile.iam.IamJWTProfileTokenIntrospectionHelper;
import it.infn.mw.iam.core.oauth.profile.iam.IamJWTProfileUserinfoHelper;
import it.infn.mw.iam.core.oauth.profile.keycloak.KeycloakGroupHelper;
import it.infn.mw.iam.core.oauth.profile.keycloak.KeycloakIdTokenCustomizer;
import it.infn.mw.iam.core.oauth.profile.keycloak.KeycloakIntrospectionHelper;
import it.infn.mw.iam.core.oauth.profile.keycloak.KeycloakJWTProfile;
import it.infn.mw.iam.core.oauth.profile.keycloak.KeycloakProfileAccessTokenBuilder;
import it.infn.mw.iam.core.oauth.profile.keycloak.KeycloakUserinfoHelper;
import it.infn.mw.iam.core.oauth.profile.wlcg.WLCGGroupHelper;
import it.infn.mw.iam.core.oauth.profile.wlcg.WLCGJWTProfile;
import it.infn.mw.iam.core.oauth.scope.matchers.DefaultScopeMatcherRegistry;
Expand Down Expand Up @@ -152,6 +159,27 @@ JWTProfile aarcJwtProfile(IamProperties props, IamAccountRepository accountRepo,
return new AarcJWTProfile(atBuilder, idHelper, uiHelper, intrHelper);
}

@Bean(name = "kcJwtProfile")
JWTProfile kcJwtProfile(IamProperties props, IamAccountRepository accountRepo,
ScopeClaimTranslationService converter, UserInfoService userInfoService, ScopeMatcherRegistry registry, ClaimValueHelper claimHelper) {

KeycloakGroupHelper groupHelper = new KeycloakGroupHelper();

KeycloakProfileAccessTokenBuilder atBuilder =
new KeycloakProfileAccessTokenBuilder(props, groupHelper);

KeycloakUserinfoHelper uiHelper =
new KeycloakUserinfoHelper(props, userInfoService);

KeycloakIdTokenCustomizer idHelper =
new KeycloakIdTokenCustomizer(accountRepo, converter, claimHelper, groupHelper, props);

BaseIntrospectionHelper intrHelper = new KeycloakIntrospectionHelper(props,
new DefaultIntrospectionResultAssembler(), registry, groupHelper);

return new KeycloakJWTProfile(atBuilder, idHelper, uiHelper, intrHelper);
}

@Bean(name = "iamJwtProfile")
JWTProfile iamJwtProfile(IamProperties props, IamAccountRepository accountRepo,
ScopeClaimTranslationService converter, ClaimValueHelper claimHelper,
Expand Down Expand Up @@ -188,7 +216,9 @@ attributeMapHelper, new DefaultIntrospectionResultAssembler(), registry,
@Bean
JWTProfileResolver jwtProfileResolver(@Qualifier("iamJwtProfile") JWTProfile iamProfile,
@Qualifier("wlcgJwtProfile") JWTProfile wlcgProfile,
@Qualifier("aarcJwtProfile") JWTProfile aarcProfile, IamProperties properties,
@Qualifier("aarcJwtProfile") JWTProfile aarcProfile,
@Qualifier("kcJwtProfile") JWTProfile kcProfile,
IamProperties properties,
ClientDetailsService clientDetailsService) {

JWTProfile defaultProfile = iamProfile;
Expand All @@ -203,10 +233,16 @@ JWTProfileResolver jwtProfileResolver(@Qualifier("iamJwtProfile") JWTProfile iam
defaultProfile = aarcProfile;
}

if (it.infn.mw.iam.config.IamProperties.JWTProfile.Profile.KC
.equals(properties.getJwtProfile().getDefaultProfile())) {
defaultProfile = kcProfile;
}

Map<String, JWTProfile> profileMap = Maps.newHashMap();
profileMap.put(IAM_PROFILE_ID, iamProfile);
profileMap.put(WLCG_PROFILE_ID, wlcgProfile);
profileMap.put(AARC_PROFILE_ID, aarcProfile);
profileMap.put(KC_PROFILE_ID, kcProfile);

LOG.info("Default JWT profile: {}", defaultProfile.name());
return new ScopeAwareProfileResolver(defaultProfile, profileMap, clientDetailsService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,8 @@ public static class JWTProfile {
public enum Profile {
IAM,
WLCG,
AARC
AARC,
KC
}

Profile defaultProfile = Profile.IAM;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ public class ScopeAwareProfileResolver implements JWTProfileResolver {
public static final String AARC_PROFILE_ID = "aarc";
public static final String IAM_PROFILE_ID = "iam";
public static final String WLCG_PROFILE_ID = "wlcg";
public static final String KC_PROFILE_ID = "kc";

private static final Set<String> SUPPORTED_PROFILES =
newHashSet(AARC_PROFILE_ID, IAM_PROFILE_ID, WLCG_PROFILE_ID);
newHashSet(AARC_PROFILE_ID, IAM_PROFILE_ID, WLCG_PROFILE_ID, KC_PROFILE_ID);

private final Map<String, JWTProfile> profileMap;
private final JWTProfile defaultProfile;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.infn.mw.iam.core.oauth.profile.keycloak;

import java.util.Set;
import java.util.stream.Collectors;

import it.infn.mw.iam.persistence.model.IamUserInfo;

public class KeycloakGroupHelper {

public static final String KEYCLOAK_ROLES_CLAIM = "roles";

public Set<String> resolveGroupNames(IamUserInfo userInfo) {

return userInfo.getGroups().stream().map(g -> g.getName()).collect(Collectors.toSet());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.infn.mw.iam.core.oauth.profile.keycloak;

import java.util.Set;

import org.mitre.oauth2.model.ClientDetailsEntity;
import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
import org.mitre.openid.connect.service.ScopeClaimTranslationService;
import org.springframework.security.oauth2.provider.OAuth2Request;

import com.nimbusds.jwt.JWTClaimsSet.Builder;

import it.infn.mw.iam.config.IamProperties;
import it.infn.mw.iam.core.oauth.profile.iam.ClaimValueHelper;
import it.infn.mw.iam.core.oauth.profile.iam.IamJWTProfileIdTokenCustomizer;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.model.IamUserInfo;
import it.infn.mw.iam.persistence.repository.IamAccountRepository;

@SuppressWarnings("deprecation")
public class KeycloakIdTokenCustomizer extends IamJWTProfileIdTokenCustomizer {

private final KeycloakGroupHelper groupHelper;

public KeycloakIdTokenCustomizer(IamAccountRepository accountRepo,
ScopeClaimTranslationService scopeClaimConverter, ClaimValueHelper claimValueHelper,
KeycloakGroupHelper groupHelper, IamProperties properties) {
super(accountRepo, scopeClaimConverter, claimValueHelper, properties);
this.groupHelper = groupHelper;
}

@Override
public void customizeIdTokenClaims(Builder idClaims, ClientDetailsEntity client,
OAuth2Request request, String sub, OAuth2AccessTokenEntity accessToken, IamAccount account) {

super.customizeIdTokenClaims(idClaims, client, request, sub, accessToken, account);

IamUserInfo info = account.getUserInfo();
Set<String> groupNames = groupHelper.resolveGroupNames(info);

if (!groupNames.isEmpty()) {
idClaims.claim(KeycloakGroupHelper.KEYCLOAK_ROLES_CLAIM, groupNames);
}

// Drop group claims as set by IAM JWT profile
idClaims.claim("groups", null);

includeLabelsInIdToken(idClaims, account);

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.infn.mw.iam.core.oauth.profile.keycloak;

import java.util.Map;
import java.util.Set;

import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
import org.mitre.oauth2.service.IntrospectionResultAssembler;
import org.mitre.openid.connect.model.UserInfo;

import it.infn.mw.iam.config.IamProperties;
import it.infn.mw.iam.core.oauth.profile.common.BaseIntrospectionHelper;
import it.infn.mw.iam.core.oauth.scope.matchers.ScopeMatcherRegistry;
import it.infn.mw.iam.persistence.repository.UserInfoAdapter;


public class KeycloakIntrospectionHelper extends BaseIntrospectionHelper {

private final KeycloakGroupHelper groupHelper;

public KeycloakIntrospectionHelper(IamProperties props, IntrospectionResultAssembler assembler,
ScopeMatcherRegistry registry, KeycloakGroupHelper helper) {
super(props, assembler, registry);
this.groupHelper = helper;
}

@Override
public Map<String, Object> assembleIntrospectionResult(OAuth2AccessTokenEntity accessToken,
UserInfo userInfo, Set<String> authScopes) {

Map<String, Object> result = getAssembler().assembleFrom(accessToken, userInfo, authScopes);

addIssuerClaim(result);
addAudience(result, accessToken);
addScopeClaim(result, filterScopes(accessToken, authScopes));

Set<String> groups =
groupHelper.resolveGroupNames(((UserInfoAdapter) userInfo).getUserinfo());

if (!groups.isEmpty()) {
result.put(KeycloakGroupHelper.KEYCLOAK_ROLES_CLAIM, groups);
}

return result;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.infn.mw.iam.core.oauth.profile.keycloak;

import it.infn.mw.iam.core.oauth.profile.IDTokenCustomizer;
import it.infn.mw.iam.core.oauth.profile.IntrospectionResultHelper;
import it.infn.mw.iam.core.oauth.profile.JWTAccessTokenBuilder;
import it.infn.mw.iam.core.oauth.profile.UserInfoHelper;
import it.infn.mw.iam.core.oauth.profile.iam.IamJWTProfile;

public class KeycloakJWTProfile extends IamJWTProfile {

public static final String PROFILE_VERSION = "1.0";
public static final String PROFILE_NAME = "Keycloak JWT profile " + PROFILE_VERSION;

public KeycloakJWTProfile(JWTAccessTokenBuilder accessTokenBuilder,
IDTokenCustomizer idTokenBuilder, UserInfoHelper userInfoHelper,
IntrospectionResultHelper introspectionHelper) {

super(accessTokenBuilder, idTokenBuilder, userInfoHelper, introspectionHelper);
}

@Override
public String name() {
return PROFILE_NAME;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.infn.mw.iam.core.oauth.profile.keycloak;

import static java.util.Objects.isNull;
import static java.util.stream.Collectors.joining;

import java.time.Instant;
import java.util.Date;
import java.util.Set;

import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
import org.mitre.openid.connect.model.UserInfo;
import org.springframework.security.oauth2.provider.OAuth2Authentication;

import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTClaimsSet.Builder;

import it.infn.mw.iam.config.IamProperties;
import it.infn.mw.iam.core.oauth.profile.common.BaseAccessTokenBuilder;
import it.infn.mw.iam.persistence.repository.UserInfoAdapter;

@SuppressWarnings("deprecation")
public class KeycloakProfileAccessTokenBuilder extends BaseAccessTokenBuilder {

public static final String PROFILE_VERSION = "1.0";

final KeycloakGroupHelper groupHelper;

public KeycloakProfileAccessTokenBuilder(IamProperties properties, KeycloakGroupHelper groupHelper) {
super(properties);
this.groupHelper = groupHelper;
}


@Override
public JWTClaimsSet buildAccessToken(OAuth2AccessTokenEntity token,
OAuth2Authentication authentication, UserInfo userInfo, Instant issueTime) {

Builder builder = baseJWTSetup(token, authentication, userInfo, issueTime);

builder.notBeforeTime(Date.from(issueTime));

if (!token.getScope().isEmpty()) {
builder.claim(SCOPE_CLAIM_NAME, token.getScope().stream().collect(joining(SPACE)));
}

if (!isNull(userInfo)) {
Set<String> groupNames =
groupHelper.resolveGroupNames(((UserInfoAdapter) userInfo).getUserinfo());

if (!groupNames.isEmpty()) {
builder.claim(KeycloakGroupHelper.KEYCLOAK_ROLES_CLAIM, groupNames);
}
}

return builder.build();
}

}
Loading

0 comments on commit 283d791

Please sign in to comment.