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