diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 5072eec..7c88c88 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -5,7 +5,7 @@ info: license: name: MIT url: https://github.com/tiki/l0-storage/blob/main/LICENSE - version: 0.0.12 + version: 0.0.13 servers: - url: https://storage.l0.mytiki.com paths: @@ -14,7 +14,7 @@ paths: tags: - STORAGE summary: Upload a block - operationId: post + operationId: put requestBody: content: application/json: @@ -43,7 +43,7 @@ paths: tags: - STORAGE summary: Request a new token - operationId: post_1 + operationId: post parameters: - name: x-api-id in: header @@ -76,7 +76,7 @@ paths: tags: - STORAGE summary: Submit a usage report - operationId: post_2 + operationId: post_1 requestBody: content: application/json: diff --git a/pom.xml b/pom.xml index acef26c..1764515 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ com.mytiki l0_storage - 0.0.12 + 0.0.13 jar L0 Storage diff --git a/src/main/java/com/mytiki/l0_storage/features/latest/FeaturesConfig.java b/src/main/java/com/mytiki/l0_storage/features/latest/FeaturesConfig.java index b0c2f0f..3f1ae59 100644 --- a/src/main/java/com/mytiki/l0_storage/features/latest/FeaturesConfig.java +++ b/src/main/java/com/mytiki/l0_storage/features/latest/FeaturesConfig.java @@ -6,8 +6,8 @@ package com.mytiki.l0_storage.features.latest; import com.mytiki.l0_storage.features.latest.api_id.ApiIdConfig; -import com.mytiki.l0_storage.features.latest.token.TokenConfig; import com.mytiki.l0_storage.features.latest.report.ReportConfig; +import com.mytiki.l0_storage.features.latest.token.TokenConfig; import org.springframework.context.annotation.Import; @Import({ diff --git a/src/main/java/com/mytiki/l0_storage/features/latest/token/TokenConfig.java b/src/main/java/com/mytiki/l0_storage/features/latest/token/TokenConfig.java index 52f1184..1c6c328 100644 --- a/src/main/java/com/mytiki/l0_storage/features/latest/token/TokenConfig.java +++ b/src/main/java/com/mytiki/l0_storage/features/latest/token/TokenConfig.java @@ -26,6 +26,7 @@ import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.math.ec.ECPoint; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.Bean; @@ -56,13 +57,13 @@ public TokenController tokenController(@Autowired TokenService service){ @Bean public TokenService tokenService( @Autowired TokenRepository repository, - @Autowired JWSSigner signer, + @Autowired @Qualifier("tokenJwsSigner") JWSSigner signer, @Autowired ApiIdService apiIdService, @Value("${com.mytiki.l0_storage.token.exp}") long exp){ return new TokenService(repository, signer, apiIdService, exp); } - @Bean + @Bean("tokenJwkSet") public JWKSet jwkSet( @Value("${com.mytiki.l0_storage.token.private_key}") String pkcs8, @Value("${com.mytiki.l0_storage.token.kid}") String kid) @@ -76,7 +77,7 @@ public JWKSet jwkSet( return new JWKSet(keyBuilder.build()); } - @Bean + @Bean("tokenJwsSigner") public JWSSigner jwsSigner( @Autowired JWKSet jwkSet, @Value("${com.mytiki.l0_storage.token.kid}") String kid) @@ -84,7 +85,7 @@ public JWSSigner jwsSigner( return new ECDSASigner(jwkSet.getKeyByKeyId(kid).toECKey().toECPrivateKey(), Curve.P_256); } - @Bean + @Bean("tokenJwtDecoder") public JwtDecoder jwtDecoder(@Autowired JWKSet jwkSet) { DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); ImmutableJWKSet immutableJWKSet = new ImmutableJWKSet<>(jwkSet); diff --git a/src/main/java/com/mytiki/l0_storage/main/AppConfig.java b/src/main/java/com/mytiki/l0_storage/main/AppConfig.java index 5e0068e..cee204d 100644 --- a/src/main/java/com/mytiki/l0_storage/main/AppConfig.java +++ b/src/main/java/com/mytiki/l0_storage/main/AppConfig.java @@ -80,7 +80,7 @@ public OpenAPI oenAPI(@Value("${springdoc.version}") String appVersion) { new PathItem().post( new Operation() .tags(Collections.singletonList("STORAGE")) - .operationId("post") + .operationId("put") .summary("Upload a block") .requestBody(new RequestBody() .content(new Content() diff --git a/src/main/java/com/mytiki/l0_storage/security/SecurityConfig.java b/src/main/java/com/mytiki/l0_storage/security/SecurityConfig.java index 0cbfbbf..9a509cc 100644 --- a/src/main/java/com/mytiki/l0_storage/security/SecurityConfig.java +++ b/src/main/java/com/mytiki/l0_storage/security/SecurityConfig.java @@ -6,10 +6,15 @@ package com.mytiki.l0_storage.security; import com.fasterxml.jackson.databind.ObjectMapper; -import com.mytiki.l0_storage.features.latest.token.TokenController; import com.mytiki.l0_storage.features.latest.report.ReportController; +import com.mytiki.l0_storage.features.latest.token.TokenController; import com.mytiki.l0_storage.utilities.Constants; import com.mytiki.spring_rest_api.ApiConstants; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.Ordered; @@ -20,13 +25,17 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.*; import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.Arrays; -import java.util.Collections; +import java.net.URL; +import java.util.*; +import java.util.function.Predicate; @Order(Ordered.HIGHEST_PRECEDENCE) @EnableWebSecurity @@ -46,17 +55,30 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { private final AuthenticationEntryPoint authenticationEntryPoint; private final String remoteWorkerId; private final String remoteWorkerSecret; + private final URL jwtJwkUri; + private final Set jwtJwsAlgorithms; + private final Set jwtAudiences; + private final String jwtIssuer; + private static final String REMOTE_WORKER_ROLE = "REMOTE"; public SecurityConfig( @Autowired ObjectMapper objectMapper, @Value("${com.mytiki.l0_storage.remote_worker.id}") String remoteWorkerId, - @Value("${com.mytiki.l0_storage.remote_worker.secret}") String remoteWorkerSecret) { + @Value("${com.mytiki.l0_storage.remote_worker.secret}") String remoteWorkerSecret, + @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") URL jwtJwkUri, + @Value("${spring.security.oauth2.resourceserver.jwt.jws-algorithms}") Set jwtJwsAlgorithms, + @Value("${spring.security.oauth2.resourceserver.jwt.audiences}") Set jwtAudiences, + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") String jwtIssuer) { super(true); this.accessDeniedHandler = new AccessDeniedHandler(objectMapper); this.authenticationEntryPoint = new AuthenticationEntryPoint(objectMapper); this.remoteWorkerId = remoteWorkerId; this.remoteWorkerSecret = remoteWorkerSecret; + this.jwtJwkUri = jwtJwkUri; + this.jwtJwsAlgorithms = jwtJwsAlgorithms; + this.jwtAudiences = jwtAudiences; + this.jwtIssuer = jwtIssuer; } @Override @@ -91,7 +113,7 @@ protected void configure(HttpSecurity http) throws Exception { .httpBasic() .authenticationEntryPoint(authenticationEntryPoint).and() .oauth2ResourceServer() - .jwt().and() + .jwt().decoder(jwtDecoder()).and() .accessDeniedHandler(accessDeniedHandler) .authenticationEntryPoint(authenticationEntryPoint); } @@ -104,7 +126,6 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { .roles(REMOTE_WORKER_ROLE); } - private CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOriginPatterns(Collections.singletonList("*")); @@ -115,4 +136,22 @@ private CorsConfigurationSource corsConfigurationSource() { source.registerCorsConfiguration("/**", configuration); return source; } + + public JwtDecoder jwtDecoder() { + DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + RemoteJWKSet remoteJWKSet = new RemoteJWKSet<>(jwtJwkUri); + jwtProcessor.setJWSKeySelector( + new JWSVerificationKeySelector<>(jwtJwsAlgorithms, remoteJWKSet)); + NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor); + List> validators = new ArrayList<>(); + validators.add(new JwtTimestampValidator()); + validators.add(new JwtIssuerValidator(jwtIssuer)); + validators.add(new JwtClaimValidator<>(JwtClaimNames.SUB, Objects::nonNull)); + validators.add(new JwtClaimValidator<>(JwtClaimNames.IAT, Objects::nonNull)); + Predicate> audienceTest = (audience) -> (audience != null) + && new HashSet<>(audience).containsAll(jwtAudiences); + validators.add(new JwtClaimValidator<>(JwtClaimNames.AUD, audienceTest)); + decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); + return decoder; + } } diff --git a/src/test/java/com/mytiki/l0_storage/TokenTest.java b/src/test/java/com/mytiki/l0_storage/TokenTest.java index 7411662..40c97fe 100644 --- a/src/test/java/com/mytiki/l0_storage/TokenTest.java +++ b/src/test/java/com/mytiki/l0_storage/TokenTest.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestMethodOrder; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpStatus; import org.springframework.security.oauth2.jwt.Jwt; @@ -52,6 +53,7 @@ public class TokenTest { private ApiIdService apiIdService; @Autowired + @Qualifier("tokenJwtDecoder") private JwtDecoder jwtDecoder; @Test diff --git a/worker/upload/package.json b/worker/upload/package.json index 993a59f..7902ddc 100644 --- a/worker/upload/package.json +++ b/worker/upload/package.json @@ -1,6 +1,6 @@ { "name": "l0-storage-upload", - "version": "0.0.12", + "version": "0.0.13", "type": "module", "devDependencies": { "@babel/core": "^7.20.2",