SAML endpoints don't match by using the roor dir - spring

In order to setup the SAML Context of my Service Provider, I'm using a configuration as follows:
// Provider of default SAML Context
#Bean
public SAMLContextProviderLB contextProvider() {
SAMLContextProviderLB provider = new SAMLContextProviderLB();
provider.setScheme("http");
provider.setServerPort(8090);
provider.setIncludeServerPortInRequestURL(true);
provider.setServerName("localhost");
provider.setContextPath("/");
return provider;
}
It returns this error (there is just a "slash" not needed):
BaseSAMLMessageDecoder: SAML message intended destination endpoint 'http://localhost:8090/saml/SSO/alias/defaultAlias' did not match the recipient endpoint 'http://localhost:8090//saml/SSO/alias/defaultAlias'
How can I fix it?

Change the provider.setContextPath("/"); to provider.setContextPath(""); or upgrade to 1.0.0.RELEASE which should be able to handle "/" correctly.

Related

Is it possible to change the service endpoint path on gateway using spring cloud

I have a running api on my local machine with url http://localhost:8080/gnk-debt/service/taxpayer-debt.
And I would like this api to be available through gateway with url http://localhost:8243/gnk/service/phystaxpayer/debt/v1.
To do so, first I set the port in property file. But I am not able set custom url for the api endpoint. I am trying to write a simple gateway using spring cloud, configuration of which as following:
#Configuration
public class SpringCloudConfig {
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder routeLocatorBuilder)
{
return routeLocatorBuilder.routes()
.route("gnkTaxpayerDebt", rt -> rt.path("/gnk-debt/**")
.uri("http://localhost:8080/"))
.build();
}
}
But at the end, gateway api endpoint is available at: http://localhost:8243/gnk-debt/service/taxpayer-debt.
The question that I am curios about is that if it is possible to change gateway api endpoint to:
http://localhost:8243/gnk/service/phystaxpayer/debt/v1
EDIT
As spencergibb mentioned that there are some options to do that. I started with RewritePath, subsequently my config has been changed as following:
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder routeLocatorBuilder)
{
return routeLocatorBuilder.routes()
.route("gnkTaxpayerDebt", rt -> rt.path("/gnk/**")
.filters(f->f.rewritePath("/gnk/service/phystaxpayer/debt/v1(?<remains>.*)","/${remains}"))
.uri("http://localhost:8080"))
.build();
}
At the end I am able to access my gateway endpoint at
http://localhost:8243/gnk/service/phystaxpayer/debt/v1/gnk-physicaltaxpayer-debt/gnk/service/physical-taxpayer-debt
How can I change the final endpoint to: http://localhost:8243/gnk/service/phystaxpayer/debt/v1
One of the ways of setting your own endpoint is to create a GlobalFilter where you change the attribute "GATEWAY_REQUEST_URL_ATTR" that comes with ServerWebExchangeUtils. You just have to set your endpoint as below
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, yourEndpoint);

Cookies path with Spring Cloud Gateway

Consider this microservices based application using Spring Boot 2.1.2 and Spring Cloud Greenwich.RELEASE:
Each microservice uses the JSESSIONID cookie to identify its own dedicated Servlet session (i.e. no global unique session shared with Spring Session and Redis).
External incoming requests are routed by Spring Cloud Gateway (and an Eureka registry used through Spring Cloud Netflix, but this should not be relevant).
When Spring Cloud Gateway returns a microservice response, it returns the "Set-Cookie" as-is, i.e. with the same "/" path.
When a second microservice is called by a client, the JSESSIONID from the first microservice is forwarded but ignored (since the corresponding session only exists in the first microservice). So the second microservice will return a new JSESSIONID. As a consequence the first session is lost.
In summary, each call to a different microservice will loose the previous session.
I expected some cookies path translation with Spring Cloud Gateway, but found no such feature in the docs. Not luck either with Google.
How can we fix this (a configuration parameter I could have missed, an
API to write such cookies path translation, etc)?
Rather than changing the JSESSIONID cookies path in a GlobalFilter, I simply changed the name of the cookie in the application.yml:
# Each microservice uses its own session cookie name to prevent conflicts
server.servlet.session.cookie.name: JSESSIONID_${spring.application.name}
I faced the same problem and found the following solution using Spring Boot 2.5.4 and Spring Cloud Gateway 2020.0.3:
To be independent from the Cookie naming of the downstream services, I decided to rename all cookies on the way through the gateway. But to avoid a duplicate session cookie in downstream requests (from the gateway itself) I also renamed the gateway cookie.
Rename the Gateway Session Cookie
Unfortunately customizing the gateway cookie name using server.servlet.session.cookie.name does not work using current gateway versions.
Therefore register a custom WebSessionManager bean (name required as the auto configurations is conditional on the bean name!) changing the cookie name (use whatever you like except typical session cookie names like SESSION, JSESSION_ID, …):
static final String SESSION_COOKIE_NAME = "GATEWAY_SESSION";
#Bean(name = WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
WebSessionManager webSessionManager(WebFluxProperties webFluxProperties) {
DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
CookieWebSessionIdResolver webSessionIdResolver = new CookieWebSessionIdResolver();
webSessionIdResolver.setCookieName(SESSION_COOKIE_NAME);
webSessionIdResolver.addCookieInitializer((cookie) -> cookie
.sameSite(webFluxProperties.getSession().getCookie().getSameSite().attribute()));
webSessionManager.setSessionIdResolver(webSessionIdResolver);
return webSessionManager;
}
Rename Cookies created
Next step is to rename (all) cookies set by the downstream server. This is easy as there is a RewriteResponseHeader filter available. I decided to simply add a prefix to every cookie name (choose a unique one for each downstream):
filters:
- "RewriteResponseHeader=Set-Cookie, ^([^=]+)=, DS1_$1="
Rename Cookies sent
Last step is to rename the cookies before sending to the downstream server. As every cookie of the downstream server has a unique prefix, just remove the prefix:
filters:
- "RewriteRequestHeader=Cookie, ^DS1_([^=]+)=, $1="
Arg, currently there is no such filter available. But based on the existing RewriteResponseHeader filter this is easy (the Cloud Gateway will use it if you register it as a bean):
#Component
class RewriteRequestHeaderGatewayFilterFactory extends RewriteResponseHeaderGatewayFilterFactory
{
#Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest().mutate()
.headers(httpHeaders -> rewriteHeaders(httpHeaders, config)).build();
return chain.filter(exchange.mutate().request(request).build());
}
#Override
public String toString() {
return filterToStringCreator(RewriteRequestHeaderGatewayFilterFactory.this)
.append("name", config.getName()).append("regexp", config.getRegexp())
.append("replacement", config.getReplacement()).toString();
}
};
}
private void rewriteHeaders(HttpHeaders httpHeaders, Config config)
{
httpHeaders.put(config.getName(), rewriteHeaders(config, httpHeaders.get(config.getName())));
}
}
Simply reset cookie name to GATEWAY_SESSION in gateway project to avoid session conflict:
#Autowired(required = false)
public void setCookieName(HttpHandler httpHandler) {
if (httpHandler == null) return;
if (!(httpHandler instanceof HttpWebHandlerAdapter)) return;
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
CookieWebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver();
sessionIdResolver.setCookieName("GATEWAY_SESSION");
sessionManager.setSessionIdResolver(sessionIdResolver);
((HttpWebHandlerAdapter) httpHandler).setSessionManager(sessionManager);
}

Invalid JWToken: kid is a required JOSE Header

I am trying to implement an Oauth2 Authorization Server with SpringBoot using this guide as a reference.
My keystore has a single key. I have successfully managed to create a JWToken (I can check it at jwt.io).
I have also a test Resource Server. When I try to access any endpoint I receive the following message:
{
"error": "invalid_token",
"error_description": "Invalid JWT/JWS: kid is a required JOSE Header"
}
The token really does not have a kid header but I can not figure out how to add it. I can only add data to its payload, using a TokenEnchancer. It also seems that I am not the first one with this issue.
Is there any way to add this header or, at least, ignore it at the resource server?
I've been working on an article that might help you out here:
https://www.baeldung.com/spring-security-oauth2-jws-jwk
So, to configure a Spring Security OAuth Authorization Server to add a JWT kid header, you can follow the steps of section 4.9:
create a new class extending the JwtAccessTokenConverter
In the constructor:
configure the parent class using the same approach you've been using
obtain a Signer object using the signing key you're using
override the encode method. The implementation will be the same as the parent one, with the only difference that you’ll also pass the custom headers when creating the String token
public class JwtCustomHeadersAccessTokenConverter extends JwtAccessTokenConverter {
private JsonParser objectMapper = JsonParserFactory.create();
final RsaSigner signer;
public JwtCustomHeadersAccessTokenConverter(KeyPair keyPair) {
super();
super.setKeyPair(keyPair);
this.signer = new RsaSigner((RSAPrivateKey) keyPair.getPrivate());
}
#Override
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String content;
try {
content = this.objectMapper.formatMap(getAccessTokenConverter().convertAccessToken(accessToken, authentication));
} catch (Exception ex) {
throw new IllegalStateException("Cannot convert access token to JSON", ex);
}
Map<String, String> customHeaders = Collections.singletonMap("kid", "my_kid");
String token = JwtHelper.encode(content, this.signer, this.customHeaders)
.getEncoded();
return token;
}
}
Then, of course, create a bean using this converter:
#Bean
public JwtAccessTokenConverter accessTokenConverter(KeyPair keyPair) {
return new JwtCustomHeadersAccessTokenConverter(keyPair);
}
Here I used a KeyPair instance to obtain the signing key and configure the converter (based on the example of the article), but you might adapt that to your configuration.
In the article I also explain the relevant endpoints provided by the Spring Security OAuth Authentication Server.
Also, regarding #Ortomala Lokni's comment, I wouldn't expect Spring Security OAuth to add any new features at this point. As an alternative, you probably can wait to have a look at Spring Security's Authorization Server features, planned to be released in 5.3.0
I managed to solve it by changing the parameter used to identify the URL where the clients will retrieve the pubkey.
On application.properties, instead of:
security.oauth2.resource.jwk.key-set-uri=http://{auth_server}/.well-known/jwks.json
I used:
security.oauth2.resource.jwt.key-uri=http://{auth_server}/oauth/token_key
If I understood correctly, the key-set-uri config points to an endpoint that presents a set of keys and there is the need for a kid. On the other side key-uri config points to an endpoint with a single key.

Spring SAML sending wrong AuthNRequest

stuck with this problem since long, any help is appreciated.
I am implementing Spring SAML SSO authentication for my application.
It's actually a huge security configuration file therefore I will attach only configuration part that I think could be important.
#Bean
public MetadataGenerator metadataGenerator() {
MetadataGenerator metadataGenerator = new MetadataGenerator();
metadataGenerator.setEntityId(env.getProperty("saml.entity.id"));
metadataGenerator.setExtendedMetadata(extendedMetadata());
metadataGenerator.setIncludeDiscoveryExtension(false);
metadataGenerator.setKeyManager(keyManager());
return metadataGenerator;
}
#Bean
#Qualifier("idp-ssocircle")
public ExtendedMetadataDelegate ssoCircleExtendedMetadataProvider() throws MetadataProviderException {
String idpSSOCircleMetadataURL = env.getProperty("saml.provider.url");
HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(this.backgroundTaskTimer, httpClient(),
idpSSOCircleMetadataURL);
httpMetadataProvider.setParserPool(parserPool());
ExtendedMetadataDelegate extendedMetadataDelegate = new ExtendedMetadataDelegate(httpMetadataProvider,
extendedMetadata());
extendedMetadataDelegate.setMetadataTrustCheck(true);
extendedMetadataDelegate.setMetadataRequireSignature(false);
backgroundTaskTimer.purge();
return extendedMetadataDelegate;
}
Values of the property file we have used in this bean is -
saml.entity.id=urn:saml2:test:s
saml.provider.url=https://fedsvc-stage.pwc.com/ofiss/FederationMetadata/2007-06/FederationMetadata.xml
My spring application is hosted on local machine and IDP is publically available. I have added entry in my hosts file so my ip is mapped with mysso.com
Now I am trying to access a url that is behind SAML authentication -
http://mysso.com:8080/sso-self/auth/login
User get's redirected to the IDP where he input credentials and after successfull authentication user get's redirected back to -
http://localhost:8080/sso-self/saml/SSO, with saml response but I get 404 on the browser, and following error on console -
org.opensaml.common.SAMLException: InResponseToField of the Response doesn't correspond to sent message a330ei589j3e99ee10d8a55bghc518i
The problem I can see is message is being stored and retrieved from 2 different session as first request came from domain name mysso.com but response is coming back to localhost
here is my AuthnRequest XML that is being sent to IDP
<?xml version="1.0" encoding="UTF-8"?><saml2p:AuthnRequest xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://localhost:8080/madison-sso-self/saml/SSO" Destination="https://fedsvc-stage.pwc.com/ofiss/" ForceAuthn="false" ID="a345ia5236e6hc2g48ea13fcf4386h7" IsPassive="false" IssueInstant="2018-12-18T07:46:41.812Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0">
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">urn:saml2:test:s</saml2:Issuer>
</saml2p:AuthnRequest>
All I could understand is AssertionConsumerServiceURL value in AuthnRequest is http://localhost:8080/madison-sso-self/saml/SSO and that's why it's coming back to this url.
Now I don't understand why this value is localhost instead of my hostname which is http://mysso.com:8080/madison-sso-self/saml/SSO.
Please reply incase you need some more information to figure it out.
Thanks in advance.
You can use SAMLContextProviderLB to fix this issues. Please replace the values in below configuration with your server URL.
#Bean
public SAMLContextProviderLB contextProvider() {
SAMLContextProviderLB samlContextProviderLB = new SAMLContextProviderLB();
samlContextProviderLB.setScheme("https");
samlContextProviderLB.setServerName("www.myserver.com");
samlContextProviderLB.setServerPort(443);
samlContextProviderLB.setContextPath("/spring-security-saml2-sample");
samlContextProviderLB.setStorageFactory(new EmptyStorageFactory());
return samlContextProviderLB;
}
The above configuration will use https://www.myserver.com/spring-security-saml2-sample/saml/SSO on redirecting from SAML service provider.
samlContextProviderLB.setStorageFactory(new EmptyStorageFactory());
The above line will help to fix the multiple sessions issue.
Please see the following paragraph in "spring-security-saml" documentation:
https://docs.spring.io/autorepo/docs/spring-security-saml/1.0.x-SNAPSHOT/reference/htmlsingle/#chapter-troubleshooting
Error 'InResponseToField doesn't correspond to sent message' during SSO
Make sure that application uses the same HttpSession during sending of the request and reception of the response. Typically, this problem arises when the authentication request is initialized from localhost address or http scheme, while response is received at a public host name or https scheme. E.g., when initializing authentication from URL https://host:port/app/saml/login, the response must be received at https://host:port/app/saml/SSO, not http://host:port/app/saml/SSO or https://localhost:port/app/saml/SSO.
See if you can access the application using same public DNS name instead of localhost

Proxy settings in Spring Boot

My application needs to fetch an XML file from the web, as follows:
#Bean
public HTTPMetadataProvider metadataProvider()
throws MetadataProviderException {
String metadataURL = "http://idp.ssocircle.com/idp-meta.xml";
final Timer backgroundTaskTimer = new Timer(true);
HTTPMetadataProvider provider =
new HTTPMetadataProvider(backgroundTaskTimer, httpClient(), metadataURL);
provider.setParserPool(parserPool());
return provider;
}
I'm working by using a filtered network, thus the app is unable to retrieve that file.
There is a way to setup an HTTP Proxy (e.g. myproxy.eu:8080) in Spring Boot?
Alternatively, I could retrieve the XML file by using the HTTPS protocol, but I should properly setup the metadata provider in order to support an encrypted connection... How?
This is not something you can configure in spring boot, HttpClient is not using java variables.
Therefor you need to set the proxy on the httpClient manually:
HostConfiguration hostConfig = new HostConfiguration();
hostConfig.setProxyHost(new ProxyHost("your.proxy.host", 8080));
httpClient.setHostConfiguration(hostConfig);

Resources