I am using Spring security SAML 1.0.3 Release version. I figured out a problem that if we upload a certificate for the IDP it does not get reflected in the Spring SAML.
The problem seems to be with MetadataCredentialResolver where there is a cache Map
Map<MetadataCacheKey, SoftReference<Collection<Credential>>> cache;
It is picking the certificate from the cache and hence the newer uploaded cert is ignored. Is there a way I can reset the cache?
I think the way to get rid of cache is to override a class and make it set the values to null for all the cache related calls:-
#Override
protected Collection<Credential> retrieveFromCache(MetadataCacheKey cacheKey)
{
//return null and let it fetch from metadata
return null;
}
#Override
protected void cacheCredentials(MetadataCacheKey cacheKey,
Collection<Credential> credentials) {
//do not put anything into cache
}
Related
I tested Spring Security as part of my Spring Boot Setup in version 6.0-M5, 6.0-RC1 and 6.0-RC2. I recognized a behavior change and wanted to ask whether this may be a bug. I return the CSRF token as a serialized JSON, but since RC1 the content of the token in the JSON is garbage.
My working code in Spring Boot 6 Milestone 5 still working as expected.
#RestController
public class CsrfController {
#GetMapping("/rest/user/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}
In my use case I query the controller using a unit test.
#LocalServerPort
int serverPort;
#Autowired
private TestRestTemplate webclient;
#Test
public void getCsrf() {
ResponseEntity<String> entity = webclient.getForEntity("http://localhost:" + serverPort +
"/rest/user/csrf", String.class);
// ... here some code to get the token from the JSON body ...
assertTrue(result.matches("^[a-f0-9\\-]+$"));
This is the first query of the server. A session object between client and server is not established in past queries. This worked in M5 but stopped working in Spring Boot 6 RC1 and RC2
The following controller code made it work again in RC2:
#GetMapping("/rest/user/csrf")
public CsrfToken csrf(HttpServletRequest request, HttpServletResponse response) {
CsrfToken repoToken = tokenRepo.loadToken(request);
if (repoToken != null) {
return repoToken;
}
// required because it is required but ay not be initialized by the tokenRepo
request.getSession();
repoToken = tokenRepo.generateToken(request);
tokenRepo.saveToken(repoToken, request, response);
return repoToken;
}
If I tried the old code in RC2, I received on client side a malformed string. I did not receive a UUID styled token in my JSON serialized response body. I think it is related to the uninitialized session object.
Is this a bug or is an uninitialized session and a resulting not working CrsfToken specified behavior?
I think the issue is in the way I try to get and use the XSFR token.
Because I want to use an Angular frontend, I configured my token repository to provide the tokens via Cookie.
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
This produces cookies the old UUID style. However the authentication expects the new tokens as generated by https://github.com/spring-projects/spring-security/issues/11960 . Probably the cookie mechanism still needs to be migrated until final Spring Boot 3.0.
When integrating spring session with spring security I'm not sure how the SecurityContextImpl#Authentication is supposed to be populated when a session is identified by spring session.
Context:
The spring-boot application does not actually handle login,logout or creating the session itself. The session is created in an external, non-spring microservice and shared via MongoDB. Sharing and mapping the session information works and was utilized without issues before spring security.
What works:
Spring session properly resolves the session-id
Spring session retrieves the session (using the session-id) from the session-repository(mongo) and the attributes are populated
The request has a populated session object including all the attributes
What does not work:
Using http.authorizeRequests().antMatchers("admin/**").authenticated() and then requestion and endpoint (with an session cookie) does by no means populate SecurityContext#Authenticated
Possible options
a) I understand, I could implement a custom CustomUserNameFromSessionFilter to prepopulate the
Authenticated in SecureContext (yet authenticated=false) and place it early in SecurityFilter chain. In addition I implement a custom AuthenticationProvider CustomFromSessionAuthenticationProvider, which then picks up Authenticated and basically sets authenticated=true if the session is valid (which is always true at this point)
b) Use RememberMeAuthenticationFilter but I'm not sure how this documentation suits that purpose
c) Somehow utilize AbstractPreAuthenticatedProcessingFilter but it rather seems to be used for external auth-requests
Every option seems not right, and the need for such an implementation seems too common that there is not an existing/better solution. What is the correct approach?
Code snippets
#Override
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().disable();
// Logout is yet handled by PHP Only, we yet cannot delete/write sessions here
http.logout().disable();
http.formLogin().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
http.authorizeRequests()
.antMatchers("/admin").authenticated();
}
Thank you #Steve Riesenberg for providing just enough of the hint for me to find the right solution!
To understand my solution and understand when one needs to go this extra route, if first explain the default integration:
Classic spring-session integration in spring-security
When you use spring-security including the authentication via your spring-boot app, the integration of spring-session and spring-security will become natural without anything else required.
When you authorize (login) your user initially the first time via an Authentication via spring-security, it will:
Store the Authentication object in the SecurityContext of that request. - Then SecurityContext will then be stored in the HttpSession (if one exists), with spring-session where ever you configured spring-session to (redis/mongo).
The SecurityContext is stored using a session attribute of the key SPRING_SECURITY_CONTEXT right in the common session data (serialized).
When you then take the session-id given to you after this authentication and make an additional request, the following happens
spring session loads the HttpSession from your storage (including the SecurityContext in the session attribute with the key SPRING_SECURITY_CONTEXT
spring security will call HttpSessionSecurityContextRepository very early in the SecurityFilter chain and check HttpSession for the existence of the session attribute SPRING_SECURITY_CONTEXT and if a SecurityContext is found. If yes, it uses this SecurityContext and loads it as the current request SecurityContext. Since this context includes the already authenticated Authentication object from the prior authentication, the AuthenticationManager/Provider will skip authentication since it is all done and your request is treated as authenticated.
This is the vanilla way and it has one requirement - the authentication process (login) needs to write the SecurityContext into the HttpSession object during the login process.
My case - external login process
In my case, an external, a non spring-boot microservice is handling the entire login process. Still, it stores the session in the (external) session storage, which in my case is MongoDB.
Spring-session is properly configured and can read this session using the session cookie/session id and load the session created externally.
The big 'but' is, this external login will not store any SecurityContext in the session data, since it cannot do that.
At this point - if you have an external login service creating the session, and it is a spring-boot service, be sure you write the SecurityContext properly. So all other microservices then can load this session properly (authenticated) just using the session id and the default spring-session/security integration.
Since this is not an option for me, and if it is none for you, the following solution seems to be the 'by design' way in spring-boot/security IMHO:
You implement your own CustomHttpSessionSecurityContextRepository and register that in the security configuration via
public class ApplicationSecurity extends WebSecurityConfigurerAdapter
{
#Override
protected void configure(HttpSecurity http) throws Exception {
http.securityContext()
.securityContextRepository(new FromExternalSessionSecurityContextRepository());
}
}
This ensure we replace the stock HttpSessionSecurityContextRepository with our own implementation.
Now our custom implementation
public class CustomExternalSessionSecurityContextRepository implements SecurityContextRepository
{
#Override
public SecurityContext loadContext(final HttpRequestResponseHolder requestResponseHolder)
{
HttpServletRequest request = requestResponseHolder.getRequest();
HttpSession httpSession = request.getSession(false);
// No session yet, thus we cannot load an authentication-context from the session. Create a new, blanc
// Authentication context and let others AuthenticationProviders deal with it.
if (httpSession == null) {
return generateNewSecurityContext();
}
Optional<Long> userId = Optional.ofNullable(httpSession.getAttribute(Attribute.SUBJECT_ID.attributeName))
SecurityContext sc = generateNewSecurityContext();
if (userId.isEmpty()) {
// Return a emtpy context if the session has neither no subjectId
return sc;
}
// This is an session of an authenticated user. Create the security context with the principal we know from
// the session and mark the user authenticated
// OurAuthentication uses userId.get() as principal and implements Authentication
var authentication = new OurAuthentication(userId.get());
authentication.setAuthenticated(true);
sc.setAuthentication(authentication);
httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY, sc);
return sc;
}
#Override
public void saveContext(
final SecurityContext context, final HttpServletRequest request, final HttpServletResponse response
)
{
// do implement storage back into HttpSession if you want spring-boot to be
// able to write it.
}
#Override
public boolean containsContext(final HttpServletRequest request)
{
HttpSession session = request.getSession(false);
if (session == null) {
return false;
}
return session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null;
}
private SecurityContext generateNewSecurityContext()
{
return SecurityContextHolder.createEmptyContext();
}
}
So now changed the behavior of how spring-security loads a SecurityContext from the session. Instead of expecting SecurityContext to be present already, we check if the session is proper and the create the SecurityContext from the given data and store return it. This will make the entire following chain use and respect this SecurityContext
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);
}
I'm talking to a service that fails in getting the user authentication cookie if there is a JSESSIONID cookie in the request, and I can't modify this service.
It also returns this session cookie on each response, so my first request work (no other cookie than the user's one), but next requests will always fail.
My restTemplate configuration uses a custom request factory that extends Spring's HttpComponentsAsyncClientHttpRequestFactory with an AsyncClient from Apache's HttpAsyncClientBuilder.
Is there a way to configure that to always ignore the session cookie ?
Thanks in advance!
It would have been nice to find a solution impliying only configuration, but I couldn't so I ended up extending the BasicCookieStore:
public class DefaultCookieStore extends BasicCookieStore {
private static String SESSION_COOKIE_NAME = "JSESSIONID";
#Override
public void addCookie(Cookie cookie) {
if (!SESSION_COOKIE_NAME.equals(cookie.getName())) {
super.addCookie(cookie);
}
}
}
And adding it to my HttpAsyncClientBuilder and HttpClientBuilder with the method setDefaultCookieStore.
Probably not the best thing, but it works well.
I have an application that connects to a CAS server, allows the user to do the CAS/SSO login, and then redirects them to the application's home page. Simple enough.
The problem is that CAS is relatively new to my organization and as of today, we only have it set up on one server. So, if the server becomes unreachable for any reason, the application can no longer be accessed as the CAS login page will not load.
What I would like is for my application to be able to detect
a) the CAS server is unreachable after a reasonable number of retries
b) subsequently redirect the user to the non-CAS/SSO login page and use the basic Spring Security config that is in place using LDAP
I don't see anything in the Spring documentation for this particular scenario. Suggestions?
You could try extending CasAuthenticationEntryPoint. Entry points in Spring Security are the things that determine where to go when authentication is necessary.
public LocalLoginFallbackCasAuthenticationEntryPoint
extends CasAuthenticationEntryPoint {
#Override
protected String createRedirectUrl(final String serviceUrl) {
if ( prevailingCircumstancesMandate() ) {
return "/our/legacy/login/page";
} else {
// go to CAS, like normal
return super.createRedirectUrl(serviceUrl);
}
}
}
And then configure in your HttpSecurity object:
#Override
protected void configure(HttpSecurity http) {
http.exceptionHandling()
.authenticationEntryPoint(myCustomEntryPoint());
}