From becc1a8db03481523f8ea9b94c9138af98e4ce8c Mon Sep 17 00:00:00 2001 From: Luca Trubbiani Date: Tue, 23 Jul 2024 08:59:53 +0200 Subject: [PATCH 01/10] Implement JwtTokenService and .well-known jwks controller --- application/pom.xml | 7 +- .../core/components/run/RunManager.java | 10 +- modules/authorization/.flattened-pom.xml | 101 +++++++++++++++ modules/authorization/pom.xml | 105 ++++++++++++++++ .../components/JWKSetKeyStore.java | 74 +++++++++++ .../authorization/config/KeyStoreConfig.java | 57 +++++++++ .../controllers/JWKController.java | 33 +++++ .../services/JwtTokenService.java | 116 ++++++++++++++++++ .../authorization/utils/JWKUtils.java | 83 +++++++++++++ pom.xml | 1 + 10 files changed, 584 insertions(+), 3 deletions(-) create mode 100644 modules/authorization/.flattened-pom.xml create mode 100644 modules/authorization/pom.xml create mode 100644 modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java create mode 100644 modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java create mode 100644 modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java create mode 100644 modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java create mode 100644 modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/utils/JWKUtils.java diff --git a/application/pom.xml b/application/pom.xml index a7554cb2..6d2334eb 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -271,7 +271,12 @@ it.smartcommunitylabdhub dh-files ${revision} - + + + it.smartcommunitylabdhub + dh-authorization + ${revision} + diff --git a/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java b/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java index 4aac7ec8..7363c6a0 100644 --- a/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java +++ b/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java @@ -1,5 +1,6 @@ package it.smartcommunitylabdhub.core.components.run; +import it.smartcommunitylabdhub.authorization.services.JwtTokenService; import it.smartcommunitylabdhub.commons.accessors.fields.StatusFieldAccessor; import it.smartcommunitylabdhub.commons.accessors.spec.RunSpecAccessor; import it.smartcommunitylabdhub.commons.events.RunnableChangedEvent; @@ -78,6 +79,9 @@ public class RunManager { @Autowired ProcessorRegistry processorRegistry; + @Autowired + JwtTokenService jwtTokenService; + public Run build(@NotNull Run run) throws NoSuchEntityException { // GET state machine, init state machine with status RunBaseSpec runBaseSpec = new RunBaseSpec(); @@ -197,8 +201,10 @@ public Run run(@NotNull Run run) throws NoSuchEntityException, InvalidTransactio //TODO refactor properly if (r instanceof SecuredRunnable) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null) { - ((SecuredRunnable) r).setCredentials(auth); + + String token = jwtTokenService.generateToken(auth); + if (token != null) { + ((SecuredRunnable) r).setCredentials(token); } } diff --git a/modules/authorization/.flattened-pom.xml b/modules/authorization/.flattened-pom.xml new file mode 100644 index 00000000..922f1e00 --- /dev/null +++ b/modules/authorization/.flattened-pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + it.smartcommunitylabdhub + dh-authorization + 0.6.0-SNAPSHOT + + + it.smartcommunitylabdhub + dh-commons + 0.6.0-SNAPSHOT + compile + + + org.springframework.boot + spring-boot-starter-security + 3.2.0 + compile + + + org.springframework.boot + spring-boot-starter-oauth2-authorization-server + 3.2.0 + compile + + + org.bouncycastle + bcpkix-jdk18on + 1.78.1 + compile + + + org.bouncycastle + bcprov-jdk18on + 1.78.1 + compile + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + 3.2.0 + compile + + + com.nimbusds + nimbus-jose-jwt + 9.39.3 + compile + + + org.projectlombok + lombok + 1.18.30 + compile + true + + + com.fasterxml.jackson.core + jackson-core + 2.15.3 + compile + + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + 2.15.3 + compile + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.15.3 + compile + + + com.fasterxml.jackson.module + jackson-module-jsonSchema + 2.15.3 + compile + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.15.3 + compile + + + org.slf4j + slf4j-api + 2.0.9 + compile + + + org.slf4j + log4j-over-slf4j + 2.0.9 + compile + + + diff --git a/modules/authorization/pom.xml b/modules/authorization/pom.xml new file mode 100644 index 00000000..feea5d22 --- /dev/null +++ b/modules/authorization/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + it.smartcommunitylabdhub + digitalhub-core + ${revision} + ../../ + + it.smartcommunitylabdhub + dh-authorization + authorization + DHCore authorization + + + + it.smartcommunitylabdhub + dh-commons + ${revision} + + + org.springframework.boot + spring-boot-starter-security + 3.2.0 + + + + org.springframework.boot + spring-boot-starter-oauth2-authorization-server + 3.2.0 + + + com.google.guava + guava + 20.0 + + + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + 3.2.0 + + + + com.nimbusds + nimbus-jose-jwt + 9.39.3 + + + org.projectlombok + lombok + ${lombok.version} + compile + true + + + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot.version} + test + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + ${jackson.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + + + com.fasterxml.jackson.module + jackson-module-jsonSchema + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + log4j-over-slf4j + ${slf4j.version} + + + + \ No newline at end of file diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java new file mode 100644 index 00000000..466ab6e4 --- /dev/null +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java @@ -0,0 +1,74 @@ +package it.smartcommunitylabdhub.authorization.components; + +import com.google.common.io.CharStreams; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import it.smartcommunitylabdhub.authorization.utils.JWKUtils; +import org.springframework.core.io.Resource; +import com.google.common.base.Charsets; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.text.ParseException; +import java.util.Optional; +import java.util.UUID; + +public class JWKSetKeyStore { + + private final JWKSet jwkSet; + private String kid; + + + public JWKSetKeyStore(Resource location, String kid) { + this.jwkSet = loadJwkSet(location); + if (kid == null) { + this.kid = jwkSet.getKeys().getFirst().getKeyID(); // prendo la prima + // se è null, non ci sono chiavi assert + Assert.notNull(this.kid, "Key ID cannot be null"); + } else { + this.kid = jwkSet.getKeyByKeyId(kid).getKeyID(); + Assert.notNull(this.kid, "Key ID cannot be null"); + } + } + + public JWKSetKeyStore() throws JOSEException { + this.jwkSet = initializeJwkSet(); + this.kid = this.jwkSet.getKeys().getFirst().getKeyID(); + Assert.notNull(this.kid, "Key ID cannot be null"); + } + + private static JWKSet loadJwkSet(Resource location) { + Assert.notNull(location, "Key Set resource cannot be null"); + if (location.exists() && location.isReadable()) { + try { + String s = CharStreams.toString( + new InputStreamReader(location.getInputStream(), Charsets.UTF_8) + ); + return JWKSet.parse(s); + } catch (IOException e) { + throw new IllegalArgumentException("Key Set resource could not be read: " + location); + } catch (ParseException e) { + throw new IllegalArgumentException("Key Set resource could not be parsed: " + location); + } + } else { + throw new IllegalArgumentException("Key Set resource could not be read: " + location); + } + } + + private static JWKSet initializeJwkSet() throws JOSEException { + String kid = UUID.randomUUID().toString(); + JWK jwk = JWKUtils.generateRsaJWK(kid, "sig", "RS256", 2048); + return new JWKSet(jwk); + } + + + public JWKSet getJwkSet() { + return jwkSet; + } + + public JWK getJwk() { + return jwkSet.getKeyByKeyId(kid); + } +} \ No newline at end of file diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java new file mode 100644 index 00000000..2871b8ac --- /dev/null +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java @@ -0,0 +1,57 @@ +package it.smartcommunitylabdhub.authorization.config; + + +import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; +import it.smartcommunitylabdhub.authorization.utils.JWKUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; + + +@Configuration +public class KeyStoreConfig { + + @Value("${keystore.path:}") + private Resource path; + + @Value("${keystore.kid:}") + private String kid; + + private JWKSetKeyStore keyStore; + + + @Bean + @Primary + public JWKSetKeyStore getJWKSetKeyStore() throws JOSEException { + if (keyStore == null) { + if (path != null) { + // load from resource + keyStore = load(path); + // check if empty + if (keyStore.getJwk() == null) { + // discard, we will generate a new one + keyStore = null; + } + } + } + + if (keyStore == null) { + // generate new in-memory keystore + keyStore = generate(); + } + return keyStore; + } + + private JWKSetKeyStore load(Resource location) { + return new JWKSetKeyStore(location, kid); + } + + private JWKSetKeyStore generate() throws JOSEException { + return new JWKSetKeyStore(); + } +} diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java new file mode 100644 index 00000000..a5b223f2 --- /dev/null +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java @@ -0,0 +1,33 @@ +package it.smartcommunitylabdhub.authorization.controllers; + + + + +import com.nimbusds.jose.jwk.JWKSet; +import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping("/api/.well-known/jwks.json") +@Slf4j +public class JWKController { + + @Autowired + private JWKSetKeyStore jwkSetKeyStore; + + @GetMapping("") + public ResponseEntity> getJWKInfo() { + JWKSet jwkSet = jwkSetKeyStore.getJwkSet(); + // Convert JWKSet to a map for easier JSON serialization + Map jwkSetMap = jwkSet.toJSONObject(); + return ResponseEntity.ok(jwkSetMap); + } +} + diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java new file mode 100644 index 00000000..f37c546a --- /dev/null +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java @@ -0,0 +1,116 @@ +package it.smartcommunitylabdhub.authorization.services; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; +import it.smartcommunitylabdhub.commons.config.ApplicationProperties; +import it.smartcommunitylabdhub.commons.config.SecurityProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Value; +import java.security.interfaces.RSAPrivateKey; +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +@Service +@Slf4j +public class JwtTokenService { + + @Autowired + private JWKSetKeyStore keyStoreUtil; + + + @Autowired + ApplicationProperties applicationProperties; + + @Autowired + SecurityProperties securityProperties; + + // Default to 30 days in milliseconds (30 * 24 * 60 * 60 * 1000) + @Value("${jwt.expiration:2592000000}") + private long jwtExpiration; + + @Value("${jwt.keyId:kid}") + private String keyId; + + public String generateToken(Authentication authentication) + throws JwtTokenServiceException { + try { + + // check that jwt is enabled via securityProperties + if (!securityProperties.getJwt().isEnabled()) { + return null; + } + + // Extract claims from authentication if it's a JwtAuthenticationToken + Map additionalClaims = extractClaims(authentication); + + + JWK jwk = keyStoreUtil.getJwk(); + RSAPrivateKey privateKey = jwk.toRSAKey().toRSAPrivateKey(); + RSASSASigner signer = new RSASSASigner(privateKey); + + // Prepare JWT claims + JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder() + .subject(authentication.getName()) + .issuer(applicationProperties.getEndpoint()) + .issueTime(new Date()) + .audience(applicationProperties.getName()) + .jwtID(UUID.randomUUID().toString()) + .expirationTime(new Date(System.currentTimeMillis() + jwtExpiration)); + + + // Add additional claims from the authentication token + if (additionalClaims != null) { + additionalClaims.forEach(claimsSetBuilder::claim); + } + + JWTClaimsSet claimsSet = claimsSetBuilder.build(); + + // Create signed JWT + SignedJWT signedJWT = new SignedJWT( + new JWSHeader.Builder(JWSAlgorithm.RS256) + .keyID(jwk.getKeyID()).build(), + claimsSet); + + // Compute the RSA signature + signedJWT.sign(signer); + + // Serialize to compact form + String jwtToken = signedJWT.serialize(); + + log.info("Generated JWT token: {}", jwtToken); + + return jwtToken; + } catch (JOSEException e) { + log.error("Error generating JWT token", e); + return null; + } + } + + + private Map extractClaims(Authentication authentication) { + if (authentication instanceof JwtAuthenticationToken jwtAuthToken) { + Jwt jwt = jwtAuthToken.getToken(); + return jwt.getClaims(); + } else { + log.warn("Authentication is not of type JwtAuthenticationToken"); + return null; + } + } + + public static class JwtTokenServiceException extends RuntimeException { + public JwtTokenServiceException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/utils/JWKUtils.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/utils/JWKUtils.java new file mode 100644 index 00000000..60eb60c2 --- /dev/null +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/utils/JWKUtils.java @@ -0,0 +1,83 @@ +package it.smartcommunitylabdhub.authorization.utils; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.gen.ECKeyGenerator; +import com.nimbusds.jose.jwk.gen.OctetSequenceKeyGenerator; +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; +import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JWKUtils { + + private static final Logger logger = LoggerFactory.getLogger(JWKUtils.class); + + public static JWK generateRsaJWK(String id, String usage, String alg, int length) + throws IllegalArgumentException, JOSEException { + logger.debug( + "generate RSA jwk for " + + id + + " use " + + usage + + " with length " + + String.valueOf(length) + + " with algorithm " + + alg + ); + + if (id == null || id.isEmpty()) { + id = UUID.randomUUID().toString(); + } + + // validate keyUse + KeyUse use = new KeyUse(usage); + + // validate algorithm + JWSAlgorithm algo = JWSAlgorithm.parse(alg); + + return new RSAKeyGenerator(length).keyUse(use).keyID(id).algorithm(algo).generate(); + } + + public static JWK generateECJWK(String id, String usage, String alg, String curve) + throws IllegalArgumentException, JOSEException { + logger.debug( + "generate EC jwk for " + id + " use " + usage + " with curve " + curve + " with algorithm " + alg + ); + + if (id == null || id.isEmpty()) { + id = UUID.randomUUID().toString(); + } + + // validate keyUse + KeyUse use = new KeyUse(usage); + + // validate curve + Curve ecurve = Curve.parse(curve); + + // validate algorithm + JWSAlgorithm algo = JWSAlgorithm.parse(alg); + + return new ECKeyGenerator(ecurve).keyUse(use).keyID(id).algorithm(algo).generate(); + } + + public static JWK generateHMACJWT(String id, String usage, String alg, int length) + throws IllegalArgumentException, JOSEException { + logger.debug("generate HMAC jwk for " + id + " use " + usage + " with algorithm " + alg); + + if (id == null || id.isEmpty()) { + id = UUID.randomUUID().toString(); + } + + // validate keyUse + KeyUse use = new KeyUse(usage); + + // validate algorithm + JWSAlgorithm algo = JWSAlgorithm.parse(alg); + + return new OctetSequenceKeyGenerator(length).keyID(id).keyUse(use).algorithm(algo).generate(); + } +} diff --git a/pom.xml b/pom.xml index 7439f784..9a627d4e 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,7 @@ modules/commons + modules/authorization modules/fsm modules/framework-k8s modules/framework-kaniko From 669aa2b9d8fb9189225b34cc53c2a176e2648027 Mon Sep 17 00:00:00 2001 From: Luca Trubbiani Date: Tue, 23 Jul 2024 16:03:56 +0200 Subject: [PATCH 02/10] Add token exchange for k8s services authorization --- application/key.jwks | 18 + .../core/components/run/RunManager.java | 955 +++++++++--------- .../core/config/SecurityConfig.java | 145 ++- .../src/main/resources/application.yml | 56 +- modules/authorization/.flattened-pom.xml | 12 +- .../components/JWKSetKeyStore.java | 3 +- .../controllers/JWKController.java | 7 +- .../services/JwtTokenService.java | 22 +- .../commons/config/SecurityProperties.java | 17 + 9 files changed, 653 insertions(+), 582 deletions(-) create mode 100644 application/key.jwks diff --git a/application/key.jwks b/application/key.jwks new file mode 100644 index 00000000..e54b2611 --- /dev/null +++ b/application/key.jwks @@ -0,0 +1,18 @@ +{ + "keys": [ + { + "p": "4LDzu4V1h2jVexI9u8Uw4AU067QxW1Puu2T4V9Y3krGru6xnnqJ9G065jAOHrbfch0QszcKr4gIBepStWR0TgWlDu4-0harN8zkUyQfKF3YrOOwjmfiCmoz-ezBNwUreGyAFKyCKEI1d6myCjQ9QmoCuVYcF1sFrg480jvMh9u0", + "kty": "RSA", + "q": "l4qu8WuDYMUCJnem6iEUySW8Dq0Iq5ZzWADKSvz-Lks67pouKLp2O8QsVpztmxxm6CmLmZS6tzO-Uf9wVAdLSju5bGSMvz-J7wwbsdoBFHnFjh3gpy4Az_ZZIZFz8sxgNo6f7wS0R5n2j10vgW2dL1oixFnN0l4jLCkR4vsI6ec", + "d": "MuRedw79v5f3a4ejPSVCXwbkiK27LYXNApPHVCNh5-eKy0FTRBMIRTKRgJfp2cePWQUHtQmI-MbTD7PxLu6AWjgQxWrFFddQl007pv2fst3B5gGZO-DrLL0G77RWekh3QxMA6Zxib6DAg7rGs9LmMkRdZjq69vhPYtAykVNvhyjqx1H5rIyqhC7chfrlZPwgoEaIOuszTjDlRu1arnaw8fEbTjxMLKRhEv-__i-QhXxvACi6io5SL_acce3JpqYIpa5Q3McibMETFp0WtLEJT1MV_Owe2u2bJ2kfVeoT_3MucWGBNZY2_JF-ypOzGoqghhPrwKDiu_m_k8izm6JfoQ", + "e": "AQAB", + "use": "sig", + "kid": "kid1", + "qi": "Wpc4HLLhx4DQdWdmXDcI2jEcB21mrPMzXOvV6m7BrVwr5Wv1CkUwMLcvFMfMTMvMm9T8A6NPbMjtQanYlzaE9rRLxMjC9qad0K8jKRhS29_Ua-NrTxs5LEa_rC_KTIUPQwJKRiLQDMqB-1tE6Wx-0f-X2xr-edqw0xFrKGG5f_w", + "dp": "sQPXUQGogQBfRzEsv3Rvt92CPrtcMxYhuzKl0BVs_L3KjPUbQUfEBjJ0TVEun_Z833gfUA8w-MIZxifBlbYpeseiW-6wCqpwzFBJBsODPA6VqKeouJGm2vl_Ny_r4f8Ikhgc2kgtXTLyLVBFXboWB1KfhbP1LqZ8_E1i28fCF1U", + "alg": "RS256", + "dq": "kJfe_LOdE4Rm3NVmCheugY7jtQ0dLLvNKrb2Tj6_OOTHQksSqAHiNnyj4bCydkHPXs3lSZFD5vIUMqEMSOD80cPS1L3MHd8-eTyQYE6moSbffQ69AEdAty1TNlhWzeB8HRJRP_q5GWiqfXhoj5JLXJcgfUFCxerUnJpDWtQ0WRk", + "n": "hQIYsylqcdO5TtgsJsyVLSLzdYIFGMwfnF5ZrLj3aszxH5uxKQXNXaplD2jZwQumVJMuCwnSNK0Z_1Yhwn97dEOSspIN-NuZZ_yZlgDNp4YH9eiNbf4xjQIewcI81NNh1oitb6iSJtOc5XMPN6z14nYDIUhVHomuhgTfofm4UZmvSDh4vO4yy3uYHTkXGGSr8t12su7z3ALM30YaY_yzO5BuuvaVmAE96Z8QyoO6KM2rbCpx6Rym_rXryF4m4NMWvRy_hw9RnT12IjxV_AxoLIEQYWLT_InuIy02P7_tHP4vbTmwfmxU1WCY8kVeYsVPhIkA7uF_W2MHPyirY8uE2w" + } + ] +} \ No newline at end of file diff --git a/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java b/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java index 7363c6a0..505f9675 100644 --- a/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java +++ b/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java @@ -3,6 +3,7 @@ import it.smartcommunitylabdhub.authorization.services.JwtTokenService; import it.smartcommunitylabdhub.commons.accessors.fields.StatusFieldAccessor; import it.smartcommunitylabdhub.commons.accessors.spec.RunSpecAccessor; +import it.smartcommunitylabdhub.commons.config.SecurityProperties; import it.smartcommunitylabdhub.commons.events.RunnableChangedEvent; import it.smartcommunitylabdhub.commons.events.RunnableMonitorObject; import it.smartcommunitylabdhub.commons.exceptions.NoSuchEntityException; @@ -34,6 +35,7 @@ import it.smartcommunitylabdhub.fsm.exceptions.InvalidTransactionException; import it.smartcommunitylabdhub.fsm.types.RunStateMachineFactory; import jakarta.validation.constraints.NotNull; + import java.io.Serializable; import java.util.HashMap; import java.util.List; @@ -41,6 +43,7 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; + import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; @@ -82,6 +85,9 @@ public class RunManager { @Autowired JwtTokenService jwtTokenService; + @Autowired + SecurityProperties securityProperties; + public Run build(@NotNull Run run) throws NoSuchEntityException { // GET state machine, init state machine with status RunBaseSpec runBaseSpec = new RunBaseSpec(); @@ -92,14 +98,14 @@ public Run build(@NotNull Run run) throws NoSuchEntityException { // Retrieve Executable String executableId = runSpecAccessor.getVersion(); Executable executable = executableEntityServiceProvider - .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) - .get(executableId); + .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) + .get(executableId); // Retrieve Task Specification where = Specification.allOf( - CommonSpecification.projectEquals(executable.getProject()), - createExecutableSpecification(TaskUtils.buildString(executable)), - createTaskKindSpecification(runSpecAccessor.getTask()) + CommonSpecification.projectEquals(executable.getProject()), + createExecutableSpecification(TaskUtils.buildString(executable)), + createTaskKindSpecification(runSpecAccessor.getTask()) ); Task task = taskEntityService.searchAll(where).stream().findFirst().orElse(null); @@ -108,26 +114,26 @@ public Run build(@NotNull Run run) throws NoSuchEntityException { // Add Internal logic to be executed when state change from CREATED to READY fsm - .getState(State.CREATED) - .getTransaction(RunEvent.BUILD) - .setInternalLogic((context, input, fsmInstance) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - - // Build RunSpec using Runtime now if wrong type is passed to a specific runtime - // an exception occur! for. - RunBaseSpec runSpecBuilt = runtime.build(executable, task, run); - - return Optional.of(runSpecBuilt); - } - return Optional.empty(); - }); + .getState(State.CREATED) + .getTransaction(RunEvent.BUILD) + .setInternalLogic((context, input, fsmInstance) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + + // Build RunSpec using Runtime now if wrong type is passed to a specific runtime + // an exception occur! for. + RunBaseSpec runSpecBuilt = runtime.build(executable, task, run); + + return Optional.of(runSpecBuilt); + } + return Optional.empty(); + }); try { // Update run state to BUILT @@ -166,32 +172,32 @@ public Run run(@NotNull Run run) throws NoSuchEntityException, InvalidTransactio // Retrieve Executable String executableId = runSpecAccessor.getVersion(); Executable executable = executableEntityServiceProvider - .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) - .get(executableId); + .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) + .get(executableId); // Retrieve state machine Fsm> fsm = createFsm(run); fsm - .getState(State.BUILT) - .getTransaction(RunEvent.RUN) - .setInternalLogic((context, input, stateMachine) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - // Create Runnable - RunRunnable runnable = runtime.run(run); - - return Optional.of(runnable); - } else { - return Optional.empty(); - } - }); + .getState(State.BUILT) + .getTransaction(RunEvent.RUN) + .setInternalLogic((context, input, stateMachine) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + // Create Runnable + RunRunnable runnable = runtime.run(run); + + return Optional.of(runnable); + } else { + return Optional.empty(); + } + }); try { Optional runnable = fsm.goToState(State.READY, null); @@ -200,11 +206,17 @@ public Run run(@NotNull Run run) throws NoSuchEntityException, InvalidTransactio //extract auth from security context to inflate secured credentials //TODO refactor properly if (r instanceof SecuredRunnable) { + // check that jwt is enabled via securityProperties Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - - String token = jwtTokenService.generateToken(auth); - if (token != null) { - ((SecuredRunnable) r).setCredentials(token); + if (auth != null) { + if (securityProperties.getJwt().isEnabled()) { + String token = jwtTokenService.generateToken(auth); + if (token != null) { + ((SecuredRunnable) r).setCredentials(token); + } + } else { + ((SecuredRunnable) r).setCredentials(auth); + } } } @@ -221,7 +233,8 @@ public Run run(@NotNull Run run) throws NoSuchEntityException, InvalidTransactio log.debug("Invalid transaction from state {} to state {}", e.getFromState(), e.getToState()); throw e; } - } catch (StoreException e) { + } catch ( + StoreException e) { log.error("store error: {}", e.getMessage()); throw new SystemException(e.getMessage()); } @@ -236,32 +249,32 @@ public Run stop(@NotNull Run run) throws NoSuchEntityException { // Retrieve Executable String executableId = runSpecAccessor.getVersion(); Executable executable = executableEntityServiceProvider - .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) - .get(executableId); + .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) + .get(executableId); // Retrieve state machine Fsm> fsm = createFsm(run); fsm - .getState(State.RUNNING) - .getTransaction(RunEvent.STOP) - .setInternalLogic((context, input, stateMachine) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - // Create Runnable - RunRunnable runnable = runtime.stop(run); - - return Optional.of(runnable); - } else { - return Optional.empty(); - } - }); + .getState(State.RUNNING) + .getTransaction(RunEvent.STOP) + .setInternalLogic((context, input, stateMachine) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + // Create Runnable + RunRunnable runnable = runtime.stop(run); + + return Optional.of(runnable); + } else { + return Optional.empty(); + } + }); try { Optional runnable = fsm.goToState(State.STOP, null); @@ -294,93 +307,93 @@ public Run delete(@NotNull Run run) throws NoSuchEntityException { // Retrieve Executable String executableId = runSpecAccessor.getVersion(); Executable executable = executableEntityServiceProvider - .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) - .get(executableId); + .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) + .get(executableId); // Retrieve state machine Fsm> fsm = createFsm(run); fsm - .getState(State.RUNNING) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, stateMachine) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - // Create Runnable - RunRunnable runnable = runtime.delete(run); - - return Optional.ofNullable(runnable); - } else { - return Optional.empty(); - } - }); + .getState(State.RUNNING) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, stateMachine) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + // Create Runnable + RunRunnable runnable = runtime.delete(run); + + return Optional.ofNullable(runnable); + } else { + return Optional.empty(); + } + }); fsm - .getState(State.STOPPED) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, stateMachine) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - // Create Runnable - RunRunnable runnable = runtime.delete(run); - - return Optional.ofNullable(runnable); - } else { - return Optional.empty(); - } - }); + .getState(State.STOPPED) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, stateMachine) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + // Create Runnable + RunRunnable runnable = runtime.delete(run); + + return Optional.ofNullable(runnable); + } else { + return Optional.empty(); + } + }); fsm - .getState(State.ERROR) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, stateMachine) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - // Create Runnable - RunRunnable runnable = runtime.delete(run); - - return Optional.ofNullable(runnable); - } else { - return Optional.empty(); - } - }); + .getState(State.ERROR) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, stateMachine) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + // Create Runnable + RunRunnable runnable = runtime.delete(run); + + return Optional.ofNullable(runnable); + } else { + return Optional.empty(); + } + }); fsm - .getState(State.COMPLETED) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, stateMachine) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - // Create Runnable - RunRunnable runnable = runtime.delete(run); - - return Optional.ofNullable(runnable); - } else { - return Optional.empty(); - } - }); + .getState(State.COMPLETED) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, stateMachine) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + // Create Runnable + RunRunnable runnable = runtime.delete(run); + + return Optional.ofNullable(runnable); + } else { + return Optional.empty(); + } + }); try { Optional runnable = fsm.goToState(State.DELETING, null); @@ -421,62 +434,62 @@ public void onChangedEvent(RunnableChangedEvent event) throws Store // Use service to retrieve the run and check if state is changed Optional - .ofNullable(entityService.find(runnableMonitorObject.getRunId())) - .ifPresentOrElse( - run -> { - try { - if ( - //either signal an update or track progress (running state) - !Objects.equals( - StatusFieldAccessor.with(run.getStatus()).getState(), - runnableMonitorObject.getStateId() - ) || - State.RUNNING == State.valueOf(runnableMonitorObject.getStateId()) - ) { - switch (State.valueOf(runnableMonitorObject.getStateId())) { - case COMPLETED: - onCompleted(run, event); - break; - case ERROR: - onError(run, event); - break; - case RUNNING: - onRunning(run, event); - break; - case STOPPED: - onStopped(run, event); - break; - case DELETED: - onDeleted(run, event); - break; - default: + .ofNullable(entityService.find(runnableMonitorObject.getRunId())) + .ifPresentOrElse( + run -> { + try { + if ( + //either signal an update or track progress (running state) + !Objects.equals( + StatusFieldAccessor.with(run.getStatus()).getState(), + runnableMonitorObject.getStateId() + ) || + State.RUNNING == State.valueOf(runnableMonitorObject.getStateId()) + ) { + switch (State.valueOf(runnableMonitorObject.getStateId())) { + case COMPLETED: + onCompleted(run, event); + break; + case ERROR: + onError(run, event); + break; + case RUNNING: + onRunning(run, event); + break; + case STOPPED: + onStopped(run, event); + break; + case DELETED: + onDeleted(run, event); + break; + default: + log.debug( + "State {} for run id {} not managed", + runnableMonitorObject.getStateId(), + runnableMonitorObject.getRunId() + ); + break; + } + } else { log.debug( - "State {} for run id {} not managed", - runnableMonitorObject.getStateId(), - runnableMonitorObject.getRunId() + "State {} for run id {} not changed", + runnableMonitorObject.getStateId(), + runnableMonitorObject.getRunId() ); - break; + } + } catch (StoreException e) { + log.error("store error for {}:{}", runnableMonitorObject.getRunId(), e.getMessage()); } - } else { - log.debug( - "State {} for run id {} not changed", - runnableMonitorObject.getStateId(), - runnableMonitorObject.getRunId() - ); + }, + () -> { + log.error("Run with id {} not found", runnableMonitorObject.getRunId()); } - } catch (StoreException e) { - log.error("store error for {}:{}", runnableMonitorObject.getRunId(), e.getMessage()); - } - }, - () -> { - log.error("Run with id {} not found", runnableMonitorObject.getRunId()); - } - ); + ); } // Callback Methods private void onRunning(Run run, RunnableChangedEvent event) - throws NoSuchEntityException, StoreException { + throws NoSuchEntityException, StoreException { // Try to move forward state machine based on current state Fsm> fsm = createFsm(run); @@ -485,72 +498,72 @@ private void onRunning(Run run, RunnableChangedEvent event) // Retrieve Runtime Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); // Define logic for state READY fsm - .getState(State.READY) - .getTransaction(RunEvent.EXECUTE) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state RUNNING, " + "event :{}, input: {}", - RunEvent.EXECUTE, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); - } + .getState(State.READY) + .getTransaction(RunEvent.EXECUTE) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state RUNNING, " + "event :{}, input: {}", + RunEvent.EXECUTE, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); + } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onRunning(run, runnable); - return Optional.ofNullable(runStatus); - }); + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onRunning(run, runnable); + return Optional.ofNullable(runStatus); + }); fsm - .getState(State.RUNNING) - .getTransaction(RunEvent.LOOP) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state RUNNING, " + "event :{}, input: {}", - RunEvent.LOOP, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onRunning(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.RUNNING) + .getTransaction(RunEvent.LOOP) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state RUNNING, " + "event :{}, input: {}", + RunEvent.LOOP, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onRunning(run, runnable); + return Optional.ofNullable(runStatus); + }); try { //TODO call registry processor to retrieve all processor for onRunning and call process() Optional runStatus = fsm.goToState(State.RUNNING, null); // Update run status RunBaseStatus runBaseStatus = runStatus - .map(r -> { - r.setState(State.RUNNING.toString()); - return r; - }) - .orElseGet(() -> new RunBaseStatus(State.RUNNING.toString())); + .map(r -> { + r.setState(State.RUNNING.toString()); + return r; + }) + .orElseGet(() -> new RunBaseStatus(State.RUNNING.toString())); RunRunnable runRunnable = event != null ? event.getRunnable() : null; // Iterate over all processor and store all RunBaseStatus as optional List processorsStatus = processorRegistry - .getProcessors("onRunning") - .stream() - .map(processor -> processor.process(run, runRunnable, runBaseStatus)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .getProcessors("onRunning") + .stream() + .map(processor -> processor.process(run, runRunnable, runBaseStatus)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); Map runStatusMap = processorsStatus - .stream() - .map(RunBaseStatus::toMap) - .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); + .stream() + .map(RunBaseStatus::toMap) + .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); run.setStatus(MapUtils.mergeMultipleMaps(run.getStatus(), runBaseStatus.toMap(), runStatusMap)); @@ -568,7 +581,7 @@ private void onRunning(Run run, RunnableChangedEvent event) * @throws NoSuchEntityException if the entity being accessed does not exist */ private void onCompleted(Run run, RunnableChangedEvent event) - throws NoSuchEntityException, StoreException { + throws NoSuchEntityException, StoreException { // Try to move forward state machine based on current state Fsm> fsm = createFsm(run); @@ -577,70 +590,70 @@ private void onCompleted(Run run, RunnableChangedEvent event) // Retrieve Runtime Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); // Define logic for state RUNNING fsm - .getState(State.RUNNING) - .getTransaction(RunEvent.COMPLETE) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state RUNNING, " + "event :{}, input: {}", - RunEvent.COMPLETE, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onComplete(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.RUNNING) + .getTransaction(RunEvent.COMPLETE) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state RUNNING, " + "event :{}, input: {}", + RunEvent.COMPLETE, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onComplete(run, runnable); + return Optional.ofNullable(runStatus); + }); fsm - .getState(State.COMPLETED) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state COMPLETED, " + "event :{}, input: {}", - RunEvent.DELETING, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state COMPLETED, " + "context: {}", context); - } - RunRunnable runnable = runtime.delete(run); - return Optional.ofNullable(runnable); - }); + .getState(State.COMPLETED) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state COMPLETED, " + "event :{}, input: {}", + RunEvent.DELETING, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state COMPLETED, " + "context: {}", context); + } + RunRunnable runnable = runtime.delete(run); + return Optional.ofNullable(runnable); + }); try { Optional runStatus = fsm.goToState(State.COMPLETED, null); RunBaseStatus runBaseStatus = runStatus - .map(r -> { - r.setState(State.COMPLETED.toString()); - return r; - }) - .orElseGet(() -> new RunBaseStatus(State.COMPLETED.toString())); + .map(r -> { + r.setState(State.COMPLETED.toString()); + return r; + }) + .orElseGet(() -> new RunBaseStatus(State.COMPLETED.toString())); RunRunnable runRunnable = event != null ? event.getRunnable() : null; // Iterate over all processor and store all RunBaseStatus as optional List processorsStatus = processorRegistry - .getProcessors("onCompleted") - .stream() - .map(processor -> processor.process(run, runRunnable, runBaseStatus)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .getProcessors("onCompleted") + .stream() + .map(processor -> processor.process(run, runRunnable, runBaseStatus)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); Map runStatusMap = processorsStatus - .stream() - .map(RunBaseStatus::toMap) - .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); + .stream() + .map(RunBaseStatus::toMap) + .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); run.setStatus(MapUtils.mergeMultipleMaps(run.getStatus(), runBaseStatus.toMap(), runStatusMap)); @@ -670,7 +683,7 @@ private void onCompleted(Run run, RunnableChangedEvent event) * @throws NoSuchEntityException if the entity being accessed does not exist */ private void onStopped(Run run, RunnableChangedEvent event) - throws NoSuchEntityException, StoreException { + throws NoSuchEntityException, StoreException { // Try to move forward state machine based on current state Fsm> fsm = createFsm(run); @@ -679,51 +692,51 @@ private void onStopped(Run run, RunnableChangedEvent event) // Retrieve Runtime Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); // Define logic for state STOP fsm - .getState(State.STOP) - .getTransaction(RunEvent.STOP) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug("Executing internal logic for state STOP, " + "event :{}, input: {}", RunEvent.STOP, input); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state STOP, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onStopped(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.STOP) + .getTransaction(RunEvent.STOP) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug("Executing internal logic for state STOP, " + "event :{}, input: {}", RunEvent.STOP, input); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state STOP, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onStopped(run, runnable); + return Optional.ofNullable(runStatus); + }); try { Optional runStatus = fsm.goToState(State.STOPPED, null); // Update run status RunBaseStatus runBaseStatus = runStatus - .map(r -> { - r.setState(State.STOPPED.toString()); - return r; - }) - .orElseGet(() -> new RunBaseStatus(State.STOPPED.toString())); + .map(r -> { + r.setState(State.STOPPED.toString()); + return r; + }) + .orElseGet(() -> new RunBaseStatus(State.STOPPED.toString())); RunRunnable runRunnable = event != null ? event.getRunnable() : null; // Iterate over all processor and store all RunBaseStatus as optional List processorsStatus = processorRegistry - .getProcessors("onStopped") - .stream() - .map(processor -> processor.process(run, runRunnable, runBaseStatus)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .getProcessors("onStopped") + .stream() + .map(processor -> processor.process(run, runRunnable, runBaseStatus)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); Map runStatusMap = processorsStatus - .stream() - .map(RunBaseStatus::toMap) - .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); + .stream() + .map(RunBaseStatus::toMap) + .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); run.setStatus(MapUtils.mergeMultipleMaps(run.getStatus(), runBaseStatus.toMap(), runStatusMap)); @@ -741,7 +754,7 @@ private void onStopped(Run run, RunnableChangedEvent event) * @throws NoSuchEntityException if the entity being accessed does not exist */ private void onError(Run run, RunnableChangedEvent event) - throws NoSuchEntityException, StoreException { + throws NoSuchEntityException, StoreException { // Try to move forward state machine based on current state Fsm> fsm = createFsm(run); try { @@ -749,87 +762,87 @@ private void onError(Run run, RunnableChangedEvent event) Executable executable = retrieveExecutable(run); Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); fsm - .getState(State.RUNNING) - .getTransaction(RunEvent.ERROR) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state RUNNING, " + "event :{}, input: {}", - RunEvent.ERROR, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onError(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.RUNNING) + .getTransaction(RunEvent.ERROR) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state RUNNING, " + "event :{}, input: {}", + RunEvent.ERROR, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onError(run, runnable); + return Optional.ofNullable(runStatus); + }); fsm - .getState(State.STOP) - .getTransaction(RunEvent.ERROR) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state STOP, " + "event :{}, input: {}", - RunEvent.ERROR, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state STOP, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onError(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.STOP) + .getTransaction(RunEvent.ERROR) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state STOP, " + "event :{}, input: {}", + RunEvent.ERROR, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state STOP, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onError(run, runnable); + return Optional.ofNullable(runStatus); + }); fsm - .getState(State.ERROR) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state ERROR, " + "event :{}, input: {}", - RunEvent.DELETING, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state ERROR, " + "context: {}", context); - } - RunRunnable runnable = runtime.delete(run); - return Optional.ofNullable(runnable); - }); + .getState(State.ERROR) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state ERROR, " + "event :{}, input: {}", + RunEvent.DELETING, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state ERROR, " + "context: {}", context); + } + RunRunnable runnable = runtime.delete(run); + return Optional.ofNullable(runnable); + }); try { Optional runStatus = fsm.goToState(State.ERROR, null); // Update run status RunBaseStatus runBaseStatus = runStatus - .map(r -> { - r.setState(State.ERROR.toString()); - return r; - }) - .orElseGet(() -> new RunBaseStatus(State.ERROR.toString())); + .map(r -> { + r.setState(State.ERROR.toString()); + return r; + }) + .orElseGet(() -> new RunBaseStatus(State.ERROR.toString())); RunRunnable runRunnable = event != null ? event.getRunnable() : null; // Iterate over all processor and store all RunBaseStatus as optional List processorsStatus = processorRegistry - .getProcessors("onError") - .stream() - .map(processor -> processor.process(run, runRunnable, runBaseStatus)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .getProcessors("onError") + .stream() + .map(processor -> processor.process(run, runRunnable, runBaseStatus)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); Map runStatusMap = processorsStatus - .stream() - .map(RunBaseStatus::toMap) - .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); + .stream() + .map(RunBaseStatus::toMap) + .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); run.setStatus(MapUtils.mergeMultipleMaps(run.getStatus(), runBaseStatus.toMap(), runStatusMap)); @@ -863,7 +876,7 @@ private void onError(Run run, RunnableChangedEvent event) * @throws NoSuchEntityException if the entity being accessed does not exist */ private void onDeleted(Run run, RunnableChangedEvent event) - throws NoSuchEntityException, StoreException { + throws NoSuchEntityException, StoreException { // Try to move forward state machine based on current state Fsm> fsm = createFsm(run); @@ -874,109 +887,109 @@ private void onDeleted(Run run, RunnableChangedEvent event) // Retrieve Runtime Executable executable = retrieveExecutable(run); Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); // Define logic for state DELETING fsm - .getState(State.DELETING) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state DELETING, " + "event :{}, input: {}", - RunEvent.DELETING, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state DELETING, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onDeleted(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.DELETING) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state DELETING, " + "event :{}, input: {}", + RunEvent.DELETING, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state DELETING, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onDeleted(run, runnable); + return Optional.ofNullable(runStatus); + }); // Define logic for state ERROR fsm - .getState(State.ERROR) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state ERROR, " + "event :{}, input: {}", - RunEvent.DELETING, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state ERROR, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onDeleted(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.ERROR) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state ERROR, " + "event :{}, input: {}", + RunEvent.DELETING, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state ERROR, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onDeleted(run, runnable); + return Optional.ofNullable(runStatus); + }); // Define logic for state COMPLETED fsm - .getState(State.COMPLETED) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state COMPLETED, " + "event :{}, input: {}", - RunEvent.DELETING, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state COMPLETED, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onDeleted(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.COMPLETED) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state COMPLETED, " + "event :{}, input: {}", + RunEvent.DELETING, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state COMPLETED, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onDeleted(run, runnable); + return Optional.ofNullable(runStatus); + }); try { Optional runStatus = fsm.goToState(State.DELETED, null); // Update run status RunBaseStatus runBaseStatus = runStatus - .map(r -> { - if (toDelete) { - //explicit delete request leads to deleted status - r.setState(State.DELETED.toString()); - } else { - //keep state as-is - r.setState(curState); - } + .map(r -> { + if (toDelete) { + //explicit delete request leads to deleted status + r.setState(State.DELETED.toString()); + } else { + //keep state as-is + r.setState(curState); + } - return r; - }) - .orElseGet(() -> { - RunBaseStatus r = new RunBaseStatus(); - if (toDelete) { - //explicit delete request leads to deleted status - r.setState(State.DELETED.toString()); - } else { - //keep state as-is - r.setState(curState); - } + return r; + }) + .orElseGet(() -> { + RunBaseStatus r = new RunBaseStatus(); + if (toDelete) { + //explicit delete request leads to deleted status + r.setState(State.DELETED.toString()); + } else { + //keep state as-is + r.setState(curState); + } - return r; - }); + return r; + }); RunRunnable runRunnable = event != null ? event.getRunnable() : null; // Iterate over all processor and store all RunBaseStatus as optional List processorsStatus = processorRegistry - .getProcessors("onDeleted") - .stream() - .map(processor -> processor.process(run, runRunnable, runBaseStatus)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .getProcessors("onDeleted") + .stream() + .map(processor -> processor.process(run, runRunnable, runBaseStatus)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); Map runStatusMap = processorsStatus - .stream() - .map(RunBaseStatus::toMap) - .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); + .stream() + .map(RunBaseStatus::toMap) + .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); run.setStatus(MapUtils.mergeMultipleMaps(run.getStatus(), runBaseStatus.toMap(), runStatusMap)); @@ -1006,8 +1019,8 @@ private Fsm> createFsm(Run run) { // Initialize state machine Fsm> fsm = runStateMachine.builder( - State.valueOf(StatusFieldAccessor.with(run.getStatus()).getState()), - ctx + State.valueOf(StatusFieldAccessor.with(run.getStatus()).getState()), + ctx ); // On state change delegate state machine to update the run @@ -1035,8 +1048,8 @@ private Executable retrieveExecutable(Run run) throws NoSuchEntityException, Sto // Retrieve Executable String executableId = runSpecAccessor.getVersion(); return executableEntityServiceProvider - .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) - .get(executableId); + .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) + .get(executableId); } private Specification createExecutableSpecification(String executable) { diff --git a/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java b/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java index 1e5d0f0a..44bf34d7 100644 --- a/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java +++ b/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java @@ -1,17 +1,26 @@ package it.smartcommunitylabdhub.core.config; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.JWK; +import it.smartcommunitylabdhub.authorization.config.KeyStoreConfig; +import it.smartcommunitylabdhub.commons.config.ApplicationProperties; import it.smartcommunitylabdhub.commons.config.SecurityProperties; import jakarta.servlet.http.HttpServletRequest; + +import java.security.interfaces.RSAPrivateKey; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; + import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.AuditorAware; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -23,13 +32,10 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtClaimNames; -import org.springframework.security.oauth2.jwt.JwtClaimValidator; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtValidators; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.*; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.AccessDeniedHandlerImpl; @@ -60,19 +66,26 @@ public class SecurityConfig { @Value("${management.endpoints.web.base-path}") private String managementBasePath; + @Autowired + KeyStoreConfig keyStoreConfig; + + @Autowired + ApplicationProperties applicationProperties; + @Bean("apiSecurityFilterChain") public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { + HttpSecurity securityChain = http - .securityMatcher(getApiRequestMatcher()) - .authorizeHttpRequests(auth -> { - auth.requestMatchers(getApiRequestMatcher()).hasRole("USER").anyRequest().authenticated(); - }) - // disable request cache - .requestCache(requestCache -> requestCache.disable()) - //disable csrf - .csrf(csrf -> csrf.disable()) - // we don't want a session for these endpoints, each request should be evaluated - .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + .securityMatcher(getApiRequestMatcher()) + .authorizeHttpRequests(auth -> { + auth.requestMatchers(getApiRequestMatcher()).hasRole("USER").anyRequest().authenticated(); + }) + // disable request cache + .requestCache(requestCache -> requestCache.disable()) + //disable csrf + .csrf(csrf -> csrf.disable()) + // we don't want a session for these endpoints, each request should be evaluated + .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // allow cors securityChain.cors(cors -> { @@ -87,14 +100,22 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce if (properties.isRequired()) { if (properties.isBasicAuthEnabled()) { securityChain - .httpBasic(basic -> basic.authenticationEntryPoint(new Http403ForbiddenEntryPoint())) - .userDetailsService(userDetailsService()); + .httpBasic(basic -> basic.authenticationEntryPoint(new Http403ForbiddenEntryPoint())) + .userDetailsService(userDetailsService()); } if (properties.isJwtAuthEnabled()) { + + JwtAuthenticationProvider jwtAacAuthProvider = new JwtAuthenticationProvider(jwtDecoder()); + JwtAuthenticationProvider jwtExchangeAuthProvider = new JwtAuthenticationProvider(exchangeJwtDecoder()); + + jwtAacAuthProvider.setJwtAuthenticationConverter(jwtAuthenticationConverter()); + jwtExchangeAuthProvider.setJwtAuthenticationConverter(jwtAuthenticationConverter()); + + // Create authentication Manager + AuthenticationManager authenticationManager = new ProviderManager(jwtAacAuthProvider, jwtExchangeAuthProvider); + securityChain.oauth2ResourceServer(oauth2 -> - oauth2.jwt(jwt -> jwt.decoder(jwtDecoder()).jwtAuthenticationConverter(jwtAuthenticationConverter()) - ) - ); + oauth2.jwt(jwt -> jwt.authenticationManager(authenticationManager))); } //disable anonymous @@ -109,8 +130,8 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce securityChain.exceptionHandling(handling -> { handling - .authenticationEntryPoint(new Http403ForbiddenEntryPoint()) - .accessDeniedHandler(new AccessDeniedHandlerImpl()); // use 403 + .authenticationEntryPoint(new Http403ForbiddenEntryPoint()) + .accessDeniedHandler(new AccessDeniedHandlerImpl()); // use 403 }); return securityChain.build(); @@ -119,11 +140,11 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce public UserDetailsService userDetailsService() { //create admin user with full permissions UserDetails admin = User - .withDefaultPasswordEncoder() - .username(properties.getBasic().getUsername()) - .password(properties.getBasic().getPassword()) - .roles("ADMIN", "USER") - .build(); + .withDefaultPasswordEncoder() + .username(properties.getBasic().getUsername()) + .password(properties.getBasic().getPassword()) + .roles("ADMIN", "USER") + .build(); return new InMemoryUserDetailsManager(admin); } @@ -133,8 +154,8 @@ private JwtDecoder jwtDecoder() { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(jwtProps.getIssuerUri()).build(); OAuth2TokenValidator audienceValidator = new JwtClaimValidator>( - JwtClaimNames.AUD, - (aud -> aud != null && aud.contains(jwtProps.getAudience())) + JwtClaimNames.AUD, + (aud -> aud != null && aud.contains(jwtProps.getAudience())) ); OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(jwtProps.getIssuerUri()); @@ -144,6 +165,22 @@ private JwtDecoder jwtDecoder() { return jwtDecoder; } + private JwtDecoder exchangeJwtDecoder() throws JOSEException { + JWK jwk = keyStoreConfig.getJWKSetKeyStore().getJwk(); + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(jwk.toRSAKey().toRSAPublicKey()).build(); + + OAuth2TokenValidator audienceValidator = new JwtClaimValidator>( + JwtClaimNames.AUD, + (aud -> aud != null && aud.contains(applicationProperties.getName())) + ); + + OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(applicationProperties.getEndpoint()); + OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); + jwtDecoder.setJwtValidator(withAudience); + + return jwtDecoder; + } + private JwtAuthenticationConverter jwtAuthenticationConverter() { SecurityProperties.JwtAuthenticationProperties jwtProps = properties.getJwt(); String claim = jwtProps.getClaim(); @@ -177,41 +214,41 @@ private JwtAuthenticationConverter jwtAuthenticationConverter() { @Bean("h2SecurityFilterChain") public SecurityFilterChain h2SecurityFilterChain(HttpSecurity http) throws Exception { return http - .securityMatcher(new AntPathRequestMatcher("/h2-console/**")) - .authorizeHttpRequests(auth -> { - auth.anyRequest().permitAll(); - }) - //disable csrf - .csrf(csrf -> csrf.disable()) - // we don't want a session for these endpoints, each request should be evaluated - .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - // enable frame options - .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())) - .build(); + .securityMatcher(new AntPathRequestMatcher("/h2-console/**")) + .authorizeHttpRequests(auth -> { + auth.anyRequest().permitAll(); + }) + //disable csrf + .csrf(csrf -> csrf.disable()) + // we don't want a session for these endpoints, each request should be evaluated + .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // enable frame options + .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())) + .build(); } @Bean("coreSecurityFilterChain") public SecurityFilterChain coreSecurityFilterChain(HttpSecurity http) throws Exception { return http - .authorizeHttpRequests(auth -> { - auth.anyRequest().permitAll(); - }) - // disable request cache - .requestCache(requestCache -> requestCache.disable()) - .build(); + .authorizeHttpRequests(auth -> { + auth.anyRequest().permitAll(); + }) + // disable request cache + .requestCache(requestCache -> requestCache.disable()) + .build(); } @Bean("monitoringSecurityFilterChain") public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // match only actuator endpoints return http - .securityMatcher(getManagementRequestMatcher()) - .authorizeHttpRequests(auth -> { - auth.anyRequest().permitAll(); - }) - .exceptionHandling(handling -> handling.authenticationEntryPoint(new Http403ForbiddenEntryPoint())) - .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .build(); + .securityMatcher(getManagementRequestMatcher()) + .authorizeHttpRequests(auth -> { + auth.anyRequest().permitAll(); + }) + .exceptionHandling(handling -> handling.authenticationEntryPoint(new Http403ForbiddenEntryPoint())) + .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .build(); } public RequestMatcher getManagementRequestMatcher() { diff --git a/application/src/main/resources/application.yml b/application/src/main/resources/application.yml index 71ba4929..6b6ca58d 100644 --- a/application/src/main/resources/application.yml +++ b/application/src/main/resources/application.yml @@ -1,16 +1,16 @@ # server config server: - host: ${SERVER_HOST:localhost} - port: ${SERVER_PORT:8080} - servlet: - context-path: ${SERVER_CONTEXT:/} - tomcat: - remoteip: + host: ${SERVER_HOST:localhost} + port: ${SERVER_PORT:8080} + servlet: + context-path: ${SERVER_CONTEXT:/} + tomcat: + remoteip: remote_ip_header: ${SERVER_TOMCAT_REMOTE_IP_HEADER:x-forwarded-for} protocol_header: ${SERVER_TOMCAT_PROTOCOL_HEADER:x-forwarded-proto} - max-http-request-header-size: 32000 - error: - include-stacktrace: never + max-http-request-header-size: 32000 + error: + include-stacktrace: never # Spring configuration spring: @@ -42,18 +42,18 @@ spring: # Management Endpoints management: - server: - port: ${MANAGEMENT_PORT:8081} - endpoints: - enabled-by-default: false - web: - base-path: - exposure.include: "health,info,metrics" - endpoint: - info: - enabled: true - health: - enabled: true + server: + port: ${MANAGEMENT_PORT:8081} + endpoints: + enabled-by-default: false + web: + base-path: + exposure.include: "health,info,metrics" + endpoint: + info: + enabled: true + health: + enabled: true # Runtimes @@ -84,7 +84,7 @@ kaniko: image-prefix: ${KANIKO_IMAGE_PREFIX:dhcore} image-registry: ${KANIKO_IMAGE_REGISTRY:${registry.name}} secret: ${KANIKO_SECRET:${registry.secret}} - args: ${KANIKO_ARGS:} + args: ${KANIKO_ARGS:} # MLRun config mlrun: @@ -93,7 +93,7 @@ mlrun: image-registry: ${MLRUN_IMAGE_REGISTRY:} # registry -registry: +registry: name: ${DOCKER_REGISTRY:} secret: ${DOCKER_REGISTRY_SECRET:} @@ -134,9 +134,9 @@ application: logging: - level: - ROOT: INFO - it.smartcommunitylabdhub: ${LOG_LEVEL:INFO} + level: + ROOT: INFO + it.smartcommunitylabdhub: ${LOG_LEVEL:INFO} security: api: @@ -147,9 +147,9 @@ security: password: ${DH_AUTH_BASIC_PASSWORD:} jwt: issuer-uri: ${DH_AUTH_JWT_ISSUER_URI:${security.oidc.issuer-uri}} - audience: ${DH_AUTH_JWT_AUDIENCE:${security.oidc.client-id}} + audience: ${DH_AUTH_JWT_AUDIENCE:${security.oidc.issuer-uri}} claim: ${DH_AUTH_JWT_CLAIM:roles} - oidc: + oidc: issuer-uri: ${DH_AUTH_OIDC_ISSUER_URI:} client-id: ${DH_AUTH_OIDC_CLIENT_ID:} scope: ${DH_AUTH_OIDC_SCOPE:openid,email,profile} diff --git a/modules/authorization/.flattened-pom.xml b/modules/authorization/.flattened-pom.xml index 922f1e00..1a7cf73d 100644 --- a/modules/authorization/.flattened-pom.xml +++ b/modules/authorization/.flattened-pom.xml @@ -25,15 +25,9 @@ compile - org.bouncycastle - bcpkix-jdk18on - 1.78.1 - compile - - - org.bouncycastle - bcprov-jdk18on - 1.78.1 + com.google.guava + guava + 20.0 compile diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java index 466ab6e4..550aa30a 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java @@ -8,6 +8,7 @@ import org.springframework.core.io.Resource; import com.google.common.base.Charsets; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import java.io.IOException; import java.io.InputStreamReader; @@ -23,7 +24,7 @@ public class JWKSetKeyStore { public JWKSetKeyStore(Resource location, String kid) { this.jwkSet = loadJwkSet(location); - if (kid == null) { + if (!StringUtils.hasText(kid)) { this.kid = jwkSet.getKeys().getFirst().getKeyID(); // prendo la prima // se è null, non ci sono chiavi assert Assert.notNull(this.kid, "Key ID cannot be null"); diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java index a5b223f2..12a2f637 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java @@ -1,28 +1,23 @@ package it.smartcommunitylabdhub.authorization.controllers; - - - import com.nimbusds.jose.jwk.JWKSet; import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController -@RequestMapping("/api/.well-known/jwks.json") @Slf4j public class JWKController { @Autowired private JWKSetKeyStore jwkSetKeyStore; - @GetMapping("") + @GetMapping("/.well-known/jwks") public ResponseEntity> getJWKInfo() { JWKSet jwkSet = jwkSetKeyStore.getJwkSet(); // Convert JWKSet to a map for easier JSON serialization diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java index f37c546a..8cb44cc6 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java @@ -16,8 +16,11 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Service; import org.springframework.beans.factory.annotation.Value; + +import java.io.Serializable; import java.security.interfaces.RSAPrivateKey; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -28,7 +31,6 @@ public class JwtTokenService { @Autowired private JWKSetKeyStore keyStoreUtil; - @Autowired ApplicationProperties applicationProperties; @@ -46,13 +48,7 @@ public String generateToken(Authentication authentication) throws JwtTokenServiceException { try { - // check that jwt is enabled via securityProperties - if (!securityProperties.getJwt().isEnabled()) { - return null; - } - // Extract claims from authentication if it's a JwtAuthenticationToken - Map additionalClaims = extractClaims(authentication); JWK jwk = keyStoreUtil.getJwk(); @@ -68,12 +64,12 @@ public String generateToken(Authentication authentication) .jwtID(UUID.randomUUID().toString()) .expirationTime(new Date(System.currentTimeMillis() + jwtExpiration)); - - // Add additional claims from the authentication token - if (additionalClaims != null) { - additionalClaims.forEach(claimsSetBuilder::claim); + List additionalClaims = extractClaims(authentication); + if(additionalClaims != null){ + claimsSetBuilder.claim(securityProperties.getJwt().getClaim(), additionalClaims); } + // Build claims set JWTClaimsSet claimsSet = claimsSetBuilder.build(); // Create signed JWT @@ -98,10 +94,10 @@ public String generateToken(Authentication authentication) } - private Map extractClaims(Authentication authentication) { + private List extractClaims(Authentication authentication) { if (authentication instanceof JwtAuthenticationToken jwtAuthToken) { Jwt jwt = jwtAuthToken.getToken(); - return jwt.getClaims(); + return jwt.getClaimAsStringList(securityProperties.getJwt().getClaim()); } else { log.warn("Authentication is not of type JwtAuthenticationToken"); return null; diff --git a/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java b/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java index b57ad4dc..30b8290b 100644 --- a/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java +++ b/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java @@ -19,6 +19,9 @@ public class SecurityProperties { @NestedConfigurationProperty private JwtAuthenticationProperties jwt; + @NestedConfigurationProperty + private JwtExchangeAuthenticationProperties jwtExchange; + @NestedConfigurationProperty private OidcAuthenticationProperties oidc; @@ -63,6 +66,20 @@ public boolean isEnabled() { } } + @Getter + @Setter + public static class JwtExchangeAuthenticationProperties { + + private String issuerUri; + private String audience; + private String claim; + + public boolean isEnabled() { + return StringUtils.hasText(issuerUri) && StringUtils.hasText(audience); + } + } + + @Getter @Setter public static class OidcAuthenticationProperties { From 052e9ba3c6e7c2c2f35ffcd3b1f5547fed24eaab Mon Sep 17 00:00:00 2001 From: Luca Trubbiani Date: Wed, 24 Jul 2024 15:02:23 +0200 Subject: [PATCH 03/10] Update and fix authorization module --- .../src/main/resources/application.yml | 6 ++- .../components/JWKSetKeyStore.java | 36 ++++++++++++--- .../authorization/config/KeyStoreConfig.java | 46 ++++++++++++------- .../services/JwtTokenService.java | 4 -- .../commons/config/SecurityProperties.java | 16 ------- 5 files changed, 65 insertions(+), 43 deletions(-) diff --git a/application/src/main/resources/application.yml b/application/src/main/resources/application.yml index 6b6ca58d..1c883fdb 100644 --- a/application/src/main/resources/application.yml +++ b/application/src/main/resources/application.yml @@ -193,4 +193,8 @@ files: secret-key: ${AWS_SECRET_KEY:} endpoint: ${S3_ENDPOINT:} bucket: ${S3_BUCKET:} - \ No newline at end of file + +# Keystore configuration +keystore: + path: ${KEYSTORE_PATH:./data/keystore.jwks} + kid: ${KEYSTORE_KID:} \ No newline at end of file diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java index 550aa30a..cf2ba656 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java @@ -5,6 +5,7 @@ import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; import it.smartcommunitylabdhub.authorization.utils.JWKUtils; +import lombok.Getter; import org.springframework.core.io.Resource; import com.google.common.base.Charsets; import org.springframework.util.Assert; @@ -12,17 +13,23 @@ import java.io.IOException; import java.io.InputStreamReader; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.text.ParseException; -import java.util.Optional; import java.util.UUID; public class JWKSetKeyStore { + @Getter private final JWKSet jwkSet; - private String kid; + private final String kid; - public JWKSetKeyStore(Resource location, String kid) { + public JWKSetKeyStore(Resource location, String kid) throws IllegalArgumentException { this.jwkSet = loadJwkSet(location); if (!StringUtils.hasText(kid)) { this.kid = jwkSet.getKeys().getFirst().getKeyID(); // prendo la prima @@ -40,7 +47,7 @@ public JWKSetKeyStore() throws JOSEException { Assert.notNull(this.kid, "Key ID cannot be null"); } - private static JWKSet loadJwkSet(Resource location) { + private static JWKSet loadJwkSet(Resource location) throws IllegalArgumentException { Assert.notNull(location, "Key Set resource cannot be null"); if (location.exists() && location.isReadable()) { try { @@ -65,10 +72,27 @@ private static JWKSet initializeJwkSet() throws JOSEException { } - public JWKSet getJwkSet() { - return jwkSet; + public void saveJwkSet(Resource location) { + Assert.notNull(location, "Key Set resource cannot be null"); + try { + Path path = Paths.get(location.getURI()); + if (!Files.exists(path)) { + // Create directories if they do not exist + Files.createDirectories(path.getParent()); + } + + try (Writer writer = Files.newBufferedWriter( + path, StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING)) { + writer.write(jwkSet.toJSONObject(false).toString()); + } + } catch (IOException e) { + throw new IllegalArgumentException("Key Set resource could not be written: " + location, e); + } } + public JWK getJwk() { return jwkSet.getKeyByKeyId(kid); } diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java index 2871b8ac..40a41ef9 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java @@ -2,24 +2,28 @@ import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; -import it.smartcommunitylabdhub.authorization.utils.JWKUtils; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + @Configuration +@Slf4j public class KeyStoreConfig { - @Value("${keystore.path:}") - private Resource path; + @Value("${keystore.path}") + private String keyStorePath; - @Value("${keystore.kid:}") + @Value("${keystore.kid}") private String kid; private JWKSetKeyStore keyStore; @@ -28,30 +32,40 @@ public class KeyStoreConfig { @Bean @Primary public JWKSetKeyStore getJWKSetKeyStore() throws JOSEException { + Path path = Paths.get(keyStorePath).toAbsolutePath().normalize(); + Resource resource = new FileSystemResource(path); + if (keyStore == null) { - if (path != null) { - // load from resource - keyStore = load(path); - // check if empty + if (Files.exists(path) && Files.isReadable(path)) { + // Load from resource + keyStore = load(resource); + // Check if empty if (keyStore.getJwk() == null) { - // discard, we will generate a new one + // Discard, we will generate a new one keyStore = null; } } - } - if (keyStore == null) { - // generate new in-memory keystore - keyStore = generate(); + if (keyStore == null) { + // Generate new in-memory keystore + keyStore = generate(); + // Save to file + save(keyStore, resource); + } } return keyStore; } - private JWKSetKeyStore load(Resource location) { + + private JWKSetKeyStore load(Resource location) throws IllegalArgumentException { return new JWKSetKeyStore(location, kid); } private JWKSetKeyStore generate() throws JOSEException { return new JWKSetKeyStore(); } + + private void save(JWKSetKeyStore keyStore, Resource location) { + keyStore.saveJwkSet(location); + } } diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java index 8cb44cc6..0c241f22 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java @@ -41,16 +41,12 @@ public class JwtTokenService { @Value("${jwt.expiration:2592000000}") private long jwtExpiration; - @Value("${jwt.keyId:kid}") - private String keyId; public String generateToken(Authentication authentication) throws JwtTokenServiceException { try { // Extract claims from authentication if it's a JwtAuthenticationToken - - JWK jwk = keyStoreUtil.getJwk(); RSAPrivateKey privateKey = jwk.toRSAKey().toRSAPrivateKey(); RSASSASigner signer = new RSASSASigner(privateKey); diff --git a/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java b/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java index 30b8290b..3fec4fe3 100644 --- a/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java +++ b/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java @@ -19,9 +19,6 @@ public class SecurityProperties { @NestedConfigurationProperty private JwtAuthenticationProperties jwt; - @NestedConfigurationProperty - private JwtExchangeAuthenticationProperties jwtExchange; - @NestedConfigurationProperty private OidcAuthenticationProperties oidc; @@ -66,19 +63,6 @@ public boolean isEnabled() { } } - @Getter - @Setter - public static class JwtExchangeAuthenticationProperties { - - private String issuerUri; - private String audience; - private String claim; - - public boolean isEnabled() { - return StringUtils.hasText(issuerUri) && StringUtils.hasText(audience); - } - } - @Getter @Setter From db31058ad306793f6e29912fad154e0318bc8780 Mon Sep 17 00:00:00 2001 From: Luca Trubbiani Date: Mon, 29 Jul 2024 14:37:14 +0200 Subject: [PATCH 04/10] Small fix on authorization module and reformat code --- application/key.jwks | 18 - .../core/components/run/RunManager.java | 947 +++++++++--------- .../core/config/SecurityConfig.java | 17 +- .../src/main/resources/application.yml | 10 +- .../components/JWKSetKeyStore.java | 64 +- .../authorization/config/KeyStoreConfig.java | 78 +- .../controllers/JWKController.java | 14 +- .../exceptions/JwtTokenServiceException.java | 8 + .../services/JwtTokenService.java | 62 +- .../authorization/utils/JWKUtils.java | 24 +- 10 files changed, 597 insertions(+), 645 deletions(-) delete mode 100644 application/key.jwks create mode 100644 modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/exceptions/JwtTokenServiceException.java diff --git a/application/key.jwks b/application/key.jwks deleted file mode 100644 index e54b2611..00000000 --- a/application/key.jwks +++ /dev/null @@ -1,18 +0,0 @@ -{ - "keys": [ - { - "p": "4LDzu4V1h2jVexI9u8Uw4AU067QxW1Puu2T4V9Y3krGru6xnnqJ9G065jAOHrbfch0QszcKr4gIBepStWR0TgWlDu4-0harN8zkUyQfKF3YrOOwjmfiCmoz-ezBNwUreGyAFKyCKEI1d6myCjQ9QmoCuVYcF1sFrg480jvMh9u0", - "kty": "RSA", - "q": "l4qu8WuDYMUCJnem6iEUySW8Dq0Iq5ZzWADKSvz-Lks67pouKLp2O8QsVpztmxxm6CmLmZS6tzO-Uf9wVAdLSju5bGSMvz-J7wwbsdoBFHnFjh3gpy4Az_ZZIZFz8sxgNo6f7wS0R5n2j10vgW2dL1oixFnN0l4jLCkR4vsI6ec", - "d": "MuRedw79v5f3a4ejPSVCXwbkiK27LYXNApPHVCNh5-eKy0FTRBMIRTKRgJfp2cePWQUHtQmI-MbTD7PxLu6AWjgQxWrFFddQl007pv2fst3B5gGZO-DrLL0G77RWekh3QxMA6Zxib6DAg7rGs9LmMkRdZjq69vhPYtAykVNvhyjqx1H5rIyqhC7chfrlZPwgoEaIOuszTjDlRu1arnaw8fEbTjxMLKRhEv-__i-QhXxvACi6io5SL_acce3JpqYIpa5Q3McibMETFp0WtLEJT1MV_Owe2u2bJ2kfVeoT_3MucWGBNZY2_JF-ypOzGoqghhPrwKDiu_m_k8izm6JfoQ", - "e": "AQAB", - "use": "sig", - "kid": "kid1", - "qi": "Wpc4HLLhx4DQdWdmXDcI2jEcB21mrPMzXOvV6m7BrVwr5Wv1CkUwMLcvFMfMTMvMm9T8A6NPbMjtQanYlzaE9rRLxMjC9qad0K8jKRhS29_Ua-NrTxs5LEa_rC_KTIUPQwJKRiLQDMqB-1tE6Wx-0f-X2xr-edqw0xFrKGG5f_w", - "dp": "sQPXUQGogQBfRzEsv3Rvt92CPrtcMxYhuzKl0BVs_L3KjPUbQUfEBjJ0TVEun_Z833gfUA8w-MIZxifBlbYpeseiW-6wCqpwzFBJBsODPA6VqKeouJGm2vl_Ny_r4f8Ikhgc2kgtXTLyLVBFXboWB1KfhbP1LqZ8_E1i28fCF1U", - "alg": "RS256", - "dq": "kJfe_LOdE4Rm3NVmCheugY7jtQ0dLLvNKrb2Tj6_OOTHQksSqAHiNnyj4bCydkHPXs3lSZFD5vIUMqEMSOD80cPS1L3MHd8-eTyQYE6moSbffQ69AEdAty1TNlhWzeB8HRJRP_q5GWiqfXhoj5JLXJcgfUFCxerUnJpDWtQ0WRk", - "n": "hQIYsylqcdO5TtgsJsyVLSLzdYIFGMwfnF5ZrLj3aszxH5uxKQXNXaplD2jZwQumVJMuCwnSNK0Z_1Yhwn97dEOSspIN-NuZZ_yZlgDNp4YH9eiNbf4xjQIewcI81NNh1oitb6iSJtOc5XMPN6z14nYDIUhVHomuhgTfofm4UZmvSDh4vO4yy3uYHTkXGGSr8t12su7z3ALM30YaY_yzO5BuuvaVmAE96Z8QyoO6KM2rbCpx6Rym_rXryF4m4NMWvRy_hw9RnT12IjxV_AxoLIEQYWLT_InuIy02P7_tHP4vbTmwfmxU1WCY8kVeYsVPhIkA7uF_W2MHPyirY8uE2w" - } - ] -} \ No newline at end of file diff --git a/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java b/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java index 505f9675..d87e7c63 100644 --- a/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java +++ b/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java @@ -35,7 +35,6 @@ import it.smartcommunitylabdhub.fsm.exceptions.InvalidTransactionException; import it.smartcommunitylabdhub.fsm.types.RunStateMachineFactory; import jakarta.validation.constraints.NotNull; - import java.io.Serializable; import java.util.HashMap; import java.util.List; @@ -43,7 +42,6 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; - import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; @@ -98,14 +96,14 @@ public Run build(@NotNull Run run) throws NoSuchEntityException { // Retrieve Executable String executableId = runSpecAccessor.getVersion(); Executable executable = executableEntityServiceProvider - .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) - .get(executableId); + .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) + .get(executableId); // Retrieve Task Specification where = Specification.allOf( - CommonSpecification.projectEquals(executable.getProject()), - createExecutableSpecification(TaskUtils.buildString(executable)), - createTaskKindSpecification(runSpecAccessor.getTask()) + CommonSpecification.projectEquals(executable.getProject()), + createExecutableSpecification(TaskUtils.buildString(executable)), + createTaskKindSpecification(runSpecAccessor.getTask()) ); Task task = taskEntityService.searchAll(where).stream().findFirst().orElse(null); @@ -114,26 +112,26 @@ public Run build(@NotNull Run run) throws NoSuchEntityException { // Add Internal logic to be executed when state change from CREATED to READY fsm - .getState(State.CREATED) - .getTransaction(RunEvent.BUILD) - .setInternalLogic((context, input, fsmInstance) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - - // Build RunSpec using Runtime now if wrong type is passed to a specific runtime - // an exception occur! for. - RunBaseSpec runSpecBuilt = runtime.build(executable, task, run); - - return Optional.of(runSpecBuilt); - } - return Optional.empty(); - }); + .getState(State.CREATED) + .getTransaction(RunEvent.BUILD) + .setInternalLogic((context, input, fsmInstance) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + + // Build RunSpec using Runtime now if wrong type is passed to a specific runtime + // an exception occur! for. + RunBaseSpec runSpecBuilt = runtime.build(executable, task, run); + + return Optional.of(runSpecBuilt); + } + return Optional.empty(); + }); try { // Update run state to BUILT @@ -172,32 +170,32 @@ public Run run(@NotNull Run run) throws NoSuchEntityException, InvalidTransactio // Retrieve Executable String executableId = runSpecAccessor.getVersion(); Executable executable = executableEntityServiceProvider - .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) - .get(executableId); + .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) + .get(executableId); // Retrieve state machine Fsm> fsm = createFsm(run); fsm - .getState(State.BUILT) - .getTransaction(RunEvent.RUN) - .setInternalLogic((context, input, stateMachine) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - // Create Runnable - RunRunnable runnable = runtime.run(run); - - return Optional.of(runnable); - } else { - return Optional.empty(); - } - }); + .getState(State.BUILT) + .getTransaction(RunEvent.RUN) + .setInternalLogic((context, input, stateMachine) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + // Create Runnable + RunRunnable runnable = runtime.run(run); + + return Optional.of(runnable); + } else { + return Optional.empty(); + } + }); try { Optional runnable = fsm.goToState(State.READY, null); @@ -209,13 +207,9 @@ public Run run(@NotNull Run run) throws NoSuchEntityException, InvalidTransactio // check that jwt is enabled via securityProperties Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null) { - if (securityProperties.getJwt().isEnabled()) { - String token = jwtTokenService.generateToken(auth); - if (token != null) { - ((SecuredRunnable) r).setCredentials(token); - } - } else { - ((SecuredRunnable) r).setCredentials(auth); + String token = jwtTokenService.generateToken(auth); + if (token != null) { + ((SecuredRunnable) r).setCredentials(token); } } } @@ -233,8 +227,7 @@ public Run run(@NotNull Run run) throws NoSuchEntityException, InvalidTransactio log.debug("Invalid transaction from state {} to state {}", e.getFromState(), e.getToState()); throw e; } - } catch ( - StoreException e) { + } catch (StoreException e) { log.error("store error: {}", e.getMessage()); throw new SystemException(e.getMessage()); } @@ -249,32 +242,32 @@ public Run stop(@NotNull Run run) throws NoSuchEntityException { // Retrieve Executable String executableId = runSpecAccessor.getVersion(); Executable executable = executableEntityServiceProvider - .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) - .get(executableId); + .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) + .get(executableId); // Retrieve state machine Fsm> fsm = createFsm(run); fsm - .getState(State.RUNNING) - .getTransaction(RunEvent.STOP) - .setInternalLogic((context, input, stateMachine) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - // Create Runnable - RunRunnable runnable = runtime.stop(run); - - return Optional.of(runnable); - } else { - return Optional.empty(); - } - }); + .getState(State.RUNNING) + .getTransaction(RunEvent.STOP) + .setInternalLogic((context, input, stateMachine) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + // Create Runnable + RunRunnable runnable = runtime.stop(run); + + return Optional.of(runnable); + } else { + return Optional.empty(); + } + }); try { Optional runnable = fsm.goToState(State.STOP, null); @@ -307,93 +300,93 @@ public Run delete(@NotNull Run run) throws NoSuchEntityException { // Retrieve Executable String executableId = runSpecAccessor.getVersion(); Executable executable = executableEntityServiceProvider - .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) - .get(executableId); + .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) + .get(executableId); // Retrieve state machine Fsm> fsm = createFsm(run); fsm - .getState(State.RUNNING) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, stateMachine) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - // Create Runnable - RunRunnable runnable = runtime.delete(run); - - return Optional.ofNullable(runnable); - } else { - return Optional.empty(); - } - }); + .getState(State.RUNNING) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, stateMachine) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + // Create Runnable + RunRunnable runnable = runtime.delete(run); + + return Optional.ofNullable(runnable); + } else { + return Optional.empty(); + } + }); fsm - .getState(State.STOPPED) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, stateMachine) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - // Create Runnable - RunRunnable runnable = runtime.delete(run); - - return Optional.ofNullable(runnable); - } else { - return Optional.empty(); - } - }); + .getState(State.STOPPED) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, stateMachine) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + // Create Runnable + RunRunnable runnable = runtime.delete(run); + + return Optional.ofNullable(runnable); + } else { + return Optional.empty(); + } + }); fsm - .getState(State.ERROR) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, stateMachine) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - // Create Runnable - RunRunnable runnable = runtime.delete(run); - - return Optional.ofNullable(runnable); - } else { - return Optional.empty(); - } - }); + .getState(State.ERROR) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, stateMachine) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + // Create Runnable + RunRunnable runnable = runtime.delete(run); + + return Optional.ofNullable(runnable); + } else { + return Optional.empty(); + } + }); fsm - .getState(State.COMPLETED) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, stateMachine) -> { - if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { - // Retrieve Runtime and build run - Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); - // Create Runnable - RunRunnable runnable = runtime.delete(run); - - return Optional.ofNullable(runnable); - } else { - return Optional.empty(); - } - }); + .getState(State.COMPLETED) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, stateMachine) -> { + if (!Optional.ofNullable(runBaseSpec.getLocalExecution()).orElse(Boolean.FALSE)) { + // Retrieve Runtime and build run + Runtime< + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); + // Create Runnable + RunRunnable runnable = runtime.delete(run); + + return Optional.ofNullable(runnable); + } else { + return Optional.empty(); + } + }); try { Optional runnable = fsm.goToState(State.DELETING, null); @@ -434,62 +427,62 @@ public void onChangedEvent(RunnableChangedEvent event) throws Store // Use service to retrieve the run and check if state is changed Optional - .ofNullable(entityService.find(runnableMonitorObject.getRunId())) - .ifPresentOrElse( - run -> { - try { - if ( - //either signal an update or track progress (running state) - !Objects.equals( - StatusFieldAccessor.with(run.getStatus()).getState(), - runnableMonitorObject.getStateId() - ) || - State.RUNNING == State.valueOf(runnableMonitorObject.getStateId()) - ) { - switch (State.valueOf(runnableMonitorObject.getStateId())) { - case COMPLETED: - onCompleted(run, event); - break; - case ERROR: - onError(run, event); - break; - case RUNNING: - onRunning(run, event); - break; - case STOPPED: - onStopped(run, event); - break; - case DELETED: - onDeleted(run, event); - break; - default: - log.debug( - "State {} for run id {} not managed", - runnableMonitorObject.getStateId(), - runnableMonitorObject.getRunId() - ); - break; - } - } else { + .ofNullable(entityService.find(runnableMonitorObject.getRunId())) + .ifPresentOrElse( + run -> { + try { + if ( + //either signal an update or track progress (running state) + !Objects.equals( + StatusFieldAccessor.with(run.getStatus()).getState(), + runnableMonitorObject.getStateId() + ) || + State.RUNNING == State.valueOf(runnableMonitorObject.getStateId()) + ) { + switch (State.valueOf(runnableMonitorObject.getStateId())) { + case COMPLETED: + onCompleted(run, event); + break; + case ERROR: + onError(run, event); + break; + case RUNNING: + onRunning(run, event); + break; + case STOPPED: + onStopped(run, event); + break; + case DELETED: + onDeleted(run, event); + break; + default: log.debug( - "State {} for run id {} not changed", - runnableMonitorObject.getStateId(), - runnableMonitorObject.getRunId() + "State {} for run id {} not managed", + runnableMonitorObject.getStateId(), + runnableMonitorObject.getRunId() ); - } - } catch (StoreException e) { - log.error("store error for {}:{}", runnableMonitorObject.getRunId(), e.getMessage()); + break; } - }, - () -> { - log.error("Run with id {} not found", runnableMonitorObject.getRunId()); + } else { + log.debug( + "State {} for run id {} not changed", + runnableMonitorObject.getStateId(), + runnableMonitorObject.getRunId() + ); } - ); + } catch (StoreException e) { + log.error("store error for {}:{}", runnableMonitorObject.getRunId(), e.getMessage()); + } + }, + () -> { + log.error("Run with id {} not found", runnableMonitorObject.getRunId()); + } + ); } // Callback Methods private void onRunning(Run run, RunnableChangedEvent event) - throws NoSuchEntityException, StoreException { + throws NoSuchEntityException, StoreException { // Try to move forward state machine based on current state Fsm> fsm = createFsm(run); @@ -498,72 +491,72 @@ private void onRunning(Run run, RunnableChangedEvent event) // Retrieve Runtime Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); // Define logic for state READY fsm - .getState(State.READY) - .getTransaction(RunEvent.EXECUTE) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state RUNNING, " + "event :{}, input: {}", - RunEvent.EXECUTE, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); - } + .getState(State.READY) + .getTransaction(RunEvent.EXECUTE) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state RUNNING, " + "event :{}, input: {}", + RunEvent.EXECUTE, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); + } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onRunning(run, runnable); - return Optional.ofNullable(runStatus); - }); + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onRunning(run, runnable); + return Optional.ofNullable(runStatus); + }); fsm - .getState(State.RUNNING) - .getTransaction(RunEvent.LOOP) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state RUNNING, " + "event :{}, input: {}", - RunEvent.LOOP, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onRunning(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.RUNNING) + .getTransaction(RunEvent.LOOP) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state RUNNING, " + "event :{}, input: {}", + RunEvent.LOOP, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onRunning(run, runnable); + return Optional.ofNullable(runStatus); + }); try { //TODO call registry processor to retrieve all processor for onRunning and call process() Optional runStatus = fsm.goToState(State.RUNNING, null); // Update run status RunBaseStatus runBaseStatus = runStatus - .map(r -> { - r.setState(State.RUNNING.toString()); - return r; - }) - .orElseGet(() -> new RunBaseStatus(State.RUNNING.toString())); + .map(r -> { + r.setState(State.RUNNING.toString()); + return r; + }) + .orElseGet(() -> new RunBaseStatus(State.RUNNING.toString())); RunRunnable runRunnable = event != null ? event.getRunnable() : null; // Iterate over all processor and store all RunBaseStatus as optional List processorsStatus = processorRegistry - .getProcessors("onRunning") - .stream() - .map(processor -> processor.process(run, runRunnable, runBaseStatus)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .getProcessors("onRunning") + .stream() + .map(processor -> processor.process(run, runRunnable, runBaseStatus)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); Map runStatusMap = processorsStatus - .stream() - .map(RunBaseStatus::toMap) - .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); + .stream() + .map(RunBaseStatus::toMap) + .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); run.setStatus(MapUtils.mergeMultipleMaps(run.getStatus(), runBaseStatus.toMap(), runStatusMap)); @@ -581,7 +574,7 @@ private void onRunning(Run run, RunnableChangedEvent event) * @throws NoSuchEntityException if the entity being accessed does not exist */ private void onCompleted(Run run, RunnableChangedEvent event) - throws NoSuchEntityException, StoreException { + throws NoSuchEntityException, StoreException { // Try to move forward state machine based on current state Fsm> fsm = createFsm(run); @@ -590,70 +583,70 @@ private void onCompleted(Run run, RunnableChangedEvent event) // Retrieve Runtime Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); // Define logic for state RUNNING fsm - .getState(State.RUNNING) - .getTransaction(RunEvent.COMPLETE) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state RUNNING, " + "event :{}, input: {}", - RunEvent.COMPLETE, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onComplete(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.RUNNING) + .getTransaction(RunEvent.COMPLETE) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state RUNNING, " + "event :{}, input: {}", + RunEvent.COMPLETE, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onComplete(run, runnable); + return Optional.ofNullable(runStatus); + }); fsm - .getState(State.COMPLETED) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state COMPLETED, " + "event :{}, input: {}", - RunEvent.DELETING, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state COMPLETED, " + "context: {}", context); - } - RunRunnable runnable = runtime.delete(run); - return Optional.ofNullable(runnable); - }); + .getState(State.COMPLETED) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state COMPLETED, " + "event :{}, input: {}", + RunEvent.DELETING, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state COMPLETED, " + "context: {}", context); + } + RunRunnable runnable = runtime.delete(run); + return Optional.ofNullable(runnable); + }); try { Optional runStatus = fsm.goToState(State.COMPLETED, null); RunBaseStatus runBaseStatus = runStatus - .map(r -> { - r.setState(State.COMPLETED.toString()); - return r; - }) - .orElseGet(() -> new RunBaseStatus(State.COMPLETED.toString())); + .map(r -> { + r.setState(State.COMPLETED.toString()); + return r; + }) + .orElseGet(() -> new RunBaseStatus(State.COMPLETED.toString())); RunRunnable runRunnable = event != null ? event.getRunnable() : null; // Iterate over all processor and store all RunBaseStatus as optional List processorsStatus = processorRegistry - .getProcessors("onCompleted") - .stream() - .map(processor -> processor.process(run, runRunnable, runBaseStatus)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .getProcessors("onCompleted") + .stream() + .map(processor -> processor.process(run, runRunnable, runBaseStatus)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); Map runStatusMap = processorsStatus - .stream() - .map(RunBaseStatus::toMap) - .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); + .stream() + .map(RunBaseStatus::toMap) + .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); run.setStatus(MapUtils.mergeMultipleMaps(run.getStatus(), runBaseStatus.toMap(), runStatusMap)); @@ -683,7 +676,7 @@ private void onCompleted(Run run, RunnableChangedEvent event) * @throws NoSuchEntityException if the entity being accessed does not exist */ private void onStopped(Run run, RunnableChangedEvent event) - throws NoSuchEntityException, StoreException { + throws NoSuchEntityException, StoreException { // Try to move forward state machine based on current state Fsm> fsm = createFsm(run); @@ -692,51 +685,51 @@ private void onStopped(Run run, RunnableChangedEvent event) // Retrieve Runtime Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); // Define logic for state STOP fsm - .getState(State.STOP) - .getTransaction(RunEvent.STOP) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug("Executing internal logic for state STOP, " + "event :{}, input: {}", RunEvent.STOP, input); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state STOP, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onStopped(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.STOP) + .getTransaction(RunEvent.STOP) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug("Executing internal logic for state STOP, " + "event :{}, input: {}", RunEvent.STOP, input); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state STOP, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onStopped(run, runnable); + return Optional.ofNullable(runStatus); + }); try { Optional runStatus = fsm.goToState(State.STOPPED, null); // Update run status RunBaseStatus runBaseStatus = runStatus - .map(r -> { - r.setState(State.STOPPED.toString()); - return r; - }) - .orElseGet(() -> new RunBaseStatus(State.STOPPED.toString())); + .map(r -> { + r.setState(State.STOPPED.toString()); + return r; + }) + .orElseGet(() -> new RunBaseStatus(State.STOPPED.toString())); RunRunnable runRunnable = event != null ? event.getRunnable() : null; // Iterate over all processor and store all RunBaseStatus as optional List processorsStatus = processorRegistry - .getProcessors("onStopped") - .stream() - .map(processor -> processor.process(run, runRunnable, runBaseStatus)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .getProcessors("onStopped") + .stream() + .map(processor -> processor.process(run, runRunnable, runBaseStatus)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); Map runStatusMap = processorsStatus - .stream() - .map(RunBaseStatus::toMap) - .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); + .stream() + .map(RunBaseStatus::toMap) + .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); run.setStatus(MapUtils.mergeMultipleMaps(run.getStatus(), runBaseStatus.toMap(), runStatusMap)); @@ -754,7 +747,7 @@ private void onStopped(Run run, RunnableChangedEvent event) * @throws NoSuchEntityException if the entity being accessed does not exist */ private void onError(Run run, RunnableChangedEvent event) - throws NoSuchEntityException, StoreException { + throws NoSuchEntityException, StoreException { // Try to move forward state machine based on current state Fsm> fsm = createFsm(run); try { @@ -762,87 +755,87 @@ private void onError(Run run, RunnableChangedEvent event) Executable executable = retrieveExecutable(run); Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); fsm - .getState(State.RUNNING) - .getTransaction(RunEvent.ERROR) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state RUNNING, " + "event :{}, input: {}", - RunEvent.ERROR, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onError(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.RUNNING) + .getTransaction(RunEvent.ERROR) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state RUNNING, " + "event :{}, input: {}", + RunEvent.ERROR, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state RUNNING, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onError(run, runnable); + return Optional.ofNullable(runStatus); + }); fsm - .getState(State.STOP) - .getTransaction(RunEvent.ERROR) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state STOP, " + "event :{}, input: {}", - RunEvent.ERROR, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state STOP, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onError(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.STOP) + .getTransaction(RunEvent.ERROR) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state STOP, " + "event :{}, input: {}", + RunEvent.ERROR, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state STOP, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onError(run, runnable); + return Optional.ofNullable(runStatus); + }); fsm - .getState(State.ERROR) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state ERROR, " + "event :{}, input: {}", - RunEvent.DELETING, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state ERROR, " + "context: {}", context); - } - RunRunnable runnable = runtime.delete(run); - return Optional.ofNullable(runnable); - }); + .getState(State.ERROR) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state ERROR, " + "event :{}, input: {}", + RunEvent.DELETING, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state ERROR, " + "context: {}", context); + } + RunRunnable runnable = runtime.delete(run); + return Optional.ofNullable(runnable); + }); try { Optional runStatus = fsm.goToState(State.ERROR, null); // Update run status RunBaseStatus runBaseStatus = runStatus - .map(r -> { - r.setState(State.ERROR.toString()); - return r; - }) - .orElseGet(() -> new RunBaseStatus(State.ERROR.toString())); + .map(r -> { + r.setState(State.ERROR.toString()); + return r; + }) + .orElseGet(() -> new RunBaseStatus(State.ERROR.toString())); RunRunnable runRunnable = event != null ? event.getRunnable() : null; // Iterate over all processor and store all RunBaseStatus as optional List processorsStatus = processorRegistry - .getProcessors("onError") - .stream() - .map(processor -> processor.process(run, runRunnable, runBaseStatus)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .getProcessors("onError") + .stream() + .map(processor -> processor.process(run, runRunnable, runBaseStatus)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); Map runStatusMap = processorsStatus - .stream() - .map(RunBaseStatus::toMap) - .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); + .stream() + .map(RunBaseStatus::toMap) + .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); run.setStatus(MapUtils.mergeMultipleMaps(run.getStatus(), runBaseStatus.toMap(), runStatusMap)); @@ -876,7 +869,7 @@ private void onError(Run run, RunnableChangedEvent event) * @throws NoSuchEntityException if the entity being accessed does not exist */ private void onDeleted(Run run, RunnableChangedEvent event) - throws NoSuchEntityException, StoreException { + throws NoSuchEntityException, StoreException { // Try to move forward state machine based on current state Fsm> fsm = createFsm(run); @@ -887,109 +880,109 @@ private void onDeleted(Run run, RunnableChangedEvent event) // Retrieve Runtime Executable executable = retrieveExecutable(run); Runtime< - ? extends ExecutableBaseSpec, - ? extends RunBaseSpec, - ? extends RunBaseStatus, - ? extends RunRunnable - > runtime = runtimeFactory.getRuntime(executable.getKind()); + ? extends ExecutableBaseSpec, + ? extends RunBaseSpec, + ? extends RunBaseStatus, + ? extends RunRunnable + > runtime = runtimeFactory.getRuntime(executable.getKind()); // Define logic for state DELETING fsm - .getState(State.DELETING) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state DELETING, " + "event :{}, input: {}", - RunEvent.DELETING, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state DELETING, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onDeleted(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.DELETING) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state DELETING, " + "event :{}, input: {}", + RunEvent.DELETING, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state DELETING, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onDeleted(run, runnable); + return Optional.ofNullable(runStatus); + }); // Define logic for state ERROR fsm - .getState(State.ERROR) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state ERROR, " + "event :{}, input: {}", - RunEvent.DELETING, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state ERROR, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onDeleted(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.ERROR) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state ERROR, " + "event :{}, input: {}", + RunEvent.DELETING, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state ERROR, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onDeleted(run, runnable); + return Optional.ofNullable(runStatus); + }); // Define logic for state COMPLETED fsm - .getState(State.COMPLETED) - .getTransaction(RunEvent.DELETING) - .setInternalLogic((context, input, fsmInstance) -> { - log.debug( - "Executing internal logic for state COMPLETED, " + "event :{}, input: {}", - RunEvent.DELETING, - input - ); - if (log.isTraceEnabled()) { - log.trace("Executing internal logic for state COMPLETED, " + "context: {}", context); - } - RunRunnable runnable = event != null ? event.getRunnable() : null; - RunBaseStatus runStatus = runtime.onDeleted(run, runnable); - return Optional.ofNullable(runStatus); - }); + .getState(State.COMPLETED) + .getTransaction(RunEvent.DELETING) + .setInternalLogic((context, input, fsmInstance) -> { + log.debug( + "Executing internal logic for state COMPLETED, " + "event :{}, input: {}", + RunEvent.DELETING, + input + ); + if (log.isTraceEnabled()) { + log.trace("Executing internal logic for state COMPLETED, " + "context: {}", context); + } + RunRunnable runnable = event != null ? event.getRunnable() : null; + RunBaseStatus runStatus = runtime.onDeleted(run, runnable); + return Optional.ofNullable(runStatus); + }); try { Optional runStatus = fsm.goToState(State.DELETED, null); // Update run status RunBaseStatus runBaseStatus = runStatus - .map(r -> { - if (toDelete) { - //explicit delete request leads to deleted status - r.setState(State.DELETED.toString()); - } else { - //keep state as-is - r.setState(curState); - } + .map(r -> { + if (toDelete) { + //explicit delete request leads to deleted status + r.setState(State.DELETED.toString()); + } else { + //keep state as-is + r.setState(curState); + } - return r; - }) - .orElseGet(() -> { - RunBaseStatus r = new RunBaseStatus(); - if (toDelete) { - //explicit delete request leads to deleted status - r.setState(State.DELETED.toString()); - } else { - //keep state as-is - r.setState(curState); - } + return r; + }) + .orElseGet(() -> { + RunBaseStatus r = new RunBaseStatus(); + if (toDelete) { + //explicit delete request leads to deleted status + r.setState(State.DELETED.toString()); + } else { + //keep state as-is + r.setState(curState); + } - return r; - }); + return r; + }); RunRunnable runRunnable = event != null ? event.getRunnable() : null; // Iterate over all processor and store all RunBaseStatus as optional List processorsStatus = processorRegistry - .getProcessors("onDeleted") - .stream() - .map(processor -> processor.process(run, runRunnable, runBaseStatus)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .getProcessors("onDeleted") + .stream() + .map(processor -> processor.process(run, runRunnable, runBaseStatus)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); Map runStatusMap = processorsStatus - .stream() - .map(RunBaseStatus::toMap) - .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); + .stream() + .map(RunBaseStatus::toMap) + .reduce(new HashMap<>(), MapUtils::mergeMultipleMaps); run.setStatus(MapUtils.mergeMultipleMaps(run.getStatus(), runBaseStatus.toMap(), runStatusMap)); @@ -1019,8 +1012,8 @@ private Fsm> createFsm(Run run) { // Initialize state machine Fsm> fsm = runStateMachine.builder( - State.valueOf(StatusFieldAccessor.with(run.getStatus()).getState()), - ctx + State.valueOf(StatusFieldAccessor.with(run.getStatus()).getState()), + ctx ); // On state change delegate state machine to update the run @@ -1048,8 +1041,8 @@ private Executable retrieveExecutable(Run run) throws NoSuchEntityException, Sto // Retrieve Executable String executableId = runSpecAccessor.getVersion(); return executableEntityServiceProvider - .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) - .get(executableId); + .getEntityServiceByRuntime(runSpecAccessor.getRuntime()) + .get(executableId); } private Specification createExecutableSpecification(String executable) { diff --git a/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java b/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java index 44bf34d7..3ea005ba 100644 --- a/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java +++ b/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java @@ -103,21 +103,26 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce .httpBasic(basic -> basic.authenticationEntryPoint(new Http403ForbiddenEntryPoint())) .userDetailsService(userDetailsService()); } + JwtAuthenticationProvider jwtExchangeAuthProvider = new JwtAuthenticationProvider(exchangeJwtDecoder()); + jwtExchangeAuthProvider.setJwtAuthenticationConverter(jwtAuthenticationConverter()); + + // Create authentication Manager + securityChain.oauth2ResourceServer(oauth2 -> + oauth2.jwt(jwt -> jwt.authenticationManager(new ProviderManager(jwtExchangeAuthProvider)))); + if (properties.isJwtAuthEnabled()) { JwtAuthenticationProvider jwtAacAuthProvider = new JwtAuthenticationProvider(jwtDecoder()); - JwtAuthenticationProvider jwtExchangeAuthProvider = new JwtAuthenticationProvider(exchangeJwtDecoder()); jwtAacAuthProvider.setJwtAuthenticationConverter(jwtAuthenticationConverter()); - jwtExchangeAuthProvider.setJwtAuthenticationConverter(jwtAuthenticationConverter()); - - // Create authentication Manager - AuthenticationManager authenticationManager = new ProviderManager(jwtAacAuthProvider, jwtExchangeAuthProvider); securityChain.oauth2ResourceServer(oauth2 -> - oauth2.jwt(jwt -> jwt.authenticationManager(authenticationManager))); + oauth2.jwt(jwt -> jwt.authenticationManager( + new ProviderManager(jwtExchangeAuthProvider, jwtAacAuthProvider)))); + } + //disable anonymous securityChain.anonymous(anon -> anon.disable()); } else { diff --git a/application/src/main/resources/application.yml b/application/src/main/resources/application.yml index 1c883fdb..45502277 100644 --- a/application/src/main/resources/application.yml +++ b/application/src/main/resources/application.yml @@ -195,6 +195,10 @@ files: bucket: ${S3_BUCKET:} # Keystore configuration -keystore: - path: ${KEYSTORE_PATH:./data/keystore.jwks} - kid: ${KEYSTORE_KID:} \ No newline at end of file +jwks: + keystore: + path: ${KEYSTORE_PATH:classpath:/keystore.jwks} + kid: ${KEYSTORE_KID:} + jwt: + expiration: ${JWT_EXPIRATION:} + cache-control: ${JWKS_CACHE_CONTROL:public, max-age=900, must-revalidate, no-transform} \ No newline at end of file diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java index cf2ba656..7566a63d 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java @@ -1,39 +1,32 @@ package it.smartcommunitylabdhub.authorization.components; +import com.google.common.base.Charsets; import com.google.common.io.CharStreams; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; import it.smartcommunitylabdhub.authorization.utils.JWKUtils; -import lombok.Getter; -import org.springframework.core.io.Resource; -import com.google.common.base.Charsets; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - import java.io.IOException; import java.io.InputStreamReader; -import java.io.Writer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; import java.text.ParseException; +import java.util.Optional; import java.util.UUID; +import lombok.Getter; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; public class JWKSetKeyStore { @Getter private final JWKSet jwkSet; - private final String kid; + private final String kid; public JWKSetKeyStore(Resource location, String kid) throws IllegalArgumentException { this.jwkSet = loadJwkSet(location); if (!StringUtils.hasText(kid)) { - this.kid = jwkSet.getKeys().getFirst().getKeyID(); // prendo la prima - // se è null, non ci sono chiavi assert + this.kid = jwkSet.getKeys().getFirst().getKeyID(); Assert.notNull(this.kid, "Key ID cannot be null"); } else { this.kid = jwkSet.getKeyByKeyId(kid).getKeyID(); @@ -42,18 +35,21 @@ public JWKSetKeyStore(Resource location, String kid) throws IllegalArgumentExcep } public JWKSetKeyStore() throws JOSEException { - this.jwkSet = initializeJwkSet(); - this.kid = this.jwkSet.getKeys().getFirst().getKeyID(); + this.jwkSet = createJwkSet(); + this.kid = + Optional + .of(this.jwkSet) + .flatMap(set -> set.getKeys().stream().findFirst().map(JWK::getKeyID)) + .orElseThrow(() -> new IllegalStateException("Key ID cannot be null")); + Assert.notNull(this.kid, "Key ID cannot be null"); } - private static JWKSet loadJwkSet(Resource location) throws IllegalArgumentException { + public static JWKSet loadJwkSet(Resource location) throws IllegalArgumentException { Assert.notNull(location, "Key Set resource cannot be null"); if (location.exists() && location.isReadable()) { try { - String s = CharStreams.toString( - new InputStreamReader(location.getInputStream(), Charsets.UTF_8) - ); + String s = CharStreams.toString(new InputStreamReader(location.getInputStream(), Charsets.UTF_8)); return JWKSet.parse(s); } catch (IOException e) { throw new IllegalArgumentException("Key Set resource could not be read: " + location); @@ -65,35 +61,13 @@ private static JWKSet loadJwkSet(Resource location) throws IllegalArgumentExcept } } - private static JWKSet initializeJwkSet() throws JOSEException { + public static JWKSet createJwkSet() throws JOSEException { String kid = UUID.randomUUID().toString(); JWK jwk = JWKUtils.generateRsaJWK(kid, "sig", "RS256", 2048); return new JWKSet(jwk); } - - public void saveJwkSet(Resource location) { - Assert.notNull(location, "Key Set resource cannot be null"); - try { - Path path = Paths.get(location.getURI()); - if (!Files.exists(path)) { - // Create directories if they do not exist - Files.createDirectories(path.getParent()); - } - - try (Writer writer = Files.newBufferedWriter( - path, StandardCharsets.UTF_8, - StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING)) { - writer.write(jwkSet.toJSONObject(false).toString()); - } - } catch (IOException e) { - throw new IllegalArgumentException("Key Set resource could not be written: " + location, e); - } - } - - public JWK getJwk() { return jwkSet.getKeyByKeyId(kid); } -} \ No newline at end of file +} diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java index 40a41ef9..a1bab1fb 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java @@ -1,71 +1,61 @@ package it.smartcommunitylabdhub.authorization.config; - +import com.nimbusds.jose.JOSEException; import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; -import com.nimbusds.jose.JOSEException; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Primary; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - +import org.springframework.core.io.ResourceLoader; @Configuration @Slf4j public class KeyStoreConfig { - @Value("${keystore.path}") + @Value("${jwks.keystore.path}") private String keyStorePath; - @Value("${keystore.kid}") + @Value("${jwks.keystore.kid}") private String kid; - private JWKSetKeyStore keyStore; - + @Autowired + private ResourceLoader resourceLoader; @Bean - @Primary public JWKSetKeyStore getJWKSetKeyStore() throws JOSEException { - Path path = Paths.get(keyStorePath).toAbsolutePath().normalize(); - Resource resource = new FileSystemResource(path); + Resource resource = resourceLoader.getResource(keyStorePath); - if (keyStore == null) { - if (Files.exists(path) && Files.isReadable(path)) { - // Load from resource - keyStore = load(resource); - // Check if empty - if (keyStore.getJwk() == null) { - // Discard, we will generate a new one - keyStore = null; - } - } + JWKSetKeyStore keyStore = null; - if (keyStore == null) { - // Generate new in-memory keystore - keyStore = generate(); - // Save to file - save(keyStore, resource); + if (resource.exists() && resource.isReadable()) { + // Load from resource + keyStore = new JWKSetKeyStore(resource, kid); + // Check if empty + if (keyStore.getJwk() == null) { + // Discard, we will generate a new one + keyStore = null; } } - return keyStore; - } - - private JWKSetKeyStore load(Resource location) throws IllegalArgumentException { - return new JWKSetKeyStore(location, kid); - } - - private JWKSetKeyStore generate() throws JOSEException { - return new JWKSetKeyStore(); - } + if (keyStore == null) { + // Generate new in-memory keystore + keyStore = new JWKSetKeyStore(); + + // if resource is a file, write it + if (resource.isFile()) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(resource.getFile()))) { + writer.write(keyStore.getJwkSet().toJSONObject(false).toString()); + } catch (IOException e) { + throw new IllegalArgumentException("Key Set resource could not be written: " + keyStorePath, e); + } + } + } - private void save(JWKSetKeyStore keyStore, Resource location) { - keyStore.saveJwkSet(location); + return keyStore; } } diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java index 12a2f637..08edb70a 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java @@ -2,14 +2,15 @@ import com.nimbusds.jose.jwk.JWKSet; import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.Map; - @RestController @Slf4j public class JWKController { @@ -17,12 +18,15 @@ public class JWKController { @Autowired private JWKSetKeyStore jwkSetKeyStore; - @GetMapping("/.well-known/jwks") + @Value("${jwks.cache-control}") + private String cacheControl; + + @GetMapping("/.well-known/jwks.json") public ResponseEntity> getJWKInfo() { JWKSet jwkSet = jwkSetKeyStore.getJwkSet(); // Convert JWKSet to a map for easier JSON serialization Map jwkSetMap = jwkSet.toJSONObject(); - return ResponseEntity.ok(jwkSetMap); + + return ResponseEntity.ok().header(HttpHeaders.CACHE_CONTROL, cacheControl).body(jwkSetMap); } } - diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/exceptions/JwtTokenServiceException.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/exceptions/JwtTokenServiceException.java new file mode 100644 index 00000000..4ee9449e --- /dev/null +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/exceptions/JwtTokenServiceException.java @@ -0,0 +1,8 @@ +package it.smartcommunitylabdhub.authorization.exceptions; + +public class JwtTokenServiceException extends RuntimeException { + + public JwtTokenServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java index 0c241f22..289abff2 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java @@ -3,33 +3,32 @@ import com.nimbusds.jose.*; import com.nimbusds.jose.crypto.RSASSASigner; import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; +import it.smartcommunitylabdhub.authorization.exceptions.JwtTokenServiceException; import it.smartcommunitylabdhub.commons.config.ApplicationProperties; import it.smartcommunitylabdhub.commons.config.SecurityProperties; +import java.security.interfaces.RSAPrivateKey; +import java.util.Date; +import java.util.List; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Service; -import org.springframework.beans.factory.annotation.Value; - -import java.io.Serializable; -import java.security.interfaces.RSAPrivateKey; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.UUID; @Service @Slf4j public class JwtTokenService { @Autowired - private JWKSetKeyStore keyStoreUtil; + private JWKSetKeyStore keyStore; @Autowired ApplicationProperties applicationProperties; @@ -37,31 +36,27 @@ public class JwtTokenService { @Autowired SecurityProperties securityProperties; - // Default to 30 days in milliseconds (30 * 24 * 60 * 60 * 1000) - @Value("${jwt.expiration:2592000000}") + @Value("${jwks.jwt.expiration}") private long jwtExpiration; - - public String generateToken(Authentication authentication) - throws JwtTokenServiceException { + public String generateToken(Authentication authentication) throws JwtTokenServiceException { try { - // Extract claims from authentication if it's a JwtAuthenticationToken - JWK jwk = keyStoreUtil.getJwk(); + JWK jwk = keyStore.getJwk(); RSAPrivateKey privateKey = jwk.toRSAKey().toRSAPrivateKey(); RSASSASigner signer = new RSASSASigner(privateKey); // Prepare JWT claims JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder() - .subject(authentication.getName()) - .issuer(applicationProperties.getEndpoint()) - .issueTime(new Date()) - .audience(applicationProperties.getName()) - .jwtID(UUID.randomUUID().toString()) - .expirationTime(new Date(System.currentTimeMillis() + jwtExpiration)); + .subject(authentication.getName()) + .issuer(applicationProperties.getEndpoint()) + .issueTime(new Date()) + .audience(applicationProperties.getName()) + .jwtID(UUID.randomUUID().toString()) + .expirationTime(new Date(System.currentTimeMillis() + jwtExpiration)); List additionalClaims = extractClaims(authentication); - if(additionalClaims != null){ + if (additionalClaims != null) { claimsSetBuilder.claim(securityProperties.getJwt().getClaim(), additionalClaims); } @@ -70,9 +65,9 @@ public String generateToken(Authentication authentication) // Create signed JWT SignedJWT signedJWT = new SignedJWT( - new JWSHeader.Builder(JWSAlgorithm.RS256) - .keyID(jwk.getKeyID()).build(), - claimsSet); + new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(jwk.getKeyID()).build(), + claimsSet + ); // Compute the RSA signature signedJWT.sign(signer); @@ -80,7 +75,9 @@ public String generateToken(Authentication authentication) // Serialize to compact form String jwtToken = signedJWT.serialize(); - log.info("Generated JWT token: {}", jwtToken); + if (log.isTraceEnabled()) { + log.trace("Generated JWT token: {}", jwtToken); + } return jwtToken; } catch (JOSEException e) { @@ -89,20 +86,15 @@ public String generateToken(Authentication authentication) } } - private List extractClaims(Authentication authentication) { if (authentication instanceof JwtAuthenticationToken jwtAuthToken) { Jwt jwt = jwtAuthToken.getToken(); return jwt.getClaimAsStringList(securityProperties.getJwt().getClaim()); + } else if (authentication instanceof UsernamePasswordAuthenticationToken) { + return authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList(); } else { log.warn("Authentication is not of type JwtAuthenticationToken"); return null; } } - - public static class JwtTokenServiceException extends RuntimeException { - public JwtTokenServiceException(String message, Throwable cause) { - super(message, cause); - } - } } diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/utils/JWKUtils.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/utils/JWKUtils.java index 60eb60c2..918bb099 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/utils/JWKUtils.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/utils/JWKUtils.java @@ -17,16 +17,16 @@ public class JWKUtils { private static final Logger logger = LoggerFactory.getLogger(JWKUtils.class); public static JWK generateRsaJWK(String id, String usage, String alg, int length) - throws IllegalArgumentException, JOSEException { + throws IllegalArgumentException, JOSEException { logger.debug( - "generate RSA jwk for " + - id + - " use " + - usage + - " with length " + - String.valueOf(length) + - " with algorithm " + - alg + "generate RSA jwk for " + + id + + " use " + + usage + + " with length " + + String.valueOf(length) + + " with algorithm " + + alg ); if (id == null || id.isEmpty()) { @@ -43,9 +43,9 @@ public static JWK generateRsaJWK(String id, String usage, String alg, int length } public static JWK generateECJWK(String id, String usage, String alg, String curve) - throws IllegalArgumentException, JOSEException { + throws IllegalArgumentException, JOSEException { logger.debug( - "generate EC jwk for " + id + " use " + usage + " with curve " + curve + " with algorithm " + alg + "generate EC jwk for " + id + " use " + usage + " with curve " + curve + " with algorithm " + alg ); if (id == null || id.isEmpty()) { @@ -65,7 +65,7 @@ public static JWK generateECJWK(String id, String usage, String alg, String curv } public static JWK generateHMACJWT(String id, String usage, String alg, int length) - throws IllegalArgumentException, JOSEException { + throws IllegalArgumentException, JOSEException { logger.debug("generate HMAC jwk for " + id + " use " + usage + " with algorithm " + alg); if (id == null || id.isEmpty()) { From 6ec0a09e2d4f5305e1116fe977dbf29d5f854b4b Mon Sep 17 00:00:00 2001 From: Luca Trubbiani Date: Mon, 29 Jul 2024 15:49:27 +0200 Subject: [PATCH 05/10] Clean SecurityProperties --- .../smartcommunitylabdhub/commons/config/SecurityProperties.java | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java b/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java index 3fec4fe3..b57ad4dc 100644 --- a/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java +++ b/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java @@ -63,7 +63,6 @@ public boolean isEnabled() { } } - @Getter @Setter public static class OidcAuthenticationProperties { From db5782b1b78bb870e26ffb827732ce5bb425923c Mon Sep 17 00:00:00 2001 From: Matteo Saloni Date: Mon, 5 Aug 2024 16:35:52 +0200 Subject: [PATCH 06/10] feat: implement auth server + add refresh + add endpoints + refactoring --- .../core/components/run/RunManager.java | 10 +- .../core/config/SecurityConfig.java | 282 +++++++++++++----- .../src/main/resources/application.yml | 16 +- modules/authorization/.flattened-pom.xml | 48 --- modules/authorization/pom.xml | 54 +--- .../components/JWKSetKeyStore.java | 100 ++++--- .../authorization/config/KeyStoreConfig.java | 45 +-- .../controllers/ConfigurationEndpoint.java | 62 ++++ .../{JWKController.java => JWKSEndpoint.java} | 12 +- .../controllers/TokenEndpoint.java | 176 +++++++++++ .../authorization/model/TokenResponse.java | 52 ++++ .../services/JwtTokenService.java | 241 ++++++++++++--- .../authorization/utils/JWKUtils.java | 141 +++++---- .../authorization/JwkTests.java | 22 ++ 14 files changed, 885 insertions(+), 376 deletions(-) create mode 100644 modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/ConfigurationEndpoint.java rename modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/{JWKController.java => JWKSEndpoint.java} (81%) create mode 100644 modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/TokenEndpoint.java create mode 100644 modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/model/TokenResponse.java create mode 100644 modules/authorization/src/test/java/it/smartcommunitylabdhub/authorization/JwkTests.java diff --git a/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java b/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java index d87e7c63..5b9a466a 100644 --- a/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java +++ b/application/src/main/java/it/smartcommunitylabdhub/core/components/run/RunManager.java @@ -204,12 +204,12 @@ public Run run(@NotNull Run run) throws NoSuchEntityException, InvalidTransactio //extract auth from security context to inflate secured credentials //TODO refactor properly if (r instanceof SecuredRunnable) { - // check that jwt is enabled via securityProperties + // check that auth is enabled via securityProperties Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null) { - String token = jwtTokenService.generateToken(auth); - if (token != null) { - ((SecuredRunnable) r).setCredentials(token); + if (auth != null && securityProperties.isRequired()) { + Serializable credentials = jwtTokenService.generateCredentials(auth); + if (credentials != null) { + ((SecuredRunnable) r).setCredentials(credentials); } } } diff --git a/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java b/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java index 3ea005ba..bbd48e96 100644 --- a/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java +++ b/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java @@ -2,24 +2,22 @@ import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.RSAKey; import it.smartcommunitylabdhub.authorization.config.KeyStoreConfig; import it.smartcommunitylabdhub.commons.config.ApplicationProperties; import it.smartcommunitylabdhub.commons.config.SecurityProperties; import jakarta.servlet.http.HttpServletRequest; - -import java.security.interfaces.RSAPrivateKey; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.Optional; - import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.AuditorAware; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -32,10 +30,15 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.jwt.*; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtClaimValidator; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtValidators; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; -import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.AccessDeniedHandlerImpl; @@ -54,6 +57,9 @@ public class SecurityConfig { public static final String API_PREFIX = "/api"; + @Autowired + ApplicationProperties applicationProperties; + @Autowired SecurityProperties properties; @@ -66,26 +72,28 @@ public class SecurityConfig { @Value("${management.endpoints.web.base-path}") private String managementBasePath; - @Autowired - KeyStoreConfig keyStoreConfig; + @Value("${jwt.client-id}") + private String clientId; + + @Value("${jwt.client-secret}") + private String clientSecret; @Autowired - ApplicationProperties applicationProperties; + KeyStoreConfig keyStoreConfig; @Bean("apiSecurityFilterChain") public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { - HttpSecurity securityChain = http - .securityMatcher(getApiRequestMatcher()) - .authorizeHttpRequests(auth -> { - auth.requestMatchers(getApiRequestMatcher()).hasRole("USER").anyRequest().authenticated(); - }) - // disable request cache - .requestCache(requestCache -> requestCache.disable()) - //disable csrf - .csrf(csrf -> csrf.disable()) - // we don't want a session for these endpoints, each request should be evaluated - .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + .securityMatcher(getApiRequestMatcher()) + .authorizeHttpRequests(auth -> { + auth.requestMatchers(getApiRequestMatcher()).hasRole("USER").anyRequest().authenticated(); + }) + // disable request cache + .requestCache(requestCache -> requestCache.disable()) + //disable csrf + .csrf(csrf -> csrf.disable()) + // we don't want a session for these endpoints, each request should be evaluated + .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // allow cors securityChain.cors(cors -> { @@ -98,30 +106,36 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce //authentication (when configured) if (properties.isRequired()) { - if (properties.isBasicAuthEnabled()) { - securityChain - .httpBasic(basic -> basic.authenticationEntryPoint(new Http403ForbiddenEntryPoint())) - .userDetailsService(userDetailsService()); - } - JwtAuthenticationProvider jwtExchangeAuthProvider = new JwtAuthenticationProvider(exchangeJwtDecoder()); - jwtExchangeAuthProvider.setJwtAuthenticationConverter(jwtAuthenticationConverter()); + //always enable internal jwt auth provider + JwtAuthenticationProvider coreJwtAuthProvider = new JwtAuthenticationProvider(coreJwtDecoder()); + coreJwtAuthProvider.setJwtAuthenticationConverter(coreJwtAuthenticationConverter()); // Create authentication Manager securityChain.oauth2ResourceServer(oauth2 -> - oauth2.jwt(jwt -> jwt.authenticationManager(new ProviderManager(jwtExchangeAuthProvider)))); + oauth2.jwt(jwt -> jwt.authenticationManager(new ProviderManager(coreJwtAuthProvider))) + ); if (properties.isJwtAuthEnabled()) { + // rebuild auth manager to include external jwt provider + JwtAuthenticationProvider externalJwtAuthProvider = new JwtAuthenticationProvider(externalJwtDecoder()); - JwtAuthenticationProvider jwtAacAuthProvider = new JwtAuthenticationProvider(jwtDecoder()); - - jwtAacAuthProvider.setJwtAuthenticationConverter(jwtAuthenticationConverter()); + externalJwtAuthProvider.setJwtAuthenticationConverter(externalJwtAuthenticationConverter()); securityChain.oauth2ResourceServer(oauth2 -> - oauth2.jwt(jwt -> jwt.authenticationManager( - new ProviderManager(jwtExchangeAuthProvider, jwtAacAuthProvider)))); - + oauth2.jwt(jwt -> + jwt.authenticationManager(new ProviderManager(coreJwtAuthProvider, externalJwtAuthProvider)) + ) + ); } + //enable basic if required + if (properties.isBasicAuthEnabled()) { + securityChain + .httpBasic(basic -> basic.authenticationEntryPoint(new Http403ForbiddenEntryPoint())) + .userDetailsService( + userDetailsService(properties.getBasic().getUsername(), properties.getBasic().getPassword()) + ); + } //disable anonymous securityChain.anonymous(anon -> anon.disable()); @@ -135,58 +149,111 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce securityChain.exceptionHandling(handling -> { handling - .authenticationEntryPoint(new Http403ForbiddenEntryPoint()) - .accessDeniedHandler(new AccessDeniedHandlerImpl()); // use 403 + .authenticationEntryPoint(new Http403ForbiddenEntryPoint()) + .accessDeniedHandler(new AccessDeniedHandlerImpl()); // use 403 }); return securityChain.build(); } - public UserDetailsService userDetailsService() { + /** + * Internal basic auth + */ + public UserDetailsService userDetailsService(String username, String password) { //create admin user with full permissions UserDetails admin = User - .withDefaultPasswordEncoder() - .username(properties.getBasic().getUsername()) - .password(properties.getBasic().getPassword()) - .roles("ADMIN", "USER") - .build(); + .withDefaultPasswordEncoder() + .username(username) + .password(password) + .roles("ADMIN", "USER") + .build(); return new InMemoryUserDetailsManager(admin); } - private JwtDecoder jwtDecoder() { - SecurityProperties.JwtAuthenticationProperties jwtProps = properties.getJwt(); - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(jwtProps.getIssuerUri()).build(); + /** + * Internal auth via JWT + */ + + private JwtDecoder coreJwtDecoder() throws JOSEException { + JWK jwk = keyStoreConfig.getJWKSetKeyStore().getJwk(); + + //we support only RSA keys + if (!(jwk instanceof RSAKey)) { + throw new IllegalArgumentException("the provided key is not suitable for token authentication"); + } + + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(jwk.toRSAKey().toRSAPublicKey()).build(); + + OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer( + applicationProperties.getEndpoint() + ); OAuth2TokenValidator audienceValidator = new JwtClaimValidator>( - JwtClaimNames.AUD, - (aud -> aud != null && aud.contains(jwtProps.getAudience())) + JwtClaimNames.AUD, + (aud -> aud != null && aud.contains(applicationProperties.getName())) ); - OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(jwtProps.getIssuerUri()); - OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); - jwtDecoder.setJwtValidator(withAudience); + //access tokens *do not contain* at_hash, those are refresh + OAuth2TokenValidator accessTokenValidator = new JwtClaimValidator( + IdTokenClaimNames.AT_HASH, + (Objects::isNull) + ); + + OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<>( + withIssuer, + audienceValidator, + accessTokenValidator + ); + jwtDecoder.setJwtValidator(validator); return jwtDecoder; } - private JwtDecoder exchangeJwtDecoder() throws JOSEException { - JWK jwk = keyStoreConfig.getJWKSetKeyStore().getJwk(); - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(jwk.toRSAKey().toRSAPublicKey()).build(); + private JwtAuthenticationConverter coreJwtAuthenticationConverter() { + String claim = "authorities"; + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter((Jwt source) -> { + if (source == null) return null; + + List authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority("ROLE_USER")); + + if (StringUtils.hasText(claim) && source.hasClaim(claim)) { + List roles = source.getClaimAsStringList(claim); + if (roles != null) { + roles.forEach(r -> { + //use as is + authorities.add(new SimpleGrantedAuthority(r)); + }); + } + } + + return authorities; + }); + return converter; + } + + /** + * External auth via JWT + */ + private JwtDecoder externalJwtDecoder() { + SecurityProperties.JwtAuthenticationProperties jwtProps = properties.getJwt(); + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(jwtProps.getIssuerUri()).build(); OAuth2TokenValidator audienceValidator = new JwtClaimValidator>( - JwtClaimNames.AUD, - (aud -> aud != null && aud.contains(applicationProperties.getName())) + JwtClaimNames.AUD, + (aud -> aud != null && aud.contains(jwtProps.getAudience())) ); - OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(applicationProperties.getEndpoint()); + OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(jwtProps.getIssuerUri()); OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); jwtDecoder.setJwtValidator(withAudience); return jwtDecoder; } - private JwtAuthenticationConverter jwtAuthenticationConverter() { + private JwtAuthenticationConverter externalJwtAuthenticationConverter() { SecurityProperties.JwtAuthenticationProperties jwtProps = properties.getJwt(); String claim = jwtProps.getClaim(); JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); @@ -216,44 +283,93 @@ private JwtAuthenticationConverter jwtAuthenticationConverter() { return converter; } + @Bean("authSecurityFilterChain") + public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exception { + HttpSecurity securityChain = http + .securityMatcher(getAuthRequestMatcher()) + .authorizeHttpRequests(auth -> { + auth.requestMatchers(getAuthRequestMatcher()).hasRole("USER").anyRequest().authenticated(); + }) + // disable request cache + .requestCache(requestCache -> requestCache.disable()) + //disable csrf + .csrf(csrf -> csrf.disable()) + // we don't want a session for these endpoints, each request should be evaluated + .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + // allow cors + securityChain.cors(cors -> { + if (StringUtils.hasText(corsOrigins)) { + cors.configurationSource(corsConfigurationSource(corsOrigins)); + } else { + cors.disable(); + } + }); + + //authentication (when configured) + if (StringUtils.hasText(clientId) && StringUtils.hasText(clientSecret)) { + //enable basic + securityChain + .httpBasic(basic -> basic.authenticationEntryPoint(new Http403ForbiddenEntryPoint())) + .userDetailsService(userDetailsService(clientId, clientSecret)); + + //disable anonymous + securityChain.anonymous(anon -> anon.disable()); + } else { + //assign both USER and ADMIN to anon user to bypass all scoped permission checks + securityChain.anonymous(anon -> { + anon.authorities("ROLE_USER", "ROLE_ADMIN"); + anon.principal("anonymous"); + }); + } + + securityChain.exceptionHandling(handling -> { + handling + .authenticationEntryPoint(new Http403ForbiddenEntryPoint()) + .accessDeniedHandler(new AccessDeniedHandlerImpl()); // use 403 + }); + + return securityChain.build(); + } + @Bean("h2SecurityFilterChain") public SecurityFilterChain h2SecurityFilterChain(HttpSecurity http) throws Exception { return http - .securityMatcher(new AntPathRequestMatcher("/h2-console/**")) - .authorizeHttpRequests(auth -> { - auth.anyRequest().permitAll(); - }) - //disable csrf - .csrf(csrf -> csrf.disable()) - // we don't want a session for these endpoints, each request should be evaluated - .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - // enable frame options - .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())) - .build(); + .securityMatcher(new AntPathRequestMatcher("/h2-console/**")) + .authorizeHttpRequests(auth -> { + auth.anyRequest().permitAll(); + }) + //disable csrf + .csrf(csrf -> csrf.disable()) + // we don't want a session for these endpoints, each request should be evaluated + .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // enable frame options + .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())) + .build(); } @Bean("coreSecurityFilterChain") public SecurityFilterChain coreSecurityFilterChain(HttpSecurity http) throws Exception { return http - .authorizeHttpRequests(auth -> { - auth.anyRequest().permitAll(); - }) - // disable request cache - .requestCache(requestCache -> requestCache.disable()) - .build(); + .authorizeHttpRequests(auth -> { + auth.anyRequest().permitAll(); + }) + // disable request cache + .requestCache(requestCache -> requestCache.disable()) + .build(); } @Bean("monitoringSecurityFilterChain") public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // match only actuator endpoints return http - .securityMatcher(getManagementRequestMatcher()) - .authorizeHttpRequests(auth -> { - auth.anyRequest().permitAll(); - }) - .exceptionHandling(handling -> handling.authenticationEntryPoint(new Http403ForbiddenEntryPoint())) - .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .build(); + .securityMatcher(getManagementRequestMatcher()) + .authorizeHttpRequests(auth -> { + auth.anyRequest().permitAll(); + }) + .exceptionHandling(handling -> handling.authenticationEntryPoint(new Http403ForbiddenEntryPoint())) + .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .build(); } public RequestMatcher getManagementRequestMatcher() { @@ -264,6 +380,10 @@ public RequestMatcher getApiRequestMatcher() { return new AntPathRequestMatcher(API_PREFIX + "/**"); } + public RequestMatcher getAuthRequestMatcher() { + return new AntPathRequestMatcher("/auth/**"); + } + private CorsConfigurationSource corsConfigurationSource(String origins) { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOriginPatterns(new ArrayList<>(StringUtils.commaDelimitedListToSet(origins))); diff --git a/application/src/main/resources/application.yml b/application/src/main/resources/application.yml index 45502277..094dcb40 100644 --- a/application/src/main/resources/application.yml +++ b/application/src/main/resources/application.yml @@ -122,6 +122,8 @@ kubernetes: config-map: ${DH_CONFIG_COMMON_MAPS:} secret: ${DH_CONFIG_COMMON_SECRETS:} templates: ${K8S_TEMPLATES:} + envs: + prefix: ${K8S_ENVS_PREFIX:${application.name}} # Application endpoint application: @@ -195,10 +197,14 @@ files: bucket: ${S3_BUCKET:} # Keystore configuration -jwks: +jwt: keystore: - path: ${KEYSTORE_PATH:classpath:/keystore.jwks} - kid: ${KEYSTORE_KID:} - jwt: - expiration: ${JWT_EXPIRATION:} + path: ${JWT_KEYSTORE_PATH:classpath:/keystore.jwks} + kid: ${JWT_KEYSTORE_KID:} + access-token: + duration: ${JWT_ACCESS_TOKEN_DURATION:} + refresh-token: + duration: ${JWT_REFRESH_TOKEN_DURATION:} + client-id: ${JWT_CLIENT_ID:${security.basic.username}} + client-secret: ${JWT_CLIENT_SECRET:${security.basic.password}} cache-control: ${JWKS_CACHE_CONTROL:public, max-age=900, must-revalidate, no-transform} \ No newline at end of file diff --git a/modules/authorization/.flattened-pom.xml b/modules/authorization/.flattened-pom.xml index 1a7cf73d..a85a99c4 100644 --- a/modules/authorization/.flattened-pom.xml +++ b/modules/authorization/.flattened-pom.xml @@ -18,30 +18,12 @@ 3.2.0 compile - - org.springframework.boot - spring-boot-starter-oauth2-authorization-server - 3.2.0 - compile - - - com.google.guava - guava - 20.0 - compile - org.springframework.boot spring-boot-starter-oauth2-resource-server 3.2.0 compile - - com.nimbusds - nimbus-jose-jwt - 9.39.3 - compile - org.projectlombok lombok @@ -49,36 +31,6 @@ compile true - - com.fasterxml.jackson.core - jackson-core - 2.15.3 - compile - - - com.fasterxml.jackson.dataformat - jackson-dataformat-cbor - 2.15.3 - compile - - - com.fasterxml.jackson.dataformat - jackson-dataformat-yaml - 2.15.3 - compile - - - com.fasterxml.jackson.module - jackson-module-jsonSchema - 2.15.3 - compile - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.15.3 - compile - org.slf4j slf4j-api diff --git a/modules/authorization/pom.xml b/modules/authorization/pom.xml index feea5d22..09a31625 100644 --- a/modules/authorization/pom.xml +++ b/modules/authorization/pom.xml @@ -25,31 +25,18 @@ spring-boot-starter-security 3.2.0 - org.springframework.boot - spring-boot-starter-oauth2-authorization-server + spring-boot-starter-oauth2-resource-server 3.2.0 - - com.google.guava - guava - 20.0 - - - - org.springframework.boot - spring-boot-starter-oauth2-resource-server - 3.2.0 + spring-boot-starter-test + ${spring-boot.version} + test - - com.nimbusds - nimbus-jose-jwt - 9.39.3 - org.projectlombok lombok @@ -57,39 +44,6 @@ compile true - - - - org.springframework.boot - spring-boot-starter-test - ${spring-boot.version} - test - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - com.fasterxml.jackson.dataformat - jackson-dataformat-cbor - ${jackson.version} - - - com.fasterxml.jackson.dataformat - jackson-dataformat-yaml - ${jackson.version} - - - com.fasterxml.jackson.module - jackson-module-jsonSchema - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - ${jackson.version} - org.slf4j slf4j-api diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java index 7566a63d..2b20dea2 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java @@ -1,73 +1,83 @@ package it.smartcommunitylabdhub.authorization.components; -import com.google.common.base.Charsets; -import com.google.common.io.CharStreams; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.KeyUse; import it.smartcommunitylabdhub.authorization.utils.JWKUtils; -import java.io.IOException; -import java.io.InputStreamReader; -import java.text.ParseException; -import java.util.Optional; -import java.util.UUID; +import jakarta.annotation.Nullable; +import java.util.List; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.Resource; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +@Slf4j public class JWKSetKeyStore { @Getter private final JWKSet jwkSet; + @Getter private final String kid; - public JWKSetKeyStore(Resource location, String kid) throws IllegalArgumentException { - this.jwkSet = loadJwkSet(location); - if (!StringUtils.hasText(kid)) { - this.kid = jwkSet.getKeys().getFirst().getKeyID(); - Assert.notNull(this.kid, "Key ID cannot be null"); - } else { - this.kid = jwkSet.getKeyByKeyId(kid).getKeyID(); - Assert.notNull(this.kid, "Key ID cannot be null"); - } - } + public JWKSetKeyStore(Resource location, @Nullable String kid) throws IllegalArgumentException { + Assert.notNull(location, "location can not be null"); - public JWKSetKeyStore() throws JOSEException { - this.jwkSet = createJwkSet(); - this.kid = - Optional - .of(this.jwkSet) - .flatMap(set -> set.getKeys().stream().findFirst().map(JWK::getKeyID)) - .orElseThrow(() -> new IllegalStateException("Key ID cannot be null")); - - Assert.notNull(this.kid, "Key ID cannot be null"); - } + //load + log.debug("load keyStore from {}", location); + this.jwkSet = JWKUtils.loadJwkSet(location); + JWK jwk = load(jwkSet, kid); + this.kid = jwk.getKeyID(); - public static JWKSet loadJwkSet(Resource location) throws IllegalArgumentException { - Assert.notNull(location, "Key Set resource cannot be null"); - if (location.exists() && location.isReadable()) { - try { - String s = CharStreams.toString(new InputStreamReader(location.getInputStream(), Charsets.UTF_8)); - return JWKSet.parse(s); - } catch (IOException e) { - throw new IllegalArgumentException("Key Set resource could not be read: " + location); - } catch (ParseException e) { - throw new IllegalArgumentException("Key Set resource could not be parsed: " + location); - } - } else { - throw new IllegalArgumentException("Key Set resource could not be read: " + location); - } + log.debug("use key {} for signing", kid); } - public static JWKSet createJwkSet() throws JOSEException { - String kid = UUID.randomUUID().toString(); - JWK jwk = JWKUtils.generateRsaJWK(kid, "sig", "RS256", 2048); - return new JWKSet(jwk); + public JWKSetKeyStore() throws JOSEException { + //create + log.debug("create temporary keyStore"); + this.jwkSet = JWKUtils.createJwkSet(); + JWK jwk = load(jwkSet, null); + this.kid = jwk.getKeyID(); + + log.debug("use key {} for signing", kid); } public JWK getJwk() { return jwkSet.getKeyByKeyId(kid); } + + private JWK load(JWKSet jwkSet, String kid) { + //if specified, kid must be in set + if (StringUtils.hasText(kid)) { + JWK key = jwkSet.getKeyByKeyId(kid); + Assert.notNull(key, "Provided key_id is not in the set"); + + //validate key usage + Assert.isTrue( + (key.getKeyUse() == KeyUse.SIGNATURE || key.getKeyUse() == null), + "key should be usable for signing" + ); + + //use + return key; + } else { + List keys = jwkSet.getKeys(); + Assert.notNull(keys, "keystore must contain at least one valid key"); + Assert.isTrue(!keys.isEmpty(), "keystore must contain at least one valid key"); + + //fetch the first signing key + JWK key = keys + .stream() + .filter(k -> (k.getKeyUse() == KeyUse.SIGNATURE || k.getKeyUse() == null)) + .findFirst() + .orElse(null); + + Assert.notNull(key, "No suitable key found in store"); + + //use + return key; + } + } } diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java index a1bab1fb..47cb122f 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java @@ -2,58 +2,35 @@ import com.nimbusds.jose.JOSEException; import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; -import java.io.BufferedWriter; -import java.io.FileWriter; -import java.io.IOException; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; @Configuration -@Slf4j public class KeyStoreConfig { - @Value("${jwks.keystore.path}") - private String keyStorePath; + @Value("${jwt.keystore.path}") + private Resource location; - @Value("${jwks.keystore.kid}") + @Value("${jwt.keystore.kid}") private String kid; - @Autowired - private ResourceLoader resourceLoader; + private JWKSetKeyStore keyStore; @Bean public JWKSetKeyStore getJWKSetKeyStore() throws JOSEException { - Resource resource = resourceLoader.getResource(keyStorePath); - - JWKSetKeyStore keyStore = null; - - if (resource.exists() && resource.isReadable()) { - // Load from resource - keyStore = new JWKSetKeyStore(resource, kid); - // Check if empty - if (keyStore.getJwk() == null) { - // Discard, we will generate a new one - keyStore = null; - } + if (keyStore != null) { + //re-use because we load from config *before* services are built + return keyStore; } - if (keyStore == null) { + if (location != null && location.exists() && location.isReadable()) { + // Load from resource + keyStore = new JWKSetKeyStore(location, kid); + } else { // Generate new in-memory keystore keyStore = new JWKSetKeyStore(); - - // if resource is a file, write it - if (resource.isFile()) { - try (BufferedWriter writer = new BufferedWriter(new FileWriter(resource.getFile()))) { - writer.write(keyStore.getJwkSet().toJSONObject(false).toString()); - } catch (IOException e) { - throw new IllegalArgumentException("Key Set resource could not be written: " + keyStorePath, e); - } - } } return keyStore; diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/ConfigurationEndpoint.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/ConfigurationEndpoint.java new file mode 100644 index 00000000..5014331e --- /dev/null +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/ConfigurationEndpoint.java @@ -0,0 +1,62 @@ +package it.smartcommunitylabdhub.authorization.controllers; + +import it.smartcommunitylabdhub.commons.config.ApplicationProperties; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ConfigurationEndpoint { + + @Autowired + private ApplicationProperties applicationProperties; + + @Value("${jwt.cache-control}") + private String cacheControl; + + private Map config = null; + + @GetMapping(value = { "/.well-known/openid-configuration", "/.well-known/oauth-authorization-server" }) + public ResponseEntity> getCOnfiguration() { + if (config == null) { + config = generate(); + } + + return ResponseEntity.ok().header(HttpHeaders.CACHE_CONTROL, cacheControl).body(config); + } + + private Map generate() { + /* + * OpenID Provider Metadata + * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + */ + + String baseUrl = applicationProperties.getEndpoint(); + Map m = new HashMap<>(); + + m.put("issuer", baseUrl); + m.put("jwks_uri", baseUrl + JWKSEndpoint.JWKS_URL); + m.put("response_types_supported", Collections.emptyList()); + + List grantTypes = Stream + .of(AuthorizationGrantType.CLIENT_CREDENTIALS, AuthorizationGrantType.REFRESH_TOKEN) + .map(t -> t.getValue()) + .toList(); + m.put("grant_types_supported", grantTypes); + + m.put("token_endpoint", baseUrl + TokenEndpoint.TOKEN_URL); + List authMethods = Collections.singletonList("client_secret_basic"); + m.put("token_endpoint_auth_methods_supported", authMethods); + + return m; + } +} diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKSEndpoint.java similarity index 81% rename from modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java rename to modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKSEndpoint.java index 08edb70a..1158e033 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKController.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKSEndpoint.java @@ -3,7 +3,6 @@ import com.nimbusds.jose.jwk.JWKSet; import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; import java.util.Map; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; @@ -12,19 +11,20 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@Slf4j -public class JWKController { +public class JWKSEndpoint { + + public static final String JWKS_URL = "/.well-known/jwks.json"; @Autowired private JWKSetKeyStore jwkSetKeyStore; - @Value("${jwks.cache-control}") + @Value("${jwt.cache-control}") private String cacheControl; - @GetMapping("/.well-known/jwks.json") + @GetMapping(JWKS_URL) public ResponseEntity> getJWKInfo() { + //expose the entire jwkSet as JSON JWKSet jwkSet = jwkSetKeyStore.getJwkSet(); - // Convert JWKSet to a map for easier JSON serialization Map jwkSetMap = jwkSet.toJSONObject(); return ResponseEntity.ok().header(HttpHeaders.CACHE_CONTROL, cacheControl).body(jwkSetMap); diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/TokenEndpoint.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/TokenEndpoint.java new file mode 100644 index 00000000..dad3cbe0 --- /dev/null +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/TokenEndpoint.java @@ -0,0 +1,176 @@ +package it.smartcommunitylabdhub.authorization.controllers; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.RSAKey; +import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; +import it.smartcommunitylabdhub.authorization.model.TokenResponse; +import it.smartcommunitylabdhub.authorization.services.JwtTokenService; +import it.smartcommunitylabdhub.commons.config.ApplicationProperties; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtClaimValidator; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtValidators; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TokenEndpoint implements InitializingBean { + + public static final String TOKEN_URL = "/auth/token"; + + @Value("${jwt.client-id}") + private String clientId; + + @Value("${jwt.client-secret}") + private String clientSecret; + + @Autowired + private JwtTokenService jwtTokenService; + + @Autowired + private JWKSetKeyStore jwkSetKeyStore; + + @Autowired + private ApplicationProperties applicationProperties; + + private JwtAuthenticationProvider authProvider; + + @Override + public void afterPropertiesSet() throws Exception { + Assert.notNull(jwkSetKeyStore, "jwks store is required"); + Assert.notNull(jwkSetKeyStore.getJwk(), "jwk is required"); + + //build auth provider to validate tokens + authProvider = new JwtAuthenticationProvider(coreJwtDecoder(jwkSetKeyStore.getJwk())); + JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter(); + JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); + authoritiesConverter.setAuthoritiesClaimName("authorities"); + authoritiesConverter.setAuthorityPrefix(""); + jwtConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); + authProvider.setJwtAuthenticationConverter(jwtConverter); + } + + @PostMapping(TOKEN_URL) + public TokenResponse token(@RequestParam Map parameters, Authentication authentication) { + //resolve client authentication + if (authentication == null || !(authentication.isAuthenticated())) { + throw new InsufficientAuthenticationException("Invalid or missing authentication"); + } + + //select flow + String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE); + + if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) { + return clientCredentials(parameters, authentication); + } else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) { + return refreshToken(parameters, authentication); + } + + throw new IllegalArgumentException("invalid or unsupported grant type"); + } + + private TokenResponse refreshToken(Map parameters, Authentication authentication) { + //refresh token is usable without credentials + //TODO add rotation by storing refresh tokens in db! + + String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE); + if (!AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) { + throw new IllegalArgumentException("invalid grant type"); + } + + String token = parameters.get("refresh_token"); + if (token == null) { + throw new IllegalArgumentException("invalid or missing refresh_token"); + } + + //validate via provider + try { + BearerTokenAuthenticationToken request = new BearerTokenAuthenticationToken(token); + Authentication auth = authProvider.authenticate(request); + if (!auth.isAuthenticated()) { + throw new IllegalArgumentException("invalid or missing refresh_token"); + } + + //token is valid, use as context for generation + return jwtTokenService.generateCredentials(auth); + } catch (AuthenticationException ae) { + throw new IllegalArgumentException("invalid or missing refresh_token"); + } + } + + private TokenResponse clientCredentials(Map parameters, Authentication authentication) { + //client credentials *requires* basic auth + if (authentication == null || !(authentication instanceof UsernamePasswordAuthenticationToken)) { + throw new InsufficientAuthenticationException("Invalid or missing authentication"); + } + + //for client credentials to mimic admin user client *must* match authenticated user + UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication; + if (clientId != null && !clientId.equals(auth.getName())) { + throw new InsufficientAuthenticationException("Invalid client authentication"); + } + + String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE); + if (!AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) { + throw new IllegalArgumentException("invalid grant type"); + } + + //generate as per user + return jwtTokenService.generateCredentials(authentication); + } + + private JwtDecoder coreJwtDecoder(JWK jwk) throws JOSEException { + //we support only RSA keys + if (!(jwk instanceof RSAKey)) { + throw new IllegalArgumentException("the provided key is not suitable for token authentication"); + } + + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(jwk.toRSAKey().toRSAPublicKey()).build(); + + OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer( + applicationProperties.getEndpoint() + ); + OAuth2TokenValidator audienceValidator = new JwtClaimValidator>( + JwtClaimNames.AUD, + (aud -> aud != null && aud.contains(applicationProperties.getName())) + ); + + //refresh tokens *must contain* at_hash + OAuth2TokenValidator tokenValidator = new JwtClaimValidator( + IdTokenClaimNames.AT_HASH, + (hash -> hash != null) + ); + + OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<>( + withIssuer, + audienceValidator, + tokenValidator + ); + jwtDecoder.setJwtValidator(validator); + + return jwtDecoder; + } +} diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/model/TokenResponse.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/model/TokenResponse.java new file mode 100644 index 00000000..3fb77f49 --- /dev/null +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/model/TokenResponse.java @@ -0,0 +1,52 @@ +package it.smartcommunitylabdhub.authorization.model; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.nimbusds.jwt.SignedJWT; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TokenResponse implements Serializable { + + @JsonProperty("access_token") + private SignedJWT accessToken; + + @JsonProperty("token_type") + @Builder.Default + private String tokenType = "Bearer"; + + @JsonProperty("refresh_token") + private SignedJWT refreshToken; + + @JsonProperty("expires_in") + private Integer expiration; + + @JsonProperty("client_id") + private String clientId; + + @JsonProperty("issuer") + private String issuer; + + @JsonGetter + public String getAccessToken() { + return accessToken != null ? accessToken.serialize() : null; + } + + @JsonGetter + public String getRefreshToken() { + return refreshToken != null ? refreshToken.serialize() : null; + } +} diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java index 289abff2..f2d2fdc1 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java @@ -1,99 +1,242 @@ package it.smartcommunitylabdhub.authorization.services; -import com.nimbusds.jose.*; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.crypto.ECDSAVerifier; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jose.crypto.MACVerifier; import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; import it.smartcommunitylabdhub.authorization.exceptions.JwtTokenServiceException; +import it.smartcommunitylabdhub.authorization.model.TokenResponse; +import it.smartcommunitylabdhub.authorization.utils.JWKUtils; import it.smartcommunitylabdhub.commons.config.ApplicationProperties; -import it.smartcommunitylabdhub.commons.config.SecurityProperties; -import java.security.interfaces.RSAPrivateKey; +import java.time.Instant; import java.util.Date; import java.util.List; import java.util.UUID; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; @Service @Slf4j -public class JwtTokenService { +public class JwtTokenService implements InitializingBean { + + private static final int DEFAULT_ACCESS_TOKEN_DURATION = 3600 * 8; //8 hours + private static final int DEFAULT_REFRESH_TOKEN_DURATION = 3600 * 24 * 30; //30 days @Autowired private JWKSetKeyStore keyStore; @Autowired - ApplicationProperties applicationProperties; + private ApplicationProperties applicationProperties; + + @Value("${jwt.client-id}") + private String clientId; + + private int accessTokenDuration = DEFAULT_ACCESS_TOKEN_DURATION; + private int refreshTokenDuration = DEFAULT_REFRESH_TOKEN_DURATION; + + //we need to keep the key along with singer/verifier + private JWK jwk; + private JWSSigner signer; + private JWSVerifier verifier; @Autowired - SecurityProperties securityProperties; + public void setAccessTokenDuration(@Value("${jwt.access-token.duration}") Integer accessTokenDuration) { + if (accessTokenDuration != null) { + this.accessTokenDuration = accessTokenDuration.intValue(); + } + } - @Value("${jwks.jwt.expiration}") - private long jwtExpiration; + @Autowired + public void setRefreshTokenDuration(@Value("${jwt.access-token.duration}") Integer refreshTokenDuration) { + if (refreshTokenDuration != null) { + this.refreshTokenDuration = refreshTokenDuration.intValue(); + } + } + + @Override + public void afterPropertiesSet() throws Exception { + //build signer for the given keys + this.jwk = keyStore.getJwk(); + + if (jwk != null) { + try { + if (!(jwk.getAlgorithm() instanceof JWSAlgorithm)) { + throw new JOSEException("key algorithm invalid"); + } + + if (jwk instanceof RSAKey) { + // build RSA signers & verifiers + if (jwk.isPrivate()) { // only add the signer if there's a private key + signer = new RSASSASigner((RSAKey) jwk); + } + verifier = new RSASSAVerifier((RSAKey) jwk); + } else if (jwk instanceof ECKey) { + // build EC signers & verifiers + if (jwk.isPrivate()) { + signer = new ECDSASigner((ECKey) jwk); + } + + verifier = new ECDSAVerifier((ECKey) jwk); + } else if (jwk instanceof OctetSequenceKey) { + // build HMAC signers & verifiers + + if (jwk.isPrivate()) { // technically redundant check because all HMAC keys are private + signer = new MACSigner((OctetSequenceKey) jwk); + } + + verifier = new MACVerifier((OctetSequenceKey) jwk); + } else { + log.warn("Unknown key type: " + jwk); + } + } catch (JOSEException e) { + log.warn("Exception loading signer/verifier", e); + } + } + } + + public TokenResponse generateCredentials(Authentication authentication) { + // Serialize to compact form + SignedJWT accessToken = generateAccessToken(authentication); + SignedJWT refreshToken = generateRefreshToken(authentication, accessToken); + + return TokenResponse + .builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiration(accessTokenDuration) + .clientId(clientId) + .issuer(applicationProperties.getEndpoint()) + .build(); + } + + public String generateAccessTokenAsString(Authentication authentication) throws JwtTokenServiceException { + // Serialize to compact form + SignedJWT jwt = generateAccessToken(authentication); + String jwtToken = jwt.serialize(); + + if (log.isTraceEnabled()) { + log.trace("Generated JWT token: {}", jwtToken); + } + + return jwtToken; + } + + public SignedJWT generateAccessToken(Authentication authentication) throws JwtTokenServiceException { + if (signer == null) { + throw new UnsupportedOperationException("signer not available"); + } - public String generateToken(Authentication authentication) throws JwtTokenServiceException { try { - // Extract claims from authentication if it's a JwtAuthenticationToken - JWK jwk = keyStore.getJwk(); - RSAPrivateKey privateKey = jwk.toRSAKey().toRSAPrivateKey(); - RSASSASigner signer = new RSASSASigner(privateKey); + //cast because we checked during load + JWSAlgorithm jwsAlgorithm = (JWSAlgorithm) jwk.getAlgorithm(); + + Instant now = Instant.now(); - // Prepare JWT claims - JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder() + // build access token claims + JWTClaimsSet.Builder claims = new JWTClaimsSet.Builder() .subject(authentication.getName()) .issuer(applicationProperties.getEndpoint()) - .issueTime(new Date()) + .issueTime(Date.from(now)) .audience(applicationProperties.getName()) .jwtID(UUID.randomUUID().toString()) - .expirationTime(new Date(System.currentTimeMillis() + jwtExpiration)); + .expirationTime(Date.from(now.plusSeconds(accessTokenDuration))); - List additionalClaims = extractClaims(authentication); - if (additionalClaims != null) { - claimsSetBuilder.claim(securityProperties.getJwt().getClaim(), additionalClaims); - } - - // Build claims set - JWTClaimsSet claimsSet = claimsSetBuilder.build(); - - // Create signed JWT - SignedJWT signedJWT = new SignedJWT( - new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(jwk.getKeyID()).build(), - claimsSet - ); + //define authorities as claims + List authorities = authentication + .getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .toList(); - // Compute the RSA signature - signedJWT.sign(signer); + claims.claim("authorities", authorities); - // Serialize to compact form - String jwtToken = signedJWT.serialize(); - - if (log.isTraceEnabled()) { - log.trace("Generated JWT token: {}", jwtToken); + //add client if set + if (StringUtils.hasText(clientId)) { + claims.claim("client_id", clientId); } - return jwtToken; + // build and sign + JWTClaimsSet claimsSet = claims.build(); + JWSHeader header = new JWSHeader.Builder(jwsAlgorithm).keyID(jwk.getKeyID()).build(); + SignedJWT jwt = new SignedJWT(header, claimsSet); + jwt.sign(signer); + + return jwt; } catch (JOSEException e) { log.error("Error generating JWT token", e); return null; } } - private List extractClaims(Authentication authentication) { - if (authentication instanceof JwtAuthenticationToken jwtAuthToken) { - Jwt jwt = jwtAuthToken.getToken(); - return jwt.getClaimAsStringList(securityProperties.getJwt().getClaim()); - } else if (authentication instanceof UsernamePasswordAuthenticationToken) { - return authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList(); - } else { - log.warn("Authentication is not of type JwtAuthenticationToken"); + public SignedJWT generateRefreshToken(Authentication authentication, SignedJWT accessToken) + throws JwtTokenServiceException { + if (signer == null) { + throw new UnsupportedOperationException("signer not available"); + } + + try { + //cast because we checked during load + JWSAlgorithm jwsAlgorithm = (JWSAlgorithm) jwk.getAlgorithm(); + + Instant now = Instant.now(); + + // build refresh token claims + JWTClaimsSet.Builder claims = new JWTClaimsSet.Builder() + .subject(authentication.getName()) + .issuer(applicationProperties.getEndpoint()) + .issueTime(Date.from(now)) + .audience(applicationProperties.getName()) + .jwtID(UUID.randomUUID().toString()) + .expirationTime(Date.from(now.plusSeconds(refreshTokenDuration))); + + //associate access token via hash binding + String hash = JWKUtils.getAccessTokenHash(jwsAlgorithm, accessToken); + claims.claim(IdTokenClaimNames.AT_HASH, hash); + + //define authorities as claims + List authorities = authentication + .getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .toList(); + + claims.claim("authorities", authorities); + + //add client if set + if (StringUtils.hasText(clientId)) { + claims.claim("client_id", clientId); + } + + // build and sign + JWTClaimsSet claimsSet = claims.build(); + JWSHeader header = new JWSHeader.Builder(jwsAlgorithm).keyID(jwk.getKeyID()).build(); + SignedJWT jwt = new SignedJWT(header, claimsSet); + jwt.sign(signer); + + return jwt; + } catch (JOSEException e) { + log.error("Error generating JWT token", e); return null; } } diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/utils/JWKUtils.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/utils/JWKUtils.java index 918bb099..611d9e83 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/utils/JWKUtils.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/utils/JWKUtils.java @@ -2,82 +2,117 @@ import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.jwk.Curve; import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.KeyUse; -import com.nimbusds.jose.jwk.gen.ECKeyGenerator; -import com.nimbusds.jose.jwk.gen.OctetSequenceKeyGenerator; import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; +import com.nimbusds.jose.util.Base64URL; +import com.nimbusds.jwt.SignedJWT; +import jakarta.annotation.Nullable; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.ParseException; +import java.util.Arrays; import java.util.UUID; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; public class JWKUtils { - private static final Logger logger = LoggerFactory.getLogger(JWKUtils.class); + private static final Integer DEFAULT_RSA_KEY_LENGTH = 2048; - public static JWK generateRsaJWK(String id, String usage, String alg, int length) - throws IllegalArgumentException, JOSEException { - logger.debug( - "generate RSA jwk for " + - id + - " use " + - usage + - " with length " + - String.valueOf(length) + - " with algorithm " + - alg - ); + public static JWK generateRsaJWK(@Nullable String id) throws IllegalArgumentException, JOSEException { + return generateRsaJWK(id, KeyUse.SIGNATURE, JWSAlgorithm.RS256, DEFAULT_RSA_KEY_LENGTH); + } + public static JWK generateRsaJWK( + @Nullable String id, + @Nullable KeyUse usage, + @Nullable JWSAlgorithm algorithm, + @Nullable Integer length + ) throws IllegalArgumentException, JOSEException { if (id == null || id.isEmpty()) { id = UUID.randomUUID().toString(); } - // validate keyUse - KeyUse use = new KeyUse(usage); - - // validate algorithm - JWSAlgorithm algo = JWSAlgorithm.parse(alg); - - return new RSAKeyGenerator(length).keyUse(use).keyID(id).algorithm(algo).generate(); + return new RSAKeyGenerator(length).keyUse(usage).keyID(id).algorithm(algorithm).generate(); } - public static JWK generateECJWK(String id, String usage, String alg, String curve) - throws IllegalArgumentException, JOSEException { - logger.debug( - "generate EC jwk for " + id + " use " + usage + " with curve " + curve + " with algorithm " + alg - ); - - if (id == null || id.isEmpty()) { - id = UUID.randomUUID().toString(); + public static JWKSet loadJwkSet(Resource location) throws IllegalArgumentException { + Assert.notNull(location, "Key Set resource cannot be null"); + + //read from file + if (location.exists() && location.isReadable()) { + try { + return JWKSet.parse(location.getContentAsString(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new IllegalArgumentException("Key Set resource could not be read: " + location); + } catch (ParseException e) { + throw new IllegalArgumentException("Key Set resource could not be parsed: " + location); + } + } else { + throw new IllegalArgumentException("Key Set resource could not be read: " + location); } + } - // validate keyUse - KeyUse use = new KeyUse(usage); - - // validate curve - Curve ecurve = Curve.parse(curve); - - // validate algorithm - JWSAlgorithm algo = JWSAlgorithm.parse(alg); - - return new ECKeyGenerator(ecurve).keyUse(use).keyID(id).algorithm(algo).generate(); + public static JWKSet createJwkSet() throws JOSEException { + //generate a set with a single RSA key for signing + return new JWKSet(JWKUtils.generateRsaJWK(null)); } - public static JWK generateHMACJWT(String id, String usage, String alg, int length) - throws IllegalArgumentException, JOSEException { - logger.debug("generate HMAC jwk for " + id + " use " + usage + " with algorithm " + alg); + public static String getAccessTokenHash(JWSAlgorithm signingAlg, SignedJWT token) { + byte[] tokenBytes = token.serialize().getBytes(); + Base64URL base64 = getHash(signingAlg, tokenBytes); + return base64.toString(); + } - if (id == null || id.isEmpty()) { - id = UUID.randomUUID().toString(); + public static Base64URL getHash(JWSAlgorithm signingAlg, byte[] bytes) { + //guess hash algorithm from signing algo + String alg = null; + if ( + signingAlg.equals(JWSAlgorithm.HS256) || + signingAlg.equals(JWSAlgorithm.ES256) || + signingAlg.equals(JWSAlgorithm.RS256) || + signingAlg.equals(JWSAlgorithm.PS256) + ) { + alg = "SHA-256"; + } else if ( + signingAlg.equals(JWSAlgorithm.ES384) || + signingAlg.equals(JWSAlgorithm.HS384) || + signingAlg.equals(JWSAlgorithm.RS384) || + signingAlg.equals(JWSAlgorithm.PS384) + ) { + alg = "SHA-384"; + } else if ( + signingAlg.equals(JWSAlgorithm.ES512) || + signingAlg.equals(JWSAlgorithm.HS512) || + signingAlg.equals(JWSAlgorithm.RS512) || + signingAlg.equals(JWSAlgorithm.PS512) + ) { + alg = "SHA-512"; + } + if (alg == null) { + return null; } - // validate keyUse - KeyUse use = new KeyUse(usage); + try { + MessageDigest hash = MessageDigest.getInstance(alg); + hash.reset(); + hash.update(bytes); - // validate algorithm - JWSAlgorithm algo = JWSAlgorithm.parse(alg); + //keep left-most half as per spec + byte[] hashBytes = hash.digest(); + byte[] hashBytesLeftHalf = Arrays.copyOf(hashBytes, hashBytes.length / 2); - return new OctetSequenceKeyGenerator(length).keyID(id).keyUse(use).algorithm(algo).generate(); + //encode as base64 url + return Base64URL.encode(hashBytesLeftHalf); + } catch (NoSuchAlgorithmException e) { + //shouldn't happen + return null; + } } + + private JWKUtils() {} } diff --git a/modules/authorization/src/test/java/it/smartcommunitylabdhub/authorization/JwkTests.java b/modules/authorization/src/test/java/it/smartcommunitylabdhub/authorization/JwkTests.java new file mode 100644 index 00000000..ee25fa1f --- /dev/null +++ b/modules/authorization/src/test/java/it/smartcommunitylabdhub/authorization/JwkTests.java @@ -0,0 +1,22 @@ +package it.smartcommunitylabdhub.authorization; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.KeyUse; +import it.smartcommunitylabdhub.authorization.utils.JWKUtils; +import org.junit.jupiter.api.Test; + +public class JwkTests { + + @Test + public void generateRsaKey() throws Exception { + JWK jwk = JWKUtils.generateRsaJWK(null); + assertThat(jwk).isNotNull(); + //validate content + assertThat(jwk.getKeyID()).isNotBlank(); + assertThat(jwk.getAlgorithm()).isEqualTo(JWSAlgorithm.RS256); + assertThat(jwk.getKeyUse()).isEqualTo(KeyUse.SIGNATURE); + } +} From 73fb10bec556f1dc69f17beaf7938ec806855e97 Mon Sep 17 00:00:00 2001 From: Matteo Saloni Date: Mon, 5 Aug 2024 16:36:32 +0200 Subject: [PATCH 07/10] refactor: k8s secured runnable as context holder for envs --- .../infrastructure/k8s/K8sBaseFramework.java | 2 +- .../k8s/kubernetes/K8sBuilderHelper.java | 5 ++- .../k8s/kubernetes/K8sSecretHelper.java | 27 ++++++++++++++ .../framework/k8s/runnables/K8sRunnable.java | 36 ++++++++++++++++--- 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/infrastructure/k8s/K8sBaseFramework.java b/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/infrastructure/k8s/K8sBaseFramework.java index ab9ce798..67274e31 100644 --- a/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/infrastructure/k8s/K8sBaseFramework.java +++ b/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/infrastructure/k8s/K8sBaseFramework.java @@ -804,7 +804,7 @@ protected List buildImagePullSecrets(T runnable) { protected V1Secret buildRunSecret(T runnable) { if (runnable.getCredentials() != null) { - V1Secret secret = k8sSecretHelper.convertAuthentication( + V1Secret secret = k8sSecretHelper.convertCredentials( k8sSecretHelper.getSecretName(runnable.getRuntime(), runnable.getTask(), runnable.getId()), runnable.getCredentials() ); diff --git a/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/kubernetes/K8sBuilderHelper.java b/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/kubernetes/K8sBuilderHelper.java index eb08521d..11e0f281 100644 --- a/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/kubernetes/K8sBuilderHelper.java +++ b/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/kubernetes/K8sBuilderHelper.java @@ -63,6 +63,9 @@ public class K8sBuilderHelper implements InitializingBean { @Value("${kubernetes.namespace}") private String namespace; + @Value("${kubernetes.envs.prefix}") + private String envsPrefix; + @Override public void afterPropertiesSet() { // Retrieve CoreV1Api @@ -124,7 +127,7 @@ public List getV1EnvVar() { List vars = new ArrayList<>(); //if no configMap build a minimal config if (sharedConfigMaps == null || sharedConfigMaps.isEmpty()) { - vars.add(new V1EnvVar().name("DIGITALHUB_CORE_ENDPOINT").value(coreEndpoint)); + vars.add(new V1EnvVar().name(sanitizeNames(envsPrefix).toUpperCase() + "_ENDPOINT").value(coreEndpoint)); } return vars; diff --git a/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/kubernetes/K8sSecretHelper.java b/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/kubernetes/K8sSecretHelper.java index fadcb26d..79bb9f21 100644 --- a/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/kubernetes/K8sSecretHelper.java +++ b/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/kubernetes/K8sSecretHelper.java @@ -42,6 +42,9 @@ public class K8sSecretHelper { @Value("${kubernetes.namespace}") private String namespace; + @Value("${kubernetes.envs.prefix}") + private String envsPrefix; + public K8sSecretHelper(ApiClient client) { api = new CoreV1Api(client); } @@ -183,6 +186,30 @@ public void storeSecretData(@NotNull String secretName, Map data } } + public @Nullable V1Secret convertCredentials(String name, Map credentials) { + if (credentials != null) { + //map to secret as envs under declared prefix + Map data = credentials + .entrySet() + .stream() + .collect( + Collectors.toMap( + e -> K8sBuilderHelper.sanitizeNames(envsPrefix).toUpperCase() + "_" + e.getKey().toUpperCase(), + Entry::getValue + ) + ); + + return new V1Secret() + .metadata(new V1ObjectMeta().name(name).namespace(namespace)) + .apiVersion("v1") + .kind("Secret") + .stringData(data); + } + + return null; + } + + @Deprecated public @Nullable V1Secret convertAuthentication(String name, AbstractAuthenticationToken auth) { if (auth instanceof JwtAuthenticationToken) { Jwt token = ((JwtAuthenticationToken) auth).getToken(); diff --git a/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/runnables/K8sRunnable.java b/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/runnables/K8sRunnable.java index 87e48f26..abb0a896 100644 --- a/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/runnables/K8sRunnable.java +++ b/modules/framework-k8s/src/main/java/it/smartcommunitylabdhub/framework/k8s/runnables/K8sRunnable.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; import it.smartcommunitylabdhub.commons.infrastructure.RunRunnable; import it.smartcommunitylabdhub.commons.infrastructure.SecuredRunnable; +import it.smartcommunitylabdhub.commons.jackson.JacksonMapper; import it.smartcommunitylabdhub.framework.k8s.model.ContextRef; import it.smartcommunitylabdhub.framework.k8s.model.ContextSource; import it.smartcommunitylabdhub.framework.k8s.objects.CoreAffinity; @@ -16,15 +18,17 @@ import it.smartcommunitylabdhub.framework.k8s.objects.CoreToleration; import it.smartcommunitylabdhub.framework.k8s.objects.CoreVolume; import java.io.Serializable; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.SuperBuilder; -import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.CredentialsContainer; @SuperBuilder @@ -82,7 +86,7 @@ public class K8sRunnable implements RunRunnable, SecuredRunnable, CredentialsCon @JsonIgnore private List metrics; - private AbstractAuthenticationToken credentials; + private HashMap credentials; @JsonProperty("context_refs") private List contextRefs; @@ -102,8 +106,32 @@ public void eraseCredentials() { @Override public void setCredentials(Serializable credentials) { - if (credentials instanceof AbstractAuthenticationToken) { - this.credentials = (AbstractAuthenticationToken) credentials; + if (credentials != null) { + //try to coerce into map + HashMap map = JacksonMapper.CUSTOM_OBJECT_MAPPER.convertValue( + credentials, + JacksonMapper.typeRef + ); + + this.credentials = + map + .entrySet() + .stream() + .filter(e -> e.getValue() != null) + .map(e -> { + if (e.getValue() instanceof String) { + return Map.entry(e.getKey(), (String) e.getValue()); + } + + try { + String value = JacksonMapper.CUSTOM_OBJECT_MAPPER.writeValueAsString(e.getValue()); + return Map.entry(e.getKey(), value); + } catch (JsonProcessingException je) { + return null; + } + }) + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (o1, o2) -> o1, HashMap::new)); } } } From 3c97c061139cb0f6177967b6a408a88b89e2e3c0 Mon Sep 17 00:00:00 2001 From: Matteo Saloni Date: Mon, 5 Aug 2024 17:15:55 +0200 Subject: [PATCH 08/10] fix: revert changed jwt configuration to correct default --- application/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/application.yml b/application/src/main/resources/application.yml index 094dcb40..b8302315 100644 --- a/application/src/main/resources/application.yml +++ b/application/src/main/resources/application.yml @@ -149,7 +149,7 @@ security: password: ${DH_AUTH_BASIC_PASSWORD:} jwt: issuer-uri: ${DH_AUTH_JWT_ISSUER_URI:${security.oidc.issuer-uri}} - audience: ${DH_AUTH_JWT_AUDIENCE:${security.oidc.issuer-uri}} + audience: ${DH_AUTH_JWT_AUDIENCE:${security.oidc.client-id}} claim: ${DH_AUTH_JWT_CLAIM:roles} oidc: issuer-uri: ${DH_AUTH_OIDC_ISSUER_URI:} From bc13096a21387abfbf4c667e59989dc12467bfd2 Mon Sep 17 00:00:00 2001 From: Matteo Saloni Date: Mon, 5 Aug 2024 17:18:39 +0200 Subject: [PATCH 09/10] fix: revert changed application properties and apply new config --- .../src/main/resources/application.yml | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/application/src/main/resources/application.yml b/application/src/main/resources/application.yml index b8302315..298cd87f 100644 --- a/application/src/main/resources/application.yml +++ b/application/src/main/resources/application.yml @@ -1,16 +1,16 @@ # server config server: - host: ${SERVER_HOST:localhost} - port: ${SERVER_PORT:8080} - servlet: - context-path: ${SERVER_CONTEXT:/} - tomcat: - remoteip: + host: ${SERVER_HOST:localhost} + port: ${SERVER_PORT:8080} + servlet: + context-path: ${SERVER_CONTEXT:/} + tomcat: + remoteip: remote_ip_header: ${SERVER_TOMCAT_REMOTE_IP_HEADER:x-forwarded-for} protocol_header: ${SERVER_TOMCAT_PROTOCOL_HEADER:x-forwarded-proto} - max-http-request-header-size: 32000 - error: - include-stacktrace: never + max-http-request-header-size: 32000 + error: + include-stacktrace: never # Spring configuration spring: @@ -42,18 +42,18 @@ spring: # Management Endpoints management: - server: - port: ${MANAGEMENT_PORT:8081} - endpoints: - enabled-by-default: false - web: - base-path: - exposure.include: "health,info,metrics" - endpoint: - info: - enabled: true - health: - enabled: true + server: + port: ${MANAGEMENT_PORT:8081} + endpoints: + enabled-by-default: false + web: + base-path: + exposure.include: "health,info,metrics" + endpoint: + info: + enabled: true + health: + enabled: true # Runtimes @@ -84,7 +84,7 @@ kaniko: image-prefix: ${KANIKO_IMAGE_PREFIX:dhcore} image-registry: ${KANIKO_IMAGE_REGISTRY:${registry.name}} secret: ${KANIKO_SECRET:${registry.secret}} - args: ${KANIKO_ARGS:} + args: ${KANIKO_ARGS:} # MLRun config mlrun: @@ -93,7 +93,7 @@ mlrun: image-registry: ${MLRUN_IMAGE_REGISTRY:} # registry -registry: +registry: name: ${DOCKER_REGISTRY:} secret: ${DOCKER_REGISTRY_SECRET:} @@ -107,6 +107,8 @@ kubernetes: results: ${K8S_ENABLE_RESULTS:default} security: disable-root: ${K8S_SEC_DISABLE_ROOT:false} + envs: + prefix: ${K8S_ENVS_PREFIX:${application.name}} image-pull-policy: ${K8S_IMAGE_PULL_POLICY:IfNotPresent} init-image: ${K8S_INIT_IMAGE:ghcr.io/scc-digitalhub/digitalhub-core-builder-tool:latest} resources: @@ -122,8 +124,6 @@ kubernetes: config-map: ${DH_CONFIG_COMMON_MAPS:} secret: ${DH_CONFIG_COMMON_SECRETS:} templates: ${K8S_TEMPLATES:} - envs: - prefix: ${K8S_ENVS_PREFIX:${application.name}} # Application endpoint application: @@ -136,9 +136,9 @@ application: logging: - level: - ROOT: INFO - it.smartcommunitylabdhub: ${LOG_LEVEL:INFO} + level: + ROOT: INFO + it.smartcommunitylabdhub: ${LOG_LEVEL:INFO} security: api: @@ -151,7 +151,7 @@ security: issuer-uri: ${DH_AUTH_JWT_ISSUER_URI:${security.oidc.issuer-uri}} audience: ${DH_AUTH_JWT_AUDIENCE:${security.oidc.client-id}} claim: ${DH_AUTH_JWT_CLAIM:roles} - oidc: + oidc: issuer-uri: ${DH_AUTH_OIDC_ISSUER_URI:} client-id: ${DH_AUTH_OIDC_CLIENT_ID:} scope: ${DH_AUTH_OIDC_SCOPE:openid,email,profile} @@ -196,7 +196,7 @@ files: endpoint: ${S3_ENDPOINT:} bucket: ${S3_BUCKET:} -# Keystore configuration +# JWT configuration jwt: keystore: path: ${JWT_KEYSTORE_PATH:classpath:/keystore.jwks} @@ -207,4 +207,4 @@ jwt: duration: ${JWT_REFRESH_TOKEN_DURATION:} client-id: ${JWT_CLIENT_ID:${security.basic.username}} client-secret: ${JWT_CLIENT_SECRET:${security.basic.password}} - cache-control: ${JWKS_CACHE_CONTROL:public, max-age=900, must-revalidate, no-transform} \ No newline at end of file + cache-control: ${JWKS_CACHE_CONTROL:public, max-age=900, must-revalidate, no-transform} \ No newline at end of file From 51a24beb0d10d326a8a66cfdb72bf5ee4cb0f099 Mon Sep 17 00:00:00 2001 From: Matteo Saloni Date: Mon, 5 Aug 2024 17:51:32 +0200 Subject: [PATCH 10/10] feat: add exchange token flow between client and (internal/external) jwt auth --- .../authorization/config/KeyStoreConfig.java | 14 +- .../controllers/ConfigurationEndpoint.java | 8 + .../controllers/JWKSEndpoint.java | 8 + .../controllers/TokenEndpoint.java | 209 ++++++++++++++++-- .../services/JwtTokenService.java | 75 ++++--- 5 files changed, 261 insertions(+), 53 deletions(-) diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java index 47cb122f..1bc33a74 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java @@ -2,20 +2,26 @@ import com.nimbusds.jose.JOSEException; import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.StringUtils; @Configuration public class KeyStoreConfig { @Value("${jwt.keystore.path}") - private Resource location; + private String path; @Value("${jwt.keystore.kid}") private String kid; + @Autowired + private ResourceLoader resourceLoader; + private JWKSetKeyStore keyStore; @Bean @@ -25,6 +31,12 @@ public JWKSetKeyStore getJWKSetKeyStore() throws JOSEException { return keyStore; } + if (StringUtils.hasText(path) && !path.contains(":")) { + //no protocol specified, try as file by default + this.path = "file:" + path; + } + + Resource location = resourceLoader.getResource(path); if (location != null && location.exists() && location.isReadable()) { // Load from resource keyStore = new JWKSetKeyStore(location, kid); diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/ConfigurationEndpoint.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/ConfigurationEndpoint.java index 5014331e..ad957c45 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/ConfigurationEndpoint.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/ConfigurationEndpoint.java @@ -1,6 +1,7 @@ package it.smartcommunitylabdhub.authorization.controllers; import it.smartcommunitylabdhub.commons.config.ApplicationProperties; +import it.smartcommunitylabdhub.commons.config.SecurityProperties; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -20,6 +21,9 @@ public class ConfigurationEndpoint { @Autowired private ApplicationProperties applicationProperties; + @Autowired + private SecurityProperties securityProperties; + @Value("${jwt.cache-control}") private String cacheControl; @@ -27,6 +31,10 @@ public class ConfigurationEndpoint { @GetMapping(value = { "/.well-known/openid-configuration", "/.well-known/oauth-authorization-server" }) public ResponseEntity> getCOnfiguration() { + if (!securityProperties.isRequired()) { + throw new UnsupportedOperationException(); + } + if (config == null) { config = generate(); } diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKSEndpoint.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKSEndpoint.java index 1158e033..dcf14211 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKSEndpoint.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/JWKSEndpoint.java @@ -2,6 +2,7 @@ import com.nimbusds.jose.jwk.JWKSet; import it.smartcommunitylabdhub.authorization.components.JWKSetKeyStore; +import it.smartcommunitylabdhub.commons.config.SecurityProperties; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -18,11 +19,18 @@ public class JWKSEndpoint { @Autowired private JWKSetKeyStore jwkSetKeyStore; + @Autowired + private SecurityProperties securityProperties; + @Value("${jwt.cache-control}") private String cacheControl; @GetMapping(JWKS_URL) public ResponseEntity> getJWKInfo() { + if (!securityProperties.isRequired()) { + throw new UnsupportedOperationException(); + } + //expose the entire jwkSet as JSON JWKSet jwkSet = jwkSetKeyStore.getJwkSet(); Map jwkSetMap = jwkSet.toJSONObject(); diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/TokenEndpoint.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/TokenEndpoint.java index dad3cbe0..49fb9e1f 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/TokenEndpoint.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/TokenEndpoint.java @@ -7,8 +7,11 @@ import it.smartcommunitylabdhub.authorization.model.TokenResponse; import it.smartcommunitylabdhub.authorization.services.JwtTokenService; import it.smartcommunitylabdhub.commons.config.ApplicationProperties; +import it.smartcommunitylabdhub.commons.config.SecurityProperties; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -16,6 +19,8 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; @@ -32,14 +37,18 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController +@Slf4j public class TokenEndpoint implements InitializingBean { public static final String TOKEN_URL = "/auth/token"; + public static final String TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"; + public static final String ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"; @Value("${jwt.client-id}") private String clientId; @@ -56,25 +65,45 @@ public class TokenEndpoint implements InitializingBean { @Autowired private ApplicationProperties applicationProperties; - private JwtAuthenticationProvider authProvider; + @Autowired + private SecurityProperties securityProperties; + + //TODO move to dedicated filter initalized via securityConfig! + private JwtAuthenticationProvider accessTokenAuthProvider; + private JwtAuthenticationProvider refreshTokenAuthProvider; + private JwtAuthenticationProvider externalTokenAuthProvider; @Override public void afterPropertiesSet() throws Exception { - Assert.notNull(jwkSetKeyStore, "jwks store is required"); - Assert.notNull(jwkSetKeyStore.getJwk(), "jwk is required"); - - //build auth provider to validate tokens - authProvider = new JwtAuthenticationProvider(coreJwtDecoder(jwkSetKeyStore.getJwk())); - JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter(); - JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); - authoritiesConverter.setAuthoritiesClaimName("authorities"); - authoritiesConverter.setAuthorityPrefix(""); - jwtConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); - authProvider.setJwtAuthenticationConverter(jwtConverter); + if (securityProperties.isRequired()) { + Assert.notNull(jwkSetKeyStore, "jwks store is required"); + Assert.notNull(jwkSetKeyStore.getJwk(), "jwk is required"); + + JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter(); + JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); + authoritiesConverter.setAuthoritiesClaimName("authorities"); + authoritiesConverter.setAuthorityPrefix(""); + jwtConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); + + //build auth provider to validate tokens + accessTokenAuthProvider = new JwtAuthenticationProvider(coreJwtDecoder(jwkSetKeyStore.getJwk(), false)); + accessTokenAuthProvider.setJwtAuthenticationConverter(jwtConverter); + refreshTokenAuthProvider = new JwtAuthenticationProvider(coreJwtDecoder(jwkSetKeyStore.getJwk(), true)); + refreshTokenAuthProvider.setJwtAuthenticationConverter(jwtConverter); + + if (securityProperties.isJwtAuthEnabled()) { + externalTokenAuthProvider = new JwtAuthenticationProvider(externalJwtDecoder()); + externalTokenAuthProvider.setJwtAuthenticationConverter(externalJwtAuthenticationConverter()); + } + } } @PostMapping(TOKEN_URL) public TokenResponse token(@RequestParam Map parameters, Authentication authentication) { + if (!securityProperties.isRequired()) { + throw new UnsupportedOperationException(); + } + //resolve client authentication if (authentication == null || !(authentication.isAuthenticated())) { throw new InsufficientAuthenticationException("Invalid or missing authentication"); @@ -82,17 +111,27 @@ public TokenResponse token(@RequestParam Map parameters, Authent //select flow String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE); + log.debug("token request for {}", grantType); + if (log.isTraceEnabled()) { + log.trace("authentication name {}", authentication.getName()); + } if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) { return clientCredentials(parameters, authentication); } else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) { return refreshToken(parameters, authentication); + } else if (TOKEN_EXCHANGE_GRANT_TYPE.equals(grantType)) { + return tokenExchange(parameters, authentication); } throw new IllegalArgumentException("invalid or unsupported grant type"); } private TokenResponse refreshToken(Map parameters, Authentication authentication) { + if (refreshTokenAuthProvider == null) { + throw new UnsupportedOperationException(); + } + //refresh token is usable without credentials //TODO add rotation by storing refresh tokens in db! @@ -106,10 +145,20 @@ private TokenResponse refreshToken(Map parameters, Authenticatio throw new IllegalArgumentException("invalid or missing refresh_token"); } + String cid = parameters.get("client_id"); + if (cid == null || !clientId.equals(cid)) { + throw new IllegalArgumentException("invalid or missing client_id"); + } + + log.debug("refresh token request for {}", cid); + if (log.isTraceEnabled()) { + log.trace("refresh token {}", token); + } + //validate via provider try { BearerTokenAuthenticationToken request = new BearerTokenAuthenticationToken(token); - Authentication auth = authProvider.authenticate(request); + Authentication auth = refreshTokenAuthProvider.authenticate(request); if (!auth.isAuthenticated()) { throw new IllegalArgumentException("invalid or missing refresh_token"); } @@ -138,11 +187,90 @@ private TokenResponse clientCredentials(Map parameters, Authenti throw new IllegalArgumentException("invalid grant type"); } + log.debug("client token request for {}", auth.getName()); + //generate as per user return jwtTokenService.generateCredentials(authentication); } - private JwtDecoder coreJwtDecoder(JWK jwk) throws JOSEException { + private TokenResponse tokenExchange(Map parameters, Authentication authentication) { + if (accessTokenAuthProvider == null) { + throw new UnsupportedOperationException(); + } + + //token exchange *requires* basic auth + if (authentication == null || !(authentication instanceof UsernamePasswordAuthenticationToken)) { + throw new InsufficientAuthenticationException("Invalid or missing authentication"); + } + + //for client credentials to mimic admin user client *must* match authenticated user + UsernamePasswordAuthenticationToken clientAuth = (UsernamePasswordAuthenticationToken) authentication; + if (clientId != null && !clientId.equals(clientAuth.getName())) { + throw new InsufficientAuthenticationException("Invalid client authentication"); + } + + String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE); + if (!TOKEN_EXCHANGE_GRANT_TYPE.equals(grantType)) { + throw new IllegalArgumentException("invalid grant type"); + } + + //validate token as well + String token = parameters.get("subject_token"); + if (token == null) { + throw new IllegalArgumentException("invalid or missing subject_token"); + } + + String tokenType = parameters.get("subject_token_type"); + if (!ACCESS_TOKEN_TYPE.equals(tokenType)) { + throw new IllegalArgumentException("invalid or missing subject_token_type"); + } + + log.debug("exchange token request from {}", clientAuth.getName()); + if (log.isTraceEnabled()) { + log.trace("subject token {}", token); + } + + //validate via provider + try { + BearerTokenAuthenticationToken request = new BearerTokenAuthenticationToken(token); + Authentication userAuth = accessTokenAuthProvider.authenticate(request); + if (!userAuth.isAuthenticated()) { + throw new IllegalArgumentException("invalid or missing subject_token"); + } + + log.debug("exchange token request from {} resolved for {} via internal provider", clientAuth.getName(), userAuth.getName()); + + //token is valid, use as context for generation + return jwtTokenService.generateCredentials(userAuth); + } catch (AuthenticationException ae) { + //fall back to external if available + if (externalTokenAuthProvider != null) { + try { + BearerTokenAuthenticationToken request = new BearerTokenAuthenticationToken(token); + Authentication userAuth = externalTokenAuthProvider.authenticate(request); + if (!userAuth.isAuthenticated()) { + throw new IllegalArgumentException("invalid or missing subject_token"); + } + + log.debug( + "exchange token request from {} resolved for {} via external provider", + clientAuth.getName(), + userAuth.getName() + ); + + //token is valid, use as context for generation + return jwtTokenService.generateCredentials(userAuth); + } catch (AuthenticationException ae1) { + throw new IllegalArgumentException("invalid or missing subject_token"); + } + } + + throw new IllegalArgumentException("invalid or missing subject_token"); + } + } + + //TODO move to filter + config! + private JwtDecoder coreJwtDecoder(JWK jwk, boolean asRefresh) throws JOSEException { //we support only RSA keys if (!(jwk instanceof RSAKey)) { throw new IllegalArgumentException("the provided key is not suitable for token authentication"); @@ -158,10 +286,10 @@ private JwtDecoder coreJwtDecoder(JWK jwk) throws JOSEException { (aud -> aud != null && aud.contains(applicationProperties.getName())) ); - //refresh tokens *must contain* at_hash + //refresh tokens *must contain* at_hash, access token *not* OAuth2TokenValidator tokenValidator = new JwtClaimValidator( IdTokenClaimNames.AT_HASH, - (hash -> hash != null) + (hash -> (asRefresh ? hash != null : hash == null)) ); OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<>( @@ -173,4 +301,53 @@ private JwtDecoder coreJwtDecoder(JWK jwk) throws JOSEException { return jwtDecoder; } + + /** + * External auth via JWT + */ + private JwtDecoder externalJwtDecoder() { + SecurityProperties.JwtAuthenticationProperties jwtProps = securityProperties.getJwt(); + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(jwtProps.getIssuerUri()).build(); + + OAuth2TokenValidator audienceValidator = new JwtClaimValidator>( + JwtClaimNames.AUD, + (aud -> aud != null && aud.contains(jwtProps.getAudience())) + ); + + OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(jwtProps.getIssuerUri()); + OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); + jwtDecoder.setJwtValidator(withAudience); + + return jwtDecoder; + } + + private JwtAuthenticationConverter externalJwtAuthenticationConverter() { + SecurityProperties.JwtAuthenticationProperties jwtProps = securityProperties.getJwt(); + String claim = jwtProps.getClaim(); + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter((Jwt source) -> { + if (source == null) return null; + + List authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority("ROLE_USER")); + + if (StringUtils.hasText(claim) && source.hasClaim(claim)) { + List roles = source.getClaimAsStringList(claim); + if (roles != null) { + roles.forEach(r -> { + if ("ROLE_ADMIN".equals(r) || r.contains(":")) { + //use as is + authorities.add(new SimpleGrantedAuthority(r)); + } else { + //derive a scoped USER role + authorities.add(new SimpleGrantedAuthority(r + ":ROLE_USER")); + } + }); + } + } + + return authorities; + }); + return converter; + } } diff --git a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java index f2d2fdc1..e14e2a8b 100644 --- a/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java +++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/services/JwtTokenService.java @@ -22,6 +22,7 @@ import it.smartcommunitylabdhub.authorization.model.TokenResponse; import it.smartcommunitylabdhub.authorization.utils.JWKUtils; import it.smartcommunitylabdhub.commons.config.ApplicationProperties; +import it.smartcommunitylabdhub.commons.config.SecurityProperties; import java.time.Instant; import java.util.Date; import java.util.List; @@ -49,6 +50,9 @@ public class JwtTokenService implements InitializingBean { @Autowired private ApplicationProperties applicationProperties; + @Autowired + private SecurityProperties securityProperties; + @Value("${jwt.client-id}") private String clientId; @@ -76,41 +80,42 @@ public void setRefreshTokenDuration(@Value("${jwt.access-token.duration}") Integ @Override public void afterPropertiesSet() throws Exception { - //build signer for the given keys - this.jwk = keyStore.getJwk(); - - if (jwk != null) { - try { - if (!(jwk.getAlgorithm() instanceof JWSAlgorithm)) { - throw new JOSEException("key algorithm invalid"); - } - - if (jwk instanceof RSAKey) { - // build RSA signers & verifiers - if (jwk.isPrivate()) { // only add the signer if there's a private key - signer = new RSASSASigner((RSAKey) jwk); + if (securityProperties.isRequired()) { + //build signer for the given keys + this.jwk = keyStore.getJwk(); + + if (jwk != null) { + try { + if (jwk.getAlgorithm() == null) { + throw new JOSEException("key algorithm invalid"); } - verifier = new RSASSAVerifier((RSAKey) jwk); - } else if (jwk instanceof ECKey) { - // build EC signers & verifiers - if (jwk.isPrivate()) { - signer = new ECDSASigner((ECKey) jwk); + if (jwk instanceof RSAKey) { + // build RSA signers & verifiers + if (jwk.isPrivate()) { // only add the signer if there's a private key + signer = new RSASSASigner((RSAKey) jwk); + } + verifier = new RSASSAVerifier((RSAKey) jwk); + } else if (jwk instanceof ECKey) { + // build EC signers & verifiers + if (jwk.isPrivate()) { + signer = new ECDSASigner((ECKey) jwk); + } + + verifier = new ECDSAVerifier((ECKey) jwk); + } else if (jwk instanceof OctetSequenceKey) { + // build HMAC signers & verifiers + + if (jwk.isPrivate()) { // technically redundant check because all HMAC keys are private + signer = new MACSigner((OctetSequenceKey) jwk); + } + + verifier = new MACVerifier((OctetSequenceKey) jwk); + } else { + log.warn("Unknown key type: " + jwk); } - - verifier = new ECDSAVerifier((ECKey) jwk); - } else if (jwk instanceof OctetSequenceKey) { - // build HMAC signers & verifiers - - if (jwk.isPrivate()) { // technically redundant check because all HMAC keys are private - signer = new MACSigner((OctetSequenceKey) jwk); - } - - verifier = new MACVerifier((OctetSequenceKey) jwk); - } else { - log.warn("Unknown key type: " + jwk); + } catch (JOSEException e) { + log.warn("Exception loading signer/verifier", e); } - } catch (JOSEException e) { - log.warn("Exception loading signer/verifier", e); } } } @@ -148,8 +153,7 @@ public SignedJWT generateAccessToken(Authentication authentication) throws JwtTo } try { - //cast because we checked during load - JWSAlgorithm jwsAlgorithm = (JWSAlgorithm) jwk.getAlgorithm(); + JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(jwk.getAlgorithm().getName()); Instant now = Instant.now(); @@ -196,8 +200,7 @@ public SignedJWT generateRefreshToken(Authentication authentication, SignedJWT a } try { - //cast because we checked during load - JWSAlgorithm jwsAlgorithm = (JWSAlgorithm) jwk.getAlgorithm(); + JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(jwk.getAlgorithm().getName()); Instant now = Instant.now();