[keycloak-user] Spring Security Adapter: Set locale on redirect to login page + Link back to application from login form

Danny Trunk dt at zyres.com
Thu Apr 13 05:38:13 EDT 2017


Hello everyone,

1. Set locale on redirect:

We have a multilingual application where you can choose your locale.
The login entry point then looks like 
https://localhost:8443/en_US/login.html

Now I need to tell Keycloak which locale to use.
The way I realized it isn't really clean:

I'm extending from KeycloakWebSecurityConfigurerAdapter and overriding 
the keycloakAuthenticationProcessingFilter method in order to 
instantiate my own authentication processing filter implementation:
@Bean
@Override
protected KeycloakAuthenticationProcessingFilter 
keycloakAuthenticationProcessingFilter() throws Exception {
     RequestMatcher requestMatcher = new OrRequestMatcher(new 
AntPathRequestMatcher("/*/login.html"), new 
RequestHeaderRequestMatcher(KeycloakAuthenticationProcessingFilter.AUTHORIZATION_HEADER));
     KeycloakAuthenticationProcessingFilter filter = new 
LocaleAwareKeycloakAuthenticationProcessingFilter(keycloakAuthenticationManager(), 
requestMatcher);
filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
     return filter;
}

The custom authentication processing filter is the following:
public class LocaleAwareKeycloakAuthenticationProcessingFilter extends 
KeycloakAuthenticationProcessingFilter implements ApplicationContextAware {
     private final Logger log = LogManager.getLogger(getClass());

     private ApplicationContext applicationContext;
     private AdapterDeploymentContext adapterDeploymentContext;
     private AdapterTokenStoreFactory adapterTokenStoreFactory = new 
SpringSecurityAdapterTokenStoreFactory();

     public 
LocaleAwareKeycloakAuthenticationProcessingFilter(AuthenticationManager 
authenticationManager, RequestMatcher 
requiresAuthenticationRequestMatcher) {
         super(authenticationManager, requiresAuthenticationRequestMatcher);
     }

     @Override
     public void afterPropertiesSet() {
         super.afterPropertiesSet();
         adapterDeploymentContext = 
applicationContext.getBean(AdapterDeploymentContext.class);
     }

     @Override
     public Authentication attemptAuthentication(HttpServletRequest 
request, HttpServletResponse response) {
         log.debug("Attempting Keycloak authentication");

         HttpFacade facade = new SimpleHttpFacade(request, response);
         KeycloakDeployment deployment = 
adapterDeploymentContext.resolveDeployment(facade);
         AdapterTokenStore tokenStore = 
adapterTokenStoreFactory.createAdapterTokenStore(deployment, request);
         RequestAuthenticator authenticator = new 
LocaleAwareRequestAuthenticator(facade, request, deployment, tokenStore, 
-1);

         AuthOutcome result = authenticator.authenticate();
         log.debug("Auth outcome: {}", result);

         if (AuthOutcome.FAILED.equals(result)) {
             throw new KeycloakAuthenticationException("Auth outcome: " 
+ result);
         } else if (AuthOutcome.AUTHENTICATED.equals(result)) {
             Authentication authentication = 
SecurityContextHolder.getContext().getAuthentication();
             Assert.notNull(authentication, "Authentication 
SecurityContextHolder was null");
             return getAuthenticationManager().authenticate(authentication);
         } else {
             AuthChallenge challenge = authenticator.getChallenge();
             if (challenge != null) {
                 challenge.challenge(facade);
             }

             return null;
         }
     }

     @Override
     public void setApplicationContext(ApplicationContext 
applicationContext) {
         super.setApplicationContext(applicationContext);
         this.applicationContext = applicationContext;
     }

     @Override
     public void setAdapterTokenStoreFactory(AdapterTokenStoreFactory 
adapterTokenStoreFactory) {
         super.setAdapterTokenStoreFactory(adapterTokenStoreFactory);
         this.adapterTokenStoreFactory = adapterTokenStoreFactory;
     }
}

The custom request authentication is the following:
public class LocaleAwareRequestAuthenticator extends 
SpringSecurityRequestAuthenticator {
     public LocaleAwareRequestAuthenticator(HttpFacade facade, 
HttpServletRequest request, KeycloakDeployment deployment, 
AdapterTokenStore tokenStore, int sslRedirectPort) {
         super(facade, request, deployment, tokenStore, sslRedirectPort);
     }

     @Override
     protected OAuthRequestAuthenticator createOAuthAuthenticator() {
         return new LocaleAwareOAuthRequestAuthenticator(this, facade, 
deployment, sslRedirectPort, tokenStore);
     }
}

And finally the LocaleAwareOAuthRequestAuthenticator is the following:
public class LocaleAwareOAuthRequestAuthenticator extends 
OAuthRequestAuthenticator {
     public LocaleAwareOAuthRequestAuthenticator(RequestAuthenticator 
requestAuthenticator, HttpFacade facade, KeycloakDeployment deployment, 
int sslRedirectPort, AdapterSessionStore tokenStore) {
         super(requestAuthenticator, facade, deployment, 
sslRedirectPort, tokenStore);
     }

     @Override
     protected String getRedirectUri(String state) {
         String redirect = super.getRedirectUri(state);
         if (redirect == null) {
             return null;
         }

         // getting the locale from our relative path and appending to 
the redirect uri
         String url = facade.getRequest().getRelativePath();
         return redirect + "&kc_locale=" + 
ServletUtils.getLocaleFromURL(url).getLanguage();
     }
}

As you can see I had to override many methods and had to duplicate much 
code.

Is there really no other way to set the locale when redirecting to the 
login page?

---
2. Link back to application

And another problem I had to fight with: We only use the login page from 
Keycloak. All other stuff should happen in our application as there are 
some processes we don't want to copy. As we use a custom user storage 
provider which accesses the external db from our application this isn't 
a problem.

I had to make some template in order to set the URLs to link to our 
password reminder and registering pages.
In this case I'm using "${client.baseUrl}/${.locale}" as base URL to 
link back to pwreminder.html and register.html.
As ${client.baseUrl} isn't a mandatory field in the Keycloak Admin 
Console this isn't a clean way as well.
But there's no ${client.rootUrl} to access. So this is the only chance 
to unsafely link back to our application.

Why the client root url isn't accessible in the templates? Any good 
reason not to add it to the template data model?

---
If good solutions for those problems need to be implemented I'll take a 
look at the code, opening issues and providing a pull requests on GitHub.


More information about the keycloak-user mailing list