From e3c7d26aa47658ca29a8ad2063e6b631270ab823 Mon Sep 17 00:00:00 2001 From: lucio lelii Date: Fri, 6 May 2022 15:25:35 +0200 Subject: [PATCH] - moved authorization utils library here - removed old providers --- CHANGELOG.md | 5 + pom.xml | 93 +++++--- .../library/provider/AccessTokenProvider.java | 31 --- .../provider/AuthorizationProvider.java | 35 --- .../provider/SecurityTokenProvider.java | 33 --- .../utils/clientid/ClientIDManager.java | 48 ++++ .../utils/clientid/RenewalProvider.java | 11 + .../utils/manager/SecretHolder.java | 89 +++++++ .../utils/manager/SecretManager.java | 116 +++++++++ .../utils/manager/SecretManagerProvider.java | 39 ++++ .../utils/provider/SecretProvider.java | 12 + .../utils/secret/GCubeSecret.java | 103 ++++++++ .../authorization/utils/secret/JWTSecret.java | 220 ++++++++++++++++++ .../authorization/utils/secret/Secret.java | 106 +++++++++ .../utils/secret/SecretUtility.java | 20 ++ .../authorization/utils/user/GCubeUser.java | 179 ++++++++++++++ .../utils/user/KeycloakUser.java | 100 ++++++++ .../common/authorization/utils/user/User.java | 40 ++++ 18 files changed, 1143 insertions(+), 137 deletions(-) delete mode 100644 src/main/java/org/gcube/common/authorization/library/provider/AccessTokenProvider.java delete mode 100644 src/main/java/org/gcube/common/authorization/library/provider/AuthorizationProvider.java delete mode 100644 src/main/java/org/gcube/common/authorization/library/provider/SecurityTokenProvider.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/clientid/ClientIDManager.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/clientid/RenewalProvider.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/manager/SecretHolder.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/manager/SecretManager.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/manager/SecretManagerProvider.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/provider/SecretProvider.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/secret/GCubeSecret.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/secret/JWTSecret.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/secret/Secret.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/secret/SecretUtility.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/user/GCubeUser.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/user/KeycloakUser.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/user/User.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a8d785..72a0590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v3.0.0-SNAPSHOT] - [2022-05-06] + +- merge AuthorizationUtils +- removed AccessTokenProvider, AuthorizationProvider and SecurityTokenProvider + ## [v2.5.0] - [2022-04-20] - Deprecated AccessTokenProvider, AuthorizationProvider and SecurityTokenProvider [#22871] diff --git a/pom.xml b/pom.xml index c5e8b6a..4470b2f 100644 --- a/pom.xml +++ b/pom.xml @@ -1,41 +1,58 @@ - - 4.0.0 - org.gcube.common - common-authorization - 2.5.0 - authorization service common library - - maven-parent - org.gcube.tools - 1.1.0 - - - - scm:git:https://code-repo.d4science.org/gCubeSystem/common-authorization.git - scm:git:https://code-repo.d4science.org/gCubeSystem/common-authorization.git - https://code-repo.d4science.org/gCubeSystem/common-authorization - + + 4.0.0 + org.gcube.common + common-authorization + 3.0.0-SNAPSHOT + authorization service common library - - - org.gcube.core - common-scope - [1.0.0-SNAPSHOT, 2.0.0-SNAPSHOT) - - - junit - junit - 4.11 - test - - - - org.slf4j - slf4j-api - 1.7.5 - - + + maven-parent + org.gcube.tools + 1.1.0 + - + + scm:git:https://code-repo.d4science.org/gCubeSystem/common-authorization.git + scm:git:https://code-repo.d4science.org/gCubeSystem/common-authorization.git + https://code-repo.d4science.org/gCubeSystem/common-authorization + + + + + org.gcube.common + gcube-jackson-core + 2.8.11 + + + org.gcube.common + gcube-jackson-databind + 2.8.11 + + + org.gcube.common + keycloak-client + [1.0.0,2.0.0-SNAPSHOT) + + + org.gcube.core + common-scope + [1.0.0-SNAPSHOT, 2.0.0-SNAPSHOT) + + + junit + junit + 4.11 + test + + + + org.slf4j + slf4j-api + 1.7.5 + + + + diff --git a/src/main/java/org/gcube/common/authorization/library/provider/AccessTokenProvider.java b/src/main/java/org/gcube/common/authorization/library/provider/AccessTokenProvider.java deleted file mode 100644 index e118d33..0000000 --- a/src/main/java/org/gcube/common/authorization/library/provider/AccessTokenProvider.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.gcube.common.authorization.library.provider; - -@Deprecated -public class AccessTokenProvider { - - public static AccessTokenProvider instance = new AccessTokenProvider(); - - private static final InheritableThreadLocal threadToken = new InheritableThreadLocal() { - - @Override - protected String initialValue() { - return null; - } - - }; - - private AccessTokenProvider() { - } - - public String get() { - return threadToken.get(); - } - - public void set(String jwt) { - threadToken.set(jwt); - } - - public void reset() { - threadToken.remove(); - } -} diff --git a/src/main/java/org/gcube/common/authorization/library/provider/AuthorizationProvider.java b/src/main/java/org/gcube/common/authorization/library/provider/AuthorizationProvider.java deleted file mode 100644 index eed2fe2..0000000 --- a/src/main/java/org/gcube/common/authorization/library/provider/AuthorizationProvider.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.gcube.common.authorization.library.provider; - -import org.gcube.common.authorization.library.utils.Caller; - -@Deprecated -public class AuthorizationProvider { - - public static AuthorizationProvider instance = new AuthorizationProvider(); - - // Thread local variable containing each thread's ID - private static final InheritableThreadLocal threadAuth = - new InheritableThreadLocal() { - - @Override protected Caller initialValue() { - return null; - } - - }; - - private AuthorizationProvider(){} - - public Caller get(){ - Caller info = threadAuth.get(); - return info; - } - - public void set(Caller info){ - threadAuth.set(info); - } - - public void reset(){ - threadAuth.remove(); - } - -} diff --git a/src/main/java/org/gcube/common/authorization/library/provider/SecurityTokenProvider.java b/src/main/java/org/gcube/common/authorization/library/provider/SecurityTokenProvider.java deleted file mode 100644 index c80d323..0000000 --- a/src/main/java/org/gcube/common/authorization/library/provider/SecurityTokenProvider.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.gcube.common.authorization.library.provider; - -@Deprecated -public class SecurityTokenProvider { - - public static SecurityTokenProvider instance = new SecurityTokenProvider(); - - //private static Logger logger = LoggerFactory.getLogger(SecurityTokenProvider.class); - - // Thread local variable containing each thread's ID - private static final InheritableThreadLocal threadToken = - new InheritableThreadLocal() { - - @Override protected String initialValue() { - return null; - } - - }; - - private SecurityTokenProvider(){} - - public String get(){ - return threadToken.get(); - } - - public void set(String authorizationToken){ - threadToken.set(authorizationToken); - } - - public void reset(){ - threadToken.remove(); - } -} diff --git a/src/main/java/org/gcube/common/authorization/utils/clientid/ClientIDManager.java b/src/main/java/org/gcube/common/authorization/utils/clientid/ClientIDManager.java new file mode 100644 index 0000000..81b92f5 --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/clientid/ClientIDManager.java @@ -0,0 +1,48 @@ +package org.gcube.common.authorization.utils.clientid; + +import org.gcube.common.authorization.utils.secret.JWTSecret; +import org.gcube.common.authorization.utils.secret.Secret; +import org.gcube.common.keycloak.KeycloakClientFactory; +import org.gcube.common.keycloak.model.TokenResponse; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class ClientIDManager implements RenewalProvider { + + protected final String clientID; + protected final String clientSecret; + + public ClientIDManager(String clientID, String clientSecret) { + this.clientID = clientID; + this.clientSecret = clientSecret; + } + + public Secret getSecret() throws Exception { + TokenResponse tokenResponse = KeycloakClientFactory.newInstance().queryUMAToken(clientID, clientSecret, null); + + JWTSecret jwtSecret = new JWTSecret(tokenResponse.getAccessToken()); + jwtSecret.setRenewalProvider(this); + + jwtSecret.setTokenResponse(tokenResponse); + + return jwtSecret; + } + + public Secret getSecret(String context) throws Exception { + TokenResponse tokenResponse = KeycloakClientFactory.newInstance().queryUMAToken(clientID, clientSecret, context, null); + + JWTSecret jwtSecret = new JWTSecret(tokenResponse.getAccessToken()); + jwtSecret.setRenewalProvider(this); + + jwtSecret.setTokenResponse(tokenResponse); + + return jwtSecret; + } + + @Override + public Secret renew() throws Exception { + return getSecret(); + } + +} diff --git a/src/main/java/org/gcube/common/authorization/utils/clientid/RenewalProvider.java b/src/main/java/org/gcube/common/authorization/utils/clientid/RenewalProvider.java new file mode 100644 index 0000000..003b29f --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/clientid/RenewalProvider.java @@ -0,0 +1,11 @@ +package org.gcube.common.authorization.utils.clientid; + +import org.gcube.common.authorization.utils.secret.Secret; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public interface RenewalProvider { + + public Secret renew() throws Exception; +} diff --git a/src/main/java/org/gcube/common/authorization/utils/manager/SecretHolder.java b/src/main/java/org/gcube/common/authorization/utils/manager/SecretHolder.java new file mode 100644 index 0000000..a41a821 --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/manager/SecretHolder.java @@ -0,0 +1,89 @@ +package org.gcube.common.authorization.utils.manager; + +import java.util.Collection; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.gcube.common.authorization.utils.secret.Secret; +import org.gcube.common.authorization.utils.user.User; +import org.gcube.common.scope.api.ScopeProvider; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class SecretHolder { + + private SortedSet secrets; + + public SecretHolder() { + this.secrets = new TreeSet(); + } + + public SecretHolder(Secret secret) { + this.secrets = new TreeSet(); + addSecret(secret); + } + + public SecretHolder(Collection secrets) { + this.secrets = new TreeSet(secrets); + } + + public void addSecret(Secret secret) { + if(secret!=null) { + secrets.add(secret); + } + } + + public void addSecrets(Collection secrets) { + for(Secret secret : secrets){ + addSecret(secret); + } + } + + public void set() throws Exception { + for(Secret secret : secrets) { + secret.set(); + } + } + + public SortedSet getSecrets() { + return secrets; + } + + public User getUser() { + for(Secret secret : secrets) { + try { + return secret.getUser(); + }catch (Exception e) { + // trying the next one + } + } + return null; + } + + public String getContext() { + for(Secret secret : secrets) { + try { + return secret.getContext(); + }catch (Exception e) { + // trying the next one + } + } + return ScopeProvider.instance.get(); + } + + public void reset() { + boolean first = true; + for(Secret secret : secrets) { + try { + secret.reset(); + }catch (Exception e) { + // trying the next one + } + } + if(first) { + ScopeProvider.instance.reset(); + } + } + +} diff --git a/src/main/java/org/gcube/common/authorization/utils/manager/SecretManager.java b/src/main/java/org/gcube/common/authorization/utils/manager/SecretManager.java new file mode 100644 index 0000000..7ea3dd9 --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/manager/SecretManager.java @@ -0,0 +1,116 @@ +package org.gcube.common.authorization.utils.manager; + +import java.util.Collection; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.gcube.common.authorization.utils.provider.SecretProvider; +import org.gcube.common.authorization.utils.secret.Secret; +import org.gcube.common.authorization.utils.secret.SecretUtility; +import org.gcube.common.authorization.utils.user.User; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class SecretManager { + + private SecretHolder initialSecretHolder; + private SecretHolder currentSecretHolder; + + public SecretManager() { + initialSecretHolder = new SecretHolder(); + currentSecretHolder = initialSecretHolder; + } + + public synchronized void addSecretViaProvider(SecretProvider secretProvider) { + if (currentSecretHolder != initialSecretHolder) { + throw new RuntimeException("You can't add a Secret in a session. You must terminate the session first."); + } + Secret secret = secretProvider.getSecret(); + currentSecretHolder.addSecret(secret); + } + + public synchronized void addSecret(Secret secret) { + if (currentSecretHolder != initialSecretHolder) { + throw new RuntimeException("You can't add a Secret in a session. You must terminate the session first."); + } + currentSecretHolder.addSecret(secret); + } + + public synchronized void startSession(Secret secret) throws Exception { + if (currentSecretHolder != initialSecretHolder) { + throw new RuntimeException("You are already in a session. You must terminate the session first."); + } + initialSecretHolder.reset(); + currentSecretHolder = new SecretHolder(secret); + currentSecretHolder.set(); + } + + public synchronized void startSession(Collection secrets) throws Exception { + if (currentSecretHolder != initialSecretHolder) { + throw new RuntimeException("You are already in a session. You must terminate the session first."); + } + initialSecretHolder.reset(); + currentSecretHolder = new SecretHolder(secrets); + currentSecretHolder.set(); + } + + public synchronized void startSession(SecretHolder secretHolder) throws Exception { + if (currentSecretHolder != initialSecretHolder) { + throw new RuntimeException("You are already in a session. You must terminate the session first."); + } + initialSecretHolder.reset(); + currentSecretHolder = secretHolder; + currentSecretHolder.set(); + } + + public synchronized void endSession() { + if (currentSecretHolder != initialSecretHolder) { + currentSecretHolder.reset(); + try { + initialSecretHolder.set(); + }catch (Exception e) { + throw new RuntimeException(e); + } + currentSecretHolder = initialSecretHolder; + } + } + + public synchronized void set() throws Exception { + if (currentSecretHolder != initialSecretHolder) { + throw new Exception("You are in a session. You must terminate the session first."); + } + currentSecretHolder.set(); + } + + public synchronized void reset() { + currentSecretHolder.reset(); + if (initialSecretHolder != currentSecretHolder) { + initialSecretHolder.reset(); + } + } + + public synchronized String getContext() { + return currentSecretHolder.getContext(); + } + + public synchronized User getUser() { + return currentSecretHolder.getUser(); + } + + /** + * @return a copy of the current secret holder + * to avoid modification to the original + */ + public synchronized SecretHolder getCurrentSecretHolder() { + SecretHolder secretHolder = new SecretHolder(); + SortedSet secrets = new TreeSet<>(); + SortedSet originalSecrets = currentSecretHolder.getSecrets(); + for(Secret s : originalSecrets) { + Secret secret = SecretUtility.getSecretByTokenString(s.getToken()); + secrets.add(secret); + } + secretHolder.addSecrets(secrets); + return secretHolder; + } +} diff --git a/src/main/java/org/gcube/common/authorization/utils/manager/SecretManagerProvider.java b/src/main/java/org/gcube/common/authorization/utils/manager/SecretManagerProvider.java new file mode 100644 index 0000000..42cc309 --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/manager/SecretManagerProvider.java @@ -0,0 +1,39 @@ +package org.gcube.common.authorization.utils.manager; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class SecretManagerProvider { + + public static SecretManagerProvider instance = new SecretManagerProvider(); + + // Thread local variable containing each thread's ID + private static final InheritableThreadLocal thread = new InheritableThreadLocal() { + + @Override + protected SecretManager initialValue() { + return null; + } + + }; + + private SecretManagerProvider(){} + + public SecretManager get(){ + SecretManager secretManager = thread.get(); + return secretManager; + } + + public void set(SecretManager secretManager){ + thread.set(secretManager); + } + + public void reset(){ + SecretManager secretManager = thread.get(); + if(secretManager!=null) { + secretManager.reset(); + } + thread.remove(); + } + +} diff --git a/src/main/java/org/gcube/common/authorization/utils/provider/SecretProvider.java b/src/main/java/org/gcube/common/authorization/utils/provider/SecretProvider.java new file mode 100644 index 0000000..c0cc8f0 --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/provider/SecretProvider.java @@ -0,0 +1,12 @@ +package org.gcube.common.authorization.utils.provider; + +import org.gcube.common.authorization.utils.secret.Secret; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public interface SecretProvider { + + public Secret getSecret(); + +} diff --git a/src/main/java/org/gcube/common/authorization/utils/secret/GCubeSecret.java b/src/main/java/org/gcube/common/authorization/utils/secret/GCubeSecret.java new file mode 100644 index 0000000..464d9b9 --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/secret/GCubeSecret.java @@ -0,0 +1,103 @@ +package org.gcube.common.authorization.utils.secret; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Pattern; + +import org.gcube.common.authorization.client.Constants; +import org.gcube.common.authorization.library.AuthorizationEntry; +import org.gcube.common.authorization.library.ClientType; +import org.gcube.common.authorization.library.exception.AuthorizationException; +import org.gcube.common.authorization.library.provider.ClientInfo; +import org.gcube.common.authorization.library.utils.Caller; +import org.gcube.common.authorization.utils.user.GCubeUser; +import org.gcube.common.authorization.utils.user.User; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class GCubeSecret extends Secret { + + public static final String GCUBE_TOKEN_REGEX = "^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}-[a-fA-F0-9]{8,9}){1}$"; + + protected AuthorizationEntry authorizationEntry; + + @Override + protected void check(String token) throws AuthorizationException { + super.check(token); + if(!Pattern.matches(GCubeSecret.GCUBE_TOKEN_REGEX, token)) { + throw new AuthorizationException("The GUCBE token must comply with the regex " + GCUBE_TOKEN_REGEX); + } + } + + public GCubeSecret(String token) { + super(20, token); + } + + protected AuthorizationEntry getAuthorizationEntry() { + if(authorizationEntry==null) { + try { + authorizationEntry = Constants.authorizationService().get(token); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return authorizationEntry; + } + + @Override + public ClientInfo getClientInfo() throws Exception { + return getAuthorizationEntry().getClientInfo(); + } + + @Override + public Caller getCaller() throws Exception { + ClientInfo clientInfo = getClientInfo(); + String qualifier = authorizationEntry.getQualifier(); + Caller caller = new Caller(clientInfo, qualifier); + return caller; + } + + @Override + public String getContext() { + return getAuthorizationEntry().getContext(); + } + + @Override + public Map getHTTPAuthorizationHeaders() { + Map authorizationHeaders = new HashMap<>(); + authorizationHeaders.put(org.gcube.common.authorization.client.Constants.TOKEN_HEADER_ENTRY, token); + return authorizationHeaders; + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public boolean isRefreshable() { + return false; + } + + public User getUser() { + if(user==null) { + try { + ClientInfo clientInfo = getClientInfo(); + ClientType clientType = clientInfo.getType(); + + GCubeUser gCubeUser = new GCubeUser(); + gCubeUser.setRoles(new HashSet<>(clientInfo.getRoles())); + gCubeUser.setUsername(clientInfo.getId()); + gCubeUser.setApplication(clientType!=ClientType.USER); + user = gCubeUser; + } + catch (Exception e) { + throw new RuntimeException(); + } + } + return user; + } + +} diff --git a/src/main/java/org/gcube/common/authorization/utils/secret/JWTSecret.java b/src/main/java/org/gcube/common/authorization/utils/secret/JWTSecret.java new file mode 100644 index 0000000..b77dd58 --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/secret/JWTSecret.java @@ -0,0 +1,220 @@ +package org.gcube.common.authorization.utils.secret; + +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; +import org.gcube.common.authorization.library.provider.ClientInfo; +import org.gcube.common.authorization.library.provider.ExternalServiceInfo; +import org.gcube.common.authorization.library.provider.UserInfo; +import org.gcube.common.authorization.library.utils.Caller; +import org.gcube.common.authorization.utils.clientid.RenewalProvider; +import org.gcube.common.authorization.utils.user.KeycloakUser; +import org.gcube.common.authorization.utils.user.User; +import org.gcube.common.keycloak.KeycloakClientFactory; +import org.gcube.common.keycloak.model.AccessToken; +import org.gcube.common.keycloak.model.AccessToken.Access; +import org.gcube.common.keycloak.model.ModelUtils; +import org.gcube.common.keycloak.model.RefreshToken; +import org.gcube.common.keycloak.model.TokenResponse; +import org.gcube.common.keycloak.model.util.Time; +import org.gcube.common.scope.impl.ScopeBean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class JWTSecret extends Secret { + + private static final Logger logger = LoggerFactory.getLogger(JWTSecret.class); + + /** + * The interval of time expressed in milliseconds used as guard to refresh the token before that it expires . + * TimeUnit has been used to in place of just + * using the number to have a clearer code + */ + public static final long TOLERANCE = TimeUnit.MILLISECONDS.toMillis(200); + + protected AccessToken accessToken; + protected TokenResponse tokenResponse; + protected RenewalProvider renewalProvider; + protected Set roles; + protected ClientInfo clientInfo; + protected Caller caller; + protected String context; + + public JWTSecret(String token) { + super(10, token); + } + + private String getTokenString() { + try { + boolean expired = false; + getAccessToken(); + + if(Time.currentTimeMillis()>=(accessToken.getExp()-TOLERANCE)) { + expired = true; + if(tokenResponse!=null) { + try { + KeycloakClientFactory.newInstance().refreshToken(getUsername(), tokenResponse); + expired = false; + }catch (Exception e) { + logger.warn("Unable to refresh the token with RefreshToken. Going to try to renew it if possible.", e); + } + } + } + + if(expired && renewalProvider!=null) { + try { + JWTSecret renewed = (JWTSecret) renewalProvider.renew(); + this.token = renewed.token; + this.accessToken = getAccessToken(); + }catch (Exception e) { + logger.warn("Unable to renew the token with the RenewalProvider. I'll continue using the old token.", e); + } + } + }catch (Exception e) { + logger.error("Unexpected error in the procedure to evaluate/refresh the current token. I'll continue using the old token.", e); + } + return token; + } + + + protected AccessToken getAccessToken() { + if(accessToken==null) { + String realUmaTokenEncoded = token.split("\\.")[1]; + String realUmaToken = new String(Base64.getDecoder().decode(realUmaTokenEncoded.getBytes())); + ObjectMapper mapper = new ObjectMapper(); + try { + accessToken = mapper.readValue(realUmaToken, AccessToken.class); + }catch(Exception e){ + logger.error("Error parsing JWT token",e); + throw new RuntimeException("Error parsing JWT token", e); + } + } + return accessToken; + } + + protected Set getRoles() throws Exception{ + if(roles == null) { + Map accesses = getAccessToken().getResourceAccess(); + String context = getContext(); + Access access = accesses.get(URLEncoder.encode(context, StandardCharsets.UTF_8.toString())); + if(access != null) { + roles = access.getRoles(); + }else { + roles = new HashSet<>(); + } + } + return roles; + } + + @Override + public ClientInfo getClientInfo() throws Exception { + if(clientInfo==null) { + User user = getUser(); + if(user.isApplication()) { + clientInfo = new ExternalServiceInfo(user.getUsername(), "unknown"); + }else { + clientInfo = new UserInfo(user.getUsername(), new ArrayList<>(user.getRoles()), user.getEmail(), user.getGivenName(), user.getFamilyName()); + } + } + return clientInfo; + } + + @Override + public Caller getCaller() throws Exception { + if(caller==null) { + caller = new Caller(getClientInfo(), "token"); + } + return caller; + } + + @Override + public String getContext() { + if(context==null) { + String[] audience = getAccessToken().getAudience(); + for (String aud : audience) { + if (aud != null && aud.compareTo("") != 0) { + try { + String contextToBeValidated = URLDecoder.decode(aud, StandardCharsets.UTF_8.toString()); + ScopeBean scopeBean = new ScopeBean(contextToBeValidated); + context = scopeBean.toString(); + return context; + } catch (Exception e) { + logger.error("Invalid context name for audience {} in access token. Trying next one if any.", aud, e); + } + } + } + throw new RuntimeException("Invalid context in access token"); + } + return context; + } + + @Override + public String getUsername() throws Exception { + return getAccessToken().getPreferredUsername(); + } + + @Override + public Map getHTTPAuthorizationHeaders() { + Map authorizationHeaders = new HashMap<>(); + authorizationHeaders.put("Authorization", "Bearer " + getTokenString()); + return authorizationHeaders; + } + + public void setRenewalProvider(RenewalProvider renewalProvider) { + this.renewalProvider = renewalProvider; + } + + public void setTokenResponse(TokenResponse tokenResponse) { + this.tokenResponse = tokenResponse; + } + + protected boolean isExpired(AccessToken accessToken) { + return Time.currentTimeMillis()>accessToken.getExp(); + } + + @Override + public boolean isExpired() { + return isExpired(getAccessToken()); + } + + @Override + public boolean isRefreshable() { + if(tokenResponse!=null) { + try { + RefreshToken refreshToken = ModelUtils.getRefreshTokenFrom(tokenResponse); + return isExpired(refreshToken); + } catch (Exception e) { + return false; + } + } + return false; + } + + @Override + public User getUser() { + if(user==null) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + String accessTokenString = objectMapper.writeValueAsString(getAccessToken()); + user = objectMapper.readValue(accessTokenString, KeycloakUser.class); + user.setRoles(getRoles()); + } catch (Exception e) { + throw new RuntimeException(); + } + } + return user; + } + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/common/authorization/utils/secret/Secret.java b/src/main/java/org/gcube/common/authorization/utils/secret/Secret.java new file mode 100644 index 0000000..c424a8d --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/secret/Secret.java @@ -0,0 +1,106 @@ +package org.gcube.common.authorization.utils.secret; + +import java.util.Map; +import java.util.Objects; + +import org.gcube.common.authorization.library.ClientType; +import org.gcube.common.authorization.library.exception.AuthorizationException; +import org.gcube.common.authorization.library.provider.ClientInfo; +import org.gcube.common.authorization.library.utils.Caller; +import org.gcube.common.authorization.utils.user.User; +import org.gcube.common.scope.api.ScopeProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public abstract class Secret implements Comparable { + + protected static final Logger logger = LoggerFactory.getLogger(Secret.class); + + protected int priority; + protected String token; + protected User user; + + protected void check(String token) throws AuthorizationException { + if(token == null) { + throw new AuthorizationException("token cannot be null"); + } + if(token.compareTo("")==0) { + throw new AuthorizationException("token cannot be an empty string"); + } + } + + protected Secret(int priority, String token) { + this.priority = priority; + check(token); + this.token = token; + } + + public String getToken() { + return token; + } + + public void set() throws Exception { + ScopeProvider.instance.set(getContext()); + } + + public abstract ClientInfo getClientInfo() throws Exception; + + public abstract Caller getCaller() throws Exception; + + public abstract String getContext(); + + public String getUsername() throws Exception { + return getClientInfo().getId(); + } + + public boolean isApplication() throws Exception { + return getClientInfo().getType() == ClientType.EXTERNALSERVICE; + } + + public abstract Map getHTTPAuthorizationHeaders(); + + @Override + public int hashCode() { + return Objects.hash(priority, token); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Secret other = (Secret) obj; + return priority == other.priority && Objects.equals(token, other.token); + } + + @Override + public int compareTo(Secret obj) { + if (this == obj) { + return 0; + } + if (obj == null) { + return priority; + } + if (getClass() != obj.getClass()) { + return priority; + } + return token.compareTo(obj.token); + } + + public void reset() { + ScopeProvider.instance.reset(); + } + + public abstract boolean isExpired(); + + public abstract boolean isRefreshable(); + + public abstract User getUser(); + +} diff --git a/src/main/java/org/gcube/common/authorization/utils/secret/SecretUtility.java b/src/main/java/org/gcube/common/authorization/utils/secret/SecretUtility.java new file mode 100644 index 0000000..3f9672d --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/secret/SecretUtility.java @@ -0,0 +1,20 @@ +package org.gcube.common.authorization.utils.secret; + +import java.util.regex.Pattern; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class SecretUtility { + + public static final String UUID_REGEX = "^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}-[a-fA-F0-9]{8,9}){1}$"; + + public static Secret getSecretByTokenString(String token) { + if(Pattern.matches(UUID_REGEX, token)) { + return new GCubeSecret(token); + }else { + return new JWTSecret(token); + } + } + +} diff --git a/src/main/java/org/gcube/common/authorization/utils/user/GCubeUser.java b/src/main/java/org/gcube/common/authorization/utils/user/GCubeUser.java new file mode 100644 index 0000000..1339fe8 --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/user/GCubeUser.java @@ -0,0 +1,179 @@ +package org.gcube.common.authorization.utils.user; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.gcube.com.fasterxml.jackson.annotation.JsonAnyGetter; +import org.gcube.com.fasterxml.jackson.annotation.JsonAnySetter; +import org.gcube.com.fasterxml.jackson.annotation.JsonIgnore; +import org.gcube.com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class GCubeUser implements User { + + /** + * Used to allow to have any additional properties + */ + @JsonIgnore + protected Map additionalProperties; + + @JsonProperty("id") + protected String username; + @JsonProperty("roles") + protected Set roles; + + @JsonProperty("given_name") + protected String givenName; + @JsonProperty("family_name") + protected String familyName; + @JsonProperty("email") + protected String eMail; + @JsonProperty("job_title") + protected String jobTitle; + + @JsonProperty("picture") + protected String picture; + @JsonProperty("middle_name") + protected String middleName; + + @JsonIgnore + protected boolean application; + + public GCubeUser() { + this.additionalProperties = new HashMap<>(); + + // This info are not always present. Setting an empty string to avoid null + this.givenName = ""; + this.familyName = ""; + this.eMail = ""; + this.jobTitle = ""; + this.picture = ""; + this.middleName = ""; + this.application = false; + } + + + @Override + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public boolean isApplication() { + return application; + } + + public void setApplication(boolean application) { + this.application = application; + } + + @Override + public Collection getRoles() { + return roles; + } + + @Override + public void setRoles(Collection roles) { + this.roles = new HashSet<>(roles); + } + @Override + public String getGivenName() { + return givenName; + } + + @Override + public String getFamilyName() { + return familyName; + } + + @Override + public String getEmail() { + return eMail; + } + + @Override + public String getAbout() { + return jobTitle; + } + + public String getPicture() { + return picture; + } + + public String getMiddleName() { + return middleName; + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + public Object getAdditionalProperty(String key) { + return additionalProperties.get(key); + } + + @JsonAnySetter + public void setAdditionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + } + + @Override + public String getFullName() { + return getFullName(false); + } + + @Override + public String getFullName(boolean nameSurname) { + if(isApplication()) { + return getUsername(); + } + + StringBuffer stringBuffer = new StringBuffer(); + boolean found = false; + String surname = getFamilyName(); + String name = getGivenName(); + + if(nameSurname) { + if(name!=null && name.trim().length()>0) { + stringBuffer.append(name.trim()); + found = true; + } + if(surname!=null && surname.trim().length()>0) { + if(found) { + stringBuffer.append(" "); + } + stringBuffer.append(surname.trim()); + found = true; + } + }else { + if(surname!=null && surname.trim().length()>0) { + stringBuffer.append(surname.trim()); + found = true; + } + if(name!=null && name.trim().length()>0) { + if(found) { + stringBuffer.append(" "); + } + stringBuffer.append(name.trim()); + found = true; + } + } + + return stringBuffer.toString(); + } + +} diff --git a/src/main/java/org/gcube/common/authorization/utils/user/KeycloakUser.java b/src/main/java/org/gcube/common/authorization/utils/user/KeycloakUser.java new file mode 100644 index 0000000..e604cc5 --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/user/KeycloakUser.java @@ -0,0 +1,100 @@ +package org.gcube.common.authorization.utils.user; + +import java.util.Collection; + +import org.gcube.com.fasterxml.jackson.annotation.JsonIgnore; +import org.gcube.common.keycloak.model.AccessToken; + +/** + * @author Luca Frosini (ISTI-CNR) + */ +public class KeycloakUser extends AccessToken implements User { + + /** + * Generated Serial Version UID + */ + private static final long serialVersionUID = -7083648026885406300L; + + public static final String CLIENT_ID_PROPERTY = "clientId"; + + protected Collection roles; + protected Boolean application; + + @Override + @JsonIgnore + public String getUsername() { + return getPreferredUsername(); + } + + @Override + public boolean isApplication() { + if(application==null) { + application = getOtherClaims().get(CLIENT_ID_PROPERTY)!=null; + } + return application; + } + + @Override + @JsonIgnore + public Collection getRoles() { + return roles; + } + + @Override + @JsonIgnore + public void setRoles(Collection roles) { + this.roles = roles; + } + + @Override + public String getAbout() { + return ""; + } + + @Override + public String getFullName() { + return getFullName(false); + } + + @Override + public String getFullName(boolean nameSurname) { + if(isApplication()) { + String clientID = (String) getOtherClaims().getOrDefault("clientId", getUsername()); + return clientID; + } + + StringBuffer stringBuffer = new StringBuffer(); + boolean found = false; + String surname = getFamilyName(); + String name = getGivenName(); + + if(nameSurname) { + if(name!=null && name.trim().length()>0) { + stringBuffer.append(name.trim()); + found = true; + } + if(surname!=null && surname.trim().length()>0) { + if(found) { + stringBuffer.append(" "); + } + stringBuffer.append(surname.trim()); + found = true; + } + }else { + if(surname!=null && surname.trim().length()>0) { + stringBuffer.append(surname.trim()); + found = true; + } + if(name!=null && name.trim().length()>0) { + if(found) { + stringBuffer.append(" "); + } + stringBuffer.append(name.trim()); + found = true; + } + } + + return stringBuffer.toString(); + } + +} diff --git a/src/main/java/org/gcube/common/authorization/utils/user/User.java b/src/main/java/org/gcube/common/authorization/utils/user/User.java new file mode 100644 index 0000000..80b738f --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/user/User.java @@ -0,0 +1,40 @@ +package org.gcube.common.authorization.utils.user; + +import java.util.Collection; + +/** + * @author Luca Frosini (ISTI-CNR) + */ +public interface User { + + public String getUsername(); + + public boolean isApplication(); + + public Collection getRoles(); + + public void setRoles(Collection roles); + + public String getGivenName(); + + public String getFamilyName(); + + /** + * @return the full name in the form 'Surname Name' for a person + * or the application identifier for an application; + */ + public String getFullName(); + + /** + * @param nameSurname when true the fullname will be formatted as 'Name Surname', + * when false the fullname will be formatted as 'Surname Name', + * @return the full name according to nameSurname boolean for a person + * or the application identifier for an application; + */ + public String getFullName(boolean nameSurname); + + public String getEmail(); + + public String getAbout(); + +}