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..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
@@ -1,7 +1,9 @@
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.config.SecurityProperties;
import it.smartcommunitylabdhub.commons.events.RunnableChangedEvent;
import it.smartcommunitylabdhub.commons.events.RunnableMonitorObject;
import it.smartcommunitylabdhub.commons.exceptions.NoSuchEntityException;
@@ -78,6 +80,12 @@ public class RunManager {
@Autowired
ProcessorRegistry processorRegistry;
+ @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();
@@ -196,9 +204,13 @@ 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 auth is enabled via securityProperties
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
- if (auth != null) {
- ((SecuredRunnable) r).setCredentials(auth);
+ 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 1e5d0f0a..bbd48e96 100644
--- a/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java
+++ b/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java
@@ -1,10 +1,16 @@
package it.smartcommunitylabdhub.core.config;
+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.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;
@@ -12,6 +18,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
+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,6 +30,7 @@
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.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
@@ -30,6 +38,7 @@
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
@@ -48,6 +57,9 @@ public class SecurityConfig {
public static final String API_PREFIX = "/api";
+ @Autowired
+ ApplicationProperties applicationProperties;
+
@Autowired
SecurityProperties properties;
@@ -60,6 +72,15 @@ public class SecurityConfig {
@Value("${management.endpoints.web.base-path}")
private String managementBasePath;
+ @Value("${jwt.client-id}")
+ private String clientId;
+
+ @Value("${jwt.client-secret}")
+ private String clientSecret;
+
+ @Autowired
+ KeyStoreConfig keyStoreConfig;
+
@Bean("apiSecurityFilterChain")
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
HttpSecurity securityChain = http
@@ -85,18 +106,37 @@ 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());
- }
+ //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(coreJwtAuthProvider)))
+ );
+
if (properties.isJwtAuthEnabled()) {
+ // rebuild auth manager to include external jwt provider
+ JwtAuthenticationProvider externalJwtAuthProvider = new JwtAuthenticationProvider(externalJwtDecoder());
+
+ externalJwtAuthProvider.setJwtAuthenticationConverter(externalJwtAuthenticationConverter());
+
securityChain.oauth2ResourceServer(oauth2 ->
- oauth2.jwt(jwt -> jwt.decoder(jwtDecoder()).jwtAuthenticationConverter(jwtAuthenticationConverter())
+ 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());
} else {
@@ -116,19 +156,88 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce
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())
+ .username(username)
+ .password(password)
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(admin);
}
- private JwtDecoder jwtDecoder() {
+ /**
+ * 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(applicationProperties.getName()))
+ );
+
+ //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 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();
@@ -144,7 +253,7 @@ private JwtDecoder jwtDecoder() {
return jwtDecoder;
}
- private JwtAuthenticationConverter jwtAuthenticationConverter() {
+ private JwtAuthenticationConverter externalJwtAuthenticationConverter() {
SecurityProperties.JwtAuthenticationProperties jwtProps = properties.getJwt();
String claim = jwtProps.getClaim();
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
@@ -174,6 +283,55 @@ 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
@@ -222,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 71ba4929..298cd87f 100644
--- a/application/src/main/resources/application.yml
+++ b/application/src/main/resources/application.yml
@@ -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:
@@ -193,4 +195,16 @@ files:
secret-key: ${AWS_SECRET_KEY:}
endpoint: ${S3_ENDPOINT:}
bucket: ${S3_BUCKET:}
-
\ No newline at end of file
+
+# JWT configuration
+jwt:
+ keystore:
+ 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
new file mode 100644
index 00000000..a85a99c4
--- /dev/null
+++ b/modules/authorization/.flattened-pom.xml
@@ -0,0 +1,47 @@
+
+
+ 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-resource-server
+ 3.2.0
+ compile
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+ compile
+ true
+
+
+ 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..09a31625
--- /dev/null
+++ b/modules/authorization/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 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-resource-server
+ 3.2.0
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ ${spring-boot.version}
+ test
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ compile
+ true
+
+
+ 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..2b20dea2
--- /dev/null
+++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/components/JWKSetKeyStore.java
@@ -0,0 +1,83 @@
+package it.smartcommunitylabdhub.authorization.components;
+
+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 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, @Nullable String kid) throws IllegalArgumentException {
+ Assert.notNull(location, "location can not be null");
+
+ //load
+ log.debug("load keyStore from {}", location);
+ this.jwkSet = JWKUtils.loadJwkSet(location);
+ JWK jwk = load(jwkSet, kid);
+ this.kid = jwk.getKeyID();
+
+ log.debug("use key {} for signing", kid);
+ }
+
+ 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
new file mode 100644
index 00000000..1bc33a74
--- /dev/null
+++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/config/KeyStoreConfig.java
@@ -0,0 +1,50 @@
+package it.smartcommunitylabdhub.authorization.config;
+
+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 String path;
+
+ @Value("${jwt.keystore.kid}")
+ private String kid;
+
+ @Autowired
+ private ResourceLoader resourceLoader;
+
+ private JWKSetKeyStore keyStore;
+
+ @Bean
+ public JWKSetKeyStore getJWKSetKeyStore() throws JOSEException {
+ if (keyStore != null) {
+ //re-use because we load from config *before* services are built
+ 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);
+ } else {
+ // Generate new in-memory keystore
+ keyStore = new JWKSetKeyStore();
+ }
+
+ 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..ad957c45
--- /dev/null
+++ b/modules/authorization/src/main/java/it/smartcommunitylabdhub/authorization/controllers/ConfigurationEndpoint.java
@@ -0,0 +1,70 @@
+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;
+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;
+
+ @Autowired
+ private SecurityProperties securityProperties;
+
+ @Value("${jwt.cache-control}")
+ private String cacheControl;
+
+ private Map config = null;
+
+ @GetMapping(value = { "/.well-known/openid-configuration", "/.well-known/oauth-authorization-server" })
+ public ResponseEntity