Spring SAML sending wrong AuthNRequest - spring

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

Related

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 Security + Auth LDAP : BindRequest and UnbindRequest?

After days of Google researching, Reading The F* Spring Security Manual and testing, I'm becoming desperate ...
The context : I'm implementing a Micro-Services architecture with Eureka etc...
I implemented an Auth Service which works very well with a MySQL authentication database. But now, I want to join my company LDAP through an OpenLDAP who works adequately.
So, I'm trying to join the LDAP with my spring security authentication.
The code of my configure() method (I replaced my company and domain name, the account {0} is "test"):
auth.ldapAuthentication()
.contextSource()
.url("ldap://myldap/ou=users,dc=mydomain,dc=mycompany")
.and()
.userDnPatterns("cn={0}");
I also tried different ways to write this and all the time, I get Bad Credentials or LDAP 32 error. With userDnPattern, usersearchbase method, passwordcompare, passwordencoder and others. I also tried to put DC in the root() method and the OU in the group...() method, no change (I think in fact that Spring Security sort these parameters smartly before sending LDAP Requests). To be honest, I tried 357654 differents ways to write the configure() method ...
The problem is that : When I put the same config, credentials, domains ... in an LDAP explorer software, it works correctly.
So I monitored LDAP networks exchanges with Wireshark and I saw this :
Wireshark screen
As we can see, there's 8 requests exchanged. The first 5 are OK. It find my account "test" correctly. But there's three over requests (with the unbindRequest which going back).
The problem is that Spring give me the result of the last request and say me the account doesn't exist or the credentials don't work, etc...
Have you got a clue for this ? Do you know how Spring Security works to question LDAP ? How can I do to contact my LDAP adequately with the framework ?
Thank you for reading.
Help me Stack Overflow, you're my only hope ...
I finally found the problem and got a solution.
My enterprise LDAP is an LDAP above an AD.
And this LDAP+AD needs Bind Authentication and doesn't authorize anonymous bind then authentication.
In Spring Security, there's an object which can do this : BindAuthenticator
This is how I try to use it (and it works).
#Override
protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {
authManagerBuilder.authenticationProvider(ldapAuthenticationProvider()).userDetailsService(userDetailsService());
}
#Bean
public LdapAuthenticationProvider ldapAuthenticationProvider() throws Exception {
LdapAuthenticationProvider lAP = new LdapAuthenticationProvider(ldapAuthenticator(), ldapAuthoritiesPopulator());
return lAP;
}
#Bean
public LdapContextSource ldapContextSource() throws Exception {
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource([URL of the LDAP]);
return contextSource;
}
#Bean
public LdapAuthenticator ldapAuthenticator() throws Exception {
BindAuthenticator authenticator = new BindAuthenticator(ldapContextSource());
authenticator.setUserDnPatterns(new String[] {"CN={0},"+[MY ENTERPRISE LDAP FILTER]});
return authenticator;
}
Hope this sample code will help some people ...
Thank you !

SAML endpoints don't match by using the roor dir

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.

POST request to Spring REST web service fails with HTTP status 415

I have set up a spring RESTful web service for taking in the username and password from a user. Been following the tutorial on Spring IO
My service is set up to accept user name and password as shown below:
#Controller
#RequestMapping("/users")
public class UserCommandController {
private static Logger log = LoggerFactory.getLogger(UserCommandController.class);
#RequestMapping(method = RequestMethod.POST)
public ResponseEntity createUser(#RequestBody UserDetail userDetail, UriComponentsBuilder builder) {
User newUser = new User();
newUser.setEmail(userDetail.getEmail());
newUser.setPassword(userDetail.getPassword());
newUser.setUserName(userDetail.getUsername());
try {
UserFactory.getInstance().saveNewUser(newUser);
} catch(UserException ue) {
log.error("Saving user failed. Exception: "+ue.getMessage());
}
return new ResponseEntity(HttpStatus.OK);
}
}
I am sending POST parameters to the service as a test through Google chrome plugin POSTMAN but I get "HTTP: 415..The server refused this request because the request entity is in a format not supported by the requested resource for the requested method."
Does anyone have an idea what I am doing wrong ?
Set the header:
Content-Type=application/json
This solved my problem!
The HTTP 415 response code means that the server expected data posted with a different content type. It appears you simply posted a form with username, password and email as parameters. That would result in a content-type of application/x-www-form-urlencoded.
Try posting with a content-type of application/xml or application/json. In your post body, you will need to put your data in the corresponding format. For example, if you use application.xml, the XML body should look something like:
<userDetail>
<userName>xxx</userName>
<password>xxx</password>
<email>xxx</email>
</userDatail>
Of course the exact format (i.e. element names) depends on the XML bindings. In fact, whether or not the expected format is XML or JSON or something else is also likely a server configuration.
Posting a request of this type cannot easily be done with a browser. You will need some other HTTP client. A tool like SOAP-UI might be a good bet.

Resources