From 452e78139fd13cf4389dc6be08c482a672af837a Mon Sep 17 00:00:00 2001 From: Luca Frosini Date: Mon, 6 Dec 2021 17:43:18 +0100 Subject: [PATCH] Added token refresh --- .../utils/clientid/ClienIDManager.java | 39 +++++ .../utils/clientid/ClienIDSecret.java | 90 ------------ .../clientid/ClientIDSecretProvider.java | 16 --- .../utils/clientid/RenewalProvider.java | 8 ++ .../utils/manager/SecretHolder.java | 55 +++++++ .../utils/manager/SecretManager.java | 23 ++- .../authorization/utils/secret/JWTSecret.java | 134 +++++++++++++++--- .../authorization/utils/secret/Secret.java | 11 ++ .../utils/secret/jwt/JWToken.java | 76 ---------- 9 files changed, 249 insertions(+), 203 deletions(-) create mode 100644 src/main/java/org/gcube/common/authorization/utils/clientid/ClienIDManager.java delete mode 100644 src/main/java/org/gcube/common/authorization/utils/clientid/ClienIDSecret.java delete mode 100644 src/main/java/org/gcube/common/authorization/utils/clientid/ClientIDSecretProvider.java create mode 100644 src/main/java/org/gcube/common/authorization/utils/clientid/RenewalProvider.java delete mode 100644 src/main/java/org/gcube/common/authorization/utils/secret/jwt/JWToken.java diff --git a/src/main/java/org/gcube/common/authorization/utils/clientid/ClienIDManager.java b/src/main/java/org/gcube/common/authorization/utils/clientid/ClienIDManager.java new file mode 100644 index 0000000..5e9f394 --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/clientid/ClienIDManager.java @@ -0,0 +1,39 @@ +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.ModelUtils; +import org.gcube.common.keycloak.model.RefreshToken; +import org.gcube.common.keycloak.model.TokenResponse; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class ClienIDManager implements RenewalProvider { + + protected final String clientID; + protected final String clientSecret; + + public ClienIDManager(String clientID, String clientSecret) { + this.clientID = clientID; + this.clientSecret = clientSecret; + } + + public Secret getSecret() throws Exception { + TokenResponse tr = KeycloakClientFactory.newInstance().queryUMAToken(clientID, clientSecret, null); + + JWTSecret jwtSecret = new JWTSecret(tr.getAccessToken()); + jwtSecret.setRenewalProvider(this); + + RefreshToken refreshToken = ModelUtils.getRefreshTokenFrom(tr); + jwtSecret.setRefreshToken(refreshToken); + + return jwtSecret; + } + + @Override + public Secret renew() throws Exception { + return getSecret(); + } +} diff --git a/src/main/java/org/gcube/common/authorization/utils/clientid/ClienIDSecret.java b/src/main/java/org/gcube/common/authorization/utils/clientid/ClienIDSecret.java deleted file mode 100644 index 524248e..0000000 --- a/src/main/java/org/gcube/common/authorization/utils/clientid/ClienIDSecret.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.gcube.common.authorization.utils.clientid; - -import java.util.Map; -import java.util.Objects; - -import org.gcube.common.authorization.library.provider.ClientInfo; -import org.gcube.common.authorization.library.utils.Caller; -import org.gcube.common.authorization.utils.secret.Secret; -import org.gcube.common.keycloak.KeycloakClientFactory; -import org.gcube.common.keycloak.model.TokenResponse; -import org.gcube.common.scope.api.ScopeProvider; - -/** - * @author Luca Frosini (ISTI - CNR) - */ -public class ClienIDSecret extends Secret { - - protected String clientID; - - public ClienIDSecret(String clientID, String token) { - super(30, token); - this.clientID = clientID; - } - - @Override - public String getContext() throws Exception { - return null; - } - - @Override - public String getUsername() throws Exception { - return clientID; - } - - @Override - public Map getHTTPAuthorizationHeaders() { - return null; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + Objects.hash(clientID); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!super.equals(obj)) - return false; - if (getClass() != obj.getClass()) - return false; - ClienIDSecret other = (ClienIDSecret) obj; - return Objects.equals(clientID, other.clientID); - } - - @Override - public int compareTo(Secret obj) { - int res = super.compareTo(obj); - return res == 0 ? clientID.compareTo(clientID) : res; - } - - @Override - public void setToken() throws Exception { - // TODO - TokenResponse tr = KeycloakClientFactory.newInstance().queryUMAToken(clientID, token, ScopeProvider.instance.get(), null); - logger.trace("{}", tr); - } - - @Override - public void resetToken() throws Exception { - // TODO Auto-generated method stub - } - - @Override - public ClientInfo getClientInfo() throws Exception { - // TODO Auto-generated method stub - return null; - } - - @Override - public Caller getCaller() throws Exception { - // TODO Auto-generated method stub - return null; - } - -} diff --git a/src/main/java/org/gcube/common/authorization/utils/clientid/ClientIDSecretProvider.java b/src/main/java/org/gcube/common/authorization/utils/clientid/ClientIDSecretProvider.java deleted file mode 100644 index b3332fd..0000000 --- a/src/main/java/org/gcube/common/authorization/utils/clientid/ClientIDSecretProvider.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.gcube.common.authorization.utils.clientid; - -import org.gcube.common.authorization.utils.provider.SecretProvider; -import org.gcube.common.authorization.utils.secret.Secret; - -/** - * @author Luca Frosini (ISTI - CNR) - */ -public class ClientIDSecretProvider implements SecretProvider { - - @Override - public Secret getSecret() { - return new ClienIDSecret("", ""); - } - -} 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..8805ef8 --- /dev/null +++ b/src/main/java/org/gcube/common/authorization/utils/clientid/RenewalProvider.java @@ -0,0 +1,8 @@ +package org.gcube.common.authorization.utils.clientid; + +import org.gcube.common.authorization.utils.secret.Secret; + +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 index 7aeb251..3c1b9bf 100644 --- a/src/main/java/org/gcube/common/authorization/utils/manager/SecretHolder.java +++ b/src/main/java/org/gcube/common/authorization/utils/manager/SecretHolder.java @@ -82,6 +82,17 @@ public class SecretHolder { } return ScopeProvider.instance.get(); } + + public Collection getRoles() { + for(Secret secret : secrets) { + try { + return secret.getRoles(); + }catch (Exception e) { + // trying the next one + } + } + return null; + } public void reset() { for(Secret secret : secrets) { @@ -92,5 +103,49 @@ public class SecretHolder { } } } + + public String getName() { + for(Secret secret : secrets) { + try { + secret.getName(); + }catch (Exception e) { + // trying the next one + } + } + return null; + } + + public String getSurname() { + for(Secret secret : secrets) { + try { + secret.getSurname(); + }catch (Exception e) { + // trying the next one + } + } + return null; + } + public String getEMail() { + for(Secret secret : secrets) { + try { + secret.getEMail(); + }catch (Exception e) { + // trying the next one + } + } + return null; + } + + + public String getTitle() { + for(Secret secret : secrets) { + try { + secret.getTitle(); + }catch (Exception e) { + // trying the next one + } + } + return ""; + } } 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 index 07cb22b..6d98ee8 100644 --- a/src/main/java/org/gcube/common/authorization/utils/manager/SecretManager.java +++ b/src/main/java/org/gcube/common/authorization/utils/manager/SecretManager.java @@ -1,6 +1,7 @@ package org.gcube.common.authorization.utils.manager; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.SortedSet; @@ -16,8 +17,6 @@ public class SecretManager { public static final InheritableThreadLocal instance = new InheritableThreadLocal() { - - @Override protected SecretManager initialValue() { return new SecretManager(); @@ -92,6 +91,10 @@ public class SecretManager { return currentSecretHolder.getContext(); } + public Collection getRoles() { + return currentSecretHolder.getRoles(); + } + public void reset() { initialSecretHolder.reset(); if(initialSecretHolder!=currentSecretHolder) { @@ -99,4 +102,20 @@ public class SecretManager { } instance.remove(); } + + public String getName() { + return currentSecretHolder.getName(); + } + + public String getSurname() { + return currentSecretHolder.getSurname(); + } + + public String getEMail() { + return currentSecretHolder.getEMail(); + } + + public String getTitle() { + return currentSecretHolder.getTitle(); + } } 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 index 9154718..9827f90 100644 --- a/src/main/java/org/gcube/common/authorization/utils/secret/JWTSecret.java +++ b/src/main/java/org/gcube/common/authorization/utils/secret/JWTSecret.java @@ -1,7 +1,12 @@ package org.gcube.common.authorization.utils.secret; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Base64; +import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; @@ -9,7 +14,10 @@ import org.gcube.common.authorization.library.provider.AccessTokenProvider; import org.gcube.common.authorization.library.provider.ClientInfo; import org.gcube.common.authorization.library.provider.UserInfo; import org.gcube.common.authorization.library.utils.Caller; -import org.gcube.common.authorization.utils.secret.jwt.JWToken; +import org.gcube.common.authorization.utils.clientid.RenewalProvider; +import org.gcube.common.keycloak.model.AccessToken; +import org.gcube.common.keycloak.model.RefreshToken; +import org.gcube.common.keycloak.model.util.Time; import org.gcube.common.scope.impl.ScopeBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,15 +29,46 @@ public class JWTSecret extends Secret { private static final Logger logger = LoggerFactory.getLogger(JWTSecret.class); - protected JWToken jwt; + /** + * + */ + public static long TOLERANCE = 200; + + protected AccessToken accessToken; + protected RefreshToken refreshToken; + protected RenewalProvider renewalProvider; public JWTSecret(String token) { super(10, token); } + private String getTokenString() { + try { + boolean expired = false; + getAccessToken(); + + if(Time.currentTimeMillis()>=(accessToken.getExp()-TOLERANCE)) { + expired = true; + if(refreshToken!=null) { + // TODO refresh + expired = false; + } + } + + if(expired && renewalProvider!=null) { + JWTSecret renewed = (JWTSecret) renewalProvider.renew(); + this.token = renewed.token; + this.accessToken = getAccessToken(); + } + }catch (Exception e) { + // TODO log + } + return token; + } + @Override public void setToken() throws Exception { - AccessTokenProvider.instance.set(token); + AccessTokenProvider.instance.set(getTokenString()); } @Override @@ -37,26 +76,26 @@ public class JWTSecret extends Secret { AccessTokenProvider.instance.reset(); } - protected JWToken getJWToken() throws Exception { - if(jwt==null) { + protected AccessToken getAccessToken() throws Exception { + if(accessToken==null) { String realUmaTokenEncoded = token.split("\\.")[1]; String realUmaToken = new String(Base64.getDecoder().decode(realUmaTokenEncoded.getBytes())); ObjectMapper mapper = new ObjectMapper(); try { - jwt = mapper.readValue(realUmaToken, JWToken.class); + accessToken = mapper.readValue(realUmaToken, AccessToken.class); }catch(Exception e){ logger.error("Error parsing JWT token",e); throw new Exception("Error parsing JWT token", e); } } - return jwt; + return accessToken; } - @Override public ClientInfo getClientInfo() throws Exception { - getJWToken(); - ClientInfo clientInfo = new UserInfo(jwt.getUsername(), jwt.getRoles(), jwt.getEmail(), jwt.getFirstName(), jwt.getLastName()); + getAccessToken(); + List roles = new ArrayList<>(accessToken.getRealmAccess().getRoles()); + ClientInfo clientInfo = new UserInfo(accessToken.getPreferredUsername(), roles, accessToken.getEmail(), accessToken.getGivenName(), accessToken.getFamilyName()); return clientInfo; } @@ -68,21 +107,78 @@ public class JWTSecret extends Secret { @Override public String getContext() throws Exception { - ScopeBean scopeBean = null; - try { - scopeBean = new ScopeBean(getJWToken().getContext()); - }catch(Exception e){ - logger.error("Invalid context in access token",e); - throw new Exception("Invalid context in access token"); + String context = null; + String[] audience = getAccessToken().getAudience(); + for (String aud : audience) { + if (aud != null && aud.compareTo("") != 0) { + try { + context = URLDecoder.decode(context, StandardCharsets.UTF_8.toString()); + ScopeBean scopeBean = new ScopeBean(context); + return scopeBean.toString(); + } catch (Exception e) { + logger.error("Invalid context name for audience {} in access token. Trying next one if any.", aud, e); + } + } } - return scopeBean.toString(); + throw new Exception("Invalid context in access token"); } + @Override + public String getUsername() throws Exception { + return accessToken.getPreferredUsername(); + } + @Override public Map getHTTPAuthorizationHeaders() { Map authorizationHeaders = new HashMap<>(); - authorizationHeaders.put("Authorization", "Bearer " + token); + authorizationHeaders.put("Authorization", "Bearer " + getTokenString()); return authorizationHeaders; } -} + public void setRenewalProvider(RenewalProvider renewalProvider) { + this.renewalProvider = renewalProvider; + } + + public void setRefreshToken(RefreshToken refreshToken) { + this.refreshToken = refreshToken; + } + + protected boolean isExpired(AccessToken accessToken) { + return Time.currentTimeMillis()>accessToken.getExp(); + } + + @Override + public boolean isExpired() { + return isExpired(accessToken); + } + + @Override + public boolean isRefreshable() { + return refreshToken!=null && isExpired(refreshToken); + } + + @Override + public Collection getRoles() throws Exception { + return getAccessToken().getRealmAccess().getRoles(); + } + + @Override + public String getName() throws Exception { + return getAccessToken().getGivenName(); + } + + @Override + public String getSurname() throws Exception { + return getAccessToken().getFamilyName(); + } + + @Override + public String getEMail() throws Exception { + return getAccessToken().getEmail(); + } + + @Override + public String getTitle() throws Exception { + return ""; + } +} \ 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 index 65b69e0..81dd5f0 100644 --- a/src/main/java/org/gcube/common/authorization/utils/secret/Secret.java +++ b/src/main/java/org/gcube/common/authorization/utils/secret/Secret.java @@ -1,5 +1,6 @@ package org.gcube.common.authorization.utils.secret; +import java.util.Collection; import java.util.Map; import java.util.Objects; @@ -112,8 +113,18 @@ public abstract class Secret implements Comparable { ScopeProvider.instance.reset(); } + public abstract boolean isExpired(); + public abstract boolean isRefreshable(); + + public abstract Collection getRoles() throws Exception; + public abstract String getName() throws Exception; + public abstract String getSurname() throws Exception; + + public abstract String getEMail() throws Exception; + + public abstract String getTitle() throws Exception; } diff --git a/src/main/java/org/gcube/common/authorization/utils/secret/jwt/JWToken.java b/src/main/java/org/gcube/common/authorization/utils/secret/jwt/JWToken.java deleted file mode 100644 index 59b19cb..0000000 --- a/src/main/java/org/gcube/common/authorization/utils/secret/jwt/JWToken.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.gcube.common.authorization.utils.secret.jwt; - -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.gcube.com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import org.gcube.com.fasterxml.jackson.annotation.JsonProperty; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class JWToken { - - @JsonProperty("aud") - private String context; - - @JsonProperty("resource_access") - private Map contextAccess = new HashMap<>(); - - @JsonProperty("preferred_username") - private String username; - - @JsonProperty("given_name") - private String firstName; - - @JsonProperty("family_name") - private String lastName; - - @JsonProperty("email") - private String email; - - public List getRoles(){ - return contextAccess.get(this.context).roles; - } - - public String getContext() { - try { - return URLDecoder.decode(context, StandardCharsets.UTF_8.toString()); - }catch (UnsupportedEncodingException e) { - return context; - } - } - - public String getUsername() { - return username; - } - - public String getFirstName() { - return firstName; - } - - public String getLastName() { - return lastName; - } - - public String getEmail() { - return email; - } - - @Override - public String toString() { - return "GcubeJwt [context=" + getContext() + ", roles=" + getRoles() + ", username=" + username - + ", firstName=" + firstName + ", lastName=" + lastName + ", email=" + email + "]"; - } - - public static class Roles { - - @JsonProperty("roles") - List roles = new ArrayList<>(); - - } - -}