Implement Feign Client with Okta as Authorization Server - spring-boot
I was finally able to protect a rest api with Okta as OAuth2.0 security provider (basic with defaults). Also able to get the bearer token using curl, to call rest api via postman and get back results.
curl --location --request POST 'https://dev-XXXXXX.okta.com/oauth2/default/v1/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=XXXXXXXXXXXXX' \
--data-urlencode 'client_secret=XXXXXXXXXXXXXXXXXXXXXXXXXXX' \
--data-urlencode 'grant_type=client_credentials
Now i am trying to implement FeignClient (Rest client) in Spring Boot 2.X app to call the protected api, but facing difficulty in finding right documentation/samples as guide. Appreciate any directions/suggestions?
To collect the token with Feign you need the following:
import java.util.Map;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.cloud.netflix.feign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
#FeignClient(name = "oauth2", url = "https://dev-XXXXXX.okta.com/oauth2/default/v1")
public interface TokenFetcher {
#PostMapping(value = "/token", consumes = APPLICATION_FORM_URLENCODED_VALUE)
String token(#RequestBody Map<String, ?> form);
class Configuration {
#Bean
Encoder feignFormEncoder(ObjectFactory<HttpMessageConverters> converters) {
return new SpringFormEncoder(new SpringEncoder(converters));
}
}
}
Use the client like this:
#Autowired
TokenFetcher tokenFetcher;
public void test() {
Map<String, Object> form = new HashMap<>();
form.put("client_id", "xxxxxx");
form.put("client_secret", "xxxxxx");
form.put("grant_type", "client_credentials");
String jwt = tokenFetcher.token(form);
}
Dependencies are:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
In order to use the token you must to add it token as the 'authorization' header with a prefix of 'Bearer ' (note the space) on each call. The easiest way to do this is be adding a RequestInterceptor to your FeignClient as follows:
public class FeignConfiguration {
#Bean
public RequestInterceptor requestInterceptor() {
#Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("Authorization", "Bearer " + jwtTokenStoredSomewhere);
}
}
}
Related
I'm using Cognito + Spring security. There are any way to use authorization?
I'm using spring Security and cognito for authentication and authorization. I entered some custom roles via aws IAM and I would like to know if there was a method to grant controlled access to resources. On the web I found some that set the cognito:groups as a role and used that, but they use deprecated classes and methods on it. Is there any way to do this with the latest versions? I tried to create a class: package com.projectname.name.Configurations; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.lang.NonNull; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import java.util.Collection; import java.util.Collections; import java.util.stream.Collectors; import java.util.stream.Stream; public class CognitoAccessTokenConverter implements Converter<Jwt, AbstractAuthenticationToken> { private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); public CognitoAccessTokenConverter() { } #Override public AbstractAuthenticationToken convert(#NonNull final Jwt jwt) { Collection<GrantedAuthority> authorities = Stream .concat(defaultGrantedAuthoritiesConverter.convert(jwt).stream(), extractResourceRoles(jwt).stream()) .collect(Collectors.toSet()); return new JwtAuthenticationToken(jwt, authorities); } private static Collection<? extends GrantedAuthority> extractResourceRoles(final Jwt jwt) { Collection<String> userRoles = jwt.getClaimAsStringList("cognito:groups"); //System.out.println("\n!!!!!!!!" +userRoles +"!!!!!!!!!!\n"); DEBUG if (userRoles != null) return userRoles .stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) .collect(Collectors.toSet()); return Collections.emptySet(); } } /* import java.util.Map; import java.util.Set; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; import org.springframework.stereotype.Component; import org.springframework. #Component public class CognitoAccessTokenConverter extends OAuth2AuthorizationCodeRequestAuthenticationToken{ private static final String COGNITO_GROUPS = "cognito:groups"; private static final String SPRING_AUTHORITIES = "authorities"; private static final String COGNITO_USERNAME = "username"; private static final String SPRING_USER_NAME = "user_name"; } #Component public class CognitoAccessTokenConverter extends { // Note: This the core part. private static final String COGNITO_GROUPS = "cognito:groups"; private static final String SPRING_AUTHORITIES = "authorities"; private static final String COGNITO_USERNAME = "username"; private static final String SPRING_USER_NAME = "user_name"; #SuppressWarnings("unchecked") #Override public OAuth2Authentication extractAuthentication(Map<String, ?> claims) { if (claims.containsKey(COGNITO_GROUPS)) ((Map<String, Object>) claims).put(SPRING_AUTHORITIES, claims.get(COGNITO_GROUPS)); if (claims.containsKey(COGNITO_USERNAME)) ((Map<String, Object>) claims).put(SPRING_USER_NAME, claims.get(COGNITO_USERNAME)); return super.extractAuthentication(claims); } } */ how can I use this conversion in my spring security configuration? package com.SSDProject.Booked.Configurations; import java.io.*; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.event.AuthenticationSuccessEvent; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; #Configuration #EnableWebSecurity public class SecurityConfiguration { #Bean SecurityFilterChain web(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/admin").hasAuthority("max") .requestMatchers("/**").permitAll() .anyRequest().authenticated() ) .oauth2Login(); return http.build(); } Help me, I tried to implements it and search everywhere. Some helps? Have you an idea?
I've recently created the same PoC using SpringBoot 2.x and Java 17. In my case I don't have any deprecation warning from my IDE, here my example: #Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf() .and() .requestMatchers().antMatchers("/api/**") .and() .authorizeRequests().anyRequest().authenticated() .and() .userDetailsService(null) .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor()))); return http.build(); } private JwtAuthenticationConverter grantedAuthoritiesExtractor() { JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> { String[] scopes; if (jwt.getClaims().containsKey("cognito:groups")) { scopes = ((JSONArray) jwt.getClaims().get("cognito:groups")).toArray(new String[0]); } else { scopes = ((String) jwt.getClaims().getOrDefault("scope", "")).split(" "); } return Arrays.stream(scopes) .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase(Locale.ROOT))) .collect(Collectors.toSet()); } ); return jwtAuthenticationConverter; } Exactly which line is deprecated in your code? And what version of resource-server are you using? For me spring-boot-starter-oauth2-resource-server is 2.7.5. This is actually not an answer but I don't have the reputation for add comment to the question :)
Is your Spring application serving server-side rendered UI (Thymeleaf, JSF or alike) or is it a REST API (#RestController or #Controller with #ResponseBody)? In second case, your app is a resource-server. OAuth2 login should be handled by clients, not resource-server: clients acquire access token and send it as Authorization header to resource-server. In my answer to Use Keycloak Spring Adapter with Spring Boot 3, I explain how to configure both Spring resource-servers and clients. All you'll have to adapt for Cognito are issuer URI and the private-claims name to extract authorities from. Configuring a resource-server with authorities mapped from cognito:groups using my starters (thin wrappers around spring-boot-starter-oauth2-resource-server) can be as simple as: <dependency> <groupId>com.c4-soft.springaddons</groupId> <artifactId>spring-addons-webmvc-jwt-resource-server</artifactId> <version>6.0.10</version> </dependency> #Configuration #EnableMethodSecurity public class SecurityConfig { } com.c4-soft.springaddons.security.issuers[0].location=https://cognito-idp.Region.amazonaws.com/your user pool ID/.well-known/openid-configuration com.c4-soft.springaddons.security.issuers[0].authorities.claims=cognito:groups # This is probably too permissive but can be fine tuned (origins, headers and methods can be defined per path) com.c4-soft.springaddons.security.cors[0].path=/** If your application is only a client, my starters won't be of any help. If your app is both a resource-server and a client (serves JSON payloads and server-side rendered UI with, for instance, Thymeleaf), then you'll have to define a second SecurityFilterChain bean. Details in the answer linked earlier. If you don't want to use my starters, then you'll have to write quite some java conf. Details in the previously linked answer, again.
Get response body from NoFallbackAvailableException in spring cloud circuit breaker resilience4j
I want to call a third party API. I use spring cloud circuit breaker resilience4j. Here is my service class : package ir.co.isc.resilience4jservice.service; import ir.co.isc.resilience4jservice.model.Employee; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; #Service public class EmployeeService { #Autowired private RestTemplate restTemplate; #Autowired private CircuitBreakerFactory circuitBreakerFactory; public Employee getEmployee() { try { String url = "http://localhost:8090/employee"; CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuit-breaker"); return circuitBreaker.run(() -> restTemplate.getForObject(url, Employee.class)); } catch (NoFallbackAvailableException e) { //I should extract error response body and do right action then return correct answer return null; } } } ResilienceConfig: package ir.co.isc.resilience4jservice.config; import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; import io.github.resilience4j.timelimiter.TimeLimiterConfig; import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory; import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder; import org.springframework.cloud.client.circuitbreaker.Customizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.time.Duration; #Configuration public class CircuitBreakerConfiguration { #Bean public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() { CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) .slidingWindowSize(10) .minimumNumberOfCalls(10) .failureRateThreshold(25) .permittedNumberOfCallsInHalfOpenState(3) .build(); TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom() .timeoutDuration(Duration.ofSeconds(4)) .build(); return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id) .circuitBreakerConfig(circuitBreakerConfig) .timeLimiterConfig(timeLimiterConfig) .build()); } } in some situation third party api return ResponseEntity with statusCode = 500 and body = {"errorCode":"CCBE"}. response is look like this : [503] during [POST] to [http://localhost:8090/employee]:[{"errorCode":"CCBE"}] When I call this API and get internal server error with body, my catch block catchs api response. In catch block I need retrieve response body and do some actions according to errorCode. But I can not do this. How can I extract body in this situation?
Spring Security 5.3 Resource Server Multiple Keys
We have the following scenario: Multiple 'legacy' Spring Security Oauth2 Auth Servers (2.3.4) - each with a different RSA key configured for creation of the JWT tokens. Single newer (SS 5.3.3, SB 2.3.1) Resource Server which we want to accept tokens from either auth server. Problem is the resource server is only configured with 1 key (currently)- so it can only accept tokens from 1 auth-server. Is there any conceivable way to support multiple keys in our resource server to decode JWTs coming from different auth-servers? We basically want to do this but with multiple keys: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2resourceserver-jwt-decoder-public-key Spring Security 5.3 indicates this may be possible with 'multi-tenancy' https://docs.spring.io/spring-security/site/docs/current/reference/html5/#webflux-oauth2resourceserver-multitenancy It's a basic configuration #Value("${security.oauth2.resourceserver.jwt.key-value}") RSAPublicKey key; #Override protected void configure(HttpSecurity http) throws Exception { http // using new Spring Security SpE"{{LOCATOR_BASE_URL}}"L //https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#webflux-oauth2resourceserver-jwt-authorization .authorizeRequests(authorizeRequests -> authorizeRequests .antMatchers("/shipments/**").hasAuthority("SCOPE_DOMPick") .anyRequest().authenticated() ) .csrf().disable() // ****** this is the new DSL way in Spring Security 5.2 instead of Spring Security Oauth #EnableResourceServer ****** .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer .jwt(jwt -> jwt.decoder(jwtDecoder()) ) ); } // static key #Bean JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withPublicKey(this.key).build();
Yes Spring Security 5.3 allow's you to use multiple jwk-uri key's. Please read my answer here: https://stackoverflow.com/a/61615389/12053054 If you cannot use this version of SS it is possible to manually configure spring security to use multiple jwk-uri key's. (Follow link i have provided to see how). This part of Spring Security doc's specify how to do it with Spring Security 5.3: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2resourceserver-multitenancy JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo"); http .authorizeRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .authenticationManagerResolver(authenticationManagerResolver) ); Note that issuer url's are resolved from incoming token (JWT oauth2 token always contains issuer url where uri for jwk to verify JWT token can be found). By manual configuration (answer i have posted) you can add custom behavior for example: instead of finding which ulr should be used to verify token directly from JWT you can check header's for information that resolves which issuer URL (you have specified them in your spring app) should be used with this request to verify JWT token.
I know it's a bit late, but it was exactly what we needed in our company. No issuer url's for auth-servers. Further more, no need for auth-servers, too, as clients requesting the protected resource on the resource server, just have to generate signed JWT with the private key and send it in the http header as Authorization Bearer token. On the resource server only clients having their public key (certificates) imported in the truststore will be allowed to access the resources. So thanks to the tips given by #Norbert Dopjera, I implemented a custom AuthenticationManagerResolver that will look in the JWT header for kid (key id) transporting the alias for a certificate (public key) stored in a truststore.jks file, will retrieve this public key and create a JWTDecoder that will check if the incoming JWT as Authorization Bearer from the http header, was signed with the corresponding private key. Here is the whole code using Spring Boot 2.7.1: import java.io.FileInputStream; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.interfaces.RSAPublicKey; import java.text.ParseException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; 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.InvalidBearerTokenException; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; import org.springframework.util.StringUtils; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jwt.JWTParser; public class TenantAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> { private static final Logger log = LoggerFactory.getLogger(TenantAuthenticationManagerResolver.class); private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>(); private final BearerTokenResolver resolver = new DefaultBearerTokenResolver(); private String trustetoreFile; private char[] storePasswd; public TenantAuthenticationManagerResolver(String truststoreFile, char[] storePasswd) { super(); this.trustetoreFile = truststoreFile; this.storePasswd = storePasswd; } #Override public AuthenticationManager resolve(HttpServletRequest request) { try { return this.authenticationManagers.computeIfAbsent(toTenant(request), this::fromTenant); } catch (Exception e) { throw new InvalidBearerTokenException(e.getMessage()); } } private String toTenant(HttpServletRequest request) throws ParseException { String jwt = this.resolver.resolve(request); String keyId = ((JWSHeader) JWTParser.parse(jwt).getHeader()).getKeyID(); if (!StringUtils.hasText(keyId)) { throw new IllegalArgumentException("KeyID missing"); } return keyId; } private AuthenticationManager fromTenant(String tenant) { return new JwtAuthenticationProvider(jwtDecoder(tenant))::authenticate; } private JwtDecoder jwtDecoder(String kid) { log.info("Building JwtDecoder for {}", kid); try { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(getPublicKeyFromTruststore(kid)).signatureAlgorithm(SignatureAlgorithm.from("RS512")).build(); OAuth2TokenValidator<Jwt> withDefault = JwtValidators.createDefault(); OAuth2TokenValidator<Jwt> withDelegating = new DelegatingOAuth2TokenValidator<>(withDefault); jwtDecoder.setJwtValidator(withDelegating); return jwtDecoder; } catch (Exception e) { throw new IllegalStateException(e.getMessage()); } } private RSAPublicKey getPublicKeyFromTruststore(String certificateAlias) throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException { try (FileInputStream myKeys = new FileInputStream(trustetoreFile)) { log.info("Opening truststore"); KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType()); myTrustStore.load(myKeys, storePasswd); Certificate certificate = myTrustStore.getCertificate(certificateAlias); if (certificate == null) { throw new IllegalArgumentException("No entry found for alias " + certificateAlias); } return (RSAPublicKey) certificate.getPublicKey(); } } } And now the security configuration: import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import your_package.TenantAuthenticationManagerResolver; #EnableWebSecurity public class SecurityConfig { #Value("${jwt.keystore.location}") private String keyStore; #Value("${jwt.keystore.password}") private char[] storePasswd; #Value("${jwt.algorithm}") private String algorithm; #Bean public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManagerResolver<HttpServletRequest> tenantAuthManagerResolver) throws Exception { //#formatter:off http .authorizeRequests() .mvcMatchers("/").permitAll() .mvcMatchers("/protectedservice/**").authenticated() .and().cors() .and().oauth2ResourceServer(oauth2 -> oauth2 .authenticationManagerResolver(tenantAuthManagerResolver) ); //#formatter:on return http.build(); } #Bean public AuthenticationManagerResolver<HttpServletRequest> tenantAuthManagerResolver() { return new TenantAuthenticationManagerResolver(keyStore, storePasswd); } } Dependencies: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> Properties in the application.properties: jwt.keystore.location=/absolute_path_to/truststore.jks jwt.keystore.password=your_trustsrore_passwd jwt.algorithm=RS512 Further requirements: Creation of a keypair (a self signed certificate) in a keystore with Java keytool (stays on the authentication/authorization server): keytool -genkey -keyalg RSA -alias my_alias -keystore my_keystore_file.jks -storepass my_keystore_pass -validity 360 -keysize 2048 -storetype JKS Extract the public key (the certificate) from the keystore: keytool -exportcert -alias my_alias -keystore my_keystore.jks -storepass my_keystore_pass -rfc -file my_cert_file.pem Import this certificate in a new keystore (truststore) holding the public keys (stays on the resource server): keytool -importcert -alias my_alias -file my_cert_file.pem -keystore my_truststore_file.jks -storepass my_store_pass For multi-tenancy, add more keypairs with different aliases to the keystore then extract the certificate (public key) and add it to the truststore. The my_truststore_file.jks will be used in the configured property jwt.keystore.location of the resource server. Code for generating signed JWT with the private key stored in the keystore (this should be implemented on the Security Oauth2 Auth Servers). I put this code in a JUnit test class: import static org.junit.jupiter.api.Assertions.assertNotNull; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.time.Instant; import java.time.temporal.ChronoField; import java.util.Base64; import java.util.Date; import java.util.UUID; import org.junit.jupiter.api.Test; import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.crypto.RSASSASigner; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; class TestJWTGeneration { #Test void testCreateJWTNimusKS() throws Exception { PrivateKey privateKey = getPrivateKeyFromKeystore("/absolute_path_to/keystore.jks", "my_alias"); // Create RSA-signer with the private key JWSSigner signer = new RSASSASigner(privateKey); //#formatter:off // Prepare JWT with claims set // 1 day JWT validity Date expirationDate = Date.from(Instant.now().plus(1L, ChronoField.DAY_OF_MONTH.getBaseUnit())); JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() .subject("my_subject") .issuer("https://my_oauth2_server.com/") .audience("my_audience") .issueTime(new Date()) .claim("nonce", Base64.getEncoder().encodeToString(UUID.randomUUID().toString().getBytes())) .expirationTime(expirationDate) .build(); //#formatter:on // put the certificate alias in the JWT header as "kid" field (Key ID) final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS512).type(JOSEObjectType.JWT).keyID("my_alias").build(); final SignedJWT signedJWT = new SignedJWT(header, claimsSet); signedJWT.sign(signer); String jwtSigned = signedJWT.serialize(); assertNotNull(jwtSigned); System.out.println("##Nimbus JWT=" + jwtSigned); } public static PrivateKey getPrivateKeyFromKeystore(String pubKeyFile, String keyAlias) throws FileNotFoundException, IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException { try (FileInputStream myKeys = new FileInputStream(pubKeyFile)) { KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType()); myTrustStore.load(myKeys, "my_keystore_pass".toCharArray()); Key key = myTrustStore.getKey(keyAlias, "my_keystore_pass".toCharArray()); return (PrivateKey) key; } } }
How to add SAML token to CXF client request in Spring Boot
We're building a CXF client in Spring Boot. The SAML token to authenticate/authorize against the SOAP server is provided to our app in custom HTTP header from an external auth proxy with every request. Hence, I need a way to add the provided token to every outgoing CXF request. I know that I could possibly register a custom CXF out interceptor for that. However, How would I go about registering that interceptor in Spring Boot? If not done with an interceptor what would be the alternatives? Currently, the Spring config looks like this: #Configuration public class MyConfig { #Bean public PartnerServicePortType partnerServicePortType() { PartnerServicePortType partnerServicePortType = new PartnerServiceV0().getPartnerService(); (PartnerServiceV0 is generated from the service's WSDL with Maven.) In the above config class we don't currently declare/configure a CXF bus bean.
One possible solution is this: #Configuration public class MyConfig { #Bean public PartnerServicePortType partnerServicePortType() { PartnerServicePortType service = new PartnerServiceV0().getPartnerService(); configure(service, path, baseUrl); return service; } private void configureService(BindingProvider bindingProvider, String path, String baseUrl) { // maybe try the approach outlined at https://github // .com/kprasad99/kp-soap-ws-client/blob/master/src/main/java/com/kp/swasthik/soap/CxfConfig.java#L24 // as an alternative bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, baseUrl + path); Endpoint cxfEndpoint = ClientProxy.getClient(bindingProvider).getEndpoint(); cxfEndpoint.getInInterceptors().add(cxfLoggingInInterceptor); cxfEndpoint.getInFaultInterceptors().add(cxfLoggingInInterceptor); cxfEndpoint.getOutInterceptors().add(addSamlAssertionInterceptor); } } And the interceptor import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.cxf.binding.soap.SoapHeader; import org.apache.cxf.binding.soap.SoapMessage; import org.apache.cxf.binding.soap.interceptor.AbstractSoapInterceptor; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.phase.Phase; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.XMLObjectBuilder; import org.opensaml.core.xml.XMLObjectBuilderFactory; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.io.Marshaller; import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.soap.wssecurity.Created; import org.opensaml.soap.wssecurity.Expires; import org.opensaml.soap.wssecurity.Security; import org.opensaml.soap.wssecurity.Timestamp; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.w3c.dom.Element; import javax.xml.namespace.QName; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; /** * Adding SOAP header with SAML assertion to request. */ #Slf4j #Component public class AddSamlAssertionInterceptor extends AbstractSoapInterceptor { private final SamlAssertionExtractor samlAssertionExtractor; #Autowired public AddSamlAssertionInterceptor(SamlAssertionExtractor samlAssertionExtractor) { super(Phase.POST_LOGICAL); this.samlAssertionExtractor = samlAssertionExtractor; } #Override public void handleMessage(SoapMessage message) throws Fault { String decodedToken = SamlTokenHolder.getDecodedToken(); if (StringUtils.isBlank(decodedToken)) { log.trace("Not adding SOAP header with SAML assertion because SAML token is blank."); } else { log.trace("Got decoded SAML token: {}", decodedToken); log.trace("Adding SOAP header with SAML assertion to request."); SoapHeader header = createSoapHeaderWithSamlAssertionFrom(decodedToken); message.getHeaders().add(header); } } private SoapHeader createSoapHeaderWithSamlAssertionFrom(String decodedToken) { Assertion assertion = samlAssertionExtractor.extractAssertion(decodedToken); Security security = createNewSecurityObject(); security.getUnknownXMLObjects().add(createTimestampElementFrom(assertion)); security.getUnknownXMLObjects().add(assertion); log.trace("Creating new SOAP header with WS-Security element for '{}'.", assertion.getSubject().getNameID().getValue()); SoapHeader header = new SoapHeader(security.getElementQName(), marshallToDom(security)); header.setMustUnderstand(config.isMustUnderstandHeader()); return header; } #SneakyThrows(MarshallingException.class) private Element marshallToDom(Security security) { Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(security); return marshaller.marshall(security); } /* * SAML requirements documented at https://docs.oasis-open.org/wss/v1.1/wss-v1.1-spec-errata-os-SOAPMessageSecurity * .htm#_Toc118717167. Both timestamps must be in UTC and formatted to comply with xsd:dateTime. */ private Timestamp createTimestampElementFrom(Assertion assertion) { Timestamp timestamp = (Timestamp) createOpenSamlXmlObject(Timestamp.ELEMENT_NAME); Created created = (Created) createOpenSamlXmlObject(Created.ELEMENT_NAME); Expires expires = (Expires) createOpenSamlXmlObject(Expires.ELEMENT_NAME); // alternative would be to use timestamp from assertion like so assertion.getConditions().getNotBefore() created.setValue(ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT)); // security semantics should ensure that the expiry date here is the same as the expiry of the SAML assertion expires.setValue(assertion.getConditions().getNotOnOrAfter().toString()); timestamp.setCreated(created); timestamp.setExpires(expires); return timestamp; } private Security createNewSecurityObject() { return (Security) createOpenSamlXmlObject(Security.ELEMENT_NAME); } private XMLObject createOpenSamlXmlObject(QName elementName) { XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); XMLObjectBuilder<Security> builder = (XMLObjectBuilder<Security>) builderFactory.getBuilder(elementName); return builder.buildObject(elementName); } }
Configuring Swagger UI with Spring Boot
I am trying to configure Swagger UI with my Spring boot application. Although the v2/api-docs seems to be loading properly, the http://localhost:8080/swagger-ui.html does not load my annotated REST API. Here is what I have: pom.xml: ... <!--Swagger UI--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.6.1</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.4.0</version> </dependency> ... SwaggerConfig.java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import static springfox.documentation.builders.PathSelectors.regex; #Configuration #EnableSwagger2 public class SwaggerConfig { #Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.any()) .paths(regex("/.*")) .build().apiInfo(apiInfo()); } private ApiInfo apiInfo() { ApiInfo apiInfo = new ApiInfo( "My Project's REST API", "This is a description of your API.", "version-1", "API TOS", "me#wherever.com", "API License", "API License URL" ); return apiInfo; } } http://localhost:8080/v2/api-docs: {"swagger":"2.0","info":{"description":"This is a description of your API.","version":"version-1","title":"My Project's REST API","termsOfService":"API TOS","contact":{"name":"me#wherever.com"},"license":{"name":"API License","url":"API License URL"}},"host":"localhost:8080","basePath":"/","tags":[{"name":"test-controller","description":"Test Controller"},{"name":"custom-field-controller","description":"Custom Field Controller"},{"name":"user-controller","description":"User Controller"},{"name":"users-controller","description":"Users Controller"},{"name":"crudapi-controller","description":"CRUDAPI Controller"},{"name":"basic-error-controller","description":"Basic Error Controller"}],"paths":{"/":{"get":{"tags":["crudapi-controller"],"summary":"greeting","operationId":"greetingUsingGET","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"string"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/javainuse":{"get":{"tags":["test-controller"],"summary":"firstPage","operationId":"firstPageUsingGET","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"string"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/error":{"get":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingGET","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"head":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingHEAD","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}},"post":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingPOST","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"put":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingPUT","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"delete":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingDELETE","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}},"options":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingOPTIONS","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}},"patch":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingPATCH","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}}},"/fields":{"get":{"tags":["custom-field-controller"],"summary":"greeting","operationId":"greetingUsingGET_1","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"string"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/fields/{id}":{"get":{"tags":["custom-field-controller"],"summary":"fieldAPIController","operationId":"fieldAPIControllerUsingGET","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"id","in":"path","description":"id","required":true,"type":"integer","format":"int32"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/CustomField"}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/users":{"get":{"tags":["user-controller"],"summary":"greeting","operationId":"greetingUsingGET_2","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"string"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/users/":{"get":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingGET","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"head":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingHEAD","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}},"post":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingPOST","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"put":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingPUT","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"delete":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingDELETE","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}},"options":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingOPTIONS","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}},"patch":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingPATCH","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}}},"/users/{id}":{"get":{"tags":["user-controller"],"summary":"userAPIController","operationId":"userAPIControllerUsingGET","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"id","in":"path","description":"id","required":true,"type":"integer","format":"int32"}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/Collection«UserJPA»"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}}},"definitions":{"UserJPA":{"type":"object"},"Collection«UserJPA»":{"type":"object"},"ModelAndView":{"type":"object","properties":{"empty":{"type":"boolean"},"model":{"type":"object"},"modelMap":{"type":"object","additionalProperties":{"type":"object"}},"reference":{"type":"boolean"},"status":{"type":"string","enum":["100","101","102","103","200","201","202","203","204","205","206","207","208","226","300","301","302","303","304","305","307","308","400","401","402","403","404","405","406","407","408","409","410","411","412","413","414","415","416","417","418","419","420","421","422","423","424","426","428","429","431","451","500","501","502","503","504","505","506","507","508","509","510","511"]},"view":{"$ref":"#/definitions/View"},"viewName":{"type":"string"}}},"CustomField":{"type":"object","properties":{"name":{"type":"string"}}},"View":{"type":"object","properties":{"contentType":{"type":"string"}}}}} The swagger-ui.html (http://localhost:8080/swagger-ui.html) does not show the expected REST calls: The error in swagger-ui.html from the code inspection: Failed to load resource: the server responded with a status of 404 (). I have googled around (tried web-config mvc too) but the error persists. Maybe I am missing a resource reference in the .iml file?
I had this issue today and fixed it by matching up the versions of my springfox-swagger2 and springfox-swagger-ui dependencies: <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.6.1</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.6.1</version> </dependency> There's very little other code to just get it up and running. One simple config class: #Configuration #EnableSwagger2 class SwaggerConfiguration { #Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.foo.samples.swaggersample")) .paths(PathSelectors.any()) .build(); } } And my application.properties # location of the swagger json springfox.documentation.swagger.v2.path=/swagger.json (This is in Spring Boot).
Statement : Generate Swagger UI for the listing of all the REST APIs through Spring Boot Application. Follow the below steps to generate the Swagger UI through Spring Boot application: 1. Add following dependency in pom.xml – <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.6.1</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.6.1</version> </dependency> 2. Add the following piece of code in your main application class having the #EnableSwagger2 annotation. #EnableSwagger2 #SpringBootApplication public class MyApp { public static void main(String[] args) { SpringApplication.run(MyApp.class, args); } #Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2).select() .apis(RequestHandlerSelectors.withClassAnnotation(Api.class)) .paths(PathSelectors.any()).build().pathMapping("/") .apiInfo(apiInfo()).useDefaultResponseMessages(false); } #Bean public ApiInfo apiInfo() { final ApiInfoBuilder builder = new ApiInfoBuilder(); builder.title("My Application API through Swagger UI").version("1.0").license("(C) Copyright Test") .description("List of all the APIs of My Application App through Swagger UI"); return builder.build(); } } 3. Add the below RootController class in your code to redirect to the Swagger UI page. In this way, you don’t need to put the dist folder of Swagger-UI in your resources directory. import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; #Controller #RequestMapping("/") public class RootController { #RequestMapping(method = RequestMethod.GET) public String swaggerUi() { return "redirect:/swagger-ui.html"; } } 4. Being the final steps, add the #Api and #ApiOperation notation in all your RESTControllers like below – import static org.springframework.web.bind.annotation.RequestMethod.GET; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; #RestController #RequestMapping("/hello") #Api(value = "hello", description = "Sample hello world application") public class TestController { #ApiOperation(value = "Just to test the sample test api of My App Service") #RequestMapping(method = RequestMethod.GET, value = "/test") // #Produces(MediaType.APPLICATION_JSON) public String test() { return "Hello to check Swagger UI"; } #ResponseStatus(HttpStatus.OK) #RequestMapping(value = "/test1", method = GET) #ApiOperation(value = "My App Service get test1 API", position = 1) public String test1() { System.out.println("Testing"); if (true) { return "Tanuj"; } return "Gupta"; } } Now your are done. Now to run your Spring Boot Application, go to browser and type localhost:8080. You will see Swagger UI having all the details of your REST APIs. Happy Coding. 🙂 The source code of the above implementation is also on my blog if you feel like checking it out.
Swagger is Available with V2 and V3 version More minimal config Check this Answer - https://stackoverflow.com/a/64333853/410439
Add a config class like this #Configuration public class WebMvcConfiguration extends WebMvcConfigurationSupport { #Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { // Make Swagger meta-data available via <baseURL>/v2/api-docs/ registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); // Make Swagger UI available via <baseURL>/swagger-ui.html registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/"); } }
Nowadays, just set springdoc.swagger-ui.disable-swagger-default-url=true