Spring Websocket url hits RequestMapping but not MessageMapping - stomp

I'm having trouble setting up a websocket configuration in an existing web application.
#Configuration
#EnableWebSocketMessageBroker
public class WebsocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/mobile");
config.setApplicationDestinationPrefixes("/mobile-server");
config.setUserDestinationPrefix("/mobile-user");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/mobile-socket")
.withSockJS()
.setInterceptors(new HttpSessionHandshakeInterceptor());
}
}
Controller
#Controller
public class WebSocketInboxController{
#MessageMapping("/inbox")
#SendToUser("/mobile")
public Map<String,Object> inbox(
){
Map<String,Object> res = new HashMap<>();
res.put("hello", "hello");
return res;
}
client
const webstomp = require('webstomp-client');
const socket = webstomp.client('ws://www.dev.server.com/mobile-socket',{
debug:true
});
socket.connect('marc#gmail.com', '123456', (client) => {
console.log('connected');
socket.send("/mobile-server/inbox",)
socket.subscribe("/mobile/inbox");
}, (client, err) => {
console.log(err);
});
What I see when the client tries to connect is spring trying to match the /mobile-socket against the RequestMappings of the existing web application, finally hitting one that matches it by way of a #RequestMapping("/{somevar}").
I'm new to WebSockets, but I would expect the endpoint registration be a catchall for these kinds of connects?
Even after removing the erronous RequestMapping being hit, I can't seem to get the MessageMapping to be hit. I see this in my log
AntPathRequestMatcher.matches(150) | Request '/mobile-socket' matched by universal pattern '/**'
[MSA] DEBUG [2016-06-03T11:16:21,025] FilterSecurityInterceptor.beforeInvocation(219) | Secure object: FilterInvocation: URL: /mobile-socket; Attributes: [permitAll]
[MSA] DEBUG [2016-06-03T11:16:21,025] FilterSecurityInterceptor.authenticateIfRequired(348) | Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
[MSA] DEBUG [2016-06-03T11:16:21,025] AffirmativeBased.decide(66) | Voter: org.springframework.security.web.access.expression.WebExpressionVoter#444af45, returned: 1
[MSA] DEBUG [2016-06-03T11:16:21,025] FilterSecurityInterceptor.beforeInvocation(243) | Authorization successful
[MSA] DEBUG [2016-06-03T11:16:21,026] FilterSecurityInterceptor.beforeInvocation(256) | RunAsManager did not change Authentication object
[MSA] DEBUG [2016-06-03T11:16:21,026] FilterChainProxy.doFilter(325) | /mobile-socket at position 16 of 16 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
[MSA] DEBUG [2016-06-03T11:16:21,026] FilterChainProxy.doFilter(310) | /mobile-socket reached end of additional filter chain; proceeding with original chain
[MSA] DEBUG [2016-06-03T11:16:21,027] ExceptionTranslationFilter.doFilter(117) | Chain processed normally
[MSA] DEBUG [2016-06-03T11:16:21,027] HstsHeaderWriter.writeHeaders(130) | Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher#53cc2afb
[MSA] DEBUG [2016-06-03T11:16:21,027] HttpSessionSecurityContextRepository.saveContext(352) | SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
[MSA] DEBUG [2016-06-03T11:16:21,028] SecurityContextPersistenceFilter.doFilter(120) | SecurityContextHolder now cleared, as request processing completed

The Spring tries to match the "/mobile-socket" against the RequestMappings because all the requests go to HandlerMapping beans in the web application context to map incoming web requests to appropriate handlers. With the introduction of annotated controllers, RequestMappingHandlerMapping automatically looks for #RequestMapping annotations on all #Controller beans including the controller which has #MessageMapping.
Since the #MessageMapping can only be defined under #Controller annotation, Spring would try to match other RequestMappings as well.
One possible solution could be to introduce interceptor to handle the websocket request url to specifically map to a particular controller. You can give it a try!

Related

Mapping OAuth2 / OpenID Access / User Tokens to Sessions in SpringBoot Apps

I am trying to learn techniques for implementing OAuth2 / OpenID Connect with servlet apps and react apps. I have Authorization Server functions working correctly in Keycloak and have commandline test authorization_code, token and refresh flows so that plumbing works. When building a MVC servlet, code to enforce required authorizations works, redirects a user's browser to Keycloak for authentication and code generation, the code is returned to my servlet which properly obtains an access token for the code. However, while redirecting the user to a "main page" in the authenticated realm, I am not correct mapping the OAuth2 layer tokens to session and SecurityContext objects used in Spring Security so the subsequent page request is treated as unauthenticated.
Here is a top level summary of the components being used:
SpringBoot 2.6.7 (latest as of 5/15/2022)
SpringBoot Thymeleaf Start
Keycloak 18.0.0 (latest as of 5/15/2022)
Java JDK 18.0.1
Key dependencies from the pom.xml (just the artifactId for brevity):
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<artifactId>spring-boot-starter-security</artifactId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<artifactId>spring-boot-starter-web</artifactId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<artifactId>spring-boot-starter-webflux</artifactId>
<artifactId>spring-integration-http</artifactId>
Here are the implementation components that are working:
Keycloak is installed, running on 192.168.99.10:8011 with a client configured for use by the app
the Keycloak client is configured for OpenID Connect protocol
curl tests of authorization_code, token and refresh queries to Keycloak all function
a servlet app has been created with four key page areas:
/myapp/gui/public --- public pages, no authentication required
/myapp/gui/access -- used for functions to handle access login and logout and Oauth callbacks
/myapp/gui/clients --- pages for authenticated users with myfirmuser permissions
/myapp/gui/admin --- pages for authenticated users with myfirmadmin permissions
redirection of unauthenticated users to /myapp/gui/access/oauthproviders page
rendering of links on that oauthproviders page to /auth endpoint of defined Authorization Servers
clicking on Keycloak displays its authentication page and sends back authorization code
the /myapp/gui/access/oath2/callback/keycloak page is handled and calls the /token endpoint on Keycloak
a token is returned and the callback page creates a JSESSION, addes the access_token and
refresh_token in the HttpServletRequest then redirects to /myapp/gui/clients/mainpage
The actual (undesired) behavior is that after receiving the new access_token from the Authorization Server, the redirect sent to the browser forwarding the human user to /myapp/gui/clients/mainpage (the logged in "home page") is then processed by the security filters and no token is found so the user is redirected back to /myapp/gui/oauthproviders to start the login process again.
Clearly, I am not correctly populating the access token or JWT session token in the SecurityContext or HttpRequest or HttpResponse object for it to go out to the browser and come back. That logic is currently implemented in my AccessController class that handles the integration to the remote AuthorizationServer (Keycloak). I've tried creating classes to invoked for AuthenticationSuccessHandler and AuthenticationFailureHandler. Here are the key classes in the build.
src/main/java/com/myfirm/dependsgui/AccessController.java
src/main/java/com/myfirm/dependsgui/DependsAuthenticationFailureHandler.java
src/main/java/com/myfirm/dependsgui/DependsAuthenticationSuccessHandler.java
src/main/java/com/myfirm/dependsgui/DependsController.java
src/main/java/com/myfirm/dependsgui/DependsguiApplication.java
src/main/java/com/myfirm/dependsgui/KeycloakAuthoritiesExtractor.java
src/main/java/com/myfirm/dependsgui/KeycloakPrincipalExtractor.java
src/main/java/com/myfirm/dependsgui/SecurityConfiguration.java
The classes aimed at transforming OAuth2 layer user / authorization information are referenced in the configure() class and logs at startup DO show them firing to point to my custom classes. However, something between OAuth and Spring Security doesn't seem to be linked correct to fire those classes after successful authentication.
Key questions:
in configure(), should oauth2ResourceServer() only be used for web service builds (not MVC apps)?
in configure(), are formLogin() and oauth2Login() mutually exclusive and not to be used together?
should the mapping of userdetails from the access token into the SecurityContext Authentication object be implemented in
a) filter layer classes?
b) an AuthenticationSuccessHandler derived class?
c) in PrincipalExtractor and AuthoritiesExtractor derived classes?
c) my servlet controller class handling login / logout actions?
d) somewhere else?
I think it has to be performed in a filter layer or AuthenticationSuccessHandler. However, the run-time flow doesn't appear to be invoking my custom classes to give me a place to trace backward to the point where I'm inevitably not doing something correctly.
Code fragments are excerpted below. Any help would be greatly appreciated.
===========================
Here is the KeycloakPrincipalExtractor class:
package com.myfirm.dependsgui;
//imports omitted for brevity
public class KeycloakPrincipalExtractor implements PrincipalExtractor {
private final Logger thisLog = LoggerFactory.getLogger(this.getClass().getCanonicalName());
#Override
public Object extractPrincipal(Map<String, Object> map) {
thisLog.info("extractPrincipal() -- extracting preferred_username from Oauth token - value=" +
map.get("preferred_username").toString());
return map.get("preferred_username");
}
}
Here is the KeycloakAuthoritiesExtractor class:
package com.myfirm.dependsgui;
//imports omitted for brevity
public class KeycloakAuthoritiesExtractor implements AuthoritiesExtractor {
private final Logger thisLog = LoggerFactory.getLogger(this.getClass().getCanonicalName());
// for now, just mockup three capabilities and two sets of authorities
// * everyone has MYFIRM_USER
// * full will have MYFIRM_FULL
// * get will have MYFIRM_GET
List<GrantedAuthority> MYFIRM_USER = AuthorityUtils.commaSeparatedStringToAuthorityList(
"SCOPE_myfirmuser");
List<GrantedAuthority> MYFIRM_ADMIN = AuthorityUtils.commaSeparatedStringToAuthorityList(
"SCOPE_myfirmadmin");
List<GrantedAuthority> MYFIRM_ANONYMOUS = AuthorityUtils.commaSeparatedStringToAuthorityList(
"SCOPE_myfirmanonymous");
#Override
public List<GrantedAuthority> extractAuthorities (Map<String, Object> map) {
thisLog.info("DEBUG -- extractAuthorities() - map --> " + map.toString());
if (Objects.nonNull(map.get("realm-access"))) {
if (!((LinkedHashMap) map.get("realm-access")).get("roles").equals("myfirmuser")) {
return MYFIRM_USER;
}
if (!((LinkedHashMap) map.get("realm-access")).get("roles").equals("myfirmuser")) {
return MYFIRM_ADMIN;
}
}
return MYFIRM_ANONYMOUS;
}
}
Here is the method in my AccessController.java handling the authorization response from the Authorization Server and calling the remote /token endpoint to get an access_token.
#GetMapping("/access/oauth2/callback/{oauthprovidername}")
public String oauthCallback(
#PathVariable("oauthprovidername") String oauthprovidername,
#RequestParam("session_state") String sessionstate,
#RequestParam("code") String code,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse,
Model model) {
thisLog.info("oauthCallback() - oauthprovidername=" + oauthprovidername +
" code=" + code + " session_state=" + sessionstate);
ClientRegistration providerRegistration = clientRegistrationRepository.findByRegistrationId(oauthprovidername);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
LinkedMultiValueMap<String,String> params = new LinkedMultiValueMap<>();
params.add("grant_type","authorization_code");
params.add("scope","openid");
params.add("client_id",providerRegistration.getClientId());
params.add("client_secret",providerRegistration.getClientSecret());
params.add("code",code);
params.add("redirect_uri",providerRegistration.getRedirectUri());
RestTemplate restTemplate = new RestTemplate();
HttpEntity< LinkedMultiValueMap<String,String> > request = new HttpEntity<>(params,headers);
ResponseEntity<String> response = restTemplate.postForEntity(
providerRegistration.getProviderDetails().getTokenUri(),
request,
String.class);
// the response has this structure:
// {"access_token":"xxx","expires_in":300,"refresh_expires_in":1800,"refresh_token":"yyy", \\ others }
//
thisLog.info("oauthCallback() - completed restTemplate.postForEntity() -- response = " + response);
ObjectMapper mapper = new ObjectMapper();
String access_token = "";
String refresh_token= "";
try {
JsonNode node = mapper.readTree(response.getBody());
access_token = node.path("access_token").asText();
refresh_token = node.path("refresh_token").asText();
}
catch (Exception theE) {
thisLog.error("oauthCallback() -- Exception=" +theE);
}
// at this point, access_token can be used in any other web service call by adding
// it as a header "Authorization: Bearer $access_token"
// we need to send it back to the client so the client can re-submit it on subsequent
// requests to maintain session state at the client rather than in a cache in this servlet
thisLog.info("oauthCallback() - completed restTemplate.postForEntity() " );
Cookie accessJwtCookie = new Cookie("access_token",access_token);
Cookie refreshJwtCookie = new Cookie("refresh_token",refresh_token);
// in real implementations, these calls should be made to ensure communications is limited to HTTPS
// accessJwtCookie.setSecure(true);
// refreshJwtCookie.setSecure(true);
// these restrict the browser's ability to access the cookies to sending HTTP out, blocking script access
accessJwtCookie.setHttpOnly(true);
refreshJwtCookie.setHttpOnly(true);
// these allow browser to send back the cookie for any subsequent URLs on the site
accessJwtCookie.setPath("/");
refreshJwtCookie.setPath("/");
// these limit the retention of the cookie -- access are only good for 300 seconds, refresh for 1800
// so no point in the browser keeping them longer than that
accessJwtCookie.setMaxAge(300);
refreshJwtCookie.setMaxAge(1800);
servletResponse.addCookie(accessJwtCookie);
servletResponse.addCookie(refreshJwtCookie);
thisLog.info("oauthCallback() - attempting redirect to authenticated mainpage - servletResonse=" + servletResponse.toString());
// create a session and use that to create a JSESSION cookie in the response
HttpSession session = servletRequest.getSession(true);
session.setMaxInactiveInterval(5*60); // set to 5 minute idle timeout
// for debuggging, use the refresh_token to test our refresh2Provider() logic
//ClientRegistration testClient = refresh2Provider(refresh_token);
model.addAttribute("diagnostics", response);
model.addAttribute("exception", "(none)");
model.addAttribute("stacktrace","(none)");
// NOTE! -- this redirect is "absolute relative" to the servlet context of /depends/gui
// "redirect:clients/mainpage.html" ---> /depends/gui/access/oauth2/callback/clients/mainpage (WRONG)
// "redirect:/clients/mainpage.html" --> /depends/gui/clients/mainpage
return "redirect:/clients/mainpage";
}
Here is the entire SecurityConfiguration class.
package com.myfirm.dependsgui;
//imports omitted for brevity
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final Logger thisLog = LoggerFactory.getLogger(this.getClass().getCanonicalName());
String JWKSETURI = "http://localhost:8011/realms/myfirm/protocol/openid-connect/certs";
//------------------------------------------------------------------------------------
// webClient() - defines a bean that will map configured Authorizaton Server parameters
// from application.properties to a Http client that can call those endpoints to
// verify tokens, etc
//------------------------------------------------------------------------------------
#Bean
WebClient webClient(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
thisLog.debug("webClient() - instantiating new WebClient for this app to interact with each defined Authorization Server");
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository,
authorizedClientRepository);
oauth2.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder().apply(oauth2.oauth2Configuration()).build();
}
//----------------------------------------------------------------------------------
// authorizationRequestRepository() - defines bean used by the auto-generated
// login handling to bounce an authorization request over to the
// Authorization Server selected by the interactive user
//----------------------------------------------------------------------------------
#Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest>
authorizationRequestRepository() {
thisLog.debug("authorizedRequestRepository() - instantiating new HttpSessionOAuth2AuthorizationRequestRepository()");
return new HttpSessionOAuth2AuthorizationRequestRepository();
}
//----------------------------------------------------------------------------------
// accessTokenResponseClient() - this mirrors the default function created by the
// OAuth2 libraries for accepting access tokens sent back from an Authorization
// Server. This could be overriden / enhanced if additional info needs to be
// extracted from somewhere after successful authentication to stuff into the JWT
//---------------------------------------------------------------------------------
#Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>
accessTokenResponseClient() {
thisLog.debug("accessTokenResponseClient() - instantiating default NimbusAuthorizationCodeTokenResponseClient() for post-processing of new access tokens");
return new NimbusAuthorizationCodeTokenResponseClient();
}
// -------------------------------------------------------------------------------
// Declare bean oauthPrincipalExtractor() that returns an instance of our
// customized OauthPrincipalExtractor class to extract the desired value of an
// Oauth reply from Keycloak we want used as principal in a Spring Authorization
// -------------------------------------------------------------------------------
#Bean
public PrincipalExtractor keycloakPrincipalExtractor() {
thisLog.debug("keycloakPrincipalExtractor() - instantiating bean of custom KeycloakPrincipalExtractor");
return new KeycloakPrincipalExtractor();
}
// -------------------------------------------------------------------------------
// Declare bean oauthAuthoritiesExtractor() that returns an instance of our
// customized KeycloakAuthoritiesExtractor() class to extract grants from Oauth
// tokens into Spring Security Authentication objects
// -------------------------------------------------------------------------------
#Bean
public AuthoritiesExtractor keycloakAuthoritiesExtractor() {
thisLog.debug("keycloakAuthoritiesExtractor() -- instantiating bean of custom KeycloakAuthoritiesExtractor");
return new KeycloakAuthoritiesExtractor();
}
// -------------------------------------------------------------------------------
// Declare bean dependsAuthenticationSuccessHandler() that returns an instance of our
// customized DependsAuthenticationSuccessHandler() class to perform post-processing
// after successful authentication
// -------------------------------------------------------------------------------
public AuthenticationSuccessHandler dependsAuthenticationSuccessHandler() {
thisLog.debug("dependsAuthenticationSuccessHandler() -- instantiating bean of custom DependsAuthenticationSuccessHandler");
return new DependsAuthenticationSuccessHandler();
}
// -------------------------------------------------------------------------------
// Declare bean dependsAuthenticationFailurHandler() that returns an instance of our
// customized DependsAuthenticationFailureHandler() class to perform post-processing
// after successful authentication
// -------------------------------------------------------------------------------
public AuthenticationFailureHandler dependsAuthenticationFailureHandler() {
thisLog.debug("dependsAuthenticationFailureHandler() -- instantiating bean of custom DependsAuthenticationFailureHandler");
return new DependsAuthenticationFailureHandler();
}
// ----------------------------------------------------------------------------
// keycloakJwtAuthenticationConverter() - defines a mapping that will be used
// by token processing to map claims at the token level to authorities in the
// Spring Security layer for the app
//-----------------------------------------------------------------------------
private JwtAuthenticationConverter keycloakJwtAuthenticationConverter() {
thisLog.debug("keycloakJwtAuthenticationConverter() -- instantiating critiera for grant converter within JwtAuthenticationConverter()");
JwtGrantedAuthoritiesConverter thisgrantauthconverter = new JwtGrantedAuthoritiesConverter();
// the roles we want to extract are under "realm-access": { roles": [ xx,yy,zz] }
thisgrantauthconverter.setAuthoritiesClaimName("realm-access");
thisgrantauthconverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter thisauthconverter = new JwtAuthenticationConverter();
thisauthconverter.setJwtGrantedAuthoritiesConverter(thisgrantauthconverter);
return thisauthconverter;
}
//-----------------------------------------------------------------------------------
// configure(HttpSecurity) - key method for setting filters and OAuth2 parameters
//-----------------------------------------------------------------------------------
#Override
protected void configure(HttpSecurity http) throws Exception {
//------------------------------------------------------------------------------------------------
// NOTE: These patterns are APPENDED to the servlet context /depends/gui in application.properties
//------------------------------------------------------------------------------------------------
thisLog.info("configure(HttpSecurity) - defining access filters for application URI patterns");
// NOTE: using authorizeHttpRequests() instead of older authorizeRequests() -- many online examples
// have not reflected this new directional implmentation - older call is being deprecated
http.authorizeHttpRequests()
.antMatchers(HttpMethod.GET, "/public/**").permitAll()
.antMatchers(HttpMethod.GET, "/css/**").permitAll()
.antMatchers(HttpMethod.GET, "/js/**").permitAll()
.antMatchers(HttpMethod.GET, "/access/**").permitAll()
.antMatchers(HttpMethod.GET, "/clients/**").hasAnyAuthority("SCOPE_myfirmuser","SCOPE_myfirmadmin")
.antMatchers(HttpMethod.GET, "/admin/**").hasAuthority("SCOPE_myfirmadmin")
.anyRequest().authenticated()
.and() // return to the parent http object
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(keycloakJwtAuthenticationConverter())
;
http.formLogin()
.loginPage("/access/oauthproviders")
.successHandler(dependsAuthenticationSuccessHandler())
.failureHandler(dependsAuthenticationFailureHandler())
.and() // return to the parent http object
.oauth2Login()
.loginPage("/access/oauthproviders")
.authorizationEndpoint()
.authorizationRequestRepository(authorizationRequestRepository())
;
}
//-----------------------------------------------------------------------------
// jwtDecoder() - instantiates a JWT decoder using the JWKsetUri of an OAuth
// provider to fetch strings to decode / unencrypt a token
// NOTE -- not clear how this approach works when a single app can use
// multiple OAuth providers for google, facebook, github, keycloak, etc
// For now, this is hardcoding the JwkSetUri for a local keycloak instance.
//-----------------------------------------------------------------------------
#Bean
public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) {
thisLog.info("jwtDecoder() - returning link to method for decoding / validating JWT via Nimbus library");
thisLog.info("jwtDecoder() - incoming properties = " + properties.getJwt().getJwkSetUri());
NimbusJwtDecoder thisDecoder = NimbusJwtDecoder.withJwkSetUri(JWKSETURI).build();
return thisDecoder;
}
} // end of entire class
Here are logs at startup showing the classes referenced in the configure() method ARE getting loaded:
2022-05-12 23:42:45,163 INFO com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - configure(HttpSecurity) - defining access filters for application URI patterns
2022-05-12 23:42:45,171 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - keycloakJwtAuthenticationConverter() -- instantiating critiera for grant converter within JwtAuthenticationConverter()
2022-05-12 23:42:45,178 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - dependsAuthenticationSuccessHandler() -- instantiating bean of custom DependsAuthenticationSuccessHandler
2022-05-12 23:42:45,179 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - dependsAuthenticationFailureHandler() -- instantiating bean of custom DependsAuthenticationFailureHandler
2022-05-12 23:42:45,209 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - authorizedRequestRepository() - instantiating new HttpSessionOAuth2AuthorizationRequestRepository()
2022-05-12 23:42:45,225 INFO com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - jwtDecoder() - returning link to method for decoding / validating JWT via Nimbus library
2022-05-12 23:42:45,225 INFO com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - jwtDecoder() - incoming properties = null
2022-05-12 23:42:45,381 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - webClient() - instantiating new WebClient for this app to interact with each defined Authorization Server
2022-05-12 23:42:45,586 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - accessTokenResponseClient() - instantiating default NimbusAuthorizationCodeTokenResponseClient() for post-processing of new access tokens
2022-05-12 23:42:45,591 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - keycloakPrincipalExtractor() - instantiating bean of custom KeycloakPrincipalExtractor
2022-05-12 23:42:45,591 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - keycloakAuthoritiesExtractor() -- instantiating bean of custom KeycloakAuthoritiesExtractor

Spring Security 5 with multiple searchbase groups fails while singel group base with same credentials does not. What do i miss here?

This question is about how to use multiple group search bases instead of the one.
I used an example provided by samaddico (simple spring security + LDAP example), modified it for single group search base with provided server / user / LDAP configuration text. It uses a service account to connect to ldap and a user which then tries to authenticate for certain simple web pages.
This approach works but lacks ability to collect membership / roles from different groups in the search tree.
Spring Security provides classes LdapContextSource and MultipleLdapAuthoritiesPopulator to allow for searching for roles in different locations.
Now here is the code which will result in the error shown below:
LDAP Configuration:
* Create an implementation of org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator which can call
multiple instances of LdapAuthoritiesPopulator.
* Then create one LdapAuthoritiesPopulatorfor each 'groupSearchBase' that I wanted to query.
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
LdapContextSource contextSource = contextSource();
MultipleLdapAuthoritiesPopulator multipleLdapAuthoritiesPopulator = new MultipleLdapAuthoritiesPopulator(
new DefaultLdapAuthoritiesPopulator(contextSource, ldapGroupSearchBaseA),
new DefaultLdapAuthoritiesPopulator(contextSource, ldapGroupSearchBaseB),
new DefaultLdapAuthoritiesPopulator(contextSource, ldapGroupSearchBaseC));
auth
.ldapAuthentication()
.contextSource(contextSource)
.ldapAuthoritiesPopulator(multipleLdapAuthoritiesPopulator)
.userSearchFilter(ldapUserSearchFilter)
.userSearchBase(ldapUserSearchBase);
}
class MultipleLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator {
private List<LdapAuthoritiesPopulator> authoritiesPopulators;
public MultipleLdapAuthoritiesPopulator(LdapAuthoritiesPopulator...authoritiesPopulators) {
this.authoritiesPopulators = Arrays.asList(authoritiesPopulators);
}
#Override
public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) {
List<GrantedAuthority> grantedAuthorities = authoritiesPopulators.stream()
.map(authPopulator -> authPopulator.getGrantedAuthorities(userData, username))
.flatMap(Collection::stream)
.collect(Collectors.toList());
return grantedAuthorities;
}
}
/**
* Creates context source object instead of configuring it with AuthenticationBuilder
* #return Context source object used for accessing ldap server
*/
#Bean
public LdapContextSource contextSource() {
LdapContextSource contextSource= new LdapContextSource();
contextSource.setUrl(ldapUrl);
contextSource.setUserDn(ldapManagerDn);
contextSource.setPassword(ldapManagerPassword);
contextSource.afterPropertiesSet();
return contextSource;
}
Session Configuration:
/**
* This is essential to make sure that the Spring Security session registry is notified when the session is destroyed.
* #return
*/
#Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
The Spring Application tells me that my service account got successful connected to LDAP server.
DEBUG 17220 --- [nio-8080-exec-5] o.s.s.w.a.i.FilterSecurityInterceptor : Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#c1e15be1: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#380f4: RemoteIpAddress: 127.0.0.1; SessionId: 0A82CE8FA4FB9EB248D756EEE8134CAE; Granted Authorities: ROLE_ANONYMOUS
The error then is thrown when the found user is beeing tried to bind:
DEBUG 17220 --- [nio-8080-exec-8] o.s.s.l.a.BindAuthenticator : Failed to bind as CN=familyName\, name,OU=Group: org.springframework.ldap.AuthenticationException: [LDAP: error code 49 - 80090308: LdapErr: DSID-0C090453, comment: AcceptSecurityContext error, data 52e, v3839 ]; nested exception is javax.naming.AuthenticationException: [LDAP: error code 49 - 80090308: LdapErr: DSID-0C090453, comment: AcceptSecurityContext error, data 52e, v3839 ]
DEBUG 17220 --- [nio-8080-exec-8] w.a.UsernamePasswordAuthenticationFilter : Authentication request failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
To sum this up: my credentials are correct for a single group base without using LdapContextSource and MultipleLdapAuthoritiesPopulator. But the authentication process seems not to provide the enteret password for my user with multiple group bases.
After spending some time in figuring out a solution i had to admit that there was no efficient way to create a solution i.e. with overwriting methods or classes.
But i stumbled on a change request for spring security, precicely for this use case when multiple group search bases need to be checked.
It is implemented since Spring Security version 5.4.1 (i believe) or included when using Spring Starter parent version 2.4.2.
Simply add the option to your authentification method:
.groupSearchSubtree(true)
The complete updated method example for authentication looks like this then:
#Override
#Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth
.ldapAuthentication()
.contextSource()
.url(ldapUrl)
.managerDn(ldapManagerDn)
.managerPassword(ldapManagerPassword)
.and()
.userSearchFilter(ldapUserSearchFilter)
.userSearchBase(ldapUserSearchBase)
.groupSearchFilter(ldapGroupSearchFilter)
.groupSearchBase(ldapGroupSearchBase)
.groupSearchSubtree(true)
;
You see that there is no need for three different nodes and no more custom context object to be forwarded any more, simply add the parent node for the group search base and let the subtree search do the rest.
It might have been nice to figure out a way by myself, but using an incorporated solution of the framework is surely the better way to go.

HttpSession null after replacing AuthorizationRequest

Complete code and instructions to quickly reproduce the problem are given below.
THE PROBLEM:
The HttpSession becomes null after a custom implementation of DefaultOAuth2RequestFactory replaces the current AuthorizationRequest with a saved AuthorizationRequest. This causes failure of the subsequent request to /oauth/token because the CsrfFilter in the Spring Security filter chain preceding the /oauth/token endpoint is not able to find a session Csrf token in the null session to compare with the request's Csrf token.
CONTROL FLOW DURING THE ERROR:
The following flowchart illustrates where Step 14 and Step 15 somehow null-ify the HttpSession. (Or possibly mismatch a JSESSIONID.) A SYSO at the start of CustomOAuth2RequestFactory.java in Step 14 shows that there is indeed an HttpSession that does in fact contain the correct CsrfToken. Yet, somehow, the HttpSession has become null by the time Step 15 triggers a call from the client at the localhost:8080/login url back to the localhost:9999/oauth/token endpoint.
Breakpoints were added to every line of the HttpSessionSecurityContextRepository mentioned in the debug logs below. (It is located in the Maven Dependencies folder of the authserver eclipse project.) These breakpoints confirmed that the HttpSession is null when the final request to /oauth/token is made in the flowchart below. (Bottom-left of flowchart.) The null HttpSession might be due to the JSESSIONID that remains in the browser becoming out of date after the custom DefaultOAuth2RequestFactory code runs.
How can this problem be fixed, so that the same HttpSession remains during the final call to the /oauth/token endpoint, after the end of Step 15 in the flowchart?
RELEVANT CODE AND LOGS:
The complete code of CustomOAuth2RequestFactory.java can be viewed at a file sharing site by clicking on this link. We can guess that the null session is due to either 1.) the JSESSIONID not being updated in the browser by the code in the CustomOAuth2RequestFactory, or 2.) the HttpSession actually being null-ified.
The Spring Boot debug logs for the call to /oauth/token after Step 15 clearly state that there is no HttpSession by that point, and can be read as follows:
2016-05-30 15:33:42.630 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy : /oauth/token at position 1 of 12 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy : /oauth/token at position 2 of 12 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] w.c.HttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: null. A new one will be created.
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy : /oauth/token at position 3 of 12 in additional filter chain; firing Filter: 'HeaderWriterFilter'
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher#2fe29f4b
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy : /oauth/token at position 4 of 12 in additional filter chain; firing Filter: 'CsrfFilter'
2016-05-30 15:33:42.644 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:9999/uaa/oauth/token
2016-05-30 15:33:42.644 DEBUG 13897 --- [io-9999-exec-10] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2016-05-30 15:33:42.644 DEBUG 13897 --- [io-9999-exec-10] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
RE-CREATING THE PROBLEM ON YOUR COMPUTER:
You can recreate the problem on any computer in only a few minutes by following these simple steps:
1.) Download the zipped version of the app from a file sharing site by clicking on this link.
2.) Unzip the app by typing: tar -zxvf oauth2.tar(4).gz
3.) Launch the authserver app by navigating to oauth2/authserver and then typing mvn spring-boot:run.
4.) Launch the resource app by navigating to oauth2/resource and then typing mvn spring-boot:run
5.) Launch the ui app by navigating to oauth2/ui and then typing mvn spring-boot:run
6.) Open a web browser and navigate to http : // localhost : 8080
7.) Click Login and then enter Frodo as the user and MyRing as the password, and click to submit.
8.) Enter 5309 as the Pin Code and click submit. This will trigger the error shown above.
The Spring Boot debug logs will show A LOT of SYSO, which gives the values of variables such as XSRF-TOKEN and HttpSession at each step shown in the flowchart. The SYSO helps segment the debug logs so that they are easier to interpret. And all the SYSO is done by one class called by the other classes, so you can manipulate the SYSO-generating class to change reporting everywhere in the control flow. The name of the SYSO-generating class is TestHTTP, and its source code can be found in the same demo package.
USE THE DEBUGGER:
1.) Select the terminal window that is running the authserver app and type Ctrl-C to stop the authserver app.
2.) Import the three apps (authserver, resource, and ui) into eclipse as existing maven projects.
3.) In the authserver app's eclipse Project Explorer, click to expand the Maven Dependencies folder, then scroll down within it to click to expand the Spring-Security-web... jar as shown circled in orange in the image below. Then scroll to find and expand the org.springframework.security.web.context package. Then double click to open the HttpSessionSecurityContextRepository class highlighted in blue in the screen shot below. Add breakpoints to every line in this class. You may want to do the same to the SecurityContextPersistenceFilter class in the same package. These breakpoints will enable you to see the value of the HttpSession, which currently becomesnull before the end of the control flow, but needs to have a valid value that can be mapped to an XSRF-TOKEN in order to resolve this OP.
4.) In the app's demo package, add breakpoints inside the CustomOAuth2RequestFactory.java. Then Debug As... Spring Boot App to start the debugger.
5.) Then repeat steps 6 through 8 above. You may want to clear the browser's cache before each new attempt. And you may want the Network tab of the browser's developer tools open.
The session is not null in your authserver app at the time of the final call to localhost :9999/uaa/oauth/token. Not only is there a session, but the JSESSIONID and the csrf token of the valid session match values present in the control flow between the point where the user submits the correct pin and the point where the failed request to /oauth/token is made.
The problem is that there are two JSESSIONID values, and the wrong of the two values is selected to enter the call to /oauth/token. Therefore, the solution should come from modifying the filters to delete the bad JSESSIONID so that the correct value can be sent.
The following will summarize:
HttpSessionListener identified the valid JSESSIONID
To isolate the problem, I created an implementation of HttpSessionListener and then called it from a custom implementation of HttpLListener, as follows:
public class HttpSessionCollector implements HttpSessionListener, ServletContextListener {
private static final Set<HttpSession> sessions = ConcurrentHashMap.newKeySet();
public void sessionCreated(HttpSessionEvent event) {
sessions.add(event.getSession());
}
public void sessionDestroyed(HttpSessionEvent event) {
sessions.remove(event.getSession());
}
public static Set<HttpSession> getSessions() {
return sessions;
}
public void contextCreated(ServletContextEvent event) {
event.getServletContext().setAttribute("HttpSessionCollector.instance", this);
}
public static HttpSessionCollector getCurrentInstance(ServletContext context) {
return (HttpSessionCollector) context.getAttribute("HttpSessionCollector.instance");
}
#Override
public void contextDestroyed(ServletContextEvent arg0) {
}
#Override
public void contextInitialized(ServletContextEvent arg0) {
}
}
I then called the above HttpSessionListener in a custom implementation of OncePerRequestFilter, which I inserted into your authserver app's Spring Security Filter Chain to provide diagnostic information, as follows:
#Component
public class DiagnoseSessionFilter extends OncePerRequestFilter implements ServletContextAware {
#Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain fc) throws ServletException, IOException {
System.out.println("...........///////////// START OF DiagnoseSessionFilter.doFilterInternal() ///////////...........");
//start of request stuff
System.out.println("\\\\\\\\\\ REQUEST ATTRIBUTES ARE: ");
if(req.getAttribute("_csrf")!=null){
System.out.println("_csrf is: " + req.getAttribute("_csrf").toString());
}
if(req.getAttribute("org.springframework.security.web.csrf.CsrfToken")!=null){
CsrfToken ucsrf = (CsrfToken) req.getAttribute("org.springframework.security.web.csrf.CsrfToken");
System.out.println("ucsrf.getToken() is: " + ucsrf.getToken());
}
String reqXSRF = req.getHeader("XSRF-TOKEN");
System.out.println("request XSRF-TOKEN header is: " + reqXSRF);
String reqCookie = req.getHeader("Cookie");
System.out.println("request Cookie header is: " + reqCookie);
String reqSetCookie = req.getHeader("Set-Cookie");
System.out.println("request Set-Cookie header is: " + reqSetCookie);
String reqReferrer = req.getHeader("referrer");
System.out.println("request referrer header is: " + reqReferrer);
HttpSession rsess = req.getSession(false);
System.out.println("request.getSession(false) is: " + rsess);
if(rsess!=null){
String sessid = rsess.getId();
System.out.println("session.getId() is: "+sessid);
}
System.out.println("/////////// END OF REQUEST ATTRIBUTES ");
//end of request stuff
ServletContext servletContext = req.getServletContext();
System.out.println("\\\\\\\\\\ START OF SESSION COLLECTOR STUFF ");
HttpSessionCollector collector = HttpSessionCollector.getCurrentInstance(servletContext);
Set<HttpSession> sessions = collector.getSessions();
System.out.println("sessions.size() is: " + sessions.size());
for(HttpSession sess : sessions){
System.out.println("sess is: " + sess);
System.out.println("sess.getId() is: " + sess.getId());
CsrfToken sessCsrf = (CsrfToken) sess.getAttribute("org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN");
System.out.println("csrf is: " + sessCsrf);
if(sessCsrf!=null){
if(sessCsrf.getToken()!=null){
System.out.println("sessCsrf.getToken() is: " + sessCsrf.getToken());
} else { System.out.println("sessCsrf.getToken() is: null "); }
} else { System.out.println("sessCsrf is: null "); }
System.out.println("sess.getAttribute(SPRING_SECURITY_SAVED_REQUEST) is: " + sess.getAttribute("SPRING_SECURITY_SAVED_REQUEST") );
if(sess.getAttribute("SPRING_SECURITY_SAVED_REQUEST") instanceof DefaultSavedRequest){
System.out.println("_____ START PRINTING SAVED REQUEST");
DefaultSavedRequest savedReq = (DefaultSavedRequest) sess.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
List<Cookie> savedCookies = savedReq.getCookies();
for(Cookie cook : savedCookies){
String name = cook.getName();String value = cook.getValue();
System.out.println("cookie name, value are: " + name + " , " + value);
}
Collection<String> savedHeaderNames = savedReq.getHeaderNames();
for(String headerName : savedHeaderNames){
System.out.println("headerName is: " + headerName);
}
List<Locale> savedLocales = savedReq.getLocales();
for(Locale loc : savedLocales){
System.out.println("loc.getLanguage() is: " + loc.getLanguage());
}
String savedMethod = savedReq.getMethod();
System.out.println("savedMethod is: " + savedMethod);
Map<String, String[]> savedParamMap = savedReq.getParameterMap();
Iterator<Entry<String, String[]>> it = savedParamMap.entrySet().iterator();
while (it.hasNext()) {
Entry<String, String[]> pair = it.next();
System.out.println("savedParamMap: " + pair.getKey() + " = " + pair.getValue());
it.remove(); // avoids a ConcurrentModificationException
}
Collection<String> savedParamNames = savedReq.getParameterNames();
for(String savedParamName : savedParamNames){
System.out.println("savedParamName: " + savedParamNames);
}
System.out.println("_____ DONE PRINTING SAVED REQUEST");
}
// System.out.println("sess.getAttribute(SPRING_SECURITY_CONTEXT) is: " + sess.getAttribute("SPRING_SECURITY_CONTEXT") );
if(sess.getAttribute("SPRING_SECURITY_CONTEXT") instanceof SecurityContextImpl){
SecurityContext ctxt = (SecurityContext) sess.getAttribute("SPRING_SECURITY_CONTEXT");
Authentication auth = ctxt.getAuthentication();
if(auth.getDetails() instanceof WebAuthenticationDetails){
WebAuthenticationDetails dets = (WebAuthenticationDetails) auth.getDetails();
System.out.println( "dets.getSessionId() is: " + dets.getSessionId() );
}
System.out.println("auth.getAuthorities() is: " + auth.getAuthorities() );
System.out.println("auth.isAuthenticated() is: " + auth.isAuthenticated() );
}
}
SecurityContext context = SecurityContextHolder.getContext();
System.out.println("...........///////////// END OF DiagnoseSessionFilter.doFilterInternal() ///////////...........");
fc.doFilter(req, res);
}
}
Isolating the problem code:
The following combines and summarizes the diagnostic data from HttpSessionListener with the web browser's developer tools for the steps between the user clicking submit on the submit pin code view and the browser returning a rejection from the /oauth/token endpoint.
As you can see, there are two JSESSIONID values floating around. One of the values is correct, while the other value is not. The incorrect value gets passed into the request to /oauth/token, and causes rejection, even though the csrf passed is correct. Therefore, the solution to this problem will likely come from altering the steps below to stop placing the bad JSESSIONID in place of the good one:
1.) POST http://localhost:9999/uaa/secure/two_factor_authentication
request headers:
Referer: 9999/uaa/secure/two_factor_authentication
Cookie:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
filter chain:
DiagnoseSessionFilter:
request stuff:
Cookie header:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
request.getSession(false).getId(): ....95CB77
session collector stuff:
JSESSIONID: ....95CB77
csrf: ....862a73
SPRING_SECURITY_SAVED_REQUEST is null
user details (from Authentication object with user/request
JSESSIONID: ....ED927C
Authenticated = true, with roles
Complete the filter chain
DiagnoseSessionFilter (again)
request stuff:
csrf attribute: ....862a73
Cookie header:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
request.getSession(false).getId(): 95CB77
session collector stuff:
JSESSIONID: ....95CB77
csrf is: 862a73
SPRING_SECURITY_SAVED_REQUEST is null
user details (Authentication for user/session/request)
JSESSIONID: ....ED927C
Authenticated = true, with authorities
POST/secure/two_factor_authenticationControllerMethod
do some stuff
response:
Location: 9999/uaa/oauth/authorize?....
XSRF-TOKEN: ....862a73
2.) GET http://localhost:9999/uaa/oauth/authorize?...
request headers:
Host: localhost:9999
Referer: 9999/uaa/secure/two_factor_authentication
Cookie:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
FilterChain
DiagnoseSessionFilter
request stuff:
Cookie header is: JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
request.getSession(false).getId(): 95CB77
session collector stuff:
JSESSIONID: ....95CB77
csrf is: ....862a73
SPRING_SECURITY_SAVED_REQUEST is: null
user details (Authentication object with user/session/req)
JSESSIONID: ....ED927C
Authenticated = true with ALL roles.
rest of filter chain
TwoFactorAuthenticationFilter
request stuff:
csrf request attribute is: ....862a73
cookie header:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
request.getSession(false).getId() is: ....95CB77
updateCsrf is: ....862a73
response stuff:
XSRF-TOKEN header (after manual update): ....862a73
DiagnoseSessionFilter:
request stuff:
_csrf request attribute: ....862a73
Cookie header:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
request.getSession(false).getId() is: ....95CB77
session collector stuff:
JSESSIONID: ....95CB77
csrf is: ....862a73
SPRING_SECURITY_SAVED_REQUEST is: null
user details (Authentication for user/session/request)
JSESSIONID: ....ED927C
Authenticated is true, with ALL roles.
CustomOAuth2RequestFactory
request stuff:
_csrf request parameter is: ....862a73
Cookie header:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
request.getSession(false).getId() is: ....95CB77
updateCsrf: ....862a73
response stuff:
XSRF-TOKEN header: ....862a73
session attribute printout
csrf: ....862a73
SPRING_SECURITY_CONTEXT (not printed, so don't know values)
response:
Location: 8080/login?code=myNwd7&state=f6b3Km
XSRF-TOKEN: ....862a73
3.) GET http://localhost:8080/login?code=myNwd7&state=f6b3Km
request headers:
Host: localhost:8080
Referer: 9999/uaa/secure/two_factor_authentication
Cookie:
JSESSIONID: ....918636
XSRF-TOKEN: ....862a73
UiAppFilterChain:
HttpSessionSecurityContextRepository
creates new SPRING_SECURITY_CONTEXT to replace null one
OAuth2ClientAuthenticationProcessingFilter (position 8 of 14)
AuthorizationCodeAccessTokenProvider
Retrieving token from 9999/uaa/oauth/token
AuthServerFilterChain:
DiagnoseSessionFilter
request stuff:
XSRF-TOKEN header is: null
Cookie header is: null
Set-Cookie header is: null
referrer header is: null
request.getSession(false) is: null
session collector stuff:
JSESSIONID: ....95CB77
sessCsrf.getToken() is: 862a73
SPRING_SECURITY_SAVED_REQUEST is: null
Authenticated is true but with ONLY these roles:
ROLE_HOBBIT, ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED
SecurityContextPersistenceFilter
reports no HttpSession and no SPRING_SECURITY_CONTEXT
CsrfFilter
rejects request to /oauth/token due to no session % csrf
response headers:
Set-Cookie:
XSRF-TOKEN: ....527fbe
X-Frame-Options: DENY
I will try to spend a little more time with this to further isolate the solution, given the number of points you are offering. But the above should substantially narrow the problem.
I am posting this before it is completely finished because your bounty period is about to expire.
Have you solved your issue? I have been looking around to find a full sample of 2FA together with spring-security-oauth2. It is great that you have posted your full concepts and the complete sources.
I tried your package and your issue can simply be resolved by changing just 1 line of code in your AuthserverApplication.java
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.formLogin().loginPage("/login").permitAll()
.and()
.requestMatchers().antMatchers("/login", "/oauth/authorize", "/secure/two_factor_authentication", "/pincode")
.and()
.authorizeRequests().anyRequest().authenticated();
// #formatter:on
}
Your original configuration by passed the authentication chain of spring security which returned you a null object of authentication.
I would also recommend you to change the Bean creation of CustomOAuth2RequestFactory to the following which override all the OAuth2RequestFactory in the chain
#Bean
public OAuth2RequestFactory customOAuth2RequestFactory(){
return new CustomOAuth2RequestFactory(clientDetailsService);
}
For the code you have added for handling the CSRF, you may just simply remove them, eg. the 2FA controller:
#Controller
#RequestMapping(TwoFactorAuthenticationController.PATH)
public class TwoFactorAuthenticationController {
private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);
public static final String PATH = "/secure/two_factor_authentication";
public static final String AUTHORIZE_PATH = "/oauth/authorize";
public static final String ROLE_TWO_FACTOR_AUTHENTICATED = "ROLE_TWO_FACTOR_AUTHENTICATED";
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#RequestMapping(method = RequestMethod.GET)
public String auth(HttpServletRequest request, HttpSession session, HttpServletResponse resp/*, ....*/) {
System.out.println("-------- inside GET /secure/two_factor_authentication --------------");
if (AuthenticationUtil.isAuthenticatedWithAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
LOG.info("User {} already has {} authority - no need to enter code again", ROLE_TWO_FACTOR_AUTHENTICATED);
// throw ....;
}
else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
// LOG.warn("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
// throw ....;
}
return "pinCode";
}
#RequestMapping(method = RequestMethod.POST)
public String auth(FormData formData, HttpServletRequest req, HttpServletResponse resp,
SessionStatus sessionStatus, Principal principal, Model model)
throws IOException{
if (formData.getPinVal()!=null) {
if(formData.getPinVal().equals("5309")){
AuthenticationUtil.addAuthority(ROLE_TWO_FACTOR_AUTHENTICATED);
return "redirect:"+AUTHORIZE_PATH;
};
};
return "pinCode";
}
}
Please kindly let me know if you want a complete source codes after cleanup.

Spring Security pre-authentication - gives me a new session even though principal is unchanged

I have implemented a spring security preathentication filter in my Grails application in order to integrate with Tivoli Access Manager.
The filter getting called for every request in my web application - yet even though the filter returns the same principal as a previous request, it seems to create a new session. I have created a ApplicationListener to listen for authentication events and I can see a new AuthenticationSuccessEvent, with a new session id for each request.
This means all my session variables get cleared each request- which wouldn't be a big deal but it breaks the uploadr plugin.
When I turn debug logging on for my preauthentication filter I see that it thinks the principal has changed, even though it has not:
2015-03-04 11:34:57.769 foobar.TamAuthenticationFilter Checking secure context token: org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken#f0666480: Principal: grails.plugin.springsecurity.userdetails.GrailsUser#3125618: Username: 66734; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN,ROLE_APPROVER; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#fffde5d4: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 87928D9E25D98DD3CCFAC5D67689E609; Granted Authorities: ROLE_ADMIN, ROLE_APPROVER
2015-03-04 11:34:57.770 foobar.TamAuthenticationFilter Pre-authenticated principal has changed to 66734 and will be reauthenticated
2015-03-04 11:34:57.770 foobar.TamAuthenticationFilter Invalidating existing session
2015-03-04 11:34:57.771 foobar.TamAuthenticationFilter preAuthenticatedPrincipal = 66734, trying to authenticate
How can I make spring security use the same session for each principal returned by the pre authentication filter, rather than create a new one for every request?
here is my filter:
package foobar
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter
import grails.util.Environment
import grails.util.Holders
import groovy.util.logging.Log4j
#Log4j
class TamAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
java.lang.Object getPreAuthenticatedCredentials(javax.servlet.http.HttpServletRequest request)
{
"N/A"
}
java.lang.Object getPreAuthenticatedPrincipal(javax.servlet.http.HttpServletRequest request)
{
Long staffId = getStaffIdFromTamHeader(request)
if(!staffId)
log.error "iv-user header not found"
return staffId
}
/**
* Get Staff ID from the ivUser Tamheader.
* #param request
* #return
*/
static public Long getStaffIdFromTamHeader(request) {
return request.getHeader("iv-user")
}
}
LoggingSecurityEventListener:
package foobar
import groovy.util.logging.Log4j
import org.springframework.context.ApplicationListener
import org.springframework.security.authentication.event.AbstractAuthenticationEvent
#Log4j
class LoggingSecurityEventListener implements
ApplicationListener<AbstractAuthenticationEvent> {
void onApplicationEvent(AbstractAuthenticationEvent event) {
def username = event.authentication.principal
def address = event.authentication.details.remoteAddress
def sessionId = event.authentication.details.sessionId
log.info "event=${event.class.simpleName} username=${username} remoteAddress=${address} sessionId=${sessionId}"
}
}
resources.groovy:
beans = {
//
// grabs the user id from the tam headers
//
tamAuthenticationFilter(TamAuthenticationFilter) {
authenticationManager = ref('authenticationManager')
checkForPrincipalChanges = true
}
tamAuthenticationProvider(PreAuthenticatedAuthenticationProvider) {
preAuthenticatedUserDetailsService = ref('authenticationUserDetailsService')
}
//
// we do not want to redirect to the auth/login page since we are using tam
//
authenticationEntryPoint(Http403ForbiddenEntryPoint)
securityEventListener(LoggingSecurityEventListener)
}
config.groovy:
grails.plugin.springsecurity.useSecurityEventListener = true
grails.plugin.springsecurity.providerNames = ['tamAuthenticationProvider']
grails.plugin.springsecurity.userLookup.userDomainClassName = 'strobe.auth.User'
grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'strobe.auth.UserRole'
grails.plugin.springsecurity.authority.className = 'strobe.auth.Role'
grails.plugin.springsecurity.securityConfigType="InterceptUrlMap"
grails.plugin.springsecurity.interceptUrlMap = [
'/foobar/**': ['ROLE_USER']]
bootstrap.groovy:
def init = { servletContext -> SpringSecurityUtils.clientRegisterFilter('tamAuthenticationFilter',
SecurityFilterPosition.PRE_AUTH_FILTER.order + 10)
}
I have found the solution to my problem- I was returning a Long from getPreauthenticatedPrincipal() which confuses spring security, as the method requiresAuthentication() in AbstractPreauthenticatedProcessingFilter has this line of code:
if ((principal instanceof String) && currentUser.getName().equals(principal)) {
return false;
}
It expects the principal to be a String. Every time I returned a long it would reauthenticate and give me a new session.
I can't believe the solution was that simple and it took me 2 days to figure out!!!
Part of the problem I think is the scant documentation on preauthentication in spring and also the method signature of:
java.lang.Object getPreAuthenticatedPrincipal(javax.servlet.http.HttpServletRequest request)
which doesn't clearly suggest a return type.

Why I received an Error 403 with MockMvc and JUnit?

I have a spring mvc (3.2.5) application with spring security (3.2).
I configured my SecurityConfig.class with this method :
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/*").permitAll().and()
.formLogin().successHandler(successHandler)
.defaultSuccessUrl("/")
.failureHandler(failureHandler).failureUrl("/login?error=true")
.permitAll().and().logout()
.permitAll();
http.authorizeRequests().antMatchers("/resources/**").permitAll();
http.authorizeRequests().antMatchers("/welcome").permitAll();
http.authorizeRequests().antMatchers("/secure/*").authenticated();
http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN").anyRequest().authenticated();
}
With Spring security (3.2) I have CSRF enabled. I think it is a good idea to let it enabled.
My controller SignInController contains 2 methods with params :
EDIT : adding action= in params
#RequestMapping(value = "/signup")
public ModelAndView signup() {
boolean auth = SecurityContextHolder.getContext().getAuthentication() == null ? false
: SecurityContextHolder.getContext().getAuthentication()
.isAuthenticated()
&& (SecurityContextHolder.getContext()
.getAuthentication().getPrincipal() instanceof User);
ModelAndView result = null;
if (auth) {
result = new ModelAndView("redirect:" + "/");
} else {
UserForm user = new UserForm();
result = new ModelAndView("registration", "userForm", user);
}
return result;
}
#RequestMapping(value = "/register", params = "action=signup")
public ModelAndView registration(
#ModelAttribute(value = "userForm") #Valid UserForm userForm,
BindingResult result, HttpServletRequest request) {
if (result.hasErrors()) {
return new ModelAndView("registration");
}
Member member = profileFacade.registerNewUser(userForm);
return new ModelAndView("registration", "member", member);
}
#RequestMapping(value = "/register", params = "action=cancel")
public ModelAndView cancelRegistration() {
return new ModelAndView("redirect:" + "/");
}
and finally, I have JUnit test :
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextConfiguration(classes = { WebConfiguration.class,
JpaConfiguration.class, LoggingConfiguration.class,
SecurityConfig.class, DataSourceEmbeddedConfiguration.class,
DataSourceMySqlConfig.class, BaseValidatorConfiguration.class })
#TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
#ActiveProfiles("dev")
public class SignInControllerTest {
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private MockHttpSession session;
#Autowired
private MockHttpServletRequest request;
#Autowired
private FilterChainProxy springSecurityFilterChain;
private MockMvc mockMvc;
#Before
public void setUp() throws ServletException {
SecurityContextHolderAwareRequestFilter scharf = new SecurityContextHolderAwareRequestFilter();
scharf.afterPropertiesSet();
this.mockMvc = MockMvcBuilders
.webAppContextSetup(this.webApplicationContext)
.addFilters(springSecurityFilterChain).dispatchOptions(true).build();
SecurityContextHolder.getContext().setAuthentication(null);
}
#Test
public void signup() throws Exception {
mockMvc.perform(get("/signup")).andExpect(status().isOk())
.andExpect(model().attributeExists("userForm"));
}
#Test
#Transactional
#Rollback(true)
public void register() throws Exception {
UserForm form = new UserForm();
form.setEmail("email#email.com");
form.setUsername("aokije");
form.setPassword("klo,ksff");
form.setConfirmedPassword("klo,ksff");
mockMvc.perform(post("/register").param("action", "signup")).andExpect(status().isOk());
}
}
EDIT : update mockMvc.perform because it is working fine with http.csrf().disable() in SecurityConfig.class
Test signup run perfectly but register return an error 403.
I tried a lot of things but I received always this error.
When I try http://localhost:8080/register?signup in a browser, it is working fine.
_EDIT_
Logs :
2014-02-13 22:00:14,695 [ExpressionBasedFilterInvocationSecurityMetadataSource] processMap Adding web access control expression 'permitAll', for org.springframework.security.config.annotation.web.configurers.PermitAllSupport$ExactUrlRequestMatcher#52ee705c
2014-02-13 22:00:14,696 [ExpressionBasedFilterInvocationSecurityMetadataSource] processMap Adding web access control expression 'permitAll', for org.springframework.security.config.annotation.web.configurers.PermitAllSupport$ExactUrlRequestMatcher#2412d28d
2014-02-13 22:00:14,697 [ExpressionBasedFilterInvocationSecurityMetadataSource] processMap Adding web access control expression 'permitAll', for org.springframework.security.config.annotation.web.configurers.PermitAllSupport$ExactUrlRequestMatcher#4fbd397b
2014-02-13 22:00:14,697 [ExpressionBasedFilterInvocationSecurityMetadataSource] processMap Adding web access control expression 'permitAll', for Ant [pattern='/logout']
2014-02-13 22:00:14,698 [ExpressionBasedFilterInvocationSecurityMetadataSource] processMap Adding web access control expression 'permitAll', for org.springframework.security.config.annotation.web.configurers.PermitAllSupport$ExactUrlRequestMatcher#1008e323
2014-02-13 22:00:14,699 [ExpressionBasedFilterInvocationSecurityMetadataSource] processMap Adding web access control expression 'permitAll', for Ant [pattern='/*']
2014-02-13 22:00:14,700 [ExpressionBasedFilterInvocationSecurityMetadataSource] processMap Adding web access control expression 'permitAll', for Ant [pattern='/resources/**']
2014-02-13 22:00:14,700 [ExpressionBasedFilterInvocationSecurityMetadataSource] processMap Adding web access control expression 'permitAll', for Ant [pattern='/welcome']
2014-02-13 22:00:14,700 [ExpressionBasedFilterInvocationSecurityMetadataSource] processMap Adding web access control expression 'authenticated', for Ant [pattern='/secure/*']
2014-02-13 22:00:14,701 [ExpressionBasedFilterInvocationSecurityMetadataSource] processMap Adding web access control expression 'hasRole('ROLE_ADMIN')', for Ant [pattern='/admin/**']
2014-02-13 22:00:14,701 [ExpressionBasedFilterInvocationSecurityMetadataSource] processMap Adding web access control expression 'authenticated', for org.springframework.security.web.util.matcher.AnyRequestMatcher#1
2014-02-13 22:00:14,703 [FilterSecurityInterceptor] afterPropertiesSet Validated configuration attributes
2014-02-13 22:00:14,704 [FilterSecurityInterceptor] afterPropertiesSet Validated configuration attributes
2014-02-13 22:00:14,734 [DefaultSecurityFilterChain] <init> Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher#1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#10174779, org.springframework.security.web.context.SecurityContextPersistenceFilter#68736a7e, org.springframework.security.web.header.HeaderWriterFilter#728e5d0d, org.springframework.security.web.csrf.CsrfFilter#6e7a918b, org.springframework.security.web.authentication.logout.LogoutFilter#430e85e7, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#55eda087, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter#290c7ca, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#6dd90afc, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#12eb6a0f, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#6855612f, org.springframework.security.web.session.SessionManagementFilter#410a11a2, org.springframework.security.web.access.ExceptionTranslationFilter#59e15580, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#2257a0]
2014-02-13 22:00:14,859 [FilterChainProxy] doFilter /register at position 1 of 13 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
2014-02-13 22:00:14,863 [FilterChainProxy] doFilter /register at position 2 of 13 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
2014-02-13 22:00:14,863 [HttpSessionSecurityContextRepository] readSecurityContextFromSession HttpSession returned null object for SPRING_SECURITY_CONTEXT
2014-02-13 22:00:14,863 [HttpSessionSecurityContextRepository] loadContext No SecurityContext was available from the HttpSession: org.springframework.mock.web.MockHttpSession#4c4b529f. A new one will be created.
2014-02-13 22:00:14,864 [FilterChainProxy] doFilter /register at position 3 of 13 in additional filter chain; firing Filter: 'HeaderWriterFilter'
2014-02-13 22:00:14,865 [HstsHeaderWriter] writeHeaders Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher#5ab39e58
2014-02-13 22:00:14,865 [FilterChainProxy] doFilter /register at position 4 of 13 in additional filter chain; firing Filter: 'CsrfFilter'
2014-02-13 22:00:14,866 [CsrfFilter] doFilterInternal Invalid CSRF token found for http://localhost/register
2014-02-13 22:00:14,866 [HttpSessionSecurityContextRepository] saveContext SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2014-02-13 22:00:14,866 [SecurityContextPersistenceFilter] doFilter SecurityContextHolder now cleared, as request processing completed
Could you help me ?
Thanks a lot
EDIT
Finally, I had a bug in another class (annotation). I fix with this :
HttpSessionCsrfTokenRepository httpSessionCsrfTokenRepository = new HttpSessionCsrfTokenRepository();
CsrfToken csrfToken = httpSessionCsrfTokenRepository
.generateToken(request);
Map map = new HashMap();
map.put("userForm", form);
map.put("org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN",
csrfToken);
this.mockMvc
.perform(
post("/register")
.param("signup", "")
.param("_csrf", csrfToken.getToken())
.sessionAttrs(map)).andExpect(status().isOk());
Params csrf and sessionAttrs are mandatory.
I know this question is quite old, but this is one of the first results on Google for some queries and I believe this approach is much better and it is described on spring.io blog
1) You can create your mockMvc with Spring Security support easier, so your setUp() gets much shorter:
#Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders
.webAppContextSetup(webApplicationContext)
.apply(springSecurity())
.build();
}
2) You can use org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf() to populate your test request with correct CSRF token like this:
mockMvc.perform(post("/register")
.with(csrf())
.param("action", "signup"))
.andExpect(status().isOk());
Post requests need the CSRF token to be added to the form. So you have to pass it while testing:
var TOKEN_ATTR_NAME = "org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN";
var httpSessionCsrfTokenRepository = new HttpSessionCsrfTokenRepository();
var csrfToken = httpSessionCsrfTokenRepository.generateToken(new MockHttpServletRequest());
mockMvc.perform(
post("/your/path/here")
.sessionAttr(TOKEN_ATTR_NAME, csrfToken)
.param(csrfToken.getParameterName(), csrfToken.getToken())
...
);
Second thing, are you sure that the registration method handles your post request? Isn't RequestMapping configured for "GET" by default?
Try with #AutoConfigureMockMvc(addFilters = false)

Resources