Why JwtAccessTokenConverter - Unable to create an RSA verifier from verifierKey (ignoreable if using MAC) when start up a microservice server? - spring

Description:
When the Bet microservice server starts, it never manages to create an RSA verifier because its value is null. I'm developing a microservices-based architecture using Oauth2, taking as a guide the microservices architecture developed by Jhipster:
UAA server. It expose public key to verify JWT signature.
Eureka server.
Cloud Config Server.
Bet microservice server. On start up try to retrieve the public key
to verify signed JWT.
Euraka configuration for each server:
Eureka server:
eureka:
client:
fetch-registry: false
register-with-eureka: false
instance-info-replication-interval-seconds: 10
registry-fetch-interval-seconds: 10
service-url:
defaultZone: http://admin:${spring.security.user.password:admin}#${eureka.instance.hostname}:${server.port}/eureka/
instance:
hostname: localhost
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 10
UAA server:
eureka:
client:
service-url:
defaultZone: http://admin:eureka#localhost:8761/eureka/
instance-info-replication-interval-seconds: 10
registry-fetch-interval-seconds: 10
instance:
appname: uaa
instanceId: uaa:${spring.application.instance-id:${random.value}}
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 10
Microservice Bet:
eureka:
client:
service-url:
defaultZone: http://admin:eureka#localhost:8761/eureka/
instance-info-replication-interval-seconds: 10
registry-fetch-interval-seconds: 10
instance:
appname: bets
instanceId: bets:${spring.application.instance-id:${random.value}}
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 10
When trying to consume a service published by Bet microservice: http://localhost:8083/api/markets, it returns the following response:
{
"error": "invalid_token",
"error_description": "public key expired"
}
status: 401 Unauthorize
Tracing the code I discovered that in class org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter the attribute this.verifier is null inside the afterPropertiesSet () method throwing an Exception (logger.warn ("Unable to create an RSA verifier from verifierKey (ignoreable if using MAC) "))
because I couldn't start the new RsaVerifier object (this.verifierKey)
I thought it was due to the delay time of registering the microservice with the eureka server, so I waited more than 5 min and checked via Postman client that the public key was available and even so the Bet microservice could not create RSA verifier.
So,
Why the Bet microservice at startup failed to create RSA verifier?
Could it be that the public key could not be received when starting
the microservice that is not yet registered with eureka server?

Finally i found the issue.
I must call the tryCreateSignatureVerifier() method when the OAuth2JwtAccessTokenConverter class initialize(in constructor) and to try to retrieve the public key and create RSA verifier
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
public class OAuth2JwtAccessTokenConverter extends JwtAccessTokenConverter {
private final AppProperties properties;
private final OAuth2SignatureVerifierClient signatureVerifierClient;
/**
* When did we last fetch the public key?
*/
private long lastKeyFetchTimestamp;
public OAuth2JwtAccessTokenConverter(OAuth2SignatureVerifierClient signatureVerifierClient, AppProperties properties) {
this.properties = properties;
this.signatureVerifierClient = signatureVerifierClient;
//NOTE, it is very important to try fetch for the public key to verify the JWT signature
tryCreateSignatureVerifier();
}
/**
* Fetch a new public key from the AuthorizationServer.
*
* #return true, if we could fetch it; false, if we could not.
*/
private boolean tryCreateSignatureVerifier() {
long t = System.currentTimeMillis();
if (t - lastKeyFetchTimestamp < properties.getSignatureVerification().getPublicKeyRefreshRateLimit()) {
return false;
}
try {
SignatureVerifier verifier = signatureVerifierClient.getSignatureVerifier();
if (verifier != null) {
setVerifier(verifier);
lastKeyFetchTimestamp = t;
log.debug("Public key retrieved from OAuth2 server to create SignatureVerifier");
return true;
}
} catch (Throwable ex) {
log.error("Could not get public key from OAuth2 server to create SignatureVerifier", ex);
}
return false;
}
/**
* Try to decode the token with the current public key.
* If it fails, contact the OAuth2 server to get a new public key, then try again.
* We might not have fetched it in the first place or it might have changed.
*
* #param token the JWT token to decode.
* #return the resulting claims.
* #throws InvalidTokenException if we cannot decode the token.
*/
#Override
protected Map<String, Object> decode(String token) {
try {
//check if our public key and thus SignatureVerifier have expired
long ltt = properties.getSignatureVerification().getTtl();
if (ltt > 0 && System.currentTimeMillis() - lastKeyFetchTimestamp > ltt) {
throw new InvalidTokenException("public key expired");
}
return super.decode(token);
} catch (InvalidTokenException e) {
if (tryCreateSignatureVerifier()) {
super.decode(token);
}
throw e;
}
}
/**
* Extract JWT claims and set it to OAuth2Authentication decoded details.
* Here is how to get details:
* #param claims OAuth2JWTToken claims.
* #return {#link OAuth2Authentication}.
*/
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
OAuth2Authentication authentication = super.extractAuthentication(claims);
authentication.setDetails(claims);
return authentication;
}
Abstracts how to create a SignatureVerifier to verify JWT tokens with a public key
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
/**
* Abstracts how to create a {#link SignatureVerifier} to verify JWT tokens with a public key.
* Implementations will have to contact the OAuth2 authorization server to fetch the public key
* and use it to build a {#link SignatureVerifier} in a server specific way.
*
* #see com.example.bets.config.oauth2.OAuth2UaaSignatureVerifierClient
*/
public interface OAuth2SignatureVerifierClient {
/**
* Returns the {#link SignatureVerifier} used to verify JWT tokens.
* Fetches the public key from the Authorization server to create
* this verifier.
*
* #return the new verifier used to verify JWT signatures.
* Will be null if we cannot contact the token endpoint.
* #throws Exception if we could not create a {#link SignatureVerifier} or contact the token endpoint.
*/
SignatureVerifier getSignatureVerifier() throws Exception;
}
Implementation of OAuth2SignatureVerifierClient
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* Client fetching the public key from UAA to create a {#link SignatureVerifier}.
*/
#Slf4j
#Component
public class OAuth2UaaSignatureVerifierClient implements OAuth2SignatureVerifierClient {
private final AppProperties properties;
private final RestTemplate restTemplate;
public OAuth2UaaSignatureVerifierClient(DiscoveryClient discoveryClient,
AppProperties properties,
#Qualifier("loadBalancedRestTemplate") RestTemplate restTemplate) {
this.properties = properties;
this.restTemplate = restTemplate;
// Load available UAA servers
discoveryClient.getServices();
}
/**
* Fetches the public key from the UAA.
*
* #return the public key used to verify JWT tokens; or {#code null}.
*/
#Override
public SignatureVerifier getSignatureVerifier() {
try {
HttpEntity<Void> request = new HttpEntity<Void>(new HttpHeaders());
String key = (String) restTemplate
.exchange(getPublicKeyEndpoint(), HttpMethod.GET,request, Map.class)
.getBody()
.get("value");
return new RsaVerifier(key);
} catch (IllegalStateException ex) {
log.warn("could not contact UAA to get public key");
return null;
}
}
/**
* Returns the configured endpoint URI to retrieve the public key.
*
* #return the configured endpoint URI to retrieve the public key.
*/
private String getPublicKeyEndpoint() {
String tokenEndpointUrl = properties.getSignatureVerification().getPublicKeyEndpointUri();
if (tokenEndpointUrl == null) {
throw new InvalidClientException("no token endpoint configured in application properties");
}
return tokenEndpointUrl;
}
}
Finally Class SecurityConfiguration extends ResourceServerConfigurerAdapter
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends ResourceServerConfigurerAdapter {
private final AppProperties properties;
public SecurityConfiguration(AppProperties properties) {
this.properties = properties;
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN);
}
/**
* Apply the token converter (and enhancer) for token store.
*
* #return the {#link JwtTokenStore} managing the tokens.
*/
#Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter){
return new JwtTokenStore(jwtAccessTokenConverter);
}
/**
* This bean generates an token enhancer, which manages the exchange between JWT access tokens and Authentication
* in both directions.
*
* #return an access token converter configured with the fetched public key of the authorization server.
*/
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(OAuth2SignatureVerifierClient signatureVerifierClient) {
return new OAuth2JwtAccessTokenConverter(properties, signatureVerifierClient);
}
}
Maven Dependecies
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR4</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Related

Problem with GCP Secret Manager and Spring Boot app

Spring Boot (2.5.9) has problem to access password from the GCP Secret Manager using spring-cloud-gcp-starter-secretmanager ver 2.0.8 throwing error
AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'defaultFeignClientConfiguration': Injection of autowired dependencies failed; nested exception is org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [com.google.protobuf.ByteString$LiteralByteString] to type [java.lang.String]
for a passsword declared in the application.properties as
webservices.security.basic.user.password=${sm://my-password}
when I will replace it with regular string or even env variable it will work fine.
Failing part of the code looks like:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import feign.Retryer;
import feign.auth.BasicAuthRequestInterceptor;
import feign.codec.ErrorDecoder;
/**
* Default feign client configuration. Includes retry policies, basic auth user name and password, and HTTP status decoder.
* #author Greg Meyer
* #since 6.0
*/
public class DefaultFeignClientConfiguration
{
#Value("${webservices.retry.backoff.multiplier:3}")
protected double backoffMultiplier;
#Value("${webservices.retry.backoff.initialBackoffInterval:100}")
protected long initialBackoffInterval;
#Value("${webservices.retry.backoff.maxInterval:20000}")
protected long maxInterval;
#Value("${webservices.security.basic.user.name:}")
protected String user;
#Value("${webservices.security.basic.user.password:}")
protected String pass;
/**
* Creates an instance of the a the default HTTP status translator.
* #return An instance of the a the default HTTP status translator
*/
#Bean
public ErrorDecoder feignClientErrorDecoder()
{
return new DefaultErrorDecoder();
}
/**
* Creates an instance of BasicAuth interceptor configured with a username and password. This bean is only created if the
* "webservices.security.basic.user.name" property is set.
* #return An instance of BasicAuth interceptor configured with a username and password
*/
#Bean
#ConditionalOnProperty(name="webservices.security.basic.user.name", matchIfMissing=false)
public BasicAuthRequestInterceptor basicAuthRequestInterceptor()
{
return new BasicAuthRequestInterceptor(user, pass);
}
/**
* Creates an instance of a back off policy used in conjuntion with the retry policy.
* #return An instance of a back off policy
*/
#Bean
public LoadBalancedRetryFactory backOffPolciyFactory()
{
return new LoadBalancedRetryFactory()
{
#Override
public BackOffPolicy createBackOffPolicy(String service)
{
final ExponentialBackOffPolicy backoffPolicy = new ExponentialBackOffPolicy();
backoffPolicy.setMultiplier(backoffMultiplier);
backoffPolicy.setInitialInterval(initialBackoffInterval);
backoffPolicy.setMaxInterval(maxInterval);
return backoffPolicy;
}
};
}
/**
* Creates a default http retry policy.
* #return A default http retry policy.
*/
#Bean
public Retryer retryer()
{
/*
* Default retryer config
*/
return new Retryer.Default(200, 1000, 5);
}
}
Any thoughts?
The problem is that most likely, Feign autoconfiguration happens early on, before GcpSecretManagerEnvironmentPostProcessor had a chance to run and introduce ByteString converters.
Basically, the solution that works is to implement a Custom Converter which implements the Generic Conversion Service and register the Converter in the main method before calling SpringApplication.run.
public static void main(String[] args)
{
((DefaultConversionService)DefaultConversionService.getSharedInstance()).addConverter(new CustomConverter());
SpringApplication.run(STAApplication.class, args);
}
and custom converter:
#Component
public class CustomConverter implements GenericConverter {
#Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(ByteString.class, String.class));
}
#Override
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
if (sourceType.getType() == String.class) {
return source;
}
try {
source = ((ByteString) source).toString("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return source;
}
}

Spring Security + Firebase

I have rest backend wrote on Spring Boot and oauth2 (provided by Google) auto redirect on "/login". I want to make Firebase auth on the backend for mobile beside with oauth for web, like on the following algorithm:
User authorizes on mobile -> User sends request -> Backend gets request -> Backend checks if user openid exists in local database -> Backend returns response or exception page
The following code is my current WebSecurityConfiguration:
#Configuration
#EnableWebSecurity
#EnableOAuth2Sso
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().mvcMatchers("/","/static/**","/public/**","/assets/**","/api/sensors/**", "/emulator/**").permitAll()
.anyRequest().authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and()
.csrf().disable();
}
#Bean
public PrincipalExtractor principalExtractor(PersonRepository personRepository) {
return map -> {
String id = (String) map.get("sub");
Person person1 = personRepository.findById(id).orElseGet(() -> {
Person person = new Person();
person.setPersonId(id);
person.getDetails().setFirstName((String) map.get("given_name"));
person.getDetails().setLastName((String) map.get("family_name"));
person.getDetails().setEmail((String) map.get("email"));
person.getDetails().setPictureUrl((String) map.get("picture"));
person.getSettings().setLocale(new Locale((String) map.get("locale")));
person.setPersonRole(PersonRole.USER);
person.setStatus(PersonStatus.NORMAL);
person.newToken();
return person;
});
return personRepository.save(person1);
};
}
}
Add Firebase Configuration Bean of the form:
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.google.firebase.*;
#Configuration
public class FirebaseConfig {
#Bean
public DatabaseReference firebaseDatabse() {
DatabaseReference firebase = FirebaseDatabase.getInstance().getReference();
return firebase;
}
#Value("${firebase.database.url}")
private String databaseUrl;
#Value("${firebase.config.path}")
private String configPath;
#PostConstruct
public void init() {
/**
* https://firebase.google.com/docs/server/setup
*
* Create service account , download json
*/
InputStream inputStream = FirebaseConfig.class.getClassLoader().getResourceAsStream(configPath);
FirebaseOptions options = new FirebaseOptions.Builder().setServiceAccount(inputStream)
.setDatabaseUrl(databaseUrl).build();
FirebaseApp.initializeApp(options);
}
}
In your application.properties, add
firebase.config.path=Configuration.json
firebase.database.url=<firebase-database-path>
You can download your Configuration.json for your Firebase project by referring to this page

SOAP Proxy with Spring Integration

I'm trying to wrap my head around spring integration and would preferably like to stay away from XML-based configuration.
What I would like to do is the following:
Accept a SOAP request on a certain endpoint
Forward this request to a downstream SOAP service, without any modification (for now I just want the basics to work)
Return the result to the client
This seems like it should be pretty simple, but I don't have the faintest idea of where to start. Is this even possible with SI? As I understand it, and please correct me if I'm wrong, SI is mainly used for async data flows.
I did check out the integration sample repository, which includes example inbound WS requests, but these are all configured in XML, which, as I said, I'd preferably like to stay far away from.
Any pointers would be much appreciated; I've been reading through documentation for the past two days and I'm none the wiser!
Here is an example using the SimpleWebServiceInboundGateway. In this example we also set the "ExtractPayload" to false so that it sends the RAW soap message. But agree with above, possibly the HTTPInboundRequest is better for your use case. I also didn't find many examples using DSL for the SoapInboundGateway so wanted to share and hope it helps someone else.
#Configuration
#EnableIntegration
public class SoapGatewayConfiguration {
/**
* URL mappings used by WS endpoints
*/
public static final String[] WS_URL_MAPPINGS = {"/services/*", "*.wsdl", "*.xsd"};
public static final String GATEWAY_INBOUND_CHANNEL_NAME = "wsGatewayInboundChannel";
public static final String GATEWAY_OUTBOUND_CHANNEL_NAME = "wsGatewayOutboundChannel";
/**
* Register the servlet mapper, note that it uses MessageDispatcher
*/
#Bean
public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
servlet.setTransformWsdlLocations(true);
servlet.setTransformSchemaLocations(true);
servlet.setPublishEvents(true);
ServletRegistrationBean servletDef = new ServletRegistrationBean(servlet, WS_URL_MAPPINGS);
servletDef.setLoadOnStartup(1);
return servletDef;
}
/**
* Create a new Direct channels to handle the messages
*/
#Bean
public MessageChannel wsGatewayInboundChannel() {
return MessageChannels.direct(GATEWAY_INBOUND_CHANNEL_NAME).get();
}
#Bean
public MessageChannel wsGatewayOutboundChannel() {
return MessageChannels.direct(GATEWAY_OUTBOUND_CHANNEL_NAME).get();
}
/**
* Startup the WebServiceInboundGateway Endpoint, this will handle the incoming SOAP requests
* and place them onto the request channel
*/
#Bean
public SimpleWebServiceInboundGateway webServiceInboundGateway(
#Value("${spring.ws.request.timeout:1000}") long requestTimeout,
#Value("${spring.ws.reply.timeout:1000}") long replyTimeout,
#Value("${spring.ws.should.track:true}") boolean shouldTrack
) {
SimpleWebServiceInboundGateway wsg = new SimpleWebServiceInboundGateway();
wsg.setRequestChannel(wsGatewayInboundChannel());
wsg.setReplyChannel(wsGatewayOutboundChannel());
wsg.setExtractPayload(false); // Send the full RAW SOAPMessage and not just payload
wsg.setLoggingEnabled(true);
wsg.setShouldTrack(shouldTrack);
wsg.setReplyTimeout(replyTimeout); // Do not believe this prop supported currently
wsg.setRequestTimeout(requestTimeout); // Do not believe this prop is supported currently
wsg.setCountsEnabled(true);
return wsg;
}
/**
* You must enable debug logging on org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor
* to see the logs from this interceptor
*/
#Bean
public EndpointInterceptor soapMessageLoggingInterceptor() {
SoapEnvelopeLoggingInterceptor li = new SoapEnvelopeLoggingInterceptor();
li.setLogRequest(true);
li.setLogResponse(true);
li.setLogFault(true);
return li;
}
/**
* Validate the incoming web service against the schema
*/
#Bean
public EndpointInterceptor payloadValidatingInterceptor(XsdSchema xsdSchema
, #Value("${spring.ws.soap.validate.request:true}") boolean soapValidateRequest
, #Value("${spring.ws.soap.validate.reply:true}") boolean soapValidateResponse
, #Value("${spring.ws.soap.validate.addErrorDetail:true}") boolean soapAddValidationErrorDetail
) {
PayloadValidatingInterceptor interceptor = new PayloadValidatingInterceptor();
interceptor.setXsdSchema(xsdSchema);
interceptor.setValidateRequest(soapValidateRequest);
interceptor.setValidateResponse(soapValidateResponse);
interceptor.setAddValidationErrorDetail(soapAddValidationErrorDetail);
return interceptor;
}
/**
* Map the allowable service Uri's.
*/
#Bean
public EndpointMapping uriEndpointMapping(
PayloadValidatingInterceptor payloadValidatingInterceptor
, SimpleWebServiceInboundGateway webServiceInboundGateway
, SoapEnvelopeLoggingInterceptor loggingInterceptor) {
UriEndpointMapping mapping = new UriEndpointMapping();
mapping.setUsePath(true);
mapping.setDefaultEndpoint(webServiceInboundGateway);
mapping.setInterceptors(new EndpointInterceptor[]{loggingInterceptor, payloadValidatingInterceptor});
return mapping;
}
/**
* Expose the wsdl at http://localhost:8080/services/myService.wsdl
**/
#Bean
public Wsdl11Definition myService() {
SimpleWsdl11Definition wsdl11Definition = new SimpleWsdl11Definition();
wsdl11Definition.setWsdl(new ClassPathResource("META-INF/myService.wsdl"));
return wsdl11Definition;
}
/**
* Expose the xsd at http://localhost:8080/services/mySchema.xsd
**/
#Bean
public XsdSchema mySchema() {
return new SimpleXsdSchema(new ClassPathResource("META-INF/mySchema.xsd"));
}
#Bean
public IntegrationFlow itemLookupFlow() {
return IntegrationFlows.from("wsGatewayInboundChannel")
.log(LoggingHandler.Level.INFO)
.handle(myBeanName, "execute")
.log(LoggingHandler.Level.TRACE, "afterExecute")
.get();
}
}
If your application is just a proxy over other SOAP service, you should consider to use just plain HTTP Inbound Gateway and HTTP Outbound Gateway.
You receive an XML from client and send it into the downstream service. Receive from there an XML again and just push it back to the response for the client.
For this purpose I can suggest HTTP proxy solution via Java DSL:
#Bean
public IntegrationFlow httpProxyFlow() {
return IntegrationFlows
.from(Http.inboundGateway("/service"))
.handle(Http.outboundGateway("/service/internal")
.expectedResponseType(String.class))
.get();
}
The problem with the SimpleWebServiceInboundGateway and SimpleWebServiceOutboundGateway pair that they extract a request and parse a respose to (un)wrap to/from the SOAP envelop. This looks like an overhead for your plain proxy use-case.
I got it working thanks to Artem's answer, with a small tweak. Not sure as to why the channels are required, but at least it's now working.
package com.example.integration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.http.Http;
import org.springframework.integration.http.config.EnableIntegrationGraphController;
import org.springframework.messaging.MessageChannel;
#SpringBootApplication
#EnableIntegration
#EnableIntegrationGraphController(allowedOrigins = "*")
public class IntegrationApplication {
public static void main(String[] args) {
SpringApplication.run(IntegrationApplication.class, args);
}
#Bean
public DirectChannel directChannel() {
return new DirectChannel();
}
#Bean
public IntegrationFlow httpProxyFlow(MessageChannel directChannel) {
return IntegrationFlows
.from(Http.inboundGateway("/thing").requestChannel(directChannel).replyChannel(directChannel))
.enrichHeaders(h -> h.header("Content-Type", "application/soap+xml; charset=utf-8"))
.handle(Http.outboundGateway("http://www.webservicex.net/geoipservice.asmx").expectedResponseType(String.class))
.channel(directChannel)
.get();
}
}

What is the expected behavior when setting `security.oauth2.resource.jwk.key-set-uri` in spring boot

In spring boot, upon configuring a Resource server we have the option to set the security.oauth2.resource.jwk.key-set-uri property if the access tokens will be JWTs and the issuer provides an endpoint for clients to acquire the public RSA key for verification in JWK format.
What is the expected behavior to initiate a keystore from this JWK? The property is being loaded in the ResourceServerProperties.JWK but then what. Should spring boot call this URI and fetch the jwks then create a store for me to use in verification?
I am following this tutorial to setup the configuration of the keystore http://www.baeldung.com/spring-security-oauth-jwt
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}
But instead of loading a .pem public key I think I want to load it from a jwk.
If you want to use JWKS, use JwkTokenStore in place of JwtTokenStore.
spring-security-oauth2/jwk internally implements key loading and management according to the auth0 spec
You can also see docs on auto-configuration of the same, however i feel configuring it in quite straight-forward (see below).
We don't have to do any verification as JwkTokenStore sets up the verification with JwkDefinitionSource JwkVerifyingJwtAccessTokenConverter using JWKS exposed at #Value("{jsecurity.oauth2.resource.jwk.key-set-uri}")
However, the spring-security-oauth2/jwk classes from spring don't have any public constructors, we often need and can perform any custom steps in AccessTokenConversion, like a common need is to extract jwt content to auth context, we can always inject a custom converter to JwkTokenStore
import org.springframework.security.oauth2.provider.token.store.jwk.*;
import org.springframework.security.oauth2.provider.token.store.*
import org.springframework.security.oauth2.provider.token.*;
import java.utl.*;
#Configuration
class JwtConfiguration {
#Bean
public DefaultTokenServices tokenServices(final TokenStore tokenStore) {
final DefaultTokenServices dts = new DefaultTokenServices();
dts.setTokenStore(tokenStore);
dts.setSupportRefreshToken(true);
return dts;
}
#Bean
public TokenStore tokenStore(
#Value("{jsecurity.oauth2.resource.jwk.key-set-uri}") final String jwksUrl,
final JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwkTokenStore(jwksUrl, jwtAccessTokenConverter, null);
}
#Bean
public JwtAccessTokenConverter createJwtAccessTokenConverter() {
final JwtAccessTokenConverter converter;
converter.setAccessTokenConverter(new DefaultAccessTokenConverter() {
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
final OAuth2Authentication auth = super.extractAuthentication(map);
auth.setDetails(map); //this will get spring to copy JWT content into
return auth;
}
}
return conveter;
}
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
#Configuration
#EnableResourceServer
class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private String resourceId;
private TokenStore tokenStore;
public ResourceServerConfig(
#Value("\${jwt.reourceId}") private String resourceId,
private TokenStore tokenStore) {
this.resourceId = resourceId;
this.tokenStore = tokenStore;
}
/**
* Ensures request to all endpoints ore a
#Override
public void configure(final HttpSecurity http) {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/**").authenticated();
}
/**
* Configure resources
* Spring OAuth expects "aud" claim in JWT token. That claim's value should match to the resourceId value
* (if not specified it defaults to "oauth2-resource").
*/
#Override
public void configure(final ResourceServerSecurityConfigurer resources) {
resources.resourceId(resourceId).tokenStore(tokenStore);
}
}
The main goal of this implementation would be to verify a JWT locally using the corresponding JWK(JSON WEB TOKEN KEY SET). The JWK used for verification is matched using the kid header parameter of the JWT and the kid attribute of the JWK.
The server can validate this token locally without making any network requests, talking to a database, etc. This can potentially make session management faster because instead of needing to load the user from a database (or cache) on every request, you just need to run a small bit of local code. This is probably the single biggest reason people like using JWTs: they are stateless.

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);
}
}

Resources