I am building a Spring Cloud gateway and trying to logout keycloak but it is giving me cors errors, my code it as below:
Security class in which I defined logout code logic:
#Bean
public ServerSecurityContextRepository securityContextRepository() {
WebSessionServerSecurityContextRepository securityContextRepository =
new WebSessionServerSecurityContextRepository();
securityContextRepository.setSpringSecurityContextAttrName("langdope-security-context");
return securityContextRepository;
}
private LogoutWebFilter logoutWebFilter() {
LogoutWebFilter logoutWebFilter = new LogoutWebFilter();
SecurityContextServerLogoutHandler logoutHandler = new SecurityContextServerLogoutHandler();
logoutHandler.setSecurityContextRepository(securityContextRepository());
RedirectServerLogoutSuccessHandler logoutSuccessHandler = new RedirectServerLogoutSuccessHandler();
logoutSuccessHandler.setLogoutSuccessUrl(URI.create("http://localhost:9000/app/Default"));
logoutWebFilter.setLogoutHandler(logoutHandler());
logoutWebFilter.setLogoutSuccessHandler(logoutSuccessHandler);
logoutWebFilter.setRequiresLogoutMatcher(
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/app/logout")
);
return logoutWebFilter;
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,ReactiveClientRegistrationRepository repository) {
// Authenticate through configured OpenID Provider
http.addFilterAfter(new CustomWebFilter(), SecurityWebFiltersOrder.LAST).authorizeExchange()
.pathMatchers("/app/logout").permitAll()
.pathMatchers("/app/authenticate").authenticated()
.pathMatchers("/app/**").authenticated().and().
logout().disable()
.securityContextRepository(securityContextRepository())
.addFilterAt(logoutWebFilter(), SecurityWebFiltersOrder.LOGOUT)
.oauth2Login(Customizer.withDefaults());
// Also logout at the OpenID Connect provider
http.httpBasic().disable();
// Require authentication for all requests
// http.authorizeExchange().anyExchange().authenticated();
// Allow showing /home within a frame
http.headers().frameOptions().mode(XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN);
// Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
http.csrf().disable();
return http.build();
}
Now when from front-end I hit logout it gives me below error:
Access to XMLHttpRequest at 'http://localhost:8280/auth/realms/Default/protocol/openid-connect/auth?response_type=code&client_id=Default&scope=openid%20email%20profile&state=qVQ46iGilTo9o2Ro7CdZzl9kmsMm23jnEqckybucgII%3D&redirect_uri=http://localhost:9000/login/oauth2/code/keycloak&nonce=Z6hMnfYEJaOpuJnX44obCe6GyW8Oc6FSn3MOU_2bRg4' (redirected from 'http://localhost:9000/app/logout') from origin 'http://localhost:9000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
In Keycloak for valid URL I have given * to test but still not working. What am I missing?
I've spent my share of time figuring out keycloak CORS errors and this is what I figured out.
If you have web origins correctly configured (https://stackoverflow.com/a/59072362/20992932) and you are still getting CORS errors there is high probability that the request you are sending is incorrect (jboss handles error before checking client web origin).
To find out if that is the case for you the easiest solution would be to disable same origin policy on the browser (of course for the time of the testing only). Then in network console you should see the actual error response.
Here is how to do it in chrome based browsers:
chromium-browser --disable-web-security --user-data-dir="[some directory here]"
For more see:
https://stackoverflow.com/a/59072362/20992932
Related
I am currently using a rather simple approach to restrict a certain suburl (everything under /api/rest) and all of its subpaths via WebFluxSecurity. Some paths (everything directly under the root NOT in /api/rest) are excluded so that they can be access without authorization. However, sometimes the accessing party might send an empty authorization header which leads to unsecured endpoints returning a 401.
See the relevant code here:
#Configuration
#EnableWebFluxSecurity
public class SecurityConfiguration {
#Value(value = "${...}")
private String user;
#Value(value = "${...}")
private String pw;
#Bean
public MapReactiveUserDetailsService userDetailsService() {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
UserDetails user = User
.withUsername(user)
.password(encoder.encode(pw))
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/api/rest/**")
.authenticated()
.anyExchange()
.permitAll()
)
.httpBasic(withDefaults());
return http.build();
}
}
On stackoverflow I've only found a few suggestions how to handle this with WebSecurity. However, this is not possible for me as I use webflux security.
See e.g.
Springboot webflux throwing 401 when Authorization header sent to unrestricted endpoint
Spring Boot 2: Basic Http Auth causes unprotected endpoints to respond with 401 "Unauthorized" if Authorization header is attached
TL;DR
If you pass invalid credentials to any endpoint with httpBasic() enabled, it will return a 401 response.
One important distinction that's relevant here is the difference between authentication and authorization. The httpBasic() DSL method adds the AuthenticationWebFilter configured for HTTP Basic. The authorizeExchange(...) DSL method defines authorization rules, such as authenticated() and permitAll().
The authentication filter appears earlier in the Spring Security filter chain than the authorization filter, and so authentication happens first which we would expect. Based on your comments, it seems you are expecting authentication not to happen if you mark an endpoint as permitAll(), but this is not the case.
Whether authentication is actually attempted against a particular request depends on how the authentication filter matches the request. In the case of AuthenticationWebFilter, a ServerWebExchangeMatcher (requiresAuthenticationMatcher) determines whether authentication is required. For httpBasic(), every request requires authentication. If you pass invalid credentials to any endpoint with httpBasic() enabled, it will return a 401 response.
Additionally, a ServerAuthenticationConverter (authenticationConverter) is used to read the Authorization header and parse the credentials. This is what would fail if an invalid token (or Authorization header) is given. ServerHttpBasicAuthenticationConverter is used for httpBasic() and is fairly forgiving of invalid header values. I don't find any scenarios that fail and produce a 401 response except invalid credentials.
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.
I am using Angular6 as FrontEnd running on http:// localhost:4200 and Spring Boot (which has in built Tomcat server) as backend (exposing a GET Rest API) running on https:// localhost:8083/ldap . When I run this, I am getting CORS policy error on the browser. So I searched on internet and tried multiple fixes which were suggested on internet. I am not sure what I am missing on each of the solution below.
Unsuccessful Fix 1: I tried to run it via proxy.
-> Created a proxy.config.json in parallel to package.json with below
content.
{
"/ldap/": {
"target": "https://localhost:8083",
"secure": false,
"logLevel": "debug",
"changeOrigin": true
}
}
-> Added below entry in package.json inside script block there.
"start":"ng serve --proxy-config proxy.config.json",
-> In the service class, tried calling my spring boot backend rest API
like below.
return this.http.get('/ldap');
Now when I run my app, I got below error:
GET http:// localhost:4200/ldap 404 (Not Found) : zone.js:3243
Unsuccessful Fix 2: I added below headers before calling the Rest API in my frontend.
getUserAuthenticatedFromLDAP() {
const httpOptions = {
headers: new HttpHeaders({
'crossDomain': 'true',
'mode' : 'cors',
'allowCredentials': 'true',
'origins': '',
'allowedHeaders': '',
'Access-Control-Allow-Origin': '',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept',
'Access-Control-Max-Age': '86400'
})
};
return this.http.get('https://localhost:8083' , httpOptions);
}
Access to XMLHttpRequest at 'https://localhost:8083/' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
Unsuccessful Fix 3: Rather than making changes at front end, I tried to make changes at API level by adding below code at controller level.
#Controller
#CrossOrigin(origins = "http://localhost:4200")
public class WelcomeController {
// This is for LDAP Authentication
#GetMapping("/ldap")
#ResponseBody
public Authentication hello() {
return LdapSecurity.getAuthentication();
}
}
Here I am getting below error again:
Access to XMLHttpRequest at 'https://localhost:8083/' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
More unsuccessful fixes:
I even tried to change headers of Tomcat using application.properties file but could not find sufficient example or piece of code to make any change.
On internet, some people suggested to implement filter on API level but I am not sure that in whcih class I need to add those overriden filter method. I am stuck in this issue for last 2 days.
PS: I see some people have implemented CORS filter at API layer or implemented class like below. Now my question is if I implement below class then where do I need to refer this class ? Is it web.xml or any other place.
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
I was using Spring security feature for LDAP authentication. But now I removed Spring security feature for LDAP and used a basic program in java to make a connection with LDAP. After that I used CrossOrigin tag in controller layer and now I am not getting CORS issue. Thanks you all for your help.
please have a look here, you need to enable cors in your method/controller
#CrossOrigin(origins = "http://localhost:9000")
#GetMapping("/greeting")
public Greeting greeting(#RequestParam(required=false, defaultValue="World") String name) {
System.out.println("==== in greeting ====");
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
Below is my SecurityConfiguration class, I am using.
#SpringBootApplication
#RestController
public class WebMvcConfig {
#Configuration
protected static class SecurityConfiguration extends
WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/**").authenticated()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
}
After startup, as soon as I hit my URL (http://localhost:8080/TestApp/), it takes me to the default login page and when I enter the default user Id (user) and password (printed on the console), it take me to my index.html page mapped by the "/" URL via my AngularJS routing. I am able to navigate through the UI but as soon as I submit any $http request (I am trying with a POST request), it gives me 403 on the browser console with my $http request URL.
Can someone help?
Error 403 means that you are forbidden from accessing all the resources.
If you inspect the error details, more probably you will have a message such as Expected CSRF token not found.
From v4 onwards, spring security enables by default csrf protection. This is a good practice as csrf attacks force an end user to execute unwanted actions on a web application in which they’re currently authenticated.
So in a dev environment, adding http.csrf().disable(); will solve your problem. But you should consider adding a csrf token when you want to move to a prod env.
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.