Undertow servlet handler looses already authenticated and valid SecurityContext managed by keycloak - java

I'm trying to enable role based access control on a rest end point that I've setup using undertow, jersey and CDI. I initialize the servlet deployment as follows:
DeploymentInfo servletBuilder = Servlets.deployment()
.setClassLoader(Main.class.getClassLoader())
.setContextPath("/rest")
.setDeploymentName("sv.war")
.addListeners(listener(Listener.class))
.setLoginConfig(new LoginConfig("KEYCLOAK", "some-realm"))
.setAuthorizationManager(auth) // my dummy for testing
.addServlets(servlet("jerseyServlet", ServletContainer.class)
.setLoadOnStartup(1)
.addInitParam("javax.ws.rs.Application", SystemViewApplication.class.getName())
.addMapping("/api/*"));
I enabled kecloak authentication based on this example code.
So, my server is started as:
DeploymentManager manager = Servlets.defaultContainer().addDeployment(servletBuilder);
manager.deploy();
PathHandler path = Handlers.path(Handlers.resource(staticResources).setDirectoryListingEnabled(false).setWelcomeFiles("index.html"))
.addPrefixPath("/rest", manager.start());
Undertow server = Undertow.builder()
.addHttpListener(8087, "localhost")
.setHandler(sessionHandling(addSecurity(exchange -> {
final SecurityContext context = exchange.getSecurityContext();
if (!context.isAuthenticated()) {
exchange.endExchange();
return;
}
log.info("Authenticated: {} {} {}", context.getMechanismName(), context.getAuthenticatedAccount().getPrincipal().getName(), context.getAuthenticatedAccount().getRoles());
// propagate the request
path.handleRequest(exchange);
})))
.build();
server.start();
Where the two methods sessionHandling() and addSecurity() are lifted from the example I've linked above.
The authentication works, I am forced to log in, and the Authenticated: .. logging line is printed out with the correct details. But, once it hits the servlet handling, the security context (and account) is lost. I've traced this call and I can see that at some point along the path, it's replaced by brand new SecurityContext which has a null account.
Now my question - is there some authentication mechanism that I am missing that would propagate the state after the keycloak authentication or can I just fix the undertow code and in the SecurityContext, if the passed in context is already correctly authenticated, accept that state and move on? (the latter doesn't seem right, I'm guessing it's because the could be different authentication for the servlet deployment?) If so, is there any way to connect the servlet deployment to see the keycloak authentication has already happened?

Incase anyone comes looking here on how to authenticate servlets properly with keycloak and use role based authentication, this worked for me (note, this worked for me without the requirement of any xml files, purely with annotations.
First in the servlet application (wherever you extended ResourceConfig) register() the RolesAllowedDynamicFeature.class.
Also enable "use-resource-role-mappings": true in keycloak.json.
Next, instantiate the servlet deployment with an initial security wrapper:
DeploymentInfo servletBuilder = Servlets.deployment()
.setClassLoader(Main.class.getClassLoader())
.setContextPath("/")
.setDeploymentName("sv.war")
.addListeners(listener(Listener.class))
.setIdentityManager(idm)
.setSessionManagerFactory(new InMemorySessionManagerFactory())
.setInitialSecurityWrapper(handler -> sessionHandling(addSecurity(handler)))
.setResourceManager(staticResources)
.addWelcomePage("index.html")
.addServlets(servlet("jerseyServlet", ServletContainer.class)
.setLoadOnStartup(1)
.addInitParam("javax.ws.rs.Application", SystemViewApplication.class.getName())
.addMapping("/api/*"));
DeploymentManager manager = Servlets.defaultContainer().addDeployment(servletBuilder);
manager.deploy();
Undertow server = Undertow.builder()
.addHttpListener(8087, "localhost")
.setHandler(Handlers.path(manager.start()))
.build();
server.start();
Where sessionHandling(addSecurity(handler)) is basically the code from the linked github repo.
Now authentication via keycloak will work, and also role based authentication will work, so for example, if you have a CDI injected rest end point, such as:
#RolesAllowed({"admin", "guest"})
#GET
#Path("/{id}")
public Response findById(#PathParam("id") #NotNull Integer id){
// some method
}
As long as the roles are configured in keycloak, it should work.

Related

confirming the proxy is used with Netty - issue with wiretap

My Spring Boot 2.7.1 application needs to use 2 different Oauth2 webclients, that each has its own identity provider. One of them needs to go through a proxy, but not the other.
For the one going through the proxy, I build it like this :
#Bean
#Qualifier("systemA")
WebClient getWebClientForSystemA(OAuth2AuthorizedClientManager authorizedClientManager,
#Value("${asset-sync-service.systemA-proxy.host}") String proxyHost,
#Value("${asset-sync-service.systemA-proxy.port}") int proxyPort) {
var oauth = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth.setDefaultClientRegistrationId("systemA");
var webClientBuilder=WebClient.builder()
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.apply(oauth.oauth2Configuration());
if(StringUtils.isNotEmpty(proxyHost)){
log.info("setting proxy setting ({}:{}) on webclient for systemA webclient..",proxyHost,proxyPort);
var httpClientWithSystemAProxy=HttpClient.create()
.wiretap("systemAWebClient",LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL)
.proxy(proxy -> proxy.type(Proxy.HTTP)
.host(proxyHost)
.port(proxyPort));
webClientBuilder=webClientBuilder
.clientConnector(new ReactorClientHttpConnector(httpClientWithSystemAProxy));
}
return webClientBuilder.build();
}
The first time the webClient is called and tries to get a token, it fails with :
Caused by: org.springframework.security.oauth2.core.OAuth2AuthorizationException: [invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: I/O error on POST request for "https://systemA.my.company/connect/oauth2/access_token": systemA.my.company; nested exception is java.net.UnknownHostException: systemA.my.company
(when I try access https://systemA.my.company/connect/oauth2/access_token in my browser, it gets resovled)
I added the wiretap in the HttpClient, because I have the feeling it's not going through the proxy, and I want to see more logs. But unfortunately, I don't see anything in my logs, despite setting Logback root logger at DEBUG level.
am I doing something wrong, either ín the wiretap config or in the config of the proxy ?
I haven't been able to fix the missing logs that I expect with wiretap, but the proxy issue is solved, thanks to
https://stackoverflow.com/a/65790535/3067542
https://blog.doubleslash.de/spring-oauth2-client-authorization-request-proxy-support/
we need to configure the proxy also in the underlying restTemplate that is used by OAuth2AuthorizedClientManager, in addition to configuring it in the webClient. Not obvious at all !

Spring Boot - How to fix Session bean with cors and spring security

TL;DR
Everytime localhost:4200 (through cors filter) makes a http request to localhost:8080 it loses sessionscope bean which holds the authentications which basically makes it failing all the calls with 403. Excluding the 1st http request (which isn't behind spring security)
I have a spring boot application that works well in localhost:8080.
We are creating an angular iframe inside of it, which also works well (when deployed on localhost:8080)
But when we do it on localhost:4200 (ng serve)
it wouldn't work
It started complaing about cors so i had the following configurations except everything about cors which i added.
#Configuration
#Profile({"dev"})
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class springDevConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
http.headers().cacheControl().disable();
http.cors().configurationSource(corsConfigurationSource())
.and().authorizeRequests().anyRequest().permitAll();
}
#Bean
public CorsConfigurationSource corsConfigurationSource(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(
ImmutableList.of("http://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList(
RequestMethod.GET.name(),
RequestMethod.POST.name(),
RequestMethod.OPTIONS.name(),
RequestMethod.DELETE.name(),
RequestMethod.PUT.name()));
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
My session bean is fairly simple
#Component
#SessionScope
public class UserSession {
//Holds the Authorities for the prePostEnabled
User user;
...
}
To init the user i make a request to a certain endpoint (unprotected) and do something like this in the code
...
User user = new User(id, "", authorities);
Authentication auth = new UsernamePasswordAuthenticationToken(
user, null,authorities));
SecurityContextHolder.getContext().setAuthentication(auth);
Usersession.setUser(user);
...
When i make a http request on localhost:8080, the subsequent http requests has the same session.
But when i try from localhost:4200 making a request to localhost:8080 every requests seems to fetch a different UserSession / opens a new session perhaps? (giving me 403 on all the protected endpoints)
What is really happening and why is localhost:4200 when making a request to localhost:8080 making a new session with each call? (and what configs should be changed to fix such an issue?)
Addendum 1º: If i comment
#EnableGlobalMethodSecurity(prePostEnabled = true)
The code works well for localhost:4200 (i mean he stops having 403 codes) but probabily still is using another session scope bean in each request
Addendum 2º:
It works now
All i had to do was put ssl in the ng serve configuration (which it had at localhost:8080 but not 4200) and JSessionId started working!
Can you show a bit more of how do you embed the iframe and what serves the original page?
What seems to be happening is that you're effectively making a cross domain request without realizing it. 8080 and 4200 are different domains, and if parts of the page get loaded from one and parts (i.e. the iframe) from another, it constitutes a cross-domain request and the cookies sent by one domain do not get applied to the requests to the other, hence the session scope is not shared. Additionally, if you're making Ajax requests to a domain other than the one the original page was loaded from, they'd get rejected by default unless you set up CORS. This explains why you had to do that.
You must make sure all parts of the page (iframes and Ajax requests included) are loaded from the same domain, or that you have alternative means of attaching the session to the request. Normally, JSESSIONID cookie is used for this. Does this cookie get sent in the initial response?
You commonly have one app serving the front end including the initial page (e.g. the app on 4200), and one just responding to API calls (e.g. the app on 8080), with CORS configured. This way all the calls to the back end are done via the same domain and the cookies get applied normally.

Spring Boot - KeyCloak directed to 403 forbidden

I am new to Keycloak, I am using the official tutorial project on
https://github.com/sebastienblanc/spring-boot-keycloak-tutorial
for integrating with Springboot application, I have setup the KeyCloak server successfully and the spring boot application also directing to the client application I have created on the Realm I have created on KeyCloak, after providing the correct credentials it directs to the forbidden page.
#Controller
class ProductController {
#GetMapping(path = "/products")
public String getProducts(Model model){
model.addAttribute("products", Arrays.asList("iPad","iPhone","iPod"));
return "products";
}
#GetMapping(path = "/logout")
public String logout(HttpServletRequest request) throws ServletException {
request.logout();
return "/";
}
}
Application.properties file
keycloak.auth-server-url=http://localhost:8080/auth
keycloak.realm=springdemo
keycloak.resource=product-app
keycloak.public-client=true
keycloak.security-constraints[0].authRoles[0]=testuser
keycloak.security-
constraints[0].securityCollections[0].patterns[0]=/products/*
server.port=8081
I am not getting any error message from KeyCloak console or spring embedded tomcat console.
Check the tomcat console here - no error
Thank you.
I think you have a typo at
keycloak.security-constraints[0].authRoles[0]=testuser , you should specify the role here and not the user.
If you follow the blogpost instructions it should be :
keycloak.security-constraints[0].authRoles[0]=user
In my case here I set use-resource-role-mappings to true, considering that it would provide both realm and client roles, but it turns out that if this option is set to true, only client roles are considered.
AFAICS, there is no way to use both.
I had the same issue and the problem was that I was using variables separated by dashes, instead of camel case. For example,
I had this (incorrect):
keycloak:
auth-server-url: http://localhost:8083/auth
realm: springdemo
resource: Resource_Name
public-client: true
security-constraints[0].auth-roles[0]: user
security-constraints[0].security-collections[0].patterns[0]: /
instead of (correct):
keycloak:
authServerUrl: http://localhost:8083/auth
realm: springdemo
resource: Resource_Name
publicClient: true
securityConstraints[0].authRoles[0]: user
securityConstraints[0].securityCollections[0].patterns[0]: /
I have tried this Week End to replay the example from the very interesting DEvoxx Sebastien speak.
I had the same 403 error with the role "user" specified in the property
keycloak.security-constraints[0].authRoles[0]=user
The "user" role does not exists in the default keycloak configuration. You have to create it before in your realm (realm/configuration/roles) and assign it to your user (realm/users/user/roles mappings).
About that tutorial, I just have a problem with logout feature.
Sometimes the logout does not work.
1) I click on logout and then I click on /products, then I am not redirected to keycloak login page
2) If I click on logout, then I refresh the browser page, then I click on /products I am redirected to the keycloak login page.
It seams to be that the logout implementation from HttpServletRequest is not enough to really logout the user ?
`
#GetMapping(path = "/logout")
public String logout(HttpServletRequest request) throws ServletException{
request.logout();
return "/";
}
`
If somebody has an explanation on that behavior between springboot and keycloak. Thank you.
Late to the party, but this might help someone.
In my case, I had resource authorization enabled (so client was not public). I had to do the following
Under Client
Authorization -> Settings -> Policy Enforcement Mode
Set it to "Permissive"
In my case I have to turn off Client Authentication and Authorization (both) in client config.

Dynamic domain in Spring Security SAML MetadataGenerator

we are running a Java Web application, that is accessible to the end users via two different domains ('domain1', 'domain2').
The application uses a remote provider of SAML authentication for the user login.
We are using the Spring Security SAML library to handle the SAML communication. One of the Spring beans created in our Spring configuration is the MetadataGenerator. We have set it up so that it sets the entityBaseURL always to be 'domain1'.
#Bean
public MetadataGenerator metadataGenerator() {
String entityBaseUrl = environment.getProperty("saml.entity.base.url");
MetadataGenerator metadataGenerator = new MetadataGenerator();
...
metadataGenerator.setEntityBaseURL(entityBaseUrl);
return metadataGenerator;
}
This causes a problem when the user accesses our application via 'domain2' URL. The SAML request is sent to the SAML provider, but once the user logs in at the Provider, she gets redirected to 'domain1', her session is lost and the SAML response is not properly mapped to the original request (at least this is how we interpret the problem).
We can see this exception in the log:
2016.11.02 15:30:24.076 [ajp-nio-8009-exec-31] DEBUG o.s.s.s.s.HttpSessionStorage - Message a511iei787cgc6bi2a91c4264e83c4h not found in session 2cbc5d3a-5061-410a-9518-505e8d22d5a3
2016.11.02 15:30:24.079 [ajp-nio-8009-exec-31] DEBUG o.s.s.s.SAMLAuthenticationProvider - Error validating SAML message
org.opensaml.common.SAMLException: InResponseToField of the Response doesn't correspond to sent message a511iei787cgc6bi2a91c4264e83c4h
at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:139) ~[spring-security-saml2-core-1.0.2.RELEASE.jar:1.0.2.RELEASE]
at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87) ~[spring-security-saml2-core-1.0.2.RELEASE.jar:1.0.2.RELEASE]
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167) [spring-security-core-4.0.2.RELEASE.jar:4.0.2.RELEASE]
...
Is there some way/best practise how to setup the MetadataGenerator (or some other component of the system) to avoid this problem?
TIA
Martin

How to authenticate the client service when using CAS in Spring Security?

I have created CAS server and the CAS Client application. Basically the authentication of a service app has been done using service ticket and proxy ticket validation. In this case, initially I validate a particular user by validation of the service ticket and then authorize a user for a particular app by generating a proxy ticket using the service ticket. In this case I am able to successfully validate the service and proxy tickets in the service application. The problem that I am facing is that once I validated the user I can retrieve the user information via Assertion object using the following code segment.
Assertion assertion = serviceTicketValidator.validate(serviceTicket);
Here the serviceTicektValidator object is a class that I have written by implementing the Cas20ServiceTicketValidator valdation functions. Here the assertion object is set to the SecurityContextHolder as follows.
CasAssertionAuthenticationToken token = new CasAssertionAuthenticationToken(assertion, serviceTicket);
SecurityContextHolder.getContext().setAuthentication(token);
Here this way, I am setting the authentication object. But the fact is once the request is changed or consider a new request, in the SecurityContextPersistenceFilter filter class in the following code segment the SecurityContext object is being cleared. So once a new request is fired the already stored authentication detail is vanished from the SecurityContextHolder object. So once every request the authentication details has to be set, because a particular service or proxy ticket can only be validated once. Here the approach taken by me is keeping the Assertion object and ticket generated in the Session and then at the beginning of this class (or actually I am doing all these in a filter) I check whether these values are in the session and again at each new request the CasAssertionAuthenticationToken is set to the SecurityContextHolder.
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
} finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything else.
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
The application is working the expected way, but I want to know whether there is a better way to implement this by using advanced techniques in Spring Filters or any other option. I also considered local storage of tickets and authentication information, but I want to do this within Spring Application even without using this session storage mechanism. I want to know whether this is possible or not?

Categories