Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new JWT profile that rename 'groups' claim with 'roles' #637

Merged
merged 17 commits into from
Sep 14, 2023
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
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
Loading