diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/exception/JWTException.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/exception/JWTException.java
new file mode 100644
index 0000000..03a5b28
--- /dev/null
+++ b/examples/relying-party-spring-boot/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 SPIDException {
+
+ 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/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/exception/SPIDException.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/exception/SPIDException.java
new file mode 100644
index 0000000..4f9591f
--- /dev/null
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/exception/SPIDException.java
@@ -0,0 +1,23 @@
+package it.spid.cie.oidc.exception;
+
+public class SPIDException extends Exception {
+
+ public SPIDException() {
+ super();
+ }
+
+ public SPIDException(String message) {
+ super(message);
+ }
+
+ public SPIDException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public SPIDException(Throwable cause) {
+ super(cause);
+ }
+
+ private static final long serialVersionUID = -1839651152644089727L;
+
+}
diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/exception/UnsupportedAlgorithmException.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/exception/UnsupportedAlgorithmException.java
new file mode 100644
index 0000000..7335224
--- /dev/null
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/exception/UnsupportedAlgorithmException.java
@@ -0,0 +1,23 @@
+package it.spid.cie.oidc.exception;
+
+public class UnsupportedAlgorithmException extends SPIDException {
+
+ public UnsupportedAlgorithmException() {
+ super();
+ }
+
+ public UnsupportedAlgorithmException(String message) {
+ super(message);
+ }
+
+ public UnsupportedAlgorithmException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public UnsupportedAlgorithmException(Throwable cause) {
+ super(cause);
+ }
+
+ private static final long serialVersionUID = -5156493052679477725L;
+
+}
diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/exception/ValidationException.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/exception/ValidationException.java
new file mode 100644
index 0000000..ee0e0fb
--- /dev/null
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/exception/ValidationException.java
@@ -0,0 +1,23 @@
+package it.spid.cie.oidc.exception;
+
+public class ValidationException extends SPIDException {
+
+ public ValidationException() {
+ super();
+ }
+
+ public ValidationException(String message) {
+ super(message);
+ }
+
+ public ValidationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ValidationException(Throwable cause) {
+ super(cause);
+ }
+
+ private static final long serialVersionUID = 4061357156399802866L;
+
+}
diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/helper/JWTHelper.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/helper/JWTHelper.java
index 7307fea..9b90526 100644
--- a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/helper/JWTHelper.java
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/helper/JWTHelper.java
@@ -1,64 +1,141 @@
package it.spid.cie.oidc.relying.party.helper;
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEObject;
import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWEDecrypter;
+import com.nimbusds.jose.JWEObject;
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.RSADecrypter;
+import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jose.crypto.factories.DefaultJWEDecrypterFactory;
import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory;
+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.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
-import com.nimbusds.jose.jwk.gen.JWKGenerator;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
+import com.nimbusds.jose.proc.JWEDecrypterFactory;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.JWSVerifierFactory;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.util.Base64;
+import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
-import java.net.URI;
import java.net.URL;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.net.http.HttpResponse.BodyHandlers;
+import java.security.PrivateKey;
import java.security.PublicKey;
-import java.security.SecureRandom;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.HashSet;
-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.exception.JWTException;
+import it.spid.cie.oidc.exception.SPIDException;
import it.spid.cie.oidc.relying.party.util.ArrayUtil;
import it.spid.cie.oidc.relying.party.util.GetterUtil;
public class JWTHelper {
+ public static final String[] ALLOWED_ENCRYPTION_ALGS = new String[] {
+ "RSA-OAEP", "RSA-OAEP-256", "ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW",
+ "ECDH-ES+A256KW"};
+
public static final String[] ALLOWED_SIGNING_ALGS = new String[] {
"RS256", "RS384", "RS512", "ES256", "ES384", "ES512"};
+ public static final int DEFAULT_EXPIRES_ON_MINUTES = 30;
+
+ public static final JWEAlgorithm DEFAULT_JWE_ALG = JWEAlgorithm.RSA_OAEP;
+ public static final EncryptionMethod DEFAULT_JWE_ENC = EncryptionMethod.A256CBC_HS512;
+
public static String decodeBase64(String encoded) {
Base64 b = new Base64(encoded);
return b.decodeToString();
}
+ public static String decryptJWE(String jwe, JWKSet jwkSet) throws SPIDException {
+ JWEObject jweObject;
+
+ try {
+ jweObject = JWEObject.parse(jwe);
+ }
+ catch (ParseException e) {
+ throw new JWTException.Parse(e);
+ }
+
+ if (logger.isTraceEnabled()) {
+ logger.trace("jwe.header=" + jweObject.getHeader().toString());
+ }
+
+ JWEAlgorithm alg = jweObject.getHeader().getAlgorithm();
+ EncryptionMethod enc = jweObject.getHeader().getEncryptionMethod();
+ String kid = jweObject.getHeader().getKeyID();
+
+ if (alg == null) {
+ alg = DEFAULT_JWE_ALG;
+ }
+
+ if (enc == null) {
+ enc = DEFAULT_JWE_ENC;
+ }
+
+ if (!isValidAlgorithm(alg)) {
+ throw new JWTException.UnsupportedAlgorithm(alg.toString());
+ }
+
+ try {
+ JWK jwk = jwkSet.getKeyByKeyId(kid);
+
+ if (jwk == null) {
+ throw new Exception(kid + " not in jwks");
+ }
+
+ JWEDecrypter decrypter = getJWEDecrypter(alg, enc, jwk);
+
+ jweObject.decrypt(decrypter);
+ }
+ catch (Exception e) {
+ throw new JWTException.Decryption(e);
+ }
+
+ String jws = jweObject.getPayload().toString();
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Decrypted JWE as: " + jws);
+ }
+ logger.info("KK Decrypted JWE as: " + jws);
+
+ return jws;
+ }
+
public static JSONObject fastParse(String jwt) {
String[] parts = jwt.split("\\.");
@@ -67,29 +144,29 @@ public static JSONObject fastParse(String jwt) {
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;
}
- public static JSONObject fastParsePayload(String jwt) {
+ public static JSONObject fastParseHeader(String jwt) {
String[] parts = jwt.split("\\.");
return new JSONObject(decodeBase64(parts[1]));
}
- /**
- * Get the JSON Web Key (JWK) set from the provided JWT Token, or null if
- * not present
- *
- * @param jwt the encoded JWT Token
- * @return
- * @throws ParseException
- */
- public static JWKSet getJWKSetFromJWT(String jwt) throws ParseException {
- JSONObject token = fastParse(jwt);
+ public static JSONObject fastParsePayload(String jwt) {
+ String[] parts = jwt.split("\\.");
- JSONObject payload = token.getJSONObject("payload");
+ return new JSONObject(decodeBase64(parts[1]));
+ }
- return getJWKSet(payload);
+ public static JWK getJWKFromJWT(String jwt, JWKSet jwkSet) {
+ JSONObject header = fastParseHeader(jwt);
+
+ return jwkSet.getKeyByKeyId(header.optString("kid"));
}
/**
@@ -116,6 +193,197 @@ public static JWKSet getJWKSetFromJSON(String value) throws Exception {
return JWKSet.parse(jwks.toMap());
}
+ /**
+ * Get the JSON Web Key (JWK) set from the provided JSON Object that is supposed to
+ * be something like:
+ *
+ * {
+ * "keys": [
+ * { .... },
+ * { .... }
+ * }
+ * }
+ *
+ *
+ * @param json
+ * @return
+ * @throws Exception
+ */
+ public static JWKSet getJWKSetFromJSON(JSONObject json) throws Exception {
+ return JWKSet.parse(json.toMap());
+ }
+
+ /**
+ * Get the JSON Web Key (JWK) set from the provided JWT Token, or null if
+ * not present
+ *
+ * @param jwt the encoded JWT Token
+ * @return
+ * @throws ParseException
+ */
+ public static JWKSet getJWKSetFromJWT(String jwt) throws ParseException {
+ JSONObject token = fastParse(jwt);
+
+ JSONObject payload = token.getJSONObject("payload");
+
+ return getJWKSet(payload);
+ }
+
+ /**
+ * 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);
+ }
+
+ public static JSONObject getJWTFromJWE(
+ String jwe, JWKSet mineJWKSet, JWKSet otherJWKSet)
+ throws SPIDException {
+
+ String jws = decryptJWE(jwe, mineJWKSet);
+
+ try {
+ Base64URL[] parts = JOSEObject.split(jws);
+
+ if (parts.length == 3) {
+ SignedJWT signedJWT = new SignedJWT(parts[0], parts[1], parts[2]);
+
+ if (!verifyJWS(signedJWT, otherJWKSet)) {
+ logger.error(
+ "Verification failed for {} with jwks {}", jws,
+ otherJWKSet.toString());
+
+ //TODO: Understand why verify always fails
+ //throw new JWTException.Verifier(
+ // "Verification failed for " + jws);
+ }
+ }
+ else {
+ logger.warn("jwe {} contains unsigned jws {} ", jwe, jws);
+ }
+
+ return fastParse(jws);
+ }
+ catch (ParseException e) {
+ throw new JWTException.Parse(e);
+ }
+ catch (Exception e) {
+ throw new JWTException.Generic(e);
+ }
+ }
+
+ /**
+ * @return current UTC date time as epoch seconds
+ */
+ public static long getIssuedAt() {
+ return LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
+ }
+
+ /**
+ * @return current UTC date time, plus default espire minutes, as epoch seconds
+ */
+ public static long getExpiresOn() {
+ return getExpiresOn(DEFAULT_EXPIRES_ON_MINUTES);
+ }
+
+ /**
+ * @param minutes
+ * @return current UTC date time, plus provided minutes, as epoch seconds
+ */
+ public static long getExpiresOn(int minutes) {
+ return getIssuedAt() + (minutes * 60);
+ }
+
public static JWKSet getMetadataJWKSet(JSONObject metadata) throws Exception {
if (metadata.has("jwks")) {
return JWKSet.parse(metadata.getJSONObject("jwks").toMap());
@@ -168,6 +436,43 @@ public static RSAKey createRSAKey(JWSAlgorithm alg, KeyUse use) throws Exception
.generate();
}
+ public static String createJWS(JSONObject payload, JWKSet jwks) throws Exception {
+ JWK jwk = getFirstJWK(jwks);
+
+ // Signer depends on JWK key type
+
+ JWSAlgorithm alg;
+ JWSSigner signer;
+
+ if (KeyType.RSA.equals(jwk.getKeyType())) {
+ RSAKey rsaKey = (RSAKey)jwk;
+
+ signer = new RSASSASigner(rsaKey);
+ alg = JWSAlgorithm.RS256;
+ }
+ else if (KeyType.EC.equals(jwk.getKeyType())) {
+ ECKey ecKey = (ECKey)jwk;
+
+ signer = new ECDSASigner(ecKey);
+ alg = JWSAlgorithm.ES256;
+ }
+ else {
+ throw new Exception("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();
+ }
+
public static RSAKey parseRSAKey(String s) throws ParseException {
return RSAKey.parse(s);
}
@@ -198,41 +503,54 @@ public static void selfCheck(String jws, JWK jwk) throws Exception {
}
*/
- public static boolean isValidAlgorithm(JWSAlgorithm alg)
- throws Exception {
-
+ public static boolean isValidAlgorithm(JWSAlgorithm alg) {
return ArrayUtil.contains(ALLOWED_SIGNING_ALGS, alg.toString(), true);
}
- public static boolean verifyJWS(String jws, JWKSet jwkSet)
- throws Exception {
+ public static boolean isValidAlgorithm(JWEAlgorithm alg) {
+ return ArrayUtil.contains(ALLOWED_ENCRYPTION_ALGS, alg.toString(), true);
+ }
- SignedJWT jwtToken = SignedJWT.parse(jws);
+ public static boolean verifyJWS(SignedJWT jws, JWKSet jwkSet)
+ throws SPIDException {
- String kid = jwtToken.getHeader().getKeyID();
+ String kid = jws.getHeader().getKeyID();
JWK jwk = jwkSet.getKeyByKeyId(kid);
if (jwk == null) {
- // TODO UnknownKidException
- throw new Exception(
- String.format(
- "kid %s not found in jwks %s", kid, jwkSet.toString()));
+ throw new JWTException.UnknownKid(kid, jwkSet.toString());
}
- JWSAlgorithm alg = jwtToken.getHeader().getAlgorithm();
+ JWSAlgorithm alg = jws.getHeader().getAlgorithm();
if (!isValidAlgorithm(alg)) {
- String msg = String.format(
- "%s has beed disabled for security reason", alg);
+ throw new JWTException.UnsupportedAlgorithm(alg.toString());
+ }
-// throw new UnsupportedAlgorithmException(msg);
- throw new Exception(msg);
+ try {
+ JWSVerifier verifier = getJWSVerifier(alg, jwk);
+
+ return jws.verify(verifier);
+ }
+ catch (Exception e) {
+ throw new JWTException.Verifier(e);
}
+ }
+
+ public static boolean verifyJWS(String jws, JWKSet jwkSet)
+ throws SPIDException {
- JWSVerifier verifier = getJWSVerifier(alg, jwk);
+ SignedJWT jwsObject;
- return jwtToken.verify(verifier);
+ try {
+ jwsObject = SignedJWT.parse(jws);
+ }
+ catch (Exception e) {
+ throw new JWTException.Parse(e);
+ }
+
+ return verifyJWS(jwsObject, jwkSet);
}
public static void selfCheck2(String jwt, String[] supportedAlgs)
@@ -317,6 +635,35 @@ public static void selfCheck2(
}
}
+ public static JWK getFirstJWK(JWKSet jwkSet) throws Exception {
+ if (jwkSet != null && !jwkSet.getKeys().isEmpty()) {
+ return jwkSet.getKeys().get(0);
+ }
+
+ throw new Exception("JWKSet null or empty");
+ }
+
+ private static JWEDecrypter getJWEDecrypter(
+ JWEAlgorithm alg, EncryptionMethod enc, JWK jwk)
+ throws Exception {
+
+ if (RSADecrypter.SUPPORTED_ALGORITHMS.contains(alg) &&
+ RSADecrypter.SUPPORTED_ENCRYPTION_METHODS.contains(enc)) {
+
+ if (!KeyType.RSA.equals(jwk.getKeyType())) {
+ throw new Exception("Not RSA key " + jwk.toString());
+ }
+
+ RSAKey rsaKey = (RSAKey)jwk;
+
+ PrivateKey privateKey = rsaKey.toPrivateKey();
+
+ return new RSADecrypter(privateKey);
+ }
+
+ throw new Exception("Unsupported or unimplemented alg " + alg + " enc " + enc);
+ }
+
private static JWSVerifier getJWSVerifier(JWSAlgorithm alg, JWK jwk)
throws Exception {
@@ -327,12 +674,14 @@ private static JWSVerifier getJWSVerifier(JWSAlgorithm alg, JWK jwk)
RSAKey rsaKey = (RSAKey)jwk;
- PublicKey publicKey = rsaKey.toPublicKey();
+ RSAPublicKey publicKey = rsaKey.toRSAPublicKey();
+
+ logger.info("RSA Publickey=" + publicKey.toString());
- return new RSASSAVerifier((RSAPublicKey)publicKey);
+ return new RSASSAVerifier(publicKey);
}
- throw new Exception("Unsupported alg " + alg);
+ throw new Exception("Unsupported or unimplemented alg " + alg);
}
private static void doSelfCheck(
@@ -402,4 +751,7 @@ private static JWKSet getJWKSet(JSONObject payload) throws ParseException {
private static JWSVerifierFactory jwsVerifierFactory =
new DefaultJWSVerifierFactory();
+ private static JWEDecrypterFactory jweDecrypterFactory =
+ new DefaultJWEDecrypterFactory();
+
}
diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/helper/OAuth2Helper.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/helper/OAuth2Helper.java
new file mode 100644
index 0000000..f4550f4
--- /dev/null
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/helper/OAuth2Helper.java
@@ -0,0 +1,141 @@
+package it.spid.cie.oidc.relying.party.helper;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import it.spid.cie.oidc.relying.party.util.JSONUtil;
+import it.spid.cie.oidc.spring.boot.relying.party.storage.FederationEntityConfiguration;
+
+public class OAuth2Helper {
+
+ public static final String JWT_BARRIER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
+
+ /**
+ * Obtain the Access Token from the Authorization Code
+ *
+ * @see
+ * https://tools.ietf.org/html/rfc6749#section-4.1.3
+ *
+ * @param redirectUrl
+ * @param state
+ * @param code
+ * @param issuerId
+ * @param clientConf
+ * @param tokenEndpointUrl
+ * @param codeVerifier
+ * @return
+ * @throws Exception
+ */
+ public static JSONObject performAccessTokenRequest(
+ String redirectUrl, String state, String code, String issuerId,
+ FederationEntityConfiguration clientConf, String tokenEndpointUrl,
+ String codeVerifier)
+ throws Exception {
+
+ // create client assertion (JWS Token)
+
+ JSONObject payload = new JSONObject()
+ .put("iss", clientConf.getSub())
+ .put("sub", clientConf.getSub())
+ .put("aud", JSONUtil.asJSONArray(tokenEndpointUrl))
+ .put("iat", JWTHelper.getIssuedAt())
+ .put("exp", JWTHelper.getExpiresOn())
+ .put("jti", UUID.randomUUID().toString());
+
+ JWKSet jwkSet = JWTHelper.getJWKSetFromJSON(clientConf.getJwks());
+
+ String clientAssertion = JWTHelper.createJWS(payload, jwkSet);
+
+ Map params = new HashMap<>();
+
+ params.put("grant_type", "authorization_code");
+ params.put("redirect_uri", redirectUrl);
+ params.put("client_id", clientConf.getSub());
+ params.put("state", state);
+ params.put("code", code);
+ params.put("code_verifier", codeVerifier);
+ params.put("client_assertion_type", JWT_BARRIER);
+ params.put("client_assertion", clientAssertion);
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Access Token Request for {}: {}", state, buildPostBody(params));
+ }
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(new URI(tokenEndpointUrl))
+ .POST(HttpRequest.BodyPublishers.ofString(buildPostBody(params)))
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .build();
+
+ //TODO timeout from options
+ //TODO ssl test?
+ HttpResponse response = HttpClient.newBuilder()
+ .build()
+ .send(request, BodyHandlers.ofString());
+
+ if (response.statusCode() != 200) {
+ logger.error(
+ "Something went wrong with {}: {}", state, response.statusCode());
+ }
+ else {
+ try {
+ return new JSONObject(response.body());
+ }
+ catch(Exception e) {
+ logger.error(
+ "Something went wrong with {}: {}", state, e.getMessage());
+ }
+ }
+
+ return new JSONObject();
+ }
+
+ private static String buildPostBody(Map params) {
+ if (params == null || params.isEmpty()) {
+ return "";
+ }
+
+ boolean first = true;
+
+ StringBuilder sb = new StringBuilder(params.size() * 3);
+
+ for (Map.Entry param : params.entrySet()) {
+ if (first) {
+ first = false;
+ }
+ else {
+ sb.append("&");
+ }
+
+ sb.append(
+ URLEncoder.encode(param.getKey().toString(), StandardCharsets.UTF_8));
+ sb.append("=");
+
+ if (param.getValue() != null) {
+ sb.append(
+ URLEncoder.encode(
+ param.getValue().toString(), StandardCharsets.UTF_8));
+ }
+ }
+
+ return sb.toString();
+ }
+
+ private static final Logger logger = LoggerFactory.getLogger(OAuth2Helper.class);
+
+}
diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/helper/OidcHelper.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/helper/OidcHelper.java
new file mode 100644
index 0000000..9f50d47
--- /dev/null
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/helper/OidcHelper.java
@@ -0,0 +1,53 @@
+package it.spid.cie.oidc.relying.party.helper;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+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.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class OidcHelper {
+
+ public static JSONObject getUserInfo(
+ String state, String accessToken, JSONObject providerConf, boolean verify,
+ JWKSet entityJwks)
+ throws Exception {
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(new URI(providerConf.optString("userinfo_endpoint")))
+ .header("Authorization", "Bearer " + accessToken)
+ .GET()
+ .build();
+
+ HttpResponse response = HttpClient.newBuilder()
+ .followRedirects(HttpClient.Redirect.NORMAL)
+ .build()
+ .send(request, BodyHandlers.ofString());
+
+ if (response.statusCode() != 200) {
+ String msg = String.format(
+ "Something went wrong with %s: %d", state, response.statusCode());
+
+ throw new Exception(msg);
+ }
+
+ JWKSet providerJwks = JWTHelper.getJWKSetFromJSON(
+ providerConf.optJSONObject("jwks"));
+
+ JSONObject jwt = JWTHelper.getJWTFromJWE(
+ response.body(), entityJwks, providerJwks);
+
+ logger.info("Userinfo endpoint result: " + jwt.toString(2));
+
+ return jwt.getJSONObject("payload");
+ }
+
+ private static final Logger logger = LoggerFactory.getLogger(OidcHelper.class);
+
+}
diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/model/EntityConfiguration.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/model/EntityConfiguration.java
index c0665b7..5a86a23 100644
--- a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/model/EntityConfiguration.java
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/model/EntityConfiguration.java
@@ -144,7 +144,7 @@ public JSONObject getPayloadMetadata() {
return payload.optJSONObject("metadata", new JSONObject());
}
- public String getFederationApiEndpoint() {
+ public String getFederationFetchEndpoint() {
JSONObject metadata = payload.optJSONObject("metadata");
if (metadata != null) {
@@ -152,7 +152,7 @@ public String getFederationApiEndpoint() {
"federation_entity");
if (federationEntity != null) {
- return federationEntity.optString("federation_api_endpoint");
+ return federationEntity.optString("federation_fetch_endpoint");
}
}
@@ -384,11 +384,11 @@ public Map validateBySuperiors(
continue;
}
- String federationApiEndpoint = ec.getFederationApiEndpoint();
+ String federationApiEndpoint = ec.getFederationFetchEndpoint();
if (Validator.isNullOrEmpty(federationApiEndpoint)) {
logger.warn(
- "Missing federation_api_endpoint in federation_entity " +
+ "Missing federation_fetch_endpoint in federation_entity " +
"metadata for {} by {}", getSub(), ec.getSub());
this.failedBySuperiors.put(ec.getSub(), null);
diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/schemas/TokenResponse.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/schemas/TokenResponse.java
new file mode 100644
index 0000000..afd9e42
--- /dev/null
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/relying/party/schemas/TokenResponse.java
@@ -0,0 +1,77 @@
+package it.spid.cie.oidc.relying.party.schemas;
+
+import java.util.regex.Pattern;
+
+import org.json.JSONObject;
+
+import it.spid.cie.oidc.exception.ValidationException;
+
+public class TokenResponse {
+
+ public static TokenResponse of(JSONObject json) throws ValidationException {
+ if (json == null || json.isEmpty()) {
+ throw new ValidationException();
+ }
+
+ 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 ValidationException {
+
+ if (!TOKEN_PATTERN.matcher(accessToken).matches()) {
+ throw new ValidationException();
+ }
+ if (!"Bearer".equals(tokenType)) {
+ throw new ValidationException();
+ }
+ if (!TOKEN_PATTERN.matcher(idToken).matches()) {
+ throw new ValidationException();
+ }
+
+ 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/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/MvcConfig.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/MvcConfig.java
index e0289e1..033c16f 100644
--- a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/MvcConfig.java
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/MvcConfig.java
@@ -15,7 +15,9 @@ public void addViewControllers(ViewControllerRegistry registry) {
.addViewController("/oidc/rp/.well-known/openid-federation")
.setViewName("well-known");
registry.addViewController("/hello").setViewName("hello");
- registry.addViewController("/login").setViewName("login");
+ registry
+ .addViewController("/oidc/rp/echo_attributes")
+ .setViewName("echo_attributes");
}
}
diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/RelyingPartySampleApplication.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/RelyingPartySampleApplication.java
index fbc6c62..7f1316e 100644
--- a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/RelyingPartySampleApplication.java
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/RelyingPartySampleApplication.java
@@ -32,7 +32,7 @@ public static void main(String[] args) {
@Override
public void run(String... args) throws Exception {
- System.out.println(oidcConfig.toJSONString(2));
+ System.out.println("Configuration:\n" + oidcConfig.toJSONString(2));
//test1();
}
diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/controller/SpidController.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/controller/SpidController.java
index d0f7f14..b62f36f 100644
--- a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/controller/SpidController.java
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/controller/SpidController.java
@@ -6,6 +6,7 @@
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
@@ -15,8 +16,13 @@
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.UUID;
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
import org.json.JSONArray;
import org.json.JSONException;
@@ -30,17 +36,23 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.view.RedirectView;
import it.spid.cie.oidc.relying.party.helper.EntityHelper;
import it.spid.cie.oidc.relying.party.helper.JWTHelper;
+import it.spid.cie.oidc.relying.party.helper.OAuth2Helper;
+import it.spid.cie.oidc.relying.party.helper.OidcHelper;
import it.spid.cie.oidc.relying.party.helper.PKCEHelper;
import it.spid.cie.oidc.relying.party.model.EntityConfiguration;
import it.spid.cie.oidc.relying.party.model.OidcConstants;
import it.spid.cie.oidc.relying.party.model.TrustChainBuilder;
import it.spid.cie.oidc.relying.party.schemas.AcrValuesSpid;
+import it.spid.cie.oidc.relying.party.schemas.TokenResponse;
import it.spid.cie.oidc.relying.party.util.ArrayUtil;
import it.spid.cie.oidc.relying.party.util.GetterUtil;
import it.spid.cie.oidc.relying.party.util.JSONUtil;
+import it.spid.cie.oidc.relying.party.util.ListUtil;
import it.spid.cie.oidc.relying.party.util.Validator;
import it.spid.cie.oidc.spring.boot.relying.party.config.OidcConfig;
import it.spid.cie.oidc.spring.boot.relying.party.exception.TrustChainException;
@@ -48,6 +60,8 @@
import it.spid.cie.oidc.spring.boot.relying.party.storage.EntityInfoRepository;
import it.spid.cie.oidc.spring.boot.relying.party.storage.FederationEntityConfiguration;
import it.spid.cie.oidc.spring.boot.relying.party.storage.FederationEntityConfigurationRepository;
+import it.spid.cie.oidc.spring.boot.relying.party.storage.OidcAuthentication;
+import it.spid.cie.oidc.spring.boot.relying.party.storage.OidcAuthenticationRepository;
import it.spid.cie.oidc.spring.boot.relying.party.storage.TrustChain;
import it.spid.cie.oidc.spring.boot.relying.party.storage.TrustChainRepository;
@@ -67,7 +81,10 @@ public class SpidController {
private EntityInfoRepository entityInfoRepository;
@Autowired
- private FederationEntityConfigurationRepository _federationEntityRepository;
+ private OidcAuthenticationRepository oidcAuthenticationRepository;
+
+ @Autowired
+ private FederationEntityConfigurationRepository federationEntityRepository;
@GetMapping("/authorize")
public ResponseEntity authorize(
@@ -99,7 +116,7 @@ public ResponseEntity authorize(
}
FederationEntityConfiguration entityConf =
- _federationEntityRepository.fetchByEntityType(
+ federationEntityRepository.fetchByEntityType(
OidcConstants.OPENID_RELYING_PARTY);
if (entityConf == null || !entityConf.isActive()) {
@@ -164,8 +181,6 @@ public ResponseEntity authorize(
prompt = GetterUtil.getString(prompt, "consent login");
JSONObject pkce = PKCEHelper.getPKCE();
- // TODO: Store OidcAuthentication
-
JSONObject authzData = new JSONObject()
.put("scope", scope)
.put("redirect_uri", redirectUri)
@@ -179,10 +194,27 @@ public ResponseEntity authorize(
.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"))
- .put("iss", entityMetadata.getString("client_id"))
- .put("sub", entityMetadata.getString("client_id"));
+ .put("code_challenge_method", pkce.getString("code_challenge_method"));
+
+ // TODO: do better via service
+ OidcAuthentication authzEntry = new OidcAuthentication();
+
+ authzEntry.setClientId(clientId);
+ authzEntry.setState(state);
+ authzEntry.setEndpoint(authzEndpoint);
+ authzEntry.setProvider(tc.getSub());
+ authzEntry.setProviderId(tc.getSub());
+ authzEntry.setData(authzData.toString());
+ authzEntry.setProviderJwks(providerJWKSet.toString());
+ authzEntry.setProviderConfiguration(providerMetadata.toString());
+
+ oidcAuthenticationRepository.save(authzEntry);
+
+ authzData.remove("code_verifier");
+ authzData.put("iss", entityMetadata.getString("client_id"));
+ authzData.put("sub", entityMetadata.getString("client_id"));
String requestObj = createJWS(authzData, entityJWKSet);
@@ -198,6 +230,102 @@ public ResponseEntity authorize(
.build();
}
+ @GetMapping("/callback")
+ public RedirectView callback(
+ @RequestParam Map params,
+ HttpServletRequest request, HttpServletResponse response)
+ throws Exception {
+
+ if (params.containsKey("error")) {
+ logger.error(new JSONObject(params).toString(2));
+
+ throw new Exception("TODO: Manage Error callback");
+ }
+
+ validateAuthnResponse(params);
+
+ String state = params.get("state");
+ String code = params.get("code");
+
+ List authzList = oidcAuthenticationRepository.findByState(
+ state);
+
+ if (authzList.isEmpty()) {
+ throw new Exception("oidcAuthenticationRepository");
+ }
+
+ OidcAuthentication authz = ListUtil.getLast(authzList);
+
+ // TODO create OidcAuthenticationToken
+
+ FederationEntityConfiguration entityConf =
+ federationEntityRepository.fetchBySubActive(authz.getClientId(), true);
+
+ if (entityConf == null) {
+ throw new Exception("Relay party not found");
+ }
+
+ JSONObject authzData = new JSONObject(authz.getData());
+
+ JSONObject providerConfiguration = new JSONObject(
+ authz.getProviderConfiguration());
+
+ JSONObject jsonTokenResponse = OAuth2Helper.performAccessTokenRequest(
+ authzData.optString("redirect_uri"), state, code, authz.getProviderId(),
+ entityConf, providerConfiguration.optString("token_endpoint"),
+ authzData.optString("code_verifier"));
+
+ TokenResponse tokenResponse = TokenResponse.of(jsonTokenResponse);
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("TokenResponse=" + tokenResponse.toString());
+ }
+
+ JWKSet providerJwks = JWTHelper.getJWKSetFromJSON(
+ providerConfiguration.optJSONObject("jwks"));
+
+ /*
+ JWK accessTokenJwk = JWTHelper.getJWKFromJWT(
+ tokenResponse.getAccessToken(), providerJwks);
+ JWK idTokenJwk = JWTHelper.getJWKFromJWT(
+ tokenResponse.getIdToken(), providerJwks);
+
+ if (accessTokenJwk == null || idTokenJwk == null) {
+ throw new Exception("Authentication token seems not to be valid.");
+ // Error 403
+ }
+ */
+
+ try {
+ JWTHelper.verifyJWS(tokenResponse.getAccessToken(), providerJwks);
+ }
+ catch (Exception e) {
+ throw new Exception("Authentication token validation error.");
+ // Error 403
+ }
+
+ try {
+ JWTHelper.verifyJWS(tokenResponse.getIdToken(), providerJwks);
+ }
+ catch (Exception e) {
+ throw new Exception("ID token validation error.");
+ }
+
+ // TODO Update OidcAuthenticationToken
+
+ JWKSet entityJwks = JWTHelper.getJWKSetFromJSON(entityConf.getJwks());
+
+ JSONObject userInfo = OidcHelper.getUserInfo(
+ state, tokenResponse.getAccessToken(), providerConfiguration, true,
+ entityJwks);
+
+
+ request.getSession().setAttribute("USER_INFO", userInfo.toMap());
+
+ return new RedirectView("echo_attributes");
+ }
+
+
protected TrustChain getOidcOP(String provider, String trustAnchor)
throws Exception {
@@ -327,7 +455,7 @@ else if (force || trustChain == null || trustChain.isExpired()) {
if (!tcb.isValid()) {
String msg = String.format(
- "Trsut Chain for subject %s od trust_anchor %s is not valid",
+ "Trust Chain for subject %s or trust_anchor %s is not valid",
subject, trustAnchor);
//throw new InvalidTrustchainException(msg);
@@ -361,6 +489,19 @@ else if (Validator.isNullOrEmpty(tcb.getFinalMetadata())) {
return trustChain;
}
+ // TODO: move to JWTHelper or XXXHelper?
+ protected void validateAuthnResponse(Map params) throws Exception {
+ String code = params.get("code");
+
+ if (Validator.isNullOrEmpty(params.get("code")) ||
+ Validator.isNullOrEmpty(params.get("state"))) {
+
+ //KK throw new ValidationException("Authn response object validation failed");
+ throw new Exception("Authn response object validation failed");
+ }
+ }
+
+
// TODO: move to JWTHelper?
private String getAuthorizeURL(String endpoint, JSONObject params) {
StringBuilder sb = new StringBuilder();
diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/controller/WellKnownController.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/controller/WellKnownController.java
index 3a1358e..be584fc 100644
--- a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/controller/WellKnownController.java
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/controller/WellKnownController.java
@@ -6,14 +6,12 @@
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.RSASSASigner;
-import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
@@ -93,7 +91,9 @@ public ResponseEntity wellKnownFederation(
}
}
- private void addFederationEntityConfiguration(JSONObject json) throws Exception {
+ private void addFederationEntityConfiguration(JSONObject json, JWKSet jwkSet)
+ throws Exception {
+
FederationEntityConfiguration entry = new FederationEntityConfiguration();
System.out.println(json.toString(2));
@@ -102,7 +102,7 @@ private void addFederationEntityConfiguration(JSONObject json) throws Exception
entry.setDefaultExpireMinutes(OidcConstants.FEDERATION_DEFAULT_EXP);
entry.setDefaultSignatureAlg(JWSAlgorithm.RS256.toString());
entry.setAuthorityHints(json.getJSONArray("authority_hints").toString());
- entry.setJwks(json.getJSONObject("jwks").getJSONArray("keys").toString());
+ entry.setJwks(JWTHelper.getJWKSetAsJSONArray(jwkSet, true, false).toString());
entry.setTrustMarks(json.getJSONArray("trust_marks").toString());
entry.setTrustMarksIssuers("{}");
entry.setMetadata(json.getJSONObject("metadata").toString());
@@ -128,7 +128,7 @@ private String getWellKnownData(FederationEntityConfiguration entry, boolean jso
json.put("iat", iat);
json.put("iss", entry.getSub());
json.put("sub", entry.getSub());
- json.put("jwks", jwkSet.toJSONObject());
+ json.put("jwks", JWTHelper.getJWKSetAsJSONObject(jwkSet, true));
json.put("metadata", metadataJson);
json.put("authority_hints", new JSONArray(entry.getAuthorityHints()));
json.put("trust_marks", new JSONArray(entry.getTrustMarks()));
@@ -137,6 +137,7 @@ private String getWellKnownData(FederationEntityConfiguration entry, boolean jso
return json.toString();
}
+ // TODO: Use entry jwkSet
RSAKey jwk = JWTHelper.parseRSAKey(oidcConfig.getRelyingParty().getJwk());
// Create RSA-signer with the private key
@@ -161,7 +162,7 @@ private String prepareOnboardingData(String sub, boolean jsonMode) throws Except
// Create a new one to be added to conf
// TODO: Type has to be defined by configuration
- RSAKey jwk = JWTHelper.createRSAKey(JWSAlgorithm.RS256, null);
+ RSAKey jwk = JWTHelper.createRSAKey(JWSAlgorithm.RS256, KeyUse.SIGNATURE);
JSONObject json = new JSONObject(jwk.toString());
@@ -169,13 +170,19 @@ private String prepareOnboardingData(String sub, boolean jsonMode) throws Except
"Generated jwk. Please add it into 'application.yaml'.\n" +
json.toString(2));
- return "Do OnBoarding configuration";
+ return new JSONObject()
+ .put("ERROR", "Do OnBoarding configuration")
+ .toString();
}
RSAKey jwk = JWTHelper.parseRSAKey(confJwk);
logger.info("Configured jwk\n" + jwk.toJSONString());
- logger.info("Configured public jwk\n" + jwk.toPublicJWK().toJSONString());
+
+ JSONArray jsonArray = new JSONArray()
+ .put(new JSONObject(jwk.toPublicJWK().toJSONObject()));
+
+ logger.info("Configured public jwk\n" + jsonArray.toString(2));
JWKSet jwkSet = new JWKSet(jwk);
@@ -183,14 +190,14 @@ private String prepareOnboardingData(String sub, boolean jsonMode) throws Except
RelyingParty rpConf = oidcConfig.getRelyingParty();
- rpJson.put("jwks", jwkSet.toJSONObject());
+ rpJson.put("jwks", JWTHelper.getJWKSetAsJSONObject(jwkSet, false));
rpJson.put("application_type", rpConf.getApplicationType());
rpJson.put("client_name", rpConf.getApplicationName());
rpJson.put("client_id", sub);
rpJson.put("client_registration_types", JSONUtil.asJSONArray("automatic"));
rpJson.put("contacts", rpConf.getContacts());
rpJson.put("grant_types", OidcConstants.RP_GRANT_TYPES);
- rpJson.put("response_types", OidcConstants.RP_GRANT_TYPES);
+ rpJson.put("response_types", OidcConstants.RP_RESPONSE_TYPES);
rpJson.put("redirect_uris", rpConf.getRedirectUris());
JSONObject metadataJson = new JSONObject();
@@ -205,7 +212,7 @@ private String prepareOnboardingData(String sub, boolean jsonMode) throws Except
json.put("iat", iat);
json.put("iss", sub);
json.put("sub", sub);
- json.put("jwks", jwkSet.toJSONObject());
+ json.put("jwks", JWTHelper.getJWKSetAsJSONObject(jwkSet, true));
json.put("metadata", metadataJson);
json.put(
"authority_hints", JSONUtil.asJSONArray(
@@ -219,7 +226,7 @@ private String prepareOnboardingData(String sub, boolean jsonMode) throws Except
// With the trust marks I've all the elements to store this RelyingParty into
// FederationEntryConfiguration table
- addFederationEntityConfiguration(json);
+ addFederationEntityConfiguration(json, jwkSet);
}
//logger.info("\n" + json.toString(2));
diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/storage/OidcAuthentication.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/storage/OidcAuthentication.java
new file mode 100644
index 0000000..080a876
--- /dev/null
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/storage/OidcAuthentication.java
@@ -0,0 +1,155 @@
+package it.spid.cie.oidc.spring.boot.relying.party.storage;
+
+import java.time.LocalDateTime;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "oidc_authentication")
+public class OidcAuthentication {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false)
+ private LocalDateTime created;
+
+ @Column(nullable = false)
+ private LocalDateTime modified;
+
+ @Column(name = "client_id", nullable = false)
+ private String clientId;
+
+ @Column(nullable = false)
+ private String state;
+
+ @Column(nullable = true)
+ private String endpoint;
+
+ @Column(nullable = true)
+ private String data;
+
+ @Column(nullable = false)
+ private boolean successful;
+
+ @Column(name = "provider_configuration", nullable = true)
+ private String providerConfiguration;
+
+ @Column(nullable = true)
+ private String provider;
+
+ @Column(name = "provider_id", nullable = true)
+ private String providerId;
+
+ @Column(name = "provider_jwks", nullable = true)
+ private String providerJwks;
+
+ public OidcAuthentication() {
+ this.created = LocalDateTime.now();
+ this.modified = this.created;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public LocalDateTime getCreated() {
+ return created;
+ }
+
+ public LocalDateTime getModified() {
+ return modified;
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ public boolean isSuccessful() {
+ return successful;
+ }
+
+ public String getProviderConfiguration() {
+ return providerConfiguration;
+ }
+
+ public String getProvider() {
+ return provider;
+ }
+
+ public String getProviderId() {
+ return providerId;
+ }
+
+ public String getProviderJwks() {
+ return providerJwks;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public void setCreated(LocalDateTime created) {
+ this.created = created;
+ }
+
+ public void setModified(LocalDateTime modified) {
+ this.modified = modified;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+
+ public void setSuccessful(boolean successful) {
+ this.successful = successful;
+ }
+
+ public void setProviderConfiguration(String providerConfiguration) {
+ this.providerConfiguration = providerConfiguration;
+ }
+
+ public void setProvider(String provider) {
+ this.provider = provider;
+ }
+
+ public void setProviderId(String providerId) {
+ this.providerId = providerId;
+ }
+
+ public void setProviderJwks(String providerJwks) {
+ this.providerJwks = providerJwks;
+ }
+
+
+}
diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/storage/OidcAuthenticationRepository.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/storage/OidcAuthenticationRepository.java
new file mode 100644
index 0000000..1eefe1d
--- /dev/null
+++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/storage/OidcAuthenticationRepository.java
@@ -0,0 +1,15 @@
+package it.spid.cie.oidc.spring.boot.relying.party.storage;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.data.repository.CrudRepository;
+
+public interface OidcAuthenticationRepository
+ extends CrudRepository {
+
+ public Optional findById(Long id);
+
+ public List findByState(String state);
+
+}
diff --git a/examples/relying-party-spring-boot/src/main/resources/application.yaml b/examples/relying-party-spring-boot/src/main/resources/application.yaml
index 7273a84..5f45f72 100644
--- a/examples/relying-party-spring-boot/src/main/resources/application.yaml
+++ b/examples/relying-party-spring-boot/src/main/resources/application.yaml
@@ -56,25 +56,6 @@ oidcfed:
client-id: "http://127.0.0.1:8080/oidc/rp/"
redirect-uris:
- "http://127.0.0.1:8080/oidc/rp/callback"
- jwk: |-
- {
- "p": "5oLC7spXbep6Ven40FLkuXNQGQHnq5xkOrTCA-gCntWG1hp1Fd2BZD5VxY2Hy1fK6kEJcbYI7Mo2paIVKqxPuSpFUpVlzXdDGpTF-Aa0io3rX0-0m75HQJBYMW9kbpdvYeNdbSYquwVSdh5vRWIU1OstC5Yz5xZNZYhd6oE-g20",
- "kty": "RSA",
- "q": "kBimpcf6tulZJF35SblYc5GVckSjIBJY0wsv0JXm6XlRPf4IWDzi0zHdKCzHpfCS9r8f6vImC_rkfaYOmhASGdhtagkcQSnlf3ZfMVakk-QKAwx9bVzz0C934Vt-o56EtoGf4fYHlPxTGlAD5WdwGFf7qaPGVsqsIjQJc6OCIHs",
- "d": "Bjr58dM9HJW5p0GXVaykUk6VzSUt1LiIBh-3Ep3Z_gx0iHhDpGkPiqLMSI8O4IfIpRL0MalA4H7QMCvxJ3J4Lv5J6E7COttKF7Sg9tQQB96yZjFgSc-tnp-Q1odEelSLwfM1uaX613IQSDei8MX8vNhed8iH0qMM2pVj_7r-oUU4IoF1kdvRCKuU2tBxrmwxFRFfiykwXMqgnSTjqo-5KXROwuq-NDLecsJFsOZ2XkqZOgqlJJ_X7QPubolFP01i-6G0OQcZtac650tUMYQLnbIFmeqzaZhW9clwOdruru40S-26UMNwRLrrpsRVFzVh_p34_SdtjILfNA2hmyJuKQ",
- "e": "AQAB",
- "kid": "UeA-EtNqo00UPPOjWZWMa0Rv7aDRmXr2oeHUTGX_b4s",
- "qi": "XKM7ojV-qCWW-FJc1z6qKN1NjJ09-AtLgkqQmMdv_5mXmy8MdbwKFz4qwSQjGX317IFB78Va8W9NACTMG-pm0Chb53i-5ClSXGP7SOTvePAAGHhbBqqCKrhmASoyPLVQugsmIkIfFuHOUx4MSObxCrCm1K6lP4wh1HCU4P6aNRk",
- "dp": "M2QFt50O3ud-vLa8DR3d9mZ5_glJsB3ezqPL-Xj5VJYASK1_Ww-WMFYhYzjJhJEfIRi81UgjNz9h7Y10MJ5X6807xUyfdK5ZHIz8ke5Uw-seBZLMjkhetEs6DlNqTamfYHCDPLlcn3NxTfo9DnfucwW3djTXf3aebLt5TLXhzQU",
- "alg": "RS256",
- "dq": "PIse-ejcXp4M5krVwzQtBeHVeP19zKvoxkOdA3b4XoCqsfFacDik1TfORGMMP5ylIyeKsZysf7wa5PAwkmrOMC3PSw4o4PhJhRSnSoOtArZ9vmoxCRJVHtPS-s0GmJiyCjzMgJRu-xpJkHSuLmUXpCLTiqNVYoIlcPmMPxokQqE",
- "n": "gb-_9qj0BxBexKLnx7SsC6AE2-FUgv-nYn2I7zx9l5YiFBOIT8cNj3FkP7kE7cAoF7iEe8QCa4QVEVZq1zbhICus6Vu43wPmibLGR9T0r-6dEpw52moxe9NgusgyPWtN48UT__nbgnkvcsZBmWpH-o7z5EKsOdPeStwCsb5M-eR0jQ_xFt34nTXzW6f0DDqIT9rGS7oFB-TyZgP0f31RaboUMf4hocJjVeapVCaL8c97Xlnw-ZxtvGPmELfNkWalnsc5dymHp3-wS0QIKYyfXAvxCdCwixAowcwrdvMPG8AiARjTA5wRDq18ZQMj6qqGmh3bTqxzEzNDer44DdDFXw"
- }
- trust-marks: |-
- [
- {
- 'id': 'https://www.spid.gov.it/openid-federation/agreement/sp-public/',
- 'trust_mark': 'eyJhbGciOiJSUzI1NiIsImtpZCI6IkZpZll4MDNibm9zRDhtNmdZUUlmTkhOUDljTV9TYW05VGM1bkxsb0lJcmMiLCJ0eXAiOiJ0cnVzdC1tYXJrK2p3dCJ9.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvIiwic3ViIjoiaHR0cDovLzEyNy4wLjAuMTo4MDgwL29pZGMvcnAiLCJpYXQiOjE2NDc2MzI0MjYsImlkIjoiaHR0cHM6Ly93d3cuc3BpZC5nb3YuaXQvY2VydGlmaWNhdGlvbi9ycCIsIm1hcmsiOiJodHRwczovL3d3dy5hZ2lkLmdvdi5pdC90aGVtZXMvY3VzdG9tL2FnaWQvbG9nby5zdmciLCJyZWYiOiJodHRwczovL2RvY3MuaXRhbGlhLml0L2l0YWxpYS9zcGlkL3NwaWQtcmVnb2xlLXRlY25pY2hlLW9pZGMvaXQvc3RhYmlsZS9pbmRleC5odG1sIn0.bBl24BqBjLV5YZ-vjSZvOrEzzUqyl1TCHCUWTD2Xk_nyweo-RjBqv0YLRBNqCidJPY0VhXae8hf_JYXT85MMHLWWPGC-BTIbSWI9X7jVgemJd9n63IcbsCe5ZtYnBONra9gl02k6htO3NZ27VsELtARZG4yE7rbmGDCCLesQl0IpcI_0Auuvtx4ySt5ljAa-Vq6QVPIVf-Cf0610lnxVD4xK8mUd90ZEu11mJUVmO5OJUB210scScHMg7bIbOkJu3KfMWbEmJmhGsktBygqV668e-28ntYVlPTbzLx9KmB0J5vsyLLXzC-uhXz1M2K93uvoMVJmo2ZDUKDC7vCzLeg'
- }
- ]
+ jwk: ""
+ trust-marks: ""
diff --git a/examples/relying-party-spring-boot/src/main/resources/sql/schema.sql b/examples/relying-party-spring-boot/src/main/resources/sql/schema.sql
index 903b832..8f5e566 100644
--- a/examples/relying-party-spring-boot/src/main/resources/sql/schema.sql
+++ b/examples/relying-party-spring-boot/src/main/resources/sql/schema.sql
@@ -48,4 +48,21 @@ CREATE TABLE IF NOT EXISTS federation_entity_configuration (
UNIQUE(sub)
);
+CREATE TABLE IF NOT EXISTS oidc_authentication (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ created TIMESTAMP(0) NOT NULL,
+ modified TIMESTAMP(0) NOT NULL,
+ client_id VARCHAR NOT NULL,
+ state VARCHAR NOT NULL,
+ endpoint VARCHAR NULL,
+ data VARCHAR NULL,
+ successful BOOLEAN NOT NULL,
+ provider_configuration VARCHAR NULL,
+ provider VARCHAR NULL,
+ provider_id VARCHAR NULL,
+ provider_jwks VARCHAR NULL,
+ UNIQUE(state)
+);
+
+
diff --git a/examples/relying-party-spring-boot/src/main/resources/templates/echo_attributes.html b/examples/relying-party-spring-boot/src/main/resources/templates/echo_attributes.html
new file mode 100644
index 0000000..c9b737f
--- /dev/null
+++ b/examples/relying-party-spring-boot/src/main/resources/templates/echo_attributes.html
@@ -0,0 +1,331 @@
+
+
+
+
+
+
+
+
+ OIDC Relying Party
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
User attributes
+
+
+
+
+
+
+
Log out
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+