From 8565f243ea48a09f58035edaf089e3eea2fd2e2f Mon Sep 17 00:00:00 2001 From: Mauro Mariuzzo Date: Mon, 28 Mar 2022 20:25:36 +0200 Subject: [PATCH] feat: extract elements from MVP sample Refs: #4, #5 --- starter-kit/.gitignore | 1 - starter-kit/pom.xml | 8 + .../spid/cie/oidc/config/GlobalOptions.java | 133 ++++ .../cie/oidc/config/RelyingPartyOptions.java | 277 ++++++++ .../cie/oidc/exception/ConfigException.java | 19 + .../cie/oidc/exception/EntityException.java | 39 ++ .../spid/cie/oidc/exception/JWTException.java | 71 ++ .../cie/oidc/exception/OIDCException.java | 23 + .../oidc/exception/PersistenceException.java | 5 + .../cie/oidc/exception/SchemaException.java | 26 + .../exception/TrustChainBuilderException.java | 23 + .../oidc/exception/TrustChainException.java | 95 +++ .../cie/oidc/handler/RelyingPartyHandler.java | 418 ++++++++++++ .../spid/cie/oidc/handler/test/RPRunner.java | 19 + .../it/spid/cie/oidc/helper/EntityHelper.java | 102 +++ .../it/spid/cie/oidc/helper/JWTHelper.java | 461 +++++++++++++ .../it/spid/cie/oidc/helper/PKCEHelper.java | 56 ++ .../it/spid/cie/oidc/model/BaseModel.java | 40 ++ .../spid/cie/oidc/model/CachedEntityInfo.java | 104 +++ .../cie/oidc/model/EntityConfiguration.java | 543 +++++++++++++++ .../model/FederationEntityConfiguration.java | 64 ++ .../spid/cie/oidc/model/OIDCAuthRequest.java | 111 ++++ .../it/spid/cie/oidc/model/OIDCConstants.java | 9 + .../it/spid/cie/oidc/model/TrustChain.java | 47 ++ .../cie/oidc/model/TrustChainBuilder.java | 624 ++++++++++++++++++ .../oidc/persistence/PersistenceAdapter.java | 37 ++ .../spid/cie/oidc/schemas/AcrValuesSpid.java | 50 ++ .../it/spid/cie/oidc/schemas/OIDCProfile.java | 51 ++ .../spid/cie/oidc/schemas/TokenResponse.java | 78 +++ .../java/it/spid/cie/oidc/util/ArrayUtil.java | 90 +++ .../it/spid/cie/oidc/util/GetterUtil.java | 26 + .../java/it/spid/cie/oidc/util/JSONUtil.java | 29 + .../java/it/spid/cie/oidc/util/ListUtil.java | 40 ++ .../it/spid/cie/oidc/util/StringUtil.java | 41 ++ .../java/it/spid/cie/oidc/util/Validator.java | 23 + 35 files changed, 3782 insertions(+), 1 deletion(-) delete mode 100644 starter-kit/.gitignore create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/config/GlobalOptions.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/config/RelyingPartyOptions.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/exception/ConfigException.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/exception/EntityException.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/exception/JWTException.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/exception/OIDCException.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/exception/PersistenceException.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/exception/SchemaException.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/exception/TrustChainBuilderException.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/exception/TrustChainException.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/handler/RelyingPartyHandler.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/handler/test/RPRunner.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/helper/EntityHelper.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/helper/JWTHelper.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/helper/PKCEHelper.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/model/BaseModel.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/model/CachedEntityInfo.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/model/EntityConfiguration.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/model/FederationEntityConfiguration.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/model/OIDCAuthRequest.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/model/OIDCConstants.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/model/TrustChain.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/model/TrustChainBuilder.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/persistence/PersistenceAdapter.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/schemas/AcrValuesSpid.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/schemas/OIDCProfile.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/schemas/TokenResponse.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/util/ArrayUtil.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/util/GetterUtil.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/util/JSONUtil.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/util/ListUtil.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/util/StringUtil.java create mode 100644 starter-kit/src/main/java/it/spid/cie/oidc/util/Validator.java diff --git a/starter-kit/.gitignore b/starter-kit/.gitignore deleted file mode 100644 index b83d222..0000000 --- a/starter-kit/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/starter-kit/pom.xml b/starter-kit/pom.xml index 30b5af9..74df088 100644 --- a/starter-kit/pom.xml +++ b/starter-kit/pom.xml @@ -27,4 +27,12 @@ + + + + org.slf4j + slf4j-api + 1.7.36 + + diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/config/GlobalOptions.java b/starter-kit/src/main/java/it/spid/cie/oidc/config/GlobalOptions.java new file mode 100644 index 0000000..ae3f414 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/config/GlobalOptions.java @@ -0,0 +1,133 @@ +package it.spid.cie.oidc.config; + +import com.nimbusds.jose.JWSAlgorithm; + +import java.util.Collections; +import java.util.Set; + +import it.spid.cie.oidc.exception.ConfigException; +import it.spid.cie.oidc.exception.OIDCException; +import it.spid.cie.oidc.util.ArrayUtil; +import it.spid.cie.oidc.util.Validator; + +public class GlobalOptions> { + + public static final String DEFAULT_SIGNING_ALG = "RS256"; + + public static final String OIDC_FEDERATION_WELLKNOWN_URL = + ".well-known/openid-federation"; + + public static final String[] SUPPORTED_ENCRYPTION_ENCODINGS = new String[] { + "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128GCM", "A192GCM", + "A256GCM"}; + + public static final String[] SUPPORTED_ENCRYPTION_ALGS = new String[] { + "RSA-OAEP", "RSA-OAEP-256", "ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", + "ECDH-ES+A256KW"}; + + public static final String[] SUPPORTED_SIGNING_ALGS = new String[] { + "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"}; + + public String getDefaultJWEAlgorithm() { + return jweDefaultAlgorithm; + } + + public String getDefaultJWEEncryption() { + return jweDefaultEncryption; + } + + public String getDefaultJWSAlgorithm() { + return jwsDefaultAlgorithm; + } + + public Set getAllowedEncryptionAlgs() { + return Collections.unmodifiableSet(allowedEncryptionAlgs); + } + + public Set getAllowedSigningAlgs() { + return Collections.unmodifiableSet(allowedSigningAlgs); + } + + @SuppressWarnings("unchecked") + public T setAllowedEncryptionAlgs(String... values) { + if (values.length > 0) { + allowedEncryptionAlgs = ArrayUtil.asSet(values); + } + + return (T)this; + } + + @SuppressWarnings("unchecked") + public T setAllowedSigningAlgs(String... values) { + if (values.length > 0) { + allowedSigningAlgs = ArrayUtil.asSet(values); + } + + return (T)this; + } + + @SuppressWarnings("unchecked") + public T setDefaultJWEAlgorithm(String algorithm) { + if (!Validator.isNullOrEmpty(algorithm)) { + jweDefaultAlgorithm = algorithm; + } + + return (T)this; + } + + @SuppressWarnings("unchecked") + public T setDefaultJWEEncryption(String encMethod) { + if (!Validator.isNullOrEmpty(encMethod)) { + jweDefaultEncryption = encMethod; + } + + return (T)this; + } + + @SuppressWarnings("unchecked") + public T setDefaultJWSAlgorithm(String algorithm) { + if (!Validator.isNullOrEmpty(algorithm)) { + jwsDefaultAlgorithm = algorithm; + } + + return (T)this; + } + + protected void validate() throws OIDCException { + for (String alg : allowedEncryptionAlgs) { + if (!ArrayUtil.contains(SUPPORTED_ENCRYPTION_ALGS, alg)) { + throw new ConfigException( + "allowedEncryptionAlg %s is not supported", alg); + } + } + + if (Validator.isNullOrEmpty(jweDefaultAlgorithm)) { + throw new ConfigException( + "Invalid jweDefaultAlgorithm %s", jweDefaultAlgorithm); + } + else if (!allowedEncryptionAlgs.contains(jweDefaultAlgorithm)) { + throw new ConfigException( + "Not allowed jweDefaultAlgorithm %s", jweDefaultAlgorithm); + } + + if (Validator.isNullOrEmpty(jwsDefaultAlgorithm)) { + throw new ConfigException( + "Invalid jwsDefaultAlgorithm %s", jwsDefaultAlgorithm); + } + else if (!allowedSigningAlgs.contains(jwsDefaultAlgorithm)) { + throw new ConfigException( + "Not allowed jwsDefaultAlgorithm %s", jwsDefaultAlgorithm); + } + + } + + private String jweDefaultAlgorithm = "RSA-OAEP"; + private String jweDefaultEncryption = "A256CBC-HS512"; + private String jwsDefaultAlgorithm = "RS256"; + private Set allowedSigningAlgs = ArrayUtil.asSet( + "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"); + private Set allowedEncryptionAlgs = ArrayUtil.asSet( + "RSA-OAEP", "RSA-OAEP-256", "ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", + "ECDH-ES+A256KW"); + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/config/RelyingPartyOptions.java b/starter-kit/src/main/java/it/spid/cie/oidc/config/RelyingPartyOptions.java new file mode 100644 index 0000000..24f7fe7 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/config/RelyingPartyOptions.java @@ -0,0 +1,277 @@ +package it.spid.cie.oidc.config; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import it.spid.cie.oidc.exception.ConfigException; +import it.spid.cie.oidc.exception.OIDCException; +import it.spid.cie.oidc.model.OIDCConstants; +import it.spid.cie.oidc.schemas.AcrValuesSpid; +import it.spid.cie.oidc.schemas.OIDCProfile; +import it.spid.cie.oidc.util.ArrayUtil; +import it.spid.cie.oidc.util.Validator; + +public class RelyingPartyOptions extends GlobalOptions { + + public static final String[] SUPPORTED_APPLICATION_TYPES = new String[] { "web" }; + + public static final String[] SUPPORTED_SCOPES = new String[] { + OIDCConstants.SCOPE_OPENID, "offline_access" }; + + public String getAcrValue(OIDCProfile profile) { + return acrMap.get(profile.getValue()); + } + + public String getDefaultTrustAnchor() { + return defaultTrustAnchor; + } + + public Set getTrustAnchors() { + return Collections.unmodifiableSet(trustAnchors); + } + + public Map getSPIDProviders() { + return Collections.unmodifiableMap(spidProviders); + } + + public Map getCIEProviders() { + return Collections.unmodifiableMap(cieProviders); + } + + public String getApplicationName() { + return applicationName; + } + + public String getApplicationType() { + return applicationType; + } + + public Set getContacts() { + return Collections.unmodifiableSet(contacts); + } + + public Set getScopes() { + return Collections.unmodifiableSet(scopes); + } + + public String getClientId() { + return clientId; + } + + public Set getRedirectUris() { + return Collections.unmodifiableSet(redirectUris); + } + + public String getJwk() { + return jwk; + } + + public String getTrustMarks() { + return trustMarks; + } + + public RelyingPartyOptions setProfileAcr(OIDCProfile profile, String acr) { + if (acr != null) { + if (OIDCProfile.SPID.equals(profile)) { + AcrValuesSpid value = AcrValuesSpid.parse(acr); + + if (value != null) { + this.acrMap.put(profile.getValue(), acr); + } + } + else if (OIDCProfile.CIE.equals(profile)) { + //TODO: validate + } + } + + return this; + } + + public RelyingPartyOptions setApplicationName(String applicationName) { + if (!Validator.isNullOrEmpty(applicationName)) { + this.applicationName = applicationName; + } + + return this; + } + + public RelyingPartyOptions setClientId(String clientId) { + if (!Validator.isNullOrEmpty(clientId)) { + this.clientId = clientId; + } + + return this; + } + + public RelyingPartyOptions setContacts(Collection contacts) { + if (contacts != null && !contacts.isEmpty()) { + this.contacts.clear(); + this.contacts.addAll(contacts); + } + + return this; + } + + public RelyingPartyOptions setDefaultTrustAnchor(String defaultTrustAnchor) { + if (!Validator.isNullOrEmpty(defaultTrustAnchor)) { + this.defaultTrustAnchor = defaultTrustAnchor; + } + + return this; + } + + public RelyingPartyOptions setJWK(String jwk) { + if (!Validator.isNullOrEmpty(jwk)) { + this.jwk = jwk; + } + + return this; + } + + public RelyingPartyOptions setRedirectUris(Collection redirectUris) { + if (redirectUris != null && !redirectUris.isEmpty()) { + this.redirectUris.clear(); + this.redirectUris.addAll(redirectUris); + } + + return this; + } + + public RelyingPartyOptions setScopes(Collection scopes) { + if (scopes != null && !scopes.isEmpty()) { + this.scopes.clear(); + this.scopes.addAll(scopes); + } + + return this; + } + + public RelyingPartyOptions setTrustAnchors(Collection trustAnchors) { + if (trustAnchors != null && !trustAnchors.isEmpty()) { + this.trustAnchors.clear(); + this.trustAnchors.addAll(trustAnchors); + } + + return this; + } + + public RelyingPartyOptions setTrustMarks(String trustMarks) { + if (!Validator.isNullOrEmpty(trustMarks)) { + this.trustMarks = trustMarks; + } + + return this; + } + + public RelyingPartyOptions setSPIDProviders(Map providers) { + if (providers != null && !providers.isEmpty()) { + this.spidProviders.clear(); + + for (Map.Entry entry : providers.entrySet()) { + if (Validator.isNullOrEmpty(entry.getValue())) { + this.spidProviders.put(entry.getKey(), defaultTrustAnchor); + } + else { + this.spidProviders.put(entry.getKey(), entry.getValue()); + } + } + } + + return this; + } + + public RelyingPartyOptions setCIEProviders(Map providers) { + if (providers != null && !providers.isEmpty()) { + this.cieProviders.clear(); + + for (Map.Entry entry : providers.entrySet()) { + if (Validator.isNullOrEmpty(entry.getValue())) { + this.cieProviders.put(entry.getKey(), defaultTrustAnchor); + } + else { + this.cieProviders.put(entry.getKey(), entry.getValue()); + } + } + } + + return this; + } + + public void validate() throws OIDCException { + super.validate(); + + if (Validator.isNullOrEmpty(defaultTrustAnchor)) { + throw new ConfigException("no-default-trust-anchor"); + } + + for (Map.Entry entry : spidProviders.entrySet()) { + if (Validator.isNullOrEmpty(entry.getKey()) || + !trustAnchors.contains(entry.getValue())) { + + throw new ConfigException( + "invalid-spid-provider %s:%s", entry.getKey(), entry.getValue()); + } + } + + for (Map.Entry entry : cieProviders.entrySet()) { + if (Validator.isNullOrEmpty(entry.getKey()) || + !trustAnchors.contains(entry.getValue())) { + + throw new ConfigException( + "invalid-cie-provider %s:%s", entry.getKey(), entry.getValue()); + } + } + + if (Validator.isNullOrEmpty(clientId)) { + throw new ConfigException("no-client-id"); + } + + if (scopes.isEmpty()) { + throw new ConfigException("no-scopes"); + } + else { + for (String scope : scopes) { + if (!ArrayUtil.contains(SUPPORTED_SCOPES, scope)) { + throw new ConfigException("unsupported-scope %s", scope); + } + } + } + + if (redirectUris.isEmpty()) { + throw new ConfigException("no-redirect-uris"); + } + + if (!acrMap.containsKey(OIDCProfile.SPID.getValue())) { + acrMap.put(OIDCProfile.SPID.getValue(), AcrValuesSpid.L2.getValue()); + } + if (!acrMap.containsKey(OIDCProfile.CIE.getValue())) { + // TODO: acrMap.put(OIDCProfile.SPID.getValue(), AcrValuesSpid.L2.getValue()); + } + + } + + private String defaultTrustAnchor; + private Set trustAnchors = new HashSet<>(); + private Map spidProviders = new HashMap<>(); + private Map cieProviders = new HashMap<>(); + + private String applicationName; + private String applicationType = "web"; + private Set contacts = new HashSet<>(); + private Set scopes = ArrayUtil.asSet(SUPPORTED_SCOPES); + private String clientId; + private Set redirectUris = new HashSet<>(); + private String jwk; + private String trustMarks; + + private String loginURL = "/oidc/rp/landing"; + private String loginRedirectURL = "/oidc/rp/echo_attributes"; + private String logoutRedirectURL = "/oidc/rp/landing"; + + private Map acrMap = new HashMap<>(); + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/exception/ConfigException.java b/starter-kit/src/main/java/it/spid/cie/oidc/exception/ConfigException.java new file mode 100644 index 0000000..0fc0ff2 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/exception/ConfigException.java @@ -0,0 +1,19 @@ +package it.spid.cie.oidc.exception; + +public class ConfigException extends OIDCException { + + public ConfigException(String format, Object... values) { + super(String.format(format, values)); + } + + public ConfigException(String message, Throwable cause) { + super(message, cause); + } + + public ConfigException(Throwable cause) { + super(cause); + } + + private static final long serialVersionUID = -3082538413902538010L; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/exception/EntityException.java b/starter-kit/src/main/java/it/spid/cie/oidc/exception/EntityException.java new file mode 100644 index 0000000..53d676c --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/exception/EntityException.java @@ -0,0 +1,39 @@ +package it.spid.cie.oidc.exception; + +public class EntityException extends OIDCException { + + public static class Generic extends EntityException { + + public Generic(String message) { + super(message); + } + + public Generic(Throwable cause) { + super(cause); + } + + } + + public static class MissingJwksClaim extends EntityException { + + public MissingJwksClaim(String message) { + super(message); + } + + public MissingJwksClaim(Throwable cause) { + super(cause); + } + + } + + private EntityException(String message) { + super(message); + } + + private EntityException(Throwable cause) { + super(cause); + } + + private static final long serialVersionUID = 9206740073587833396L; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/exception/JWTException.java b/starter-kit/src/main/java/it/spid/cie/oidc/exception/JWTException.java new file mode 100644 index 0000000..2c62083 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/exception/JWTException.java @@ -0,0 +1,71 @@ +package it.spid.cie.oidc.exception; + +@SuppressWarnings("serial") +public class JWTException extends OIDCException { + + public static class Decryption extends JWTException { + + public Decryption(Throwable cause) { + super(cause); + } + + } + + public static class Parse extends JWTException { + + public Parse(Throwable cause) { + super(cause); + } + + } + + public static class Generic extends JWTException { + + public Generic(String message) { + super(message); + } + + public Generic(Throwable cause) { + super(cause); + } + + } + + public static class UnknownKid extends JWTException { + + public UnknownKid(String kid, String jwks) { + super("kid " + kid + " not found in jwks " + jwks); + } + + } + + public static class UnsupportedAlgorithm extends JWTException { + + public UnsupportedAlgorithm(String alg) { + super(alg + " has beed disabled for security reason"); + } + + } + + public static class Verifier extends JWTException { + + public Verifier(String message) { + super(message); + } + + public Verifier(Throwable cause) { + super(cause); + } + + } + + + private JWTException(String message) { + super(message); + } + + private JWTException(Throwable cause) { + super(cause); + } + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/exception/OIDCException.java b/starter-kit/src/main/java/it/spid/cie/oidc/exception/OIDCException.java new file mode 100644 index 0000000..793e774 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/exception/OIDCException.java @@ -0,0 +1,23 @@ +package it.spid.cie.oidc.exception; + +public class OIDCException extends Exception { + + public OIDCException() { + super(); + } + + public OIDCException(String message) { + super(message); + } + + public OIDCException(String message, Throwable cause) { + super(message, cause); + } + + public OIDCException(Throwable cause) { + super(cause); + } + + private static final long serialVersionUID = -1839651152644089727L; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/exception/PersistenceException.java b/starter-kit/src/main/java/it/spid/cie/oidc/exception/PersistenceException.java new file mode 100644 index 0000000..2dfac31 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/exception/PersistenceException.java @@ -0,0 +1,5 @@ +package it.spid.cie.oidc.exception; + +public class PersistenceException extends OIDCException { + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/exception/SchemaException.java b/starter-kit/src/main/java/it/spid/cie/oidc/exception/SchemaException.java new file mode 100644 index 0000000..5d18cc7 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/exception/SchemaException.java @@ -0,0 +1,26 @@ +package it.spid.cie.oidc.exception; + +@SuppressWarnings("serial") +public class SchemaException extends OIDCException { + + public static class Validation extends SchemaException { + + public Validation(String message) { + super(message); + } + + public Validation(Throwable cause) { + super(cause); + } + + } + + private SchemaException(String message) { + super(message); + } + + private SchemaException(Throwable cause) { + super(cause); + } + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/exception/TrustChainBuilderException.java b/starter-kit/src/main/java/it/spid/cie/oidc/exception/TrustChainBuilderException.java new file mode 100644 index 0000000..28895eb --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/exception/TrustChainBuilderException.java @@ -0,0 +1,23 @@ +package it.spid.cie.oidc.exception; + +public class TrustChainBuilderException extends OIDCException { + + public TrustChainBuilderException() { + super(); + } + + public TrustChainBuilderException(String message) { + super(message); + } + + public TrustChainBuilderException(String message, Throwable cause) { + super(message, cause); + } + + public TrustChainBuilderException(Throwable cause) { + super(cause); + } + + private static final long serialVersionUID = -6071661647891519660L; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/exception/TrustChainException.java b/starter-kit/src/main/java/it/spid/cie/oidc/exception/TrustChainException.java new file mode 100644 index 0000000..5adcf52 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/exception/TrustChainException.java @@ -0,0 +1,95 @@ +package it.spid.cie.oidc.exception; + +import java.time.LocalDateTime; + +import it.spid.cie.oidc.model.TrustChain; + +public class TrustChainException extends OIDCException { + + private static final long serialVersionUID = 602471019127315717L; + + @SuppressWarnings("serial") + public static class MissingProvider extends TrustChainException { + + public static final String DEFAULT_MESSAGE = + "Missing provider url. Please try '?provider=https://provider-subject/'"; + + public MissingProvider() { + super(DEFAULT_MESSAGE); + } + + } + + @SuppressWarnings("serial") + public static class InvalidRequiredTrustMark extends TrustChainException { + + public InvalidRequiredTrustMark(String message) { + super(message); + } + + } + + @SuppressWarnings("serial") + public static class InvalidTrustAnchor extends TrustChainException { + + public static final String DEFAULT_MESSAGE = "Unallowed Trust Anchor"; + + public InvalidTrustAnchor() { + super(DEFAULT_MESSAGE); + } + + } + + @SuppressWarnings("serial") + public static class InvalidTrustChain extends TrustChainException { + + public InvalidTrustChain(String message) { + super(message); + } + + } + + @SuppressWarnings("serial") + public static class MissingMetadata extends TrustChainException { + + public MissingMetadata(String message) { + super(message); + } + + } + + @SuppressWarnings("serial") + public static class TrustAnchorNeeded extends TrustChainException { + + public TrustAnchorNeeded(String message) { + super(message); + } + + } + + @SuppressWarnings("serial") + public static class TrustChainDisabled extends TrustChainException { + + public static String getDefualtMessage(LocalDateTime modifiedDate) { + return String.format( + "TrustChain DISABLED at %s", String.valueOf(modifiedDate)); + } + + public TrustChainDisabled(TrustChain trustChain) { + super(getDefualtMessage(trustChain.getModifiedDate())); + } + + public TrustChainDisabled(String message) { + super(message); + } + } + + private TrustChainException(String message) { + super(message); + } + + private TrustChainException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/handler/RelyingPartyHandler.java b/starter-kit/src/main/java/it/spid/cie/oidc/handler/RelyingPartyHandler.java new file mode 100644 index 0000000..cf14a31 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/handler/RelyingPartyHandler.java @@ -0,0 +1,418 @@ +package it.spid.cie.oidc.handler; + +import com.nimbusds.jose.jwk.JWKSet; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import it.spid.cie.oidc.config.RelyingPartyOptions; +import it.spid.cie.oidc.exception.OIDCException; +import it.spid.cie.oidc.exception.TrustChainException; +import it.spid.cie.oidc.helper.EntityHelper; +import it.spid.cie.oidc.helper.JWTHelper; +import it.spid.cie.oidc.helper.PKCEHelper; +import it.spid.cie.oidc.model.CachedEntityInfo; +import it.spid.cie.oidc.model.EntityConfiguration; +import it.spid.cie.oidc.model.FederationEntityConfiguration; +import it.spid.cie.oidc.model.OIDCAuthRequest; +import it.spid.cie.oidc.model.OIDCConstants; +import it.spid.cie.oidc.model.TrustChain; +import it.spid.cie.oidc.model.TrustChainBuilder; +import it.spid.cie.oidc.persistence.PersistenceAdapter; +import it.spid.cie.oidc.schemas.OIDCProfile; +import it.spid.cie.oidc.util.JSONUtil; +import it.spid.cie.oidc.util.Validator; + +public class RelyingPartyHandler { + + public RelyingPartyHandler( + RelyingPartyOptions options, PersistenceAdapter persistence) + throws OIDCException { + + options.validate(); + + if (persistence == null) { + throw new OIDCException("persistence is mandatory"); + } + + this.options = options; + this.persistence = persistence; + this.jwtHelper = new JWTHelper(options); + } + + /** + * Build the "authorize url": the URL a RelyingParty have to send to an OpenID + * Provider to start an SPID authorization flow + * + * @param spidProvider + * @param trustAnchor + * @param redirectUri + * @param scope + * @param profile + * @param prompt + * @return + * @throws OIDCException + */ + public String getAuthorizeURL( + String spidProvider, String trustAnchor, String redirectUri, String scope, + String profile, String prompt) + throws OIDCException { + + TrustChain tc = getSPIDProvider(spidProvider, trustAnchor); + + if (tc == null) { + throw new OIDCException("TrustChain is unavailable"); + } + + JSONObject providerMetadata; + + try { + providerMetadata = new JSONObject(tc.getMetadata()); + + if (providerMetadata.isEmpty()) { + throw new OIDCException("Provider metadata is empty"); + } + } + catch (Exception e) { + throw e; + } + + FederationEntityConfiguration entityConf = persistence.fetchFederationEntity( + OIDCConstants.OPENID_RELYING_PARTY); + + if (entityConf == null || !entityConf.isActive()) { + throw new OIDCException("Missing configuration"); + } + + JSONObject entityMetadata; + + JWKSet entityJWKSet; + + try { + entityMetadata = entityConf.getMetadataValue( + OIDCConstants.OPENID_RELYING_PARTY); + + if (entityMetadata.isEmpty()) { + throw new OIDCException("Entity metadata is empty"); + } + + entityJWKSet = JWTHelper.getJWKSetFromJSON(entityConf.getJwks()); + + if (entityJWKSet.getKeys().isEmpty()) { + throw new OIDCException("Entity with invalid or empty jwks"); + } + } + catch (OIDCException e) { + throw e; + } + + JWKSet providerJWKSet = JWTHelper.getMetadataJWKSet(providerMetadata); + + String authzEndpoint = providerMetadata.getString("authorization_endpoint"); + + JSONArray entityRedirectUris = entityMetadata.getJSONArray("redirect_uris"); + + if (entityRedirectUris.isEmpty()) { + throw new OIDCException("Entity has no redirect_uris"); + } + + if (!Validator.isNullOrEmpty(redirectUri)) { + if (!JSONUtil.contains(entityRedirectUris, redirectUri)) { + logger.warn( + "Requested for unknown redirect uri '{}'. Reverted to default '{}'", + redirectUri, entityRedirectUris.getString(0)); + + redirectUri = entityRedirectUris.getString(0); + } + } + else { + redirectUri = entityRedirectUris.getString(0); + } + + if (Validator.isNullOrEmpty(scope)) { + scope = OIDCConstants.SCOPE_OPENID; + } + + if (Validator.isNullOrEmpty(profile)) { + profile = options.getAcrValue(OIDCProfile.SPID); + } + + if (Validator.isNullOrEmpty(prompt)) { + prompt = "consent login"; + } + + String responseType = entityMetadata.getJSONArray("response_types").getString(0); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + String clientId = entityMetadata.getString("client_id"); + long issuedAt = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC); + String[] aud = new String[] { tc.getSubject(), authzEndpoint }; + JSONObject claims = getRequestedClaims(profile); + JSONObject pkce = PKCEHelper.getPKCE(); + + String acr = options.getAcrValue(OIDCProfile.SPID); + + JSONObject authzData = new JSONObject() + .put("scope", scope) + .put("redirect_uri", redirectUri) + .put("response_type", responseType) + .put("nonce", nonce) + .put("state", state) + .put("client_id", clientId) + .put("endpoint", authzEndpoint) + .put("acr_values", acr) + .put("iat", issuedAt) + .put("aud", JSONUtil.asJSONArray(aud)) + .put("claims", claims) + .put("prompt", prompt) + .put("code_verifier", pkce.getString("code_verifier")) + .put("code_challenge", pkce.getString("code_challenge")) + .put("code_challenge_method", pkce.getString("code_challenge_method")); + + OIDCAuthRequest authzEntry = new OIDCAuthRequest() + .setClientId(clientId) + .setState(state) + .setEndpoint(authzEndpoint) + .setProvider(tc.getSubject()) + .setProviderId(tc.getSubject()) + .setData(authzData.toString()) + .setProviderJwks(providerJWKSet.toString()) + .setProviderConfiguration(providerMetadata.toString()); + + authzEntry = persistence.storeOIDCAuthRequest(authzEntry); + + authzData.remove("code_verifier"); + authzData.put("iss", entityMetadata.getString("client_id")); + authzData.put("sub", entityMetadata.getString("client_id")); + + String requestObj = jwtHelper.createJWS(authzData, entityJWKSet); + + authzData.put("request", requestObj); + + String url = buildURL(authzEndpoint, authzData); + + logger.info("Starting Authz request to {}", url); + + return url; + } + + protected TrustChain getSPIDProvider(String spidProvider, String trustAnchor) + throws OIDCException { + + if (Validator.isNullOrEmpty(spidProvider)) { + if (logger.isWarnEnabled()) { + logger.warn(TrustChainException.MissingProvider.DEFAULT_MESSAGE); + } + + throw new TrustChainException.MissingProvider(); + } + + if (Validator.isNullOrEmpty(trustAnchor)) { + trustAnchor = options.getSPIDProviders().get(spidProvider); + + if (Validator.isNullOrEmpty(trustAnchor)) { + trustAnchor = options.getDefaultTrustAnchor(); + } + } + + if (!options.getTrustAnchors().contains(trustAnchor)) { + logger.warn(TrustChainException.InvalidTrustAnchor.DEFAULT_MESSAGE); + + throw new TrustChainException.InvalidTrustAnchor(); + } + + TrustChain trustChain = persistence.fetchOIDCProvider( + spidProvider, OIDCProfile.SPID); + + boolean discover = false; + + if (trustChain == null) { + logger.info("TrustChain not found for %s", spidProvider); + + discover = true; + } + else if (!trustChain.isActive()) { + String msg = TrustChainException.TrustChainDisabled.getDefualtMessage( + trustChain.getModifiedDate()); + + if (logger.isWarnEnabled()) { + logger.warn(msg); + } + + throw new TrustChainException.TrustChainDisabled(msg); + } + else if (trustChain.isExpired()) { + logger.warn( + String.format( + "TrustChain found but EXPIRED at %s.", + trustChain.getExpiredOn().toString())); + logger.warn("Try to renew the trust chain"); + + discover = true; + } + + if (discover) { + trustChain = getOrCreateTrustChain( + spidProvider, trustAnchor, "openid_provider", true); + } + + return trustChain; + } + + protected TrustChain getOrCreateTrustChain( + String subject, String trustAnchor, String metadataType, boolean force) + throws OIDCException { + + CachedEntityInfo trustAnchorEntity = persistence.fetchEntityInfo( + trustAnchor, trustAnchor); + + EntityConfiguration taConf; + + if (trustAnchorEntity == null || trustAnchorEntity.isExpired() || force) { + String jwt = EntityHelper.getEntityConfiguration(trustAnchor); + + taConf = new EntityConfiguration(jwt, jwtHelper); + + if (trustAnchorEntity == null) { + trustAnchorEntity = CachedEntityInfo.of( + trustAnchor, subject, taConf.getExpiresOn(), taConf.getIssuedAt(), + taConf.getPayload(), taConf.getJwt()); + + trustAnchorEntity = persistence.storeEntityInfo(trustAnchorEntity); + } + else { + trustAnchorEntity.setModifiedDate(LocalDateTime.now()); + trustAnchorEntity.setExpiresOn(taConf.getExpiresOn()); + trustAnchorEntity.setIssuedAt(taConf.getIssuedAt()); + trustAnchorEntity.setStatement(taConf.getPayload()); + trustAnchorEntity.setJwt(taConf.getJwt()); + + trustAnchorEntity = persistence.storeEntityInfo(trustAnchorEntity); + } + } + else { + taConf = EntityConfiguration.of(trustAnchorEntity, jwtHelper); + } + + TrustChain trustChain = persistence.fetchTrustChain(subject, trustAnchor); + + if (trustChain != null && !trustChain.isActive()) { + return null; + } + else { + TrustChainBuilder tcb = + new TrustChainBuilder(subject, metadataType, jwtHelper) + .setTrustAnchor(taConf) + .start(); + + if (!tcb.isValid()) { + String msg = String.format( + "Trust Chain for subject %s or trust_anchor %s is not valid", + subject, trustAnchor); + + throw new TrustChainException.InvalidTrustChain(msg); + } + else if (Validator.isNullOrEmpty(tcb.getFinalMetadata())) { + String msg = String.format( + "Trust chain for subject %s and trust_anchor %s doesn't have any " + + "metadata of type '%s'", subject, trustAnchor, metadataType); + + throw new TrustChainException.MissingMetadata(msg); + } + else { + logger.info("KK TCB is valid"); + } + + trustChain = persistence.fetchTrustChain(subject, trustAnchor, metadataType); + + if (trustChain == null) { + trustChain = new TrustChain(); +// subject, metadataType, tcb.getExpiration(), null, tcb.getChainAsString(), +// tcb.getPartiesInvolvedAsString(), true, null, tcb.getFinalMetadata(), +// null, trustAnchorEntity.getId(), tcb.getVerifiedTrustMarksAsString(), +// "valid", trustAnchor); + } + else { + // TODO: Update TrustChain + } + + trustChain = persistence.storeTrustChain(trustChain); + } + + return trustChain; + } + + // TODO: have to be configurable + private JSONObject getRequestedClaims(String profile) { + if (OIDCProfile.SPID.equalValue(profile)) { + JSONObject result = new JSONObject(); + + JSONObject idToken = new JSONObject() + .put( + "https://attributes.spid.gov.it/familyName", + new JSONObject().put("essential", true)) + .put( + "https://attributes.spid.gov.it/email", + new JSONObject().put("essential", true)); + + JSONObject userInfo = new JSONObject() + .put("https://attributes.spid.gov.it/name", new JSONObject()) + .put("https://attributes.spid.gov.it/familyName", new JSONObject()) + .put("https://attributes.spid.gov.it/email", new JSONObject()) + .put("https://attributes.spid.gov.it/fiscalNumber", new JSONObject()); + + result.put("id_token", idToken); + result.put("userinfo", userInfo); + + return result; + } + + return new JSONObject(); + } + + // TODO: move to an helper? + private String buildURL(String endpoint, JSONObject params) { + StringBuilder sb = new StringBuilder(); + + sb.append(endpoint); + + if (!params.isEmpty()) { + boolean first = true; + + for (String key : params.keySet()) { + if (first) { + sb.append("?"); + first = false; + } + else { + sb.append("&"); + } + + sb.append(key); + sb.append("="); + + String value = params.get(key).toString(); + + sb.append(URLEncoder.encode(value, StandardCharsets.UTF_8)); + } + } + + return sb.toString(); + } + + private static final Logger logger = LoggerFactory.getLogger( + RelyingPartyHandler.class); + + private final RelyingPartyOptions options; + private final PersistenceAdapter persistence; + private final JWTHelper jwtHelper; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/handler/test/RPRunner.java b/starter-kit/src/main/java/it/spid/cie/oidc/handler/test/RPRunner.java new file mode 100644 index 0000000..b9ca6f7 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/handler/test/RPRunner.java @@ -0,0 +1,19 @@ +package it.spid.cie.oidc.handler.test; + +import it.spid.cie.oidc.config.RelyingPartyOptions; +import it.spid.cie.oidc.handler.RelyingPartyHandler; + +public class RPRunner { + + public static void main(String[] args) throws Exception { + // TODO Auto-generated method stub + + RelyingPartyOptions options = new RelyingPartyOptions() + .setDefaultJWEAlgorithm("") + .setAllowedEncryptionAlgs("pippo"); + + RelyingPartyHandler handler = new RelyingPartyHandler(options, null); + + } + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/helper/EntityHelper.java b/starter-kit/src/main/java/it/spid/cie/oidc/helper/EntityHelper.java new file mode 100644 index 0000000..7b22c4b --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/helper/EntityHelper.java @@ -0,0 +1,102 @@ +package it.spid.cie.oidc.helper; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import it.spid.cie.oidc.config.GlobalOptions; +import it.spid.cie.oidc.exception.EntityException; +import it.spid.cie.oidc.exception.OIDCException; +import it.spid.cie.oidc.util.StringUtil; + +public class EntityHelper { + + /** + * Contacts the subject's ".well-known" endpoint to grab its federation metadata + * + * @param subject the url representing the subject, the federation entity + * @return + * @throws OIDCException + */ + public static String getEntityConfiguration(String subject) + throws OIDCException { + + String url = StringUtil.ensureTrailingSlash( + subject + ).concat( + GlobalOptions.OIDC_FEDERATION_WELLKNOWN_URL + ); + + if (logger.isInfoEnabled()) { + logger.info("Starting Entity Configuration Request for " + url); + } + + return doHttpGet(url); + } + + /** + * Fetches a statement/configuration of a Federation Entity + * + * @param url + * @return + * @throws OIDCException + */ + public static String getEntityStatement(String url) throws OIDCException { + if (logger.isInfoEnabled()) { + logger.info("Starting Entity Statement Request to " + url); + } + + return doHttpGet(url); + } + + public EntityHelper(GlobalOptions options) { + this.options = options; + } + + /** + * + * @param url + * @return + * @throws OIDCException + */ + private static String doHttpGet(String url) throws OIDCException { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(url)) + .GET() + .build(); + + HttpResponse response = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .build() + .send(request, BodyHandlers.ofString()); + + if (logger.isDebugEnabled()) { + logger.debug(url + " --> " + response.statusCode()); + } + + if (response.statusCode() != 200) { + throw new EntityException.Generic(url + " gets " + response.statusCode()); + } + + return response.body(); + } + catch (EntityException e) { + throw e; + } + catch (Exception e) { + throw new EntityException.Generic(e); + } + } + + private static final Logger logger = LoggerFactory.getLogger(EntityHelper.class); + + @SuppressWarnings("unused") + private final GlobalOptions options; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/helper/JWTHelper.java b/starter-kit/src/main/java/it/spid/cie/oidc/helper/JWTHelper.java new file mode 100644 index 0000000..5cfcd5f --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/helper/JWTHelper.java @@ -0,0 +1,461 @@ +package it.spid.cie.oidc.helper; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.KeyType; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.Base64; +import com.nimbusds.jwt.SignedJWT; + +import java.net.URL; +import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import it.spid.cie.oidc.config.GlobalOptions; +import it.spid.cie.oidc.exception.JWTException; +import it.spid.cie.oidc.exception.OIDCException; +import it.spid.cie.oidc.util.GetterUtil; + +public class JWTHelper { + + /** + * Decode a Base64 string and return it + * + * @param encoded + * @return + */ + public static String decodeBase64(String encoded) { + Base64 b = new Base64(encoded); + + return b.decodeToString(); + } + + /** + * Given a string representing a base64 encoded JWT Token (JWT, JWS, JWE) return a + * JSONObject with header and payload parts decoded + * + * @param jwt + * @return + */ + public static JSONObject fastParse(String jwt) { + String[] parts = jwt.split("\\."); + + JSONObject result = new JSONObject(); + + result.put("header", new JSONObject(decodeBase64(parts[0]))); + result.put("payload", new JSONObject(decodeBase64(parts[1]))); + + //if (parts.length == 3) { + // result.put("signature", new JSONObject(decodeBase64(parts[1]))); + //} + + return result; + } + + /** + * Given a string representing a base64 encoded JWT Token (JWT, JWS, JWE) return a + * JSONObject of the header part decoded + * + * @param jwt + * @return + */ + public static JSONObject fastParseHeader(String jwt) { + String[] parts = jwt.split("\\."); + + return new JSONObject(decodeBase64(parts[1])); + } + + /** + * Given a string representing a base64 encoded JWT Token (JWT, JWS, JWE) return a + * JSONObject of the payload part decoded + * + * @param jwt + * @return + */ + public static JSONObject fastParsePayload(String jwt) { + String[] parts = jwt.split("\\."); + + return new JSONObject(decodeBase64(parts[1])); + } + + /** + * + * @param jwkSet + * @return the first JWK in the provided JSON Web Key set + * @throws OIDCException + */ + public static JWK getFirstJWK(JWKSet jwkSet) throws OIDCException { + if (jwkSet != null && !jwkSet.getKeys().isEmpty()) { + return jwkSet.getKeys().get(0); + } + + throw new JWTException.Generic("JWKSet null or empty"); + } + + /** + * Given a JSON Web Key (JWK) set returns the JWK referenced inside the header of the + * base64 encoded jwt + * + * @param jwt + * @param jwkSet + * @return + */ + public static JWK getJWKFromJWT(String jwt, JWKSet jwkSet) { + JSONObject header = fastParseHeader(jwt); + + return jwkSet.getKeyByKeyId(header.optString("kid")); + } + + /** + * Given a JSON Web Key (JWK) set returns contained JWKs, only the public attributes, + * as JSONArray. + * + * @param jwkSet + * @param removeUse if true the "use" attribute, even if present in the JWK, will not + * be exposed + * @return + */ + public static JSONArray getJWKSetAsJSONArray(JWKSet jwkSet, boolean removeUse) { + return getJWKSetAsJSONArray(jwkSet, false, removeUse); + } + + /** + * Given a JSON Web Key (JWK) set returns contained JWKs as JSONArray. + * + * @param jwkSet + * @param privateAttrs if false only the public attributes of the JWK will be included + * @param removeUse if true the "use" attribute, even if present in the JWK, will not + * be exposed + * @return + */ + public static JSONArray getJWKSetAsJSONArray( + JWKSet jwkSet, boolean privateAttrs, boolean removeUse) { + + JSONArray keys = new JSONArray(); + + for (JWK jwk : jwkSet.getKeys()) { + JSONObject json; + + if (KeyType.RSA.equals(jwk.getKeyType())) { + RSAKey rsaKey = (RSAKey)jwk; + + if (privateAttrs) { + json = new JSONObject(rsaKey.toJSONObject()); + } + else { + json = new JSONObject(rsaKey.toPublicJWK().toJSONObject()); + } + } + else if (KeyType.EC.equals(jwk.getKeyType())) { + ECKey ecKey = (ECKey)jwk; + + if (privateAttrs) { + json = new JSONObject(ecKey.toJSONObject()); + } + else { + json = new JSONObject(ecKey.toPublicJWK().toJSONObject()); + } + } + else { + logger.error("Unsupported KeyType " + jwk.getKeyType()); + + continue; + } + + if (removeUse) { + json.remove("use"); + } + + keys.put(json); + } + + return keys; + } + + /** + * Given a JSON Web Key (JWK) set returns it, only the public attributes, as + * JSONObject. + * + * @param jwkSet + * @param removeUse if true the "use" attribute, even if present in the JWK, will not + * be exposed + * @return + */ + public static JSONObject getJWKSetAsJSONObject(JWKSet jwkSet, boolean removeUse) { + return getJWKSetAsJSONObject(jwkSet, false, removeUse); + } + + /** + * Given a JSON Web Key (JWK) set returns it as JSONObject. + * + * @param jwkSet + * @param privateAttrs if false only the public attributes of the JWK will be included + * @param removeUse if true the "use" attribute, even if present in the JWK, will not + * be exposed + * @return + */ + public static JSONObject getJWKSetAsJSONObject( + JWKSet jwkSet, boolean privateAttrs, boolean removeUse) { + + JSONArray keys = getJWKSetAsJSONArray(jwkSet, privateAttrs, removeUse); + + return new JSONObject() + .put("keys", keys); + } + + /** + * Get the JSON Web Key (JWK) set from the provided JSON string + * + * @param value a string representation of a JSONArray (array of keys) or of a + * JSONObject (complete jwks element) + * @return + * @throws OIDCException + */ + public static JWKSet getJWKSetFromJSON(String value) throws OIDCException { + try { + value = GetterUtil.getString(value, "{}").trim(); + + JSONObject jwks; + + if (value.startsWith("[")) { + jwks = new JSONObject() + .put("keys", new JSONArray(value)); + } + else { + jwks = new JSONObject(value); + } + + return JWKSet.parse(jwks.toMap()); + } + catch (Exception e) { + throw new JWTException.Parse(e); + } + } + + /** + * Get the JSON Web Key (JWK) set from the provided JSON Object that is supposed to + * be something like: + *
+	 *  {
+	 *     "keys": [
+	 *        { .... },
+	 *        { .... }
+	 *      }
+	 *  }
+	 * 
+ * + * @param json + * @return + * @throws OIDCException + */ + public static JWKSet getJWKSetFromJSON(JSONObject json) throws OIDCException { + try { + return JWKSet.parse(json.toMap()); + } + catch (Exception e) { + throw new JWTException.Parse(e); + } + } + + /** + * Get the JSON Web Key (JWK) set from the "payload" part of the provided JWT Token, + * or null if not present + * + * @param jwt the base64 encoded JWT Token + * @return + * @throws OIDCException + */ + public static JWKSet getJWKSetFromJWT(String jwt) throws OIDCException { + try { + JSONObject token = fastParse(jwt); + + JSONObject payload = token.getJSONObject("payload"); + + return getJWKSet(payload); + } + catch (Exception e) { + throw new JWTException.Parse(e); + } + } + + /** + * Get the JSON Web Key (JWK) set from a JSON representing a well-known entity + * metadata. The code is able to manage jwks embedded or remote (jwks_uri) + * + * @param metadata + * @return + * @throws Exception + */ + public static JWKSet getMetadataJWKSet(JSONObject metadata) throws OIDCException { + if (metadata.has("jwks")) { + try { + return JWKSet.parse(metadata.getJSONObject("jwks").toMap()); + } + catch (Exception e) { + throw new JWTException.Parse(e); + } + } + else if (metadata.has("jwks_uri")) { + String url = metadata.getString("jwks_uri"); + + try { + return JWKSet.load(new URL(url)); + } + catch (Exception e) { + throw new JWTException.Generic("Failed to download jwks from " + url); + } + } + + throw new JWTException.Generic("No jwks in metadata"); + } + + public JWTHelper(GlobalOptions options) { + this.options = options; + } + + public String createJWS(JSONObject payload, JWKSet jwks) throws OIDCException { + JWK jwk = getFirstJWK(jwks); + + // Signer depends on JWK key type + + JWSAlgorithm alg; + JWSSigner signer; + + try { + if (KeyType.RSA.equals(jwk.getKeyType())) { + RSAKey rsaKey = (RSAKey)jwk; + + signer = new RSASSASigner(rsaKey); + alg = JWSAlgorithm.parse(options.getDefaultJWSAlgorithm()); + } + else if (KeyType.EC.equals(jwk.getKeyType())) { + ECKey ecKey = (ECKey)jwk; + + signer = new ECDSASigner(ecKey); + alg = JWSAlgorithm.parse(options.getDefaultJWSAlgorithm()); + } + else { + throw new JWTException.Generic("Unknown key type"); + } + + // Prepare JWS object with the payload + + JWSObject jwsObject = new JWSObject( + new JWSHeader.Builder(alg).keyID(jwk.getKeyID()).build(), + new Payload(payload.toString())); + + // Compute the signature + jwsObject.sign(signer); + + // Serialize to compact form + return jwsObject.serialize(); + } + catch (Exception e) { + throw new JWTException.Generic(e); + } + } + + public boolean isValidAlgorithm(JWSAlgorithm alg) { + return options.getAllowedSigningAlgs().contains(alg.toString()); + } + + public boolean verifyJWS(SignedJWT jws, JWKSet jwkSet) throws OIDCException { + String kid = jws.getHeader().getKeyID(); + + JWK jwk = jwkSet.getKeyByKeyId(kid); + + if (jwk == null) { + throw new JWTException.UnknownKid(kid, jwkSet.toString()); + } + + JWSAlgorithm alg = jws.getHeader().getAlgorithm(); + + if (!isValidAlgorithm(alg)) { + throw new JWTException.UnsupportedAlgorithm(alg.toString()); + } + + try { + JWSVerifier verifier = getJWSVerifier(alg, jwk); + + return jws.verify(verifier); + } + catch (Exception e) { + throw new JWTException.Verifier(e); + } + } + + public boolean verifyJWS(String jws, JWKSet jwkSet) throws OIDCException { + SignedJWT jwsObject; + + try { + jwsObject = SignedJWT.parse(jws); + } + catch (Exception e) { + throw new JWTException.Parse(e); + } + + return verifyJWS(jwsObject, jwkSet); + } + + /** + * Get the JSON Web Key (JWK) set from the provided JSONObject, or null if + * not present + * + * @param json a JSONObject with a first level key named "jwks" + * @return + * @throws ParseException + */ + private static JWKSet getJWKSet(JSONObject json) throws ParseException { + JSONObject jwks = json.optJSONObject("jwks"); + + if (jwks != null) { + return JWKSet.parse(jwks.toMap()); + } + + return null; + } + + private JWSVerifier getJWSVerifier(JWSAlgorithm alg, JWK jwk) throws OIDCException { + if (RSASSAVerifier.SUPPORTED_ALGORITHMS.contains(alg)) { + if (!KeyType.RSA.equals(jwk.getKeyType())) { + throw new JWTException.Generic("Not RSA key " + jwk.toString()); + } + + RSAKey rsaKey = (RSAKey)jwk; + + RSAPublicKey publicKey; + + try { + publicKey = rsaKey.toRSAPublicKey(); + } + catch (JOSEException e) { + throw new JWTException.Generic(e); + } + + return new RSASSAVerifier(publicKey); + } + + throw new JWTException.Generic("Unsupported or unimplemented alg " + alg); + } + + private static final Logger logger = LoggerFactory.getLogger(JWTHelper.class); + + private final GlobalOptions options; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/helper/PKCEHelper.java b/starter-kit/src/main/java/it/spid/cie/oidc/helper/PKCEHelper.java new file mode 100644 index 0000000..e163e15 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/helper/PKCEHelper.java @@ -0,0 +1,56 @@ +package it.spid.cie.oidc.helper; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; + +import org.json.JSONObject; + +public class PKCEHelper { + + // TODO: Needed? public static final int CODE_CHALLENGE_LENGTH = 64; + public static final String CODE_CHALLENGE_METHOD = "S256"; + public static final int CODE_VERIFIER_LENGTH = 40; + + public static JSONObject getPKCE() { + try { + String codeVerifier = generateCodeVerifier(); + String codeChallenge = generateCodeChallange(codeVerifier); + + return new JSONObject() + .put("code_verifier", codeVerifier) + .put("code_challenge", codeChallenge) + .put("code_challenge_method", CODE_CHALLENGE_METHOD); + } + catch (Exception e) { + return new JSONObject(); + } + } + + private static String generateCodeVerifier() throws UnsupportedEncodingException { + SecureRandom secureRandom = new SecureRandom(); + + byte[] codeVerifier = new byte[CODE_VERIFIER_LENGTH]; + + secureRandom.nextBytes(codeVerifier); + + return Base64.getUrlEncoder().withoutPadding().encodeToString(codeVerifier); + } + + private static String generateCodeChallange(String codeVerifier) + throws UnsupportedEncodingException, NoSuchAlgorithmException { + + byte[] bytes = codeVerifier.getBytes("US-ASCII"); + + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + + messageDigest.update(bytes, 0, bytes.length); + + byte[] digest = messageDigest.digest(); + + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/model/BaseModel.java b/starter-kit/src/main/java/it/spid/cie/oidc/model/BaseModel.java new file mode 100644 index 0000000..debc9fb --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/model/BaseModel.java @@ -0,0 +1,40 @@ +package it.spid.cie.oidc.model; + +import java.time.LocalDateTime; + +public abstract class BaseModel { + + public LocalDateTime getCreateDate() { + return createDate; + } + + public LocalDateTime getModifiedDate() { + return modifiedDate; + } + + public String getStorageId() { + return storageId; + } + + public void setCreateDate(LocalDateTime createDate) { + this.createDate = createDate; + } + + public void setModifiedDate(LocalDateTime modifiedDate) { + this.modifiedDate = modifiedDate; + } + + public void setStorageId(String storageId) { + this.storageId = storageId; + } + + protected BaseModel() { + this.createDate = LocalDateTime.now(); + this.modifiedDate = createDate; + } + + private String storageId; + private LocalDateTime createDate; + private LocalDateTime modifiedDate; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/model/CachedEntityInfo.java b/starter-kit/src/main/java/it/spid/cie/oidc/model/CachedEntityInfo.java new file mode 100644 index 0000000..586335e --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/model/CachedEntityInfo.java @@ -0,0 +1,104 @@ +package it.spid.cie.oidc.model; + +import java.time.LocalDateTime; + +/** + * This model represent a "Fetched Entity Statement": a set of information (metadata) + * about a federation Entity provided by a the entity itself or by a Trust Anchor. + *
+ * This model helps to interact with these information generally provided as json + * + * @author Mauro Mariuzzo + */ +public class CachedEntityInfo extends BaseModel { + + public static CachedEntityInfo of( + String iss, String sub, LocalDateTime exp, LocalDateTime iat, String statement, + String jwt) { + + return new CachedEntityInfo() + .setExpiresOn(exp) + .setIssuedAt(iat) + .setIssuer(iss) + .setJwt(jwt) + .setStatement(statement) + .setSubject(sub); + } + + public LocalDateTime getExpiresOn() { + return exp; + } + + public LocalDateTime getIssuedAt() { + return iat; + } + + public String getIssuer() { + return iss; + } + + public String getJwt() { + return jwt; + } + + public String getStatement() { + return iss; + } + + public String getSubject() { + return iss; + } + + public boolean isExpired() { + return exp.isBefore(LocalDateTime.now()); + } + + + public CachedEntityInfo setExpiresOn(LocalDateTime expiresOn) { + this.exp = expiresOn; + + return this; + } + + public CachedEntityInfo setIssuedAt(LocalDateTime issuedAt) { + this.iat = issuedAt; + + return this; + } + + public CachedEntityInfo setIssuer(String issuer) { + this.iss = issuer; + + return this; + } + + public CachedEntityInfo setJwt(String jwt) { + this.jwt = jwt; + + return this; + } + + public CachedEntityInfo setStatement(String statement) { + this.statement = statement; + + return this; + } + + public CachedEntityInfo setSubject(String subject) { + this.sub = subject; + + return this; + } + + protected CachedEntityInfo() { + super(); + } + + private String iss; + private String sub; + private LocalDateTime exp; + private LocalDateTime iat; + private String statement; + private String jwt; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/model/EntityConfiguration.java b/starter-kit/src/main/java/it/spid/cie/oidc/model/EntityConfiguration.java new file mode 100644 index 0000000..78af5db --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/model/EntityConfiguration.java @@ -0,0 +1,543 @@ +package it.spid.cie.oidc.model; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import it.spid.cie.oidc.exception.EntityException; +import it.spid.cie.oidc.exception.JWTException; +import it.spid.cie.oidc.exception.OIDCException; +import it.spid.cie.oidc.exception.TrustChainException; +import it.spid.cie.oidc.helper.EntityHelper; +import it.spid.cie.oidc.helper.JWTHelper; +import it.spid.cie.oidc.util.ListUtil; +import it.spid.cie.oidc.util.StringUtil; +import it.spid.cie.oidc.util.Validator; + +public class EntityConfiguration { + + public static EntityConfiguration of(CachedEntityInfo entityInfo, JWTHelper jwtHelper) + throws OIDCException { + + return new EntityConfiguration(entityInfo.getJwt(), jwtHelper); + } + + /** + * + * @param jwt the JWS Token + */ + public EntityConfiguration(String jwt, JWTHelper jwtHelper) throws OIDCException { + this(jwt, null, jwtHelper); + } + + public EntityConfiguration( + String jwt, EntityConfiguration trustAnchor, JWTHelper jwtHelper) + throws OIDCException { + + this.jwt = jwt; + this.jwtHelper = jwtHelper; + this.trustAnchor = trustAnchor; + + JSONObject token = JWTHelper.fastParse(jwt); + + this.header = token.getJSONObject("header"); + this.payload = token.getJSONObject("payload"); + + if (logger.isDebugEnabled()) { + logger.debug("fastParse=" + token.toString()); + } + + this.sub = payload.getString("sub"); + this.iss = payload.getString("iss"); + this.exp = payload.getLong("exp"); + + extractJwks(); + } + + public void addFailedDescendantStatement(String key, JSONObject value) { + this.failedDescendantStatements.put(key, value); + } + + public void addVerifiedDescendantStatement(String key, JSONObject value) { + this.verifiedDescendantStatements.put(key, value); + } + + public int getConstraint(String key, int defaultValue) { + JSONObject json = payload.optJSONObject("constraints"); + + if (json != null) { + return json.optInt(key, defaultValue); + } + + return defaultValue; + } + + public long getExp() { + return exp; + } + + public LocalDateTime getExpiresOn() { + return LocalDateTime.ofEpochSecond(exp, 0, ZoneOffset.UTC); + } + + public String getFederationFetchEndpoint() { + JSONObject metadata = payload.optJSONObject("metadata"); + + if (metadata != null) { + JSONObject federationEntity = metadata.optJSONObject( + "federation_entity"); + + if (federationEntity != null) { + return federationEntity.optString("federation_fetch_endpoint"); + } + } + + return null; + } + + public LocalDateTime getIssuedAt() { + return LocalDateTime.ofEpochSecond(iat, 0, ZoneOffset.UTC); + } + + public String getJwt() { + return jwt; + } + + public String getPayload() { + return payload.toString(); + } + + public JSONObject getPayloadMetadata() { + return payload.optJSONObject("metadata", new JSONObject()); + } + + public String getSubject() { + return sub; + } + + /** + * Get superiors entity configurations + * + * @param maxAuthorityHints + * @param superiorHints + * @return + * @throws OIDCException + */ + public Map getSuperiors( + int maxAuthorityHints, List superiorHints) + throws OIDCException { + + List authorityHints = getPayloadStringArray("authority_hints"); + + // Apply limits on hints per hop if defined + + if (maxAuthorityHints > 0 && authorityHints.size() > maxAuthorityHints) { + int end = authorityHints.size() - maxAuthorityHints; + + logger.warn( + "Found {} but authority maximum hints is set to {}. The following " + + "authorities will be ignored: {}", authorityHints.size(), + maxAuthorityHints, StringUtil.merge( + ListUtil.subList(authorityHints, 0, end))); + + authorityHints = ListUtil.lasts(authorityHints, maxAuthorityHints); + } + + for (EntityConfiguration sup : superiorHints) { + if (authorityHints.contains(sup.getSubject())) { + logger.info( + "Getting Cached Entity Configurations for {}", sup.getSubject()); + + authorityHints.remove(sup.getSubject()); + verifiedSuperiors.put(sup.getSubject(), sup); + } + } + + logger.debug( + "Getting Entity Configurations for {}", StringUtil.merge(authorityHints)); + + for (String authorityHint : authorityHints) { + EntityConfiguration ec; + + try { + String jwt = EntityHelper.getEntityConfiguration( + authorityHint); + + ec = new EntityConfiguration(jwt, jwtHelper); + } + catch (Exception e) { + logger.warn("Get Entity Configuration for {}: {}", jwt, e); + + continue; + } + + if (ec.validateItself()) { + this.verifiedSuperiors.put(ec.getSubject(), ec); + } + else { + this.failedSuperiors.put(ec.getSubject(), ec); + } + } + + // TODO: Python code logs failed. + + return this.verifiedSuperiors; + } + + public List getVerifiedBySuperiors() { + List result = new ArrayList<>( + this.verifiedBySuperiors.values()); + + return Collections.unmodifiableList(result); + } + + public JSONObject getVerifiedDescendantPayloadMetadataPolicy(String metadataType) { + // TODO: What if we have more than one entry? + Iterator itr = this.verifiedDescendantStatements.values().iterator(); + + if (!itr.hasNext()) { + return null; + } + + JSONObject value = itr.next(); + + return value.optJSONObject( + "metadata_policy", new JSONObject() + ).optJSONObject(metadataType); + } + + public List getVerifiedDescendantStatement() { + List result = new ArrayList<>(); + + for (JSONObject value : verifiedDescendantStatements.values()) { + result.add(value.toString()); + } + + return Collections.unmodifiableList(result); + } + + /** + * @param key + * @return {@code true} if the {@code constraints} section inside {@code payload} has + * an element named {@code key} + */ + public boolean hasConstraint(String key) { + JSONObject json = payload.optJSONObject("constraints"); + + if (json != null) { + return json.has(key); + } + + return false; + } + + public boolean hasVerifiedBySuperiors() { + return !verifiedBySuperiors.isEmpty(); + } + + public boolean hasVerifiedDescendantStatement() { + return !verifiedDescendantStatements.isEmpty(); + } + + public boolean isValid() { + return valid; + } + + public void setAllowedTrustMarks(String[] allowedTrustMarks) { + this.allowedTrustMarks = Arrays.asList(allowedTrustMarks); + } + + /** + * Validate the entity configuration only if marked by a well known trust mark, issued + * by a trusted issuer + * + * @return + * @throws OIDCException + */ + public boolean validateByAllowedTrustMarks() throws OIDCException { + if (trustAnchor == null) { + throw new TrustChainException.TrustAnchorNeeded( + "To validate the trust marks the Trust Anchor Entity Configuration " + + "is needed."); + } + + if (allowedTrustMarks.isEmpty()) { + return true; + } + + JSONArray trustMarks = payload.optJSONArray("trust_marks"); + + if (trustMarks == null) { + logger.warn( + "{} doesn't have the trust marks claim in its Entity Configuration", + this.sub); + + return false; + } + + // TODO: Implement TrustMark checks + logger.error("TODO: Implement TrustMark checks"); + + return true; + } + + /** + * Validate this EntityConfiguration with the jwks contained in the statement of the + * superior + * + * @param jwt the statement issued by the superior + * @param ec the superior entity configuration + * @return + * @throws OIDCException + */ + public boolean validateBySuperior(String jwt, EntityConfiguration ec) + throws OIDCException { + + boolean valid = false; + + JSONObject payload = null; + + try { + payload = JWTHelper.fastParsePayload(jwt); + + if (ec.validateItself(false)) { + if (ec.validateDescendant(jwt)) { + + // Validate entity JWS using superior JWKSet + + JWKSet jwkSet = JWTHelper.getJWKSetFromJWT(jwt); + + valid = jwtHelper.verifyJWS(this.jwt, jwkSet); + } + } + } + catch (Exception e) { + StringBuilder sb = new StringBuilder(); + + sb.append(getSubject()); + sb.append(" failed validation with "); + sb.append(ec.getSubject()); + sb.append("'s superior statement "); + + if (payload != null) { + sb.append(payload.toString()); + } + else { + sb.append(jwt); + } + + sb.append(". Exception "); + sb.append(e); + + logger.warn(sb.toString()); + } + + if (valid) { + ec.addVerifiedDescendantStatement(getSubject(), payload); + + this.verifiedBySuperiors.put(payload.getString("iss"), ec); + this.valid = true; + } + else { + ec.addFailedDescendantStatement(getSubject(), payload); + } + + return valid; + } + /** + * Validates this entity configuration with the entity statements issued by + * its superiors. + *
+ * This method fills the following internal properties: + *
    + *
  • verifiedSuperiors
  • + *
  • failedSuperiors
  • + *
  • verifiedBySuperiors
  • + *
  • failedBySuperiors
  • + *
+ * + * @param superiors + * @return the verifiedSuperiors property + * @throws Exception + */ + public Map validateBySuperiors( + Collection superiors) + throws Exception { + + for (EntityConfiguration ec : superiors) { + if (this.verifiedBySuperiors.containsKey(ec.getSubject())) { + continue; + } + + String federationApiEndpoint = ec.getFederationFetchEndpoint(); + + if (Validator.isNullOrEmpty(federationApiEndpoint)) { + logger.warn( + "Missing federation_fetch_endpoint in federation_entity " + + "metadata for {} by {}", getSubject(), ec.getSubject()); + + this.invalidSuperiors.put(ec.getSubject(), null); + + continue; + } + + String url = federationApiEndpoint + "?sub=" + getSubject(); + + logger.info("Getting entity statements from {}", url); + + String jwt = EntityHelper.getEntityStatement(url); + + validateBySuperior(jwt, ec); + } + + return Collections.unmodifiableMap(this.verifiedBySuperiors); + } + + /** + * + * @param jwt a descendant entity statement issued by this + * @return + * @throws Exception + */ + public boolean validateDescendant(String jwt) throws OIDCException { + + // Fast decode JWT token + + JSONObject token = JWTHelper.fastParse(jwt); + + JSONObject header = token.getJSONObject("header"); + + if (logger.isDebugEnabled()) { + logger.debug("validateDescendant " + token.toString()); + } + + // Check kid coherence + + String kid = header.optString("kid"); + + if (!this.jwksKids.contains(kid)) { + throw new JWTException.UnknownKid(kid, jwkSet.toString()); + } + + if (jwtHelper.verifyJWS(jwt, this.jwkSet)) { + return true; + } + else { + // TODO: have to throw exception? + return false; + } + } + + /** + * Validate the EntityConfiguration by itself + */ + public boolean validateItself() { + try { + return validateItself(true); + } + catch (Exception e) { + // Ignore + } + + return false; + } + + /** + * Validate the EntityConfiguration by itself + * + * @param silentMode when {@code false} Exceptions will be propagated to caller + * @return {@code true} if entity jwt is self validated + * @throws Exception + */ + public boolean validateItself(boolean silentMode) throws OIDCException { + try { + this.valid = jwtHelper.verifyJWS(this.jwt, this.jwkSet); + + return this.valid; + } + catch (OIDCException e) { + logger.error(e.getMessage(), e); + + if (!silentMode) { + throw e; + } + } + + return false; + } + + protected List getPayloadStringArray(String key) { + List result = new ArrayList<>(); + + JSONArray array = payload.optJSONArray(key); + + if (array != null) { + for (int x = 0; x < array.length(); x++) { + result.add(array.getString(x)); + } + } + + return result; + } + + private void extractJwks() throws OIDCException { + JSONObject jwks = payload.optJSONObject("jwks"); + + if (jwks != null) { + this.jwkSet = JWTHelper.getJWKSetFromJSON(jwks); + } + + if (jwkSet == null || jwkSet.getKeys().size() == 0) { + String msg = String.format("Missing jwks in the statement for {}", sub); + + logger.error(msg); + + throw new EntityException.MissingJwksClaim(msg); + } + + for (JWK key : jwkSet.getKeys()) { + jwksKids.add(key.getKeyID()); + } + } + + private static final Logger logger = LoggerFactory.getLogger( + EntityConfiguration.class); + + private final String jwt; + private final JWTHelper jwtHelper; + private EntityConfiguration trustAnchor; + private JSONObject header; + private JSONObject payload; + private String sub; + private String iss; + private long exp; + private long iat; + private JWKSet jwkSet; + private List jwksKids = new ArrayList<>(); + private Map failedBySuperiors = new HashMap<>(); + private Map verifiedBySuperiors = new HashMap<>(); + private Map failedSuperiors = new HashMap<>(); + private Map invalidSuperiors = new HashMap<>(); + private Map verifiedSuperiors = new HashMap<>(); + private Map failedDescendantStatements = new HashMap<>(); + private Map verifiedDescendantStatements = new HashMap<>(); + private List allowedTrustMarks = new ArrayList<>(); + + private boolean valid = false; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/model/FederationEntityConfiguration.java b/starter-kit/src/main/java/it/spid/cie/oidc/model/FederationEntityConfiguration.java new file mode 100644 index 0000000..0c7e764 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/model/FederationEntityConfiguration.java @@ -0,0 +1,64 @@ +package it.spid.cie.oidc.model; + +import org.json.JSONObject; + +import it.spid.cie.oidc.config.GlobalOptions; + +public class FederationEntityConfiguration extends BaseModel { + + public String getJwks() { + return jwks; + } + + public String getMetadata() { + return metadata; + } + + public JSONObject getMetadataValue(String key) { + try { + JSONObject json = new JSONObject(metadata); + + return json.optJSONObject(key); + } + catch (Exception e) { + return null; + } + } + + public boolean isActive() { + return active; + } + + public FederationEntityConfiguration setActive(boolean active) { + this.active = active; + + return this; + } + + public FederationEntityConfiguration setMetadata(String metadata) { + this.metadata = metadata; + + return this; + } + + /** + * URL that identifies this Entity in the Federation. Inside {@link EntityConfiguration} + * this value will be used as {@code sub} and/or {@code iss}. + */ + private String sub; + + /** + * how many minutes from now() an issued statement must expire + */ + private int defaultExpireMinutes; + private String defaultSignatureAlg = GlobalOptions.DEFAULT_SIGNING_ALG; + private String authorityHints; + private String jwks; + private String trustMarks; + private String trustMarksIssuers; + private String metadata; + private boolean active = false; + private String constraints; + private String entityType; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/model/OIDCAuthRequest.java b/starter-kit/src/main/java/it/spid/cie/oidc/model/OIDCAuthRequest.java new file mode 100644 index 0000000..7068367 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/model/OIDCAuthRequest.java @@ -0,0 +1,111 @@ +package it.spid.cie.oidc.model; + +public class OIDCAuthRequest extends BaseModel { + + public OIDCAuthRequest() { + super(); + } + + public String getClientId() { + return clientId; + } + + public String getEndpoint() { + return endpoint; + } + + public String getData() { + return data; + } + + public String getProvider() { + return provider; + } + + public String getProviderConfiguration() { + return providerConfiguration; + } + + public String getProviderId() { + return providerId; + } + + public String getProviderJwks() { + return providerJwks; + } + + public String getState() { + return state; + } + + public boolean isSuccessful() { + return successful; + } + + public OIDCAuthRequest setClientId(String clientId) { + this.clientId = clientId; + + return this; + } + + public OIDCAuthRequest setEndpoint(String endpoint) { + this.endpoint = endpoint; + + return this; + } + + public OIDCAuthRequest setData(String data) { + this.data = data; + + return this; + } + + public OIDCAuthRequest setProvider(String provider) { + this.provider = provider; + + return this; + } + + public OIDCAuthRequest setProviderConfiguration(String providerConfiguration) { + this.providerConfiguration = providerConfiguration; + + return this; + } + + public OIDCAuthRequest setProviderId(String providerId) { + this.providerId = providerId; + + return this; + } + + public OIDCAuthRequest setProviderJwks(String providerJwks) { + this.providerJwks = providerJwks; + + return this; + } + + public OIDCAuthRequest setState(String state) { + this.state = state; + + return this; + } + + public OIDCAuthRequest setSuccessful(boolean successful) { + this.successful = successful; + + return this; + } + + + + private String clientId; + private String state; + private String endpoint; + private String data; + private boolean successful; + private String providerConfiguration; + private String provider; + private String providerId; + private String providerJwks; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/model/OIDCConstants.java b/starter-kit/src/main/java/it/spid/cie/oidc/model/OIDCConstants.java new file mode 100644 index 0000000..6b90e51 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/model/OIDCConstants.java @@ -0,0 +1,9 @@ +package it.spid.cie.oidc.model; + +public class OIDCConstants { + + public static final String OPENID_RELYING_PARTY = "openid_relying_party"; + + public static final String SCOPE_OPENID = "openid"; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/model/TrustChain.java b/starter-kit/src/main/java/it/spid/cie/oidc/model/TrustChain.java new file mode 100644 index 0000000..86c72fe --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/model/TrustChain.java @@ -0,0 +1,47 @@ +package it.spid.cie.oidc.model; + +import java.time.LocalDateTime; + +public class TrustChain extends BaseModel { + + public LocalDateTime getExpiredOn() { + return exp; + } + + public String getMetadata() { + return metadata; + } + + public String getSubject() { + return sub; + } + + public boolean isActive() { + return active; + } + + public boolean isExpired() { + return exp.isBefore(LocalDateTime.now()); + } + + public TrustChain setSubject(String subject) { + this.sub = subject; + + return this; + } + + private String sub; + private String type; + private LocalDateTime exp; + private LocalDateTime iat; + private String chain; + private String partiesInvolved; + private boolean active; + private String log; + private String metadata; + private LocalDateTime processingStart; + private String trustAnchor; + private String trustMasks; + private String status; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/model/TrustChainBuilder.java b/starter-kit/src/main/java/it/spid/cie/oidc/model/TrustChainBuilder.java new file mode 100644 index 0000000..874543a --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/model/TrustChainBuilder.java @@ -0,0 +1,624 @@ +package it.spid.cie.oidc.model; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.StringJoiner; +import java.util.TreeMap; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import it.spid.cie.oidc.exception.OIDCException; +import it.spid.cie.oidc.exception.TrustChainBuilderException; +import it.spid.cie.oidc.exception.TrustChainException; +import it.spid.cie.oidc.helper.EntityHelper; +import it.spid.cie.oidc.helper.JWTHelper; +import it.spid.cie.oidc.util.ListUtil; + +/** + * A trust walker that fetches statements and evaluate the evaluables to create a + * TrustChain object. + * + * @author Mauro Mariuzzo + */ +public class TrustChainBuilder { + + public TrustChainBuilder(String subject, String metadataType, JWTHelper jwtHelper) { + this.jwtHelper = jwtHelper; + this.metadataType = metadataType; + this.subject = subject; + } + + public String getChainAsString() { + StringJoiner sj = new StringJoiner(",", "[", "]"); + + for (EntityConfiguration ec : trustPath) { + sj.add(ec.getPayload()); + if (ec.hasVerifiedDescendantStatement()) { + StringJoiner sj2 = new StringJoiner(",", "[", "]"); + + for (String value : ec.getVerifiedDescendantStatement()) { + sj2.add(value); + } + + sj.add(sj2.toString()); + } + } + + return sj.toString(); + } + + public LocalDateTime getExpiresOn() { + return LocalDateTime.ofEpochSecond(exp, 0, ZoneOffset.UTC); + } + + public String getFinalMetadata() { + if (finalMetadata == null) { + return null; + } + + return this.finalMetadata.toString(); + } + + public String getPartiesInvolvedAsString() { + StringJoiner sj = new StringJoiner(",", "[", "]"); + + for (EntityConfiguration ec : trustPath) { + sj.add(ec.getSubject()); + } + + return sj.toString(); + } + + public String getSubject() { + return this.subject; + } + + public String getVerifiedTrustMarksAsString() { + JSONArray result = new JSONArray(); + + // TODO: manage trust mask + + return result.toString(); + } + + public boolean isValid() { + return this.valid; + } + + /** + * Means how much authorityHints to follow on each hop + * + * @param maxAuthorityHints + * @return + */ + public TrustChainBuilder setMaxAuthorityHints(int maxAuthorityHints) { + this.maxAuthorityHints = maxAuthorityHints; + + return this; + } + + public TrustChainBuilder setRequiredTrustMask(String[] requiredTrustMasks) { + this.requiredTrustMasks = requiredTrustMasks; + + return this; + } + + public TrustChainBuilder setSubjectConfiguration( + EntityConfiguration subjectConfiguration) { + + this.subjectConfiguration = subjectConfiguration; + + return this; + } + + public TrustChainBuilder setTrustAnchor(EntityConfiguration trustAnchor) { + trustAnchorConfiguration = trustAnchor; + + return this; + } + + public TrustChainBuilder setTrustAnchor(String trustAnchor) throws OIDCException { + if (logger.isInfoEnabled()) { + logger.info("Starting Metadata Discovery for {}", subject); + } + + String jwt = EntityHelper.getEntityConfiguration(trustAnchor); + + trustAnchorConfiguration = new EntityConfiguration(jwt, jwtHelper); + + return this; + } + + public TrustChainBuilder start() throws OIDCException { + try { + processTrustAnchorConfiguration(); + processSubjectConfiguration(); + discovery(); + } + catch (Exception e) { + logger.error(e.getMessage(), e); + + this.valid = false; + + if (e instanceof OIDCException) { + throw e; + } + else { + throw new OIDCException(e); + } + } + + return this; + } + + /** + * Filters the trust path from subject to trust anchor, apply the metadata + * policies along the path and returns the final metadata + * + * @throws Exception + */ + protected void applyMetadataPolicy() throws OIDCException { + if (trustPath.isEmpty()) { + trustPath.add(subjectConfiguration); + } + else { + EntityConfiguration ec = ListUtil.getLast(trustPath); + + if (trustAnchorConfiguration.getSubject().equals(ec.getSubject())) { + return; + } + } + + if (logger.isInfoEnabled()) { + logger.info( + "Applying metadata policy for {} over {} starting from {}", + this.subject, trustAnchorConfiguration.getSubject(), + ListUtil.getLast(trustPath)); + } + + List lastNodeEcs = trustsTree.get(trustPath.size() - 1); + + boolean pathFound = false; + final String trustAnchorSubject = trustAnchorConfiguration.getSubject(); + + for (EntityConfiguration ec : lastNodeEcs) { + for (EntityConfiguration supEc : ec.getVerifiedBySuperiors()) { + while ((trustPath.size() - 2) < maxPathLength) { + if (supEc.getSubject().equals(trustAnchorSubject)) { + trustPath.add(supEc); + pathFound = true; + break; + } + + if (supEc.hasVerifiedBySuperiors()) { + trustPath.add(supEc); + + applyMetadataPolicy(); + } + else { + if (logger.isInfoEnabled()) { + logger.info( + "'Huston, we have a problem' in {} for {} to {}", + supEc.getSubject(), this.subject, + trustAnchorConfiguration.getSubject()); + } + + trustPath.add(this.subjectConfiguration); + break; + } + } + } + } + + // once I filtered a concrete and unique trust path I can apply the metadata + // policy + + if (pathFound) { + logger.info("Found a trust path: {}", this.trustPath); + + this.finalMetadata = this.subjectConfiguration + .getPayloadMetadata() + .optJSONObject(metadataType); + + if (this.finalMetadata == null) { + logger.error( + "Missing {} in {}", + this.metadataType, this.subjectConfiguration.getPayloadMetadata()); + + return; + } + + for (int x = trustPath.size(); x > 0; x--) { + EntityConfiguration ec = trustPath.get(x - 1); + + JSONObject pol = ec.getVerifiedDescendantPayloadMetadataPolicy( + metadataType); + + if (pol != null) { + this.finalMetadata = applyPolicy(this.finalMetadata, pol); + } + } + } + + setExpiration(); + } + + protected JSONObject applyPolicy(JSONObject metadata, JSONObject policy) + throws OIDCException { + + for (String key : policy.keySet()) { + + // First Level is always a JSON Object + JSONObject p = policy.getJSONObject(key); + + if (!metadata.has(key)) { + if (p.has("value")) { + metadata.put(key, p.get("value")); + } + else if (p.has("add")) { + metadata.put(key, p.get("add")); + } + else if (p.has("default")) { + metadata.put(key, p.get("default")); + } + else if (p.has("essential")) { + // TODO: undestand essential + } + + continue; + } + + if (p.has("value")) { + metadata.put(key, p.get("value")); + } + else if (p.has("one_of")) { + JSONArray oneOf = p.getJSONArray("one_of"); + JSONArray ar = metadata.optJSONArray(key); + + if (ar != null) { + boolean good = false; + + for (int x = 0; x < ar.length(); x++) { + if (jsonArrayContains(oneOf, ar.get(x))) { + metadata.put(key, ar.get(x)); + good = true; + break; + } + } + + if (!good) { + throw new TrustChainBuilderException( + String.format( + "%s: None of %s among %s", key, ar.toString(), + oneOf.toString())); + } + } + else { + Object o = metadata.get(key); + + if (!jsonArrayContains(oneOf, o)) { + throw new TrustChainBuilderException( + String.format( + "%s: %s not among %s", key, ar.toString(), + oneOf.toString())); + } + } + } + else if (p.has("add")) { + metadata.put(key, jsonArrayUnion(metadata.get(key), p.get("add"))); + } + else if (p.has("subset_of")) { + JSONArray ar = jsonArrayIntersect(p.get("subset_of"), metadata.get(key)); + + if (!ar.isEmpty()) { + metadata.put(key, ar); + } + else { + throw new TrustChainBuilderException( + String.format( + "%s: %s not subset of %s", key, metadata.get(key), + p.get("subset_of"))); + } + } + else if (p.has("superset_of")) { + JSONArray ar = jsonArrayDifference( + p.get("superset_of"), metadata.get(key)); + + if (!ar.isEmpty()) { + metadata.put(key, ar); + } + else { + throw new TrustChainBuilderException( + String.format( + "%s: %s not superset of %s", key, metadata.get(key), + p.get("superset_of"))); + } + } + } + + return metadata; + } + + /** + * @return return a chain of verified statements from the lower up to the trust anchor + * @throws OIDCException + */ + protected boolean discovery() throws OIDCException { + logger.info("Starting a Walk into Metadata Discovery for " + subject); + + trustsTree.put(0, Arrays.asList(subjectConfiguration)); + + List processedSubjects = new ArrayList<>(); + + List superiorHints = Arrays.asList( + this.trustAnchorConfiguration); + + while ((trustsTree.size() -2) < maxPathLength) { + List entities = trustsTree.get(trustsTree.size() -1); + + List supEcs = new ArrayList<>(); + + for (EntityConfiguration ec : entities) { + if (processedSubjects.contains(ec.getSubject())) { + logger.warn( + "Metadata discovery loop detection for {}. " + + "Already present in {}. " + + "Discovery blocked for this path.", ec.getSubject(), + processedSubjects); + + continue; + } + + try { + Map superiors = ec.getSuperiors( + this.maxAuthorityHints, superiorHints); + + Map verifiedSuperiors = + ec.validateBySuperiors(superiors.values()); + + supEcs.addAll(verifiedSuperiors.values()); + + processedSubjects.add(ec.getSubject()); + } + catch (Exception e) { + logger.error( + "Metadata discovery exception for {}: {}", ec.getSubject(), e); + } + } + + if (!supEcs.isEmpty()) { + trustsTree.put(trustsTree.size(), supEcs); + } + else { + break; + } + } + + EntityConfiguration first = getTrustsTreeNodeValue(0, 0); + EntityConfiguration last = getTrustsTreeNodeValue(-1, 0); + + if (first != null && first.isValid() && last != null && last.isValid()) { + this.valid = true; + applyMetadataPolicy(); + } + + return this.valid; + } + + /** + * Ensure the provided Subject Entity Configuration is valid (self validable) and + * complete (at least by required elements) + * + * @throws OIDCException + * + * @throws OIDCException + */ + protected void processSubjectConfiguration() throws OIDCException { + if (subjectConfiguration != null) { + return; + } + + try { + String jwt = EntityHelper.getEntityConfiguration(subject); + + subjectConfiguration = new EntityConfiguration( + jwt, trustAnchorConfiguration, jwtHelper); + + subjectConfiguration.validateItself(); + } + catch (Exception e) { + String msg = String.format( + "Entity Configuration for %s failed: %s", subject, + e.getMessage()); + + logger.error(msg); + + throw new TrustChainBuilderException(msg); + } + + // Trust Mark filter + + if (requiredTrustMasks.length > 0) { + subjectConfiguration.setAllowedTrustMarks(requiredTrustMasks); + + if (!subjectConfiguration.validateByAllowedTrustMarks()) { + throw new TrustChainException.InvalidRequiredTrustMark( + "The required Trust Marks are not valid"); + } + + // TODO: this.verified_trust_marks.extend(sc.verified_trust_marks) + } + } + + /** + * Ensure the provided TrustAnchor Entity Configuration is valid (self validable) and + * complete (at least by required elements) + * + * @throws OIDCException + */ + protected void processTrustAnchorConfiguration() throws OIDCException { + if (trustAnchorConfiguration == null) { + throw new TrustChainBuilderException("Please set TrustAnchor"); + } + + try { + trustAnchorConfiguration.validateItself(false); + } + catch (Exception e) { + String message = + "Trust Anchor Entity Configuration validation failed with " + e; + + logger.error(message); + + throw new TrustChainBuilderException(message); + } + + if (trustAnchorConfiguration.hasConstraint("max_path_length")) { + this.maxPathLength = trustAnchorConfiguration.getConstraint( + "max_path_length", 0); + } + } + + protected void setExpiration() { + this.exp = 0; + + for (EntityConfiguration ec : this.trustPath) { + if (this.exp == 0) { + this.exp = ec.getExp(); + } + else if (ec.getExp() > this.exp) { + this.exp = ec.getExp(); + } + } + } + + private EntityConfiguration getTrustsTreeNodeValue(int nodeIdx, int valueIdx) { + List value; + + if (nodeIdx >= 0) { + value = trustsTree.get(nodeIdx); + } + else { + value = trustsTree.get(trustsTree.size() - 1); + } + + if (value != null && !value.isEmpty()) { + if (valueIdx < 0) { + return value.get(value.size() - 1); + } + else if (valueIdx < value.size()) { + return value.get(valueIdx); + } + } + + return null; + } + + private boolean jsonArrayContains(JSONArray array, Object value) { + for (int x = 0; x < array.length(); x++) { + if (Objects.equals(value, array.get(x))) { + return true; + } + } + + return false; + } + + private JSONArray jsonArrayUnion(Object o1, Object o2) { + Set result = new HashSet<>(); + + if (o1 instanceof JSONArray) { + result.addAll(((JSONArray)o1).toList()); + } + else { + result.add(o1); + } + if (o2 instanceof JSONArray) { + result.addAll(((JSONArray)o2).toList()); + } + else { + result.add(o2); + } + + return new JSONArray(result); + } + + private JSONArray jsonArrayIntersect(Object o1, Object o2) { + Set s1 = new HashSet<>(); + + if (o1 instanceof JSONArray) { + s1.addAll(((JSONArray)o1).toList()); + } + else { + s1.add(o1); + } + + Set s2 = new HashSet<>(); + + if (o2 instanceof JSONArray) { + s2.addAll(((JSONArray)o2).toList()); + } + else { + s2.add(o2); + } + + s1.retainAll(s2); + + return new JSONArray(s1); + } + + private JSONArray jsonArrayDifference(Object o1, Object o2) { + Set s1 = new HashSet<>(); + + if (o1 instanceof JSONArray) { + s1.addAll(((JSONArray)o1).toList()); + } + else { + s1.add(o1); + } + + Set s2 = new HashSet<>(); + + if (o2 instanceof JSONArray) { + s2.addAll(((JSONArray)o2).toList()); + } + else { + s2.add(o2); + } + + s1.removeAll(s2); + + return new JSONArray(s1); + } + + + private static final Logger logger = LoggerFactory.getLogger( + TrustChainBuilder.class); + + private final JWTHelper jwtHelper; + private final String metadataType; + private final String subject; + private EntityConfiguration subjectConfiguration; + private EntityConfiguration trustAnchorConfiguration; + private int maxPathLength = 0; + private int maxAuthorityHints = 10; + private String[] requiredTrustMasks = new String[0]; + private Map> trustsTree = new TreeMap<>(); + private List trustPath = new ArrayList<>(); + private long exp = 0; + private boolean valid = false; + private JSONObject finalMetadata; + private Set verifiedTrustMasks = new HashSet<>(); + + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/persistence/PersistenceAdapter.java b/starter-kit/src/main/java/it/spid/cie/oidc/persistence/PersistenceAdapter.java new file mode 100644 index 0000000..d91d956 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/persistence/PersistenceAdapter.java @@ -0,0 +1,37 @@ +package it.spid.cie.oidc.persistence; + +import it.spid.cie.oidc.exception.PersistenceException; +import it.spid.cie.oidc.model.CachedEntityInfo; +import it.spid.cie.oidc.model.FederationEntityConfiguration; +import it.spid.cie.oidc.model.OIDCAuthRequest; +import it.spid.cie.oidc.model.TrustChain; +import it.spid.cie.oidc.schemas.OIDCProfile; + +public interface PersistenceAdapter { + + public CachedEntityInfo fetchEntityInfo(String subject, String issuer) + throws PersistenceException; + + public FederationEntityConfiguration fetchFederationEntity(String entityType) + throws PersistenceException; + + public TrustChain fetchOIDCProvider(String subject, OIDCProfile profile) + throws PersistenceException; + + public TrustChain fetchTrustChain(String subject, String trustAnchor) + throws PersistenceException; + + public TrustChain fetchTrustChain( + String subject, String trustAnchor, String metadataType) + throws PersistenceException; + + public CachedEntityInfo storeEntityInfo(CachedEntityInfo entityInfo) + throws PersistenceException; + + public OIDCAuthRequest storeOIDCAuthRequest(OIDCAuthRequest authRequest) + throws PersistenceException; + + public TrustChain storeTrustChain(TrustChain trustChain) + throws PersistenceException; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/schemas/AcrValuesSpid.java b/starter-kit/src/main/java/it/spid/cie/oidc/schemas/AcrValuesSpid.java new file mode 100644 index 0000000..a8be1f5 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/schemas/AcrValuesSpid.java @@ -0,0 +1,50 @@ +package it.spid.cie.oidc.schemas; + +public enum AcrValuesSpid { + + L1("https://www.spid.gov.it/SpidL1"), + L2("https://www.spid.gov.it/SpidL2"), + L3("https://www.spid.gov.it/SpidL3"); + + public static AcrValuesSpid parse(String value) { + try { + return parse(value, false); + } + catch (Exception e) { + // Ignore + } + + return null; + } + + public static AcrValuesSpid parse(String value, boolean strict) throws Exception { + if (value != null) { + if (value.equals(L1.getValue())) { + return L1; + } + else if (value.equals(L2.getValue())) { + return L2; + } + else if (value.equals(L3.getValue())) { + return L3; + } + } + + if (strict) { + throw new Exception("Invalid value: " + value); + } + + return null; + } + + public String getValue() { + return value; + } + + private AcrValuesSpid(String value) { + this.value =value; + } + + private final String value; + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/schemas/OIDCProfile.java b/starter-kit/src/main/java/it/spid/cie/oidc/schemas/OIDCProfile.java new file mode 100644 index 0000000..403ef13 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/schemas/OIDCProfile.java @@ -0,0 +1,51 @@ +package it.spid.cie.oidc.schemas; + +import it.spid.cie.oidc.exception.OIDCException; + +public enum OIDCProfile { + + SPID("spid"), + CIE("cie"); + + public static OIDCProfile parse(String value) { + try { + return parse(value, false); + } + catch (Exception e) { + // Ignore + } + + return null; + } + + public static OIDCProfile parse(String value, boolean strict) throws OIDCException { + if (value != null) { + if (value.equals(SPID.getValue())) { + return SPID; + } + else if (value.equals(CIE.getValue())) { + return CIE; + } + } + + if (strict) { + throw new OIDCException("Invalid value: " + value); + } + + return null; + } + + public boolean equalValue(String value) { + return this.value.equals(value); + } + + public String getValue() { + return value; + } + + private OIDCProfile(String value) { + this.value =value; + } + + private final String value; +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/schemas/TokenResponse.java b/starter-kit/src/main/java/it/spid/cie/oidc/schemas/TokenResponse.java new file mode 100644 index 0000000..5e41163 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/schemas/TokenResponse.java @@ -0,0 +1,78 @@ +package it.spid.cie.oidc.schemas; + +import java.util.regex.Pattern; + +import org.json.JSONObject; + +import it.spid.cie.oidc.exception.OIDCException; +import it.spid.cie.oidc.exception.SchemaException; + +public class TokenResponse { + + public static TokenResponse of(JSONObject json) throws OIDCException { + if (json == null || json.isEmpty()) { + throw new SchemaException.Validation("Empty source"); + } + + return new TokenResponse( + json.optString("access_token"), json.optString("token_type"), + json.optInt("espires_in"), json.optString("id_token")); + } + + public String getAccessToken() { + return accessToken; + } + + public String getTokenType() { + return tokenType; + } + + public int getExpiresIn() { + return expiresIn; + } + + public String getIdToken() { + return idToken; + } + + public JSONObject toJSON() { + return new JSONObject() + .put("access_token", accessToken) + .put("token_type", tokenType) + .put("expiresIn", expiresIn) + .put("id_token", idToken); + } + + public String toString() { + return toJSON().toString(); + } + + protected TokenResponse( + String accessToken, String tokenType, int expiresIn, String idToken) + throws OIDCException { + + if (!TOKEN_PATTERN.matcher(accessToken).matches()) { + throw new SchemaException.Validation("Invalid access token"); + } + if (!"Bearer".equals(tokenType)) { + throw new SchemaException.Validation("Invalid token type"); + } + if (!TOKEN_PATTERN.matcher(idToken).matches()) { + throw new SchemaException.Validation("Invalid id token"); + } + + this.accessToken = accessToken; + this.tokenType = tokenType; + this.expiresIn = expiresIn; + this.idToken = idToken; + } + + private final String accessToken; + private final String tokenType; + private final int expiresIn; + private final String idToken; + + private static Pattern TOKEN_PATTERN = Pattern.compile( + "^[a-zA-Z0-9_\\-]+\\.[a-zA-Z0-9_\\-]+\\.[a-zA-Z0-9_\\-]+"); + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/util/ArrayUtil.java b/starter-kit/src/main/java/it/spid/cie/oidc/util/ArrayUtil.java new file mode 100644 index 0000000..17421bb --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/util/ArrayUtil.java @@ -0,0 +1,90 @@ +package it.spid.cie.oidc.util; + +import java.util.HashSet; +import java.util.Set; + +public class ArrayUtil { + + @SafeVarargs + public static Set asSet(T... values) { + Set result = new HashSet(values.length); + + for (T value : values) { + result.add(value); + } + + return result; + } + + public static boolean contains(String[] array, String value) { + return contains(array, value, false); + } + + public static boolean contains( + String[] array, String value, boolean ignoreCase) { + + if (array == null) { + return false; + } + + for (String elem : array) { + if (elem == null) { + if (value == null) { + return true; + } + } + else if (ignoreCase) { + if (elem.equalsIgnoreCase(value)) { + return true; + } + } + else if (elem.equals(value)) { + return true; + } + } + + return false; + } + + public static String[] lasts(String[] array, int count) { + int end = array.length; + int start = end - count; + + return subset(array, start, end); + } + + public static String[] subset(String[] array, int start, int end) { + start = checkStart(start); + end = checkEnd(end, array.length); + + if ((start < 0) || (end < 0) || ((end - start) < 0)) { + return array; + } + + String[] newArray = new String[end - start]; + + System.arraycopy(array, start, newArray, 0, end - start); + + return newArray; + } + + private static int checkEnd(int end, int arrayLength) { + if (end < 0) { + return arrayLength; + } + else if (end > arrayLength) { + return arrayLength; + } + + return end; + } + + private static int checkStart(int start) { + if (start < 0) { + return 0; + } + + return start; + } + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/util/GetterUtil.java b/starter-kit/src/main/java/it/spid/cie/oidc/util/GetterUtil.java new file mode 100644 index 0000000..3420753 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/util/GetterUtil.java @@ -0,0 +1,26 @@ +package it.spid.cie.oidc.util; + +public class GetterUtil { + + public static String getString(Object obj) { + if (obj instanceof String) { + return (String)obj; + } + else { + return obj.toString(); + } + } + + public static String getString(Object obj, String defaultValue) { + if (obj == null) { + return defaultValue; + } + if (obj instanceof String) { + return (String)obj; + } + else { + return obj.toString(); + } + } + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/util/JSONUtil.java b/starter-kit/src/main/java/it/spid/cie/oidc/util/JSONUtil.java new file mode 100644 index 0000000..408b386 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/util/JSONUtil.java @@ -0,0 +1,29 @@ +package it.spid.cie.oidc.util; + +import java.util.Objects; + +import org.json.JSONArray; + +public class JSONUtil { + + public static JSONArray asJSONArray(String... values) { + return new JSONArray(values); + } + + public static boolean contains(JSONArray array, String value) { + if (array.isEmpty()) { + return false; + } + + for (int x = 0; x < array.length(); x++) { + String elem = array.optString(x); + + if (Objects.equals(value, elem)) { + return true; + } + } + + return false; + } + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/util/ListUtil.java b/starter-kit/src/main/java/it/spid/cie/oidc/util/ListUtil.java new file mode 100644 index 0000000..c8881d2 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/util/ListUtil.java @@ -0,0 +1,40 @@ +package it.spid.cie.oidc.util; + +import java.util.Collections; +import java.util.List; + +public class ListUtil { + + public static E getLast(List list) { + if (list != null && !list.isEmpty()) { + return list.get(list.size() - 1); + } + + return null; + } + + public static List lasts(List list, int count) { + if (list != null && !list.isEmpty()) { + return subList(list, list.size() - count, list.size()); + } + + return Collections.emptyList(); + } + + public static List subList(List list, int start, int end) { + if (start < 0) { + start = 0; + } + + if ((end < 0) || (end > list.size())) { + end = list.size(); + } + + if (start < end) { + return list.subList(start, end); + } + + return Collections.emptyList(); + } + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/util/StringUtil.java b/starter-kit/src/main/java/it/spid/cie/oidc/util/StringUtil.java new file mode 100644 index 0000000..053457c --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/util/StringUtil.java @@ -0,0 +1,41 @@ +package it.spid.cie.oidc.util; + +import java.util.Collection; +import java.util.StringJoiner; + +public class StringUtil { + + public static final String ensureTrailingSlash(String url) { + if (url != null && !url.endsWith("/")) { + return url.concat("/"); + } + + return url; + } + + public static final String merge(String[] array) { + StringJoiner sj = new StringJoiner(","); + + for (String value : array) { + sj.add(value); + } + + return sj.toString(); + } + + public static final String merge(Collection list) { + if (list == null || list.isEmpty()) { + return ""; + } + + StringJoiner sj = new StringJoiner(","); + + for (Object object : list) { + sj.add(String.valueOf(object)); + } + + return sj.toString(); + } + + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/util/Validator.java b/starter-kit/src/main/java/it/spid/cie/oidc/util/Validator.java new file mode 100644 index 0000000..3d045d6 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/util/Validator.java @@ -0,0 +1,23 @@ +package it.spid.cie.oidc.util; + +public class Validator { + + public static boolean isNullOrEmpty(String value) { + if (value == null) { + return true; + } + + for (int x = 0; x < value.length(); x++) { + char c = value.charAt(x); + + if (c == ' ' || c == '\t') { + continue; + } + + return false; + } + + return true; + } + +}