Adding google login_hint parameter to Spring oauth2 AuthorizationCode request - spring

I have been trying to add a request parameter to the AuthorizationCode request, spring oauth2 filter makes to google, as part of the oauth2 authentication flow. Specifically, I need to add a login_hint parameter to prevent google from directing users to pick their accounts when the email address is already known.
This is my initial configuration:
#Configuration
#EnableOAuth2Client
#RequiredArgsConstructor
public class OAuthSecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String LOGIN_PATH = "/oauth/login";
private static final int OAUTH2_CLIENT_FILTER_ORDER = -100;
static {
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
private final OAuth2ClientContext oauth2ClientContext;
private final OAuth2ClientContextFilter oAuth2ClientContextFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
final OAuth2ClientAuthenticationProcessingFilter ssoFilter = new OAuth2ClientAuthenticationProcessingFilter(LOGIN_PATH);
ssoFilter.setRestTemplate(googleRestTemplate());
ssoFilter.setTokenServices(tokenServices());
ssoFilter.setAuthenticationManager(oAuth2AuthenticationManager());
final OAuth2AuthenticationProcessingFilter clientFilter = new OAuth2AuthenticationProcessingFilter();
clientFilter.setAuthenticationManager(oAuth2AuthenticationManager());
clientFilter.setStateless(false);
// #formatter:off
http
.csrf().disable()
.cors().disable()
.headers()
.frameOptions().sameOrigin()
.cacheControl().disable()
.and()
.antMatcher("/**")
.csrf().disable()
.httpBasic().disable()
.rememberMe().disable()
.addFilterBefore(ssoFilter, BasicAuthenticationFilter.class)
.addFilterBefore(clientFilter, OAuth2ClientAuthenticationProcessingFilter.class)
.authorizeRequests()
.antMatchers("/api/**").fullyAuthenticated()
.anyRequest().permitAll()
.and()
.logout().logoutUrl("/logout")
.and() .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
// #formatter:on
}
The Only way I managed to do this is as follows:
#Bean
public OAuth2RestTemplate googleRestTemplate() {
MyAuthorizationCodeAccessTokenProvider myAuthorizationCodeAccessTokenProvider =
new MyAuthorizationCodeAccessTokenProvider();
AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(
Arrays.<AccessTokenProvider>asList(
myAuthorizationCodeAccessTokenProvider, new ImplicitAccessTokenProvider(),
new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider()));
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(googleClient(), oauth2ClientContext);
oAuth2RestTemplate.setAccessTokenProvider(accessTokenProvider);
return oAuth2RestTemplate;
}
static class MyAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider {
private static String EMAIL_PARAM_NAME = "email";
#Override
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException,
OAuth2AccessDeniedException {
try {
return super.obtainAccessToken(details, request);
} catch (UserRedirectRequiredException ex) {
String email = request.containsKey(EMAIL_PARAM_NAME) ? request.get(EMAIL_PARAM_NAME).get(0) : null;
if (email != null) {
ex.getRequestParams().put("login_hint", email);
}
throw ex;
}
}
}
}
Is this the best way to customize the spring oauth2 implementation to set the login_hint parameter on the authorization request?

I needed to solve the exact same problem with Okta as the IdP. I know this is an old post, but my post answers the question with a general approach that can be used for any additional parameters (not just login_hint) that need to be appended to the authorization URL. I think my answer fits well into how Spring expects developers to solve this. My main resource for a solution is found here (https://github.com/spring-projects/spring-security/issues/5244). Another good resource is here (https://github.com/spring-projects/spring-security/issues/5521). Here's how it works: Spring uses a filter called OAuth2AuthorizationRequestRedirectFilter which is responsible for building the OAuth2 authorization request. The authorization request (OAuth2AuthorizationRequest) contains the authorization request URI. The authorization request URI needs to be altered by adding additional parameters (i.e. login_hint). Spring uses an OAuth2AuthorizationRequestResolver implementation that creates the OAuth2AuthorizationRequest. By providing your own OAuth2AuthorizationRequestResolver, you can add additional parameters to the authorization URL. So for my implementation, I created a class called ConfigurableOAuth2AuthorizationRequestResolver that wraps Spring's DefaultOAuth2AuthorizationRequestResolver. Here is some of my code.
// Credit to Joe Grandja for providing test cases found here:
// https://raw.githubusercontent.com/spring-projects/spring-security/779597af2a6ed777707f08ae8106818e0b8e299e/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java
// Much of the below code comes from his test examples.
public class ConfigurableOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver
{
private final OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver;
/**
* Wraps a OAuth2AuthorizationRequestResolver so that calls to the resolve method can be delegated.
* In our implementation, the value passed here will be a DefaultOAuth2AuthorizationRequestResolver object.
* #param oAuth2AuthorizationRequestResolver usually the default resolver from Spring.
*/
public ConfigurableOAuth2AuthorizationRequestResolver(OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver)
{
this.oAuth2AuthorizationRequestResolver = oAuth2AuthorizationRequestResolver;
}
/**
* Adds our custom code to check for extra parameters in the session. We use the session because there
* are several redirects to the browser causing any request variable to be lost. Don't really like
* storing anything in the HTTP session but OK as long as it is removed immediately after use.
*
* #param request needed for resolve method delegation and to retrieve the HTTP session.
* #return the <code>OAuth2AuthorizationRequest</code>
*/
#Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request)
{
OAuth2AuthorizationRequest authorizationRequest = this.oAuth2AuthorizationRequestResolver.resolve(request);
return processAdditionalParameters(request, authorizationRequest);
}
/**
* Required method for implementation but not used in the standard use case.
*
* #param request needed for resolve method delegation and to retrieve the HTTP session.
* #param clientRegistrationId (e. g. google, okta)
* #return the <code>OAuth2AuthorizationRequest</code>
*/
#Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId)
{
OAuth2AuthorizationRequest authorizationRequest = this.oAuth2AuthorizationRequestResolver.resolve(request, clientRegistrationId);
return processAdditionalParameters(request, authorizationRequest);
}
/**
* This method does the important task of appending any special query string parameters to the authorization
* request. For now, we are only looking for login_hint in the session. This method can be changed to
* support more parameters. We expect the login_hint key to be found in the session.
*
* #param request needed to retrieve the HTTP session.
* #param authorizationRequest can be null as a valid scenario. Not null when registrationId matches Okta (or whatever).
* #return the <code>OAuth2AuthorizationRequest</code>
*/
private OAuth2AuthorizationRequest processAdditionalParameters(HttpServletRequest request, OAuth2AuthorizationRequest authorizationRequest)
{
if (authorizationRequest == null)
{
return null;
}
// NOTE: this can be improved to support multiple parameters by storing a list instead of a single param
Map<String, Object> additionalParameters = new HashMap<>(authorizationRequest.getAdditionalParameters());
HttpSession session = request.getSession();
additionalParameters.put(SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT, session.getAttribute(SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT));
session.removeAttribute(SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT); // remove immediately after use
String customAuthorizationRequestUri = UriComponentsBuilder
.fromUriString(authorizationRequest.getAuthorizationRequestUri())
.queryParam(SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT, additionalParameters.get(SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT))
.build(true).toUriString();
return OAuth2AuthorizationRequest.from(authorizationRequest)
.additionalParameters(additionalParameters)
.authorizationRequestUri(customAuthorizationRequestUri)
.build();
}
}
The next bit of code shows how to integrate your resolver instead of Spring's default.
#Override
protected void configure(HttpSecurity http) throws Exception
{
logger.info("Configuring OAuth/OIDC HTTP security ...");
http
.oauth2Login()
.authorizationEndpoint()
.authorizationRequestResolver(this.authorizationRequestResolver()) // adds custom resolver
}
#Bean
public OAuth2AuthorizationRequestResolver authorizationRequestResolver()
{
OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(
yourClientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
return new ConfigurableOAuth2AuthorizationRequestResolver(defaultAuthorizationRequestResolver);
}
Is this the "best" approach, IDK. As of 5.1, this is the accepted approach IMHO.

Definitely borrowed heavily from Jim Kennedy's post as well as from https://www.baeldung.com/spring-security-custom-oauth-requests.
/**
* Overriding DefaultOAuth2AuthorizationRequestResolver behavior to add login_hint paramter.
*
* #see https://www.baeldung.com/spring-security-custom-oauth-requests
*/
public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
public static final String SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT = "SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT";
private OAuth2AuthorizationRequestResolver defaultResolver;
public CustomAuthorizationRequestResolver(ClientRegistrationRepository repo, String authorizationRequestBaseUri) {
defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri);
}
#Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest req = defaultResolver.resolve(request);
if (req != null) {
req = customizeAuthorizationRequest(request, req);
}
return req;
}
#Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest req = defaultResolver.resolve(request, clientRegistrationId);
if (req != null) {
req = customizeAuthorizationRequest(request, req);
}
return req;
}
private OAuth2AuthorizationRequest customizeAuthorizationRequest(HttpServletRequest request, OAuth2AuthorizationRequest req) {
Map<String, Object> additionalParameters = new HashMap<String, Object>(req.getAdditionalParameters());
// add login_hint
copySessionAttributeValueToParameter(SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT, "login_hint", additionalParameters, request);
return OAuth2AuthorizationRequest
.from(req)
.additionalParameters(additionalParameters)
.build();
}
private void copySessionAttributeValueToParameter(String attrName, String paramName, Map<String, Object> additionalParameters, HttpServletRequest request) {
Object attrValue = getAndDeleteSessionAttributeValue(attrName, request);
if (attrValue != null) {
additionalParameters.put(paramName, attrValue);
}
}
private Object getAndDeleteSessionAttributeValue(String attrName, HttpServletRequest request) {
HttpSession session = request.getSession();
Object attrValue = session.getAttribute(attrName);
session.removeAttribute(attrName);
return attrValue;
}
}
SecurityConfig.java:
http
.oauth2Login()
.authorizationEndpoint()
.authorizationRequestResolver(new CustomAuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI));

Related

Spring Security: Custom CSRF Implementation by extending CsrfRepository

I am trying to create a customized CSRF implementation in my Spring Boot application by implementing the CsrfRepository interface provided by Spring Security.
Below is how my custom repository looks like:
public class CustomCookieCsrfTokenRepository implements CsrfTokenRepository {
static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
#Override
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.DEFAULT_CSRF_HEADER_NAME, this.DEFAULT_CSRF_PARAMETER_NAME, createNewToken());
}
#Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
String tokenValue = token == null ? "" : token.getToken();
Cookie cookie = new Cookie(this.DEFAULT_CSRF_COOKIE_NAME, tokenValue);
cookie.setSecure(request.isSecure());
response.addCookie(cookie);
}
#Override
public CsrfToken loadToken(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, this.DEFAULT_CSRF_COOKIE_NAME);
if (cookie == null) {
return null;
}
String token = cookie.getValue();
if (!StringUtils.hasLength(token)) {
return null;
}
return new DefaultCsrfToken(this.DEFAULT_CSRF_HEADER_NAME, this.DEFAULT_CSRF_PARAMETER_NAME, token);
}
private String createNewToken() {
String unsignedToken = UUID.randomUUID().toString();
return RSAUtil.signMessage(unsignedToken, privateKey);
}
}
QUESTION: As you can see, I want to sign my cookie value using a private key and validate it using a public key. The question is where should this verification logic take place? I am guessing loadToken() method can have the logic to validate the signature. Is this the correct place or should it take place elsewhere?
Can someone provide some snippets or samples on how and where to handle this?
No, the verification logic should be in generateToken(HttpServletRequest request) of your custom CsrfTokenRepository implementation. The saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) should save the token (or delete the saved token when the passed 'token' param is null) and loadToken(HttpServletRequest request) should return the existing saved token (which was saved by saveToken method) for the current request/session;
#Component
public class CustomCsrfTokenRepository implements CsrfTokenRepository {
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
private String headerName = DEFAULT_CSRF_HEADER_NAME;
private String cookieName = "USER_INFO";
private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = CustomCsrfTokenRepository2.class
.getName().concat(".CSRF_TOKEN");
private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
#Override
public CsrfToken generateToken(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
if (cookie == null) {
return new DefaultCsrfToken(this.headerName, this.parameterName,
createNewToken());
}
String cookieValue = cookie.getValue();
String token = cookieValue.split("\\|")[0];
if (!StringUtils.hasLength(token)) {
return new DefaultCsrfToken(this.headerName, this.parameterName,
createNewToken());
}
return new DefaultCsrfToken(this.headerName, this.parameterName, token);
}
#Override
public void saveToken(CsrfToken token, HttpServletRequest request,
HttpServletResponse response) {
if (token == null) {
HttpSession session = request.getSession(false);
if (session != null) {
session.removeAttribute(this.sessionAttributeName);
}
}
else {
HttpSession session = request.getSession();
session.setAttribute(this.sessionAttributeName, token);
}
}
#Override
public CsrfToken loadToken(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return (CsrfToken) session.getAttribute(this.sessionAttributeName);
}
private String createNewToken() {
return UUID.randomUUID().toString();
}
}
And you need to set your customCsrfRepoImpl bean in HttpSecurity configuration as shown below
#Configuration
#EnableWebSecurity
public class SecurityConfigurarion extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {
#Autowired
private CsrfTokenRepository customCsrfTokenRepository; //your custom csrfToken repository impl class
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().csrfTokenRepository(customCsrfTokenRepository) //set your custom csrf impl in httpSecurity
.and()
.authorizeRequests()
.antMatchers(permittedUrlsArr).permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.logout()
}
}

How to add a custom OpenId Filter in a Spring boot application?

I am trying to implement the backend side of an OpenId Connect authentication. It is a stateless API so I added a filter that handles the Bearer token.
I have created the OpenIdConnect Filter that handles the Authentication and added it in a WebSecurityConfigurerAdapter.
public class OpenIdConnectFilter extends
AbstractAuthenticationProcessingFilter {
#Value("${auth0.clientId}")
private String clientId;
#Value("${auth0.issuer}")
private String issuer;
#Value("${auth0.keyUrl}")
private String jwkUrl;
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
public OpenIdConnectFilter() {
super("/connect/**");
setAuthenticationManager(new NoopAuthenticationManager());
}
#Bean
public FilterRegistrationBean registration(OpenIdConnectFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
try {
Authentication authentication = tokenExtractor.extract(request);
String accessToken = (String) authentication.getPrincipal();
String kid = JwtHelper.headers(accessToken)
.get("kid");
final Jwt tokenDecoded = JwtHelper.decodeAndVerify(accessToken, verifier(kid));
final Map<String, Object> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
verifyClaims(authInfo);
Set<String> scopes = new HashSet<String>(Arrays.asList(((String) authInfo.get("scope")).split(" ")));
int expires = (Integer) authInfo.get("exp");
OpenIdToken openIdToken = new OpenIdToken(accessToken, scopes, Long.valueOf(expires), authInfo);
final OpenIdUserDetails user = new OpenIdUserDetails((String) authInfo.get("sub"), "Test", openIdToken);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (final Exception e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
public void verifyClaims(Map claims) {
int exp = (int) claims.get("exp");
Date expireDate = new Date(exp * 1000L);
Date now = new Date();
if (expireDate.before(now) || !claims.get("iss").equals(issuer) || !claims.get("azp").equals(clientId)) {
throw new RuntimeException("Invalid claims");
}
}
private RsaVerifier verifier(String kid) throws Exception {
JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
Jwk jwk = provider.get(kid);
return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}
Here is security configuration:
#Configuration
#EnableWebSecurity
public class OpenIdConnectWebServerConfig extends
WebSecurityConfigurerAdapter {
#Bean
public OpenIdConnectFilter myFilter() {
final OpenIdConnectFilter filter = new OpenIdConnectFilter();
return filter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.antMatcher("/connect/**").authorizeRequests()
.antMatchers(HttpMethod.GET, "/connect/public").permitAll()
.antMatchers(HttpMethod.GET, "/connect/private").authenticated()
.antMatchers(HttpMethod.GET, "/connect/private-
messages").hasAuthority("read:messages")
.antMatchers(HttpMethod.GET, "/connect/private-
roles").hasAuthority("read:roles")
.and()
.addFilterBefore(myFilter(),
UsernamePasswordAuthenticationFilter.class);
}
Rest endpoints looks like following:
#RequestMapping(value = "/connect/public", method = RequestMethod.GET,
produces = "application/json")
#ResponseBody
public String publicEndpoint() throws JSONException {
return new JSONObject()
.put("message", "All good. You DO NOT need to be authenticated to
call /api/public.")
.toString();
}
#RequestMapping(value = "/connect/private", method = RequestMethod.GET,
produces = "application/json")
#ResponseBody
public String privateEndpoint() throws JSONException {
return new JSONObject()
.put("message", "All good. You can see this because you are
Authenticated.")
.toString();
}
If I remove completely the filter for configuration and also the #Bean definition, the configuration works as expected: /connect/public is accessible, while /connect/private is forbidden.
If I keep the #Bean definition and add it in filter chain the response returns a Not Found status for requests both on /connect/public and /connect/private:
"timestamp": "18.01.2019 09:46:11",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/
When debugging I noticed that filter is processing the token and returns an implementation of Authentication.
Is the filter properly added in filter chain and in correct position?
Why is the filter invoked also on /connect/public path when this is supposed to be public. Is it applied to all paths matching super("/connect/**") call?
Why is it returning the path as "/" when the request is made at /connect/private
Seems that is something wrong with the filter, cause every time it is applied, the response is messed up.

Two factor authentication with spring security oauth2

I'm looking for ideas how to implement two factor authentication (2FA) with spring security OAuth2. The requirement is that the user needs two factor authentication only for specific applications with sensitive information. Those webapps have their own client ids.
One idea that popped in my mind would be to "mis-use" the scope approval page to force the user to enter the 2FA code/PIN (or whatever).
Sample flows would look like this:
Accessing apps without and with 2FA
User is logged out
User accesses app A which does not require 2FA
Redirect to OAuth app, user logs in with username and password
Redirected back to app A and user is logged in
User accesses app B which also does not require 2FA
Redirect to OAuth app, redirect back to app B and user is directly logged in
User accesses app S which does require 2FA
Redirect to OAuth app, user needs to additionally provide the 2FA token
Redirected back to app S and user is logged in
Directly accessing app with 2FA
User is logged out
User accesses app S which does require 2FA
Redirect to OAuth app, user logs in with username and password, user needs to additionally provide the 2FA token
Redirected back to app S and user is logged in
Do you have other ideas how to apporach this?
So this is how two factor authentication has been implemented finally:
A filter is registered for the /oauth/authorize path after the spring security filter:
#Order(200)
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
#Override
protected void afterSpringSecurityFilterChain(ServletContext servletContext) {
FilterRegistration.Dynamic twoFactorAuthenticationFilter = servletContext.addFilter("twoFactorAuthenticationFilter", new DelegatingFilterProxy(AppConfig.TWO_FACTOR_AUTHENTICATION_BEAN));
twoFactorAuthenticationFilter.addMappingForUrlPatterns(null, false, "/oauth/authorize");
super.afterSpringSecurityFilterChain(servletContext);
}
}
This filter checks if the user hasn't already authenticated with a 2nd factor (by checking if the ROLE_TWO_FACTOR_AUTHENTICATED authority isn't available) and creates an OAuth AuthorizationRequest which is put into the session. The user is then redirected to the page where he has to enter the 2FA code:
/**
* Stores the oauth authorizationRequest in the session so that it can
* later be picked by the {#link com.example.CustomOAuth2RequestFactory}
* to continue with the authoriztion flow.
*/
public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private OAuth2RequestFactory oAuth2RequestFactory;
#Autowired
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService);
}
private boolean twoFactorAuthenticationEnabled(Collection<? extends GrantedAuthority> authorities) {
return authorities.stream().anyMatch(
authority -> ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED.equals(authority.getAuthority())
);
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Check if the user hasn't done the two factor authentication.
if (AuthenticationUtil.isAuthenticated() && !AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
/* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
require two factor authenticatoin. */
if (twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) ||
twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())) {
// Save the authorizationRequest in the session. This allows the CustomOAuth2RequestFactory
// to return this saved request to the AuthenticationEndpoint after the user successfully
// did the two factor authentication.
request.getSession().setAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME, authorizationRequest);
// redirect the the page where the user needs to enter the two factor authentiation code
redirectStrategy.sendRedirect(request, response,
ServletUriComponentsBuilder.fromCurrentContextPath()
.path(TwoFactorAuthenticationController.PATH)
.toUriString());
return;
} else {
request.getSession().removeAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
}
}
filterChain.doFilter(request, response);
}
private Map<String, String> paramsFromRequest(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
return params;
}
}
The TwoFactorAuthenticationController that handles entering the 2FA-code adds the authority ROLE_TWO_FACTOR_AUTHENTICATED if the code was correct and redirects the user back to the /oauth/authorize endpoint.
#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";
#RequestMapping(method = RequestMethod.GET)
public String auth(HttpServletRequest request, HttpSession session, ....) {
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 ....; // Show the form to enter the 2FA secret
}
#RequestMapping(method = RequestMethod.POST)
public String auth(....) {
if (userEnteredCorrect2FASecret()) {
AuthenticationUtil.addAuthority(ROLE_TWO_FACTOR_AUTHENTICATED);
return "forward:/oauth/authorize"; // Continue with the OAuth flow
}
return ....; // Show the form to enter the 2FA secret again
}
}
A custom OAuth2RequestFactory retrieves the previously saved AuthorizationRequest from the session if available and returns that or creates a new one if none can be found in the session.
/**
* If the session contains an {#link AuthorizationRequest}, this one is used and returned.
* The {#link com.example.TwoFactorAuthenticationFilter} saved the original AuthorizationRequest. This allows
* to redirect the user away from the /oauth/authorize endpoint during oauth authorization
* and show him e.g. a the page where he has to enter a code for two factor authentication.
* Redirecting him back to /oauth/authorize will use the original authorizationRequest from the session
* and continue with the oauth authorization.
*/
public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {
public static final String SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest";
public CustomOAuth2RequestFactory(ClientDetailsService clientDetailsService) {
super(clientDetailsService);
}
#Override
public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(false);
if (session != null) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
if (authorizationRequest != null) {
session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
return authorizationRequest;
}
}
return super.createAuthorizationRequest(authorizationParameters);
}
}
This custom OAuth2RequestFactory is set to the authorization server like:
<bean id="customOAuth2RequestFactory" class="com.example.CustomOAuth2RequestFactory">
<constructor-arg index="0" ref="clientDetailsService" />
</bean>
<!-- Configures the authorization-server and provides the /oauth/authorize endpoint -->
<oauth:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
user-approval-handler-ref="approvalStoreUserApprovalHandler" redirect-resolver-ref="redirectResolver"
authorization-request-manager-ref="customOAuth2RequestFactory">
<oauth:authorization-code authorization-code-services-ref="authorizationCodeServices"/>
<oauth:implicit />
<oauth:refresh-token />
<oauth:client-credentials />
<oauth:password />
</oauth:authorization-server>
When using java config you can create a TwoFactorAuthenticationInterceptor instead of the TwoFactorAuthenticationFilter and register it with an AuthorizationServerConfigurer with
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig implements AuthorizationServerConfigurer {
...
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.addInterceptor(twoFactorAuthenticationInterceptor())
...
.requestFactory(customOAuth2RequestFactory());
}
#Bean
public HandlerInterceptor twoFactorAuthenticationInterceptor() {
return new TwoFactorAuthenticationInterceptor();
}
}
The TwoFactorAuthenticationInterceptor contains the same logic as the TwoFactorAuthenticationFilter in its preHandle method.
I couldn't make the accepted solution work. I have been working on this for a while, and finally I wrote my solution by using the ideas explained here and on this thread "null client in OAuth2 Multi-Factor Authentication"
Here is the GitHub location for the working solution for me:
https://github.com/turgos/oauth2-2FA
I appreciate if you share your feedback in case you see any issues or better approach.
Below you can find the key configuration files for this solution.
AuthorizationServerConfig
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private ClientDetailsService clientDetailsService;
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("ClientId")
.secret("secret")
.authorizedGrantTypes("authorization_code")
.scopes("user_info")
.authorities(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED)
.autoApprove(true);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.requestFactory(customOAuth2RequestFactory());
}
#Bean
public DefaultOAuth2RequestFactory customOAuth2RequestFactory(){
return new CustomOAuth2RequestFactory(clientDetailsService);
}
#Bean
public FilterRegistrationBean twoFactorAuthenticationFilterRegistration(){
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(twoFactorAuthenticationFilter());
registration.addUrlPatterns("/oauth/authorize");
registration.setName("twoFactorAuthenticationFilter");
return registration;
}
#Bean
public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter(){
return new TwoFactorAuthenticationFilter();
}
}
CustomOAuth2RequestFactory
public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {
private static final Logger LOG = LoggerFactory.getLogger(CustomOAuth2RequestFactory.class);
public static final String SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest";
public CustomOAuth2RequestFactory(ClientDetailsService clientDetailsService) {
super(clientDetailsService);
}
#Override
public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(false);
if (session != null) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
if (authorizationRequest != null) {
session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
LOG.debug("createAuthorizationRequest(): return saved copy.");
return authorizationRequest;
}
}
LOG.debug("createAuthorizationRequest(): create");
return super.createAuthorizationRequest(authorizationParameters);
}
}
WebSecurityConfig
#EnableResourceServer
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
#Autowired
CustomDetailsService customDetailsService;
#Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
#Bean(name = "authenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/webjars/**");
web.ignoring().antMatchers("/css/**","/fonts/**","/libs/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception { // #formatter:off
http.requestMatchers()
.antMatchers("/login", "/oauth/authorize", "/secure/two_factor_authentication","/exit", "/resources/**")
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin().loginPage("/login")
.permitAll();
} // #formatter:on
#Override
#Autowired // <-- This is crucial otherwise Spring Boot creates its own
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth//.parentAuthenticationManager(authenticationManager)
// .inMemoryAuthentication()
// .withUser("demo")
// .password("demo")
// .roles("USER");
auth.userDetailsService(customDetailsService).passwordEncoder(encoder());
}
}
TwoFactorAuthenticationFilter
public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {
private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationFilter.class);
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private OAuth2RequestFactory oAuth2RequestFactory;
//These next two are added as a test to avoid the compilation errors that happened when they were not defined.
public static final String ROLE_TWO_FACTOR_AUTHENTICATED = "ROLE_TWO_FACTOR_AUTHENTICATED";
public static final String ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED = "ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED";
#Autowired
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService);
}
private boolean twoFactorAuthenticationEnabled(Collection<? extends GrantedAuthority> authorities) {
return authorities.stream().anyMatch(
authority -> ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED.equals(authority.getAuthority())
);
}
private Map<String, String> paramsFromRequest(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
return params;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Check if the user hasn't done the two factor authentication.
if (isAuthenticated() && !hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
/* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
require two factor authentication. */
if (twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) ||
twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())) {
// Save the authorizationRequest in the session. This allows the CustomOAuth2RequestFactory
// to return this saved request to the AuthenticationEndpoint after the user successfully
// did the two factor authentication.
request.getSession().setAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME, authorizationRequest);
LOG.debug("doFilterInternal(): redirecting to {}", TwoFactorAuthenticationController.PATH);
// redirect the the page where the user needs to enter the two factor authentication code
redirectStrategy.sendRedirect(request, response,
TwoFactorAuthenticationController.PATH
);
return;
}
}
LOG.debug("doFilterInternal(): without redirect.");
filterChain.doFilter(request, response);
}
public boolean isAuthenticated(){
return SecurityContextHolder.getContext().getAuthentication().isAuthenticated();
}
private boolean hasAuthority(String checkedAuthority){
return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(
authority -> checkedAuthority.equals(authority.getAuthority())
);
}
}
TwoFactorAuthenticationController
#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";
#RequestMapping(method = RequestMethod.GET)
public String auth(HttpServletRequest request, HttpSession session) {
if (isAuthenticatedWithAuthority(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED)) {
LOG.debug("User {} already has {} authority - no need to enter code again", TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED);
//throw ....;
}
else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
LOG.debug("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
//throw ....;
}
LOG.debug("auth() HTML.Get");
return "loginSecret"; // Show the form to enter the 2FA secret
}
#RequestMapping(method = RequestMethod.POST)
public String auth(#ModelAttribute(value="secret") String secret, BindingResult result, Model model) {
LOG.debug("auth() HTML.Post");
if (userEnteredCorrect2FASecret(secret)) {
addAuthority(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED);
return "forward:/oauth/authorize"; // Continue with the OAuth flow
}
model.addAttribute("isIncorrectSecret", true);
return "loginSecret"; // Show the form to enter the 2FA secret again
}
private boolean isAuthenticatedWithAuthority(String checkedAuthority){
return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(
authority -> checkedAuthority.equals(authority.getAuthority())
);
}
private boolean addAuthority(String authority){
Collection<SimpleGrantedAuthority> oldAuthorities = (Collection<SimpleGrantedAuthority>)SecurityContextHolder.getContext().getAuthentication().getAuthorities();
SimpleGrantedAuthority newAuthority = new SimpleGrantedAuthority(authority);
List<SimpleGrantedAuthority> updatedAuthorities = new ArrayList<SimpleGrantedAuthority>();
updatedAuthorities.add(newAuthority);
updatedAuthorities.addAll(oldAuthorities);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
SecurityContextHolder.getContext().getAuthentication().getPrincipal(),
SecurityContextHolder.getContext().getAuthentication().getCredentials(),
updatedAuthorities)
);
return true;
}
private boolean userEnteredCorrect2FASecret(String secret){
/* later on, we need to pass a temporary secret for each user and control it here */
/* this is just a temporary way to check things are working */
if(secret.equals("123"))
return true;
else;
return false;
}
}

Spring Security authentication via URL

I have a Spring MVC app that uses Spring Security and form based login for authorization/authentication.
Now I want to add a special URL that includes a token that should be accessible without additional information because the token is unique to a user:
http://myserver.com/special/5f6be0c0-87d7-11e2-9e96-0800200c9a66/text.pdf
How do I need to configure Spring Security to use that token for user authentication?
You need to define your custom pre auth filter.
In security app context within http tag:
<custom-filter position="PRE_AUTH_FILTER" ref="preAuthTokenFilter" />
Then define your filter bean (and its properties approprietly):
<beans:bean class="com.yourcompany.PreAuthTokenFilter"
id="preAuthTokenFilter">
<beans:property name="authenticationDetailsSource" ref="authenticationDetailsSource" />
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
</beans:bean>
Create your custom filter extended from GenericFilterBean
public class PreAuthTokenFilter extends GenericFilterBean {
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationManager authenticationManager;
private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
#Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
String token = getTokenFromHeader(request);//your method
if (StringUtils.isNotEmpty(token)) {
/* get user entity from DB by token, retrieve its username and password*/
if (isUserTokenValid(/* some args */)) {
try {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
Authentication authResult = this.authenticationManager.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authResult);
} catch (AuthenticationException e) {
}
}
}
chain.doFilter(request, response);
}
/*
other methods
*/
If you don't want or cannot retrieve a password, you need to create your own AbstractAuthenticationToken which will receive only username as param (principal) and use it instead of UsernamePasswordAuthenticationToken:
public class PreAuthToken extends AbstractAuthenticationToken {
private final Object principal;
public PreAuthToken(Object principal) {
super(null);
super.setAuthenticated(true);
this.principal = principal;
}
#Override
public Object getCredentials() {
return "";
}
#Override
public Object getPrincipal() {
return principal;
}
}
You can provide a custom PreAuthenticatedProcessingFilter and PreAuthenticatedAuthenticationProvider. See Pre-Authentication Scenarios chapter for details.
I ran into this problem, and solved it using a custom implementation of the Spring Security RembereMe Service infrastructure. Here is what you need to do.
Define your own Authentication object
public class LinkAuthentication extends AbstractAuthenticationToken
{
#Override
public Object getCredentials()
{
return null;
}
#Override
public Object getPrincipal()
{
return the prncipal that that is passed in via the constructor
}
}
Define
public class LinkRememberMeService implements RememberMeServices, LogoutHandler
{
/**
* It might appear that once this method is called and returns an authentication object, that authentication should be finished and the
* request should proceed. However, spring security does not work that way.
*
* Once this method returns a non null authentication object, spring security still wants to run it through its authentication provider
* which, is totally brain dead on the part of Spring this, is why there is also a
* LinkAuthenticationProvider
*
*/
#Override
public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response)
{
String accessUrl = ServletUtils.getApplicationUrl(request, "/special/");
String requestUrl = request.getRequestURL().toString();
if (requestUrl.startsWith(accessUrl))
{
// take appart the url extract the token, find the user details object
// and return it.
LinkAuthentication linkAuthentication = new LinkAuthentication(userDetailsInstance);
return linkAuthentication;
} else
{
return null;
}
}
#Override
public void loginFail(HttpServletRequest request, HttpServletResponse response)
{
}
#Override
public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication)
{
}
#Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
{
}
}
public class LinkAuthenticationProvider implements AuthenticationProvider
{
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
// Spring Security is totally brain dead and over engineered
return authentication;
}
#Override
public boolean supports(Class<?> authentication)
{
return LinkAuthentication.class.isAssignableFrom(authentication);
}
}
Hack up the rest rest of your spring security xml to define a custom authentication provider, and the custom remember me service.
P.S. if you do base64 encoding of the GUID in your URL it will be a few characters shorter. You can use the Apache commons codec base64 binary encoder / decoder to do safer url links.
public static String toBase64Url(UUID uuid)
{
return Base64.encodeBase64URLSafeString(toBytes(uuid));
}

Dynamic post logout redirection url based on user?

i am wondering how i could implement a post logout redirection using a custom logout handler. I have implemented a CustomLogoutSuccessHandler but i have no way off access http session data that has previous been set by the user who has logged in. The data is alway empty...
class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private static final ThreadLocal<Authentication> AUTH_HOLDER = new ThreadLocal<Authentication>()
void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
AUTH_HOLDER.set authentication
// reading session variable...
request.session?.variable // but this is always empty
try {
super.handle(request, response, authentication)
}
finally {
AUTH_HOLDER.remove()
}
}
#Override
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = AUTH_HOLDER.get()
String url = super.determineTargetUrl(request, response)
// do something with the url based on session data..
url
}
}
I do not know if there is any easy way to do this but came up with the below solution.
All you have to do is set the setTargetUrlParameter in your LogoutSuccessHandler. For that I made use of the implementation of HttpServletRequestWrapper written by Lincoln Baxter, III here for adding a parameter to the current request. Here is the relevant code.
public class PrettyFacesWrappedRequest extends HttpServletRequestWrapper
{
private final Map<String, String[]> modifiableParameters;
private Map<String, String[]> allParameters = null;
/**
* Create a new request wrapper that will merge additional parameters into
* the request object without prematurely reading parameters from the
* original request.
*
* #param request
* #param additionalParams
*/
public PrettyFacesWrappedRequest(final HttpServletRequest request,
final Map<String, String[]> additionalParams)
{
super(request);
modifiableParameters = new TreeMap<String, String[]>();
modifiableParameters.putAll(additionalParams);
}
#Override
public String getParameter(final String name)
{
String[] strings = getParameterMap().get(name);
if (strings != null)
{
return strings[0];
}
return super.getParameter(name);
}
#Override
public Map<String, String[]> getParameterMap()
{
if (allParameters == null)
{
allParameters = new TreeMap<String, String[]>();
allParameters.putAll(super.getParameterMap());
allParameters.putAll(modifiableParameters);
}
//Return an unmodifiable collection because we need to uphold the interface contract.
return Collections.unmodifiableMap(allParameters);
}
#Override
public Enumeration<String> getParameterNames()
{
return Collections.enumeration(getParameterMap().keySet());
}
#Override
public String[] getParameterValues(final String name)
{
return getParameterMap().get(name);
}
}
and then in the CustomLogoutSuccessHandler, I add this targetUrl as the parameter like this:
#Component
public class MyCustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
#Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
HttpServletRequest wrappedRequest = request;
if (authentication != null) {
//do something with the Principal and add the corresponding url
Map<String, String[]> extraParams = new TreeMap<String, String[]>();
extraParams.put("targetUrl", new String[] {"/target.xhtml"});
wrappedRequest = new PrettyFacesWrappedRequest(request, extraParams);
setTargetUrlParameter("targetUrl");
}
setDefaultTargetUrl("/general/main.xhtml");
super.onLogoutSuccess(wrappedRequest, response, authentication);
}
}
and the relevant change to the applicationContext:
<http>
<logout logout-url="/j_spring_security_logout"
success-handler-ref="myCustomLogoutSuccessHandler"
invalidate-session="true"/>
</http>
<beans:bean id="myCustomLogoutSuccessHandler" class="com.examples.MyCustomLogoutSuccessHandler"/>

Resources