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 + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+
+
+ + + OIDC Relying Party + + + + +
+ + + + +
+ +
+ +
+ +
+
+
+
+
+ + +
+
+
+
+
+
+ + + + + + + +
+ +
+ Seguici su + +
+ +
+ + Cerca + + + + + + +
+
+
+
+
+
+
+ + + +
+
+ + + + + +
+ +
+
+
+
+
+
+
+
+ + +
+

User attributes

+ +
+
+
+
+ + Log out + +
+ + +
+
+
+
+
+
+
+
+ + +
+ + + + + + + + + + +