SpringBoot Webflux - How to ignore requests with empty authorization headers - java

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.

Related

Keycloak logout give cors errors

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

Spring boot security passes bad credentials

I created a very simple http basic security for a Springboot app, the app deploys and I put the user and password. The problem is that if I call again with a different password, the request still counts as correct, instead of rejecting it.But if I change the user, then the application rejects my request and waits for the correct username and password.
my code:
#Configuration
public class SecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().anyRequest().authenticated().and().httpBasic();
return http.build();
}
}
The result with good credentials: The json output is as expected
The result with bad credentials: The json output is still as if it was succesfull
The result with different user: The app behaves like expected
This is expected for httpBasic(), see the BasicAuthenticationFilter.
After the first request, you should have a JSESSIONID cookie, which will be used to authenticate further requests. This behavior avoids doing the basic authentication flow every time a request comes in. The only reason to reauthenticate is if the HTTP Basic username is different from the authenticated user's username.

My spring boot does not validate JWT token with the Keycloak?

I have a Spring Boot + Keycloak project and I found out that the Spring Boot does not validate the JWT with the keycloak. For example if I get a token from Keycloak and turn off the Keycloak, I still can use this JWT token to access my end points. I have this security configurer class:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
#RequiredArgsConstructor
public class KeycloakSecurityConfigurer extends WebSecurityConfigurerAdapter {
private final RoleConverter converter;
#Value("${spring.security.oauth2.keycloak.jwt.issuer-uri}")
private String issuerUri;
#Override
public void configure(final HttpSecurity http) throws Exception {
http.headers().frameOptions().disable()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2ResourceServer(
oauth2ResourceServer -> oauth2ResourceServer.jwt(
jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));
http.authorizeRequests().antMatchers("/**").authenticated();
}
private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtConverter;
}
#Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromOidcIssuerLocation(issuerUri);
}
}
The "converter" is nothing special, just extracts the roles out of JWT token and returns a list of them.
How to force the Spring Security to validate the JWT token?
application.yml:
spring:
security:
oauth2:
keycloak:
jwt:
issuer-uri: http://localhost:8180/auth/realms/test-realm
You can look at the implementation of JwtDecoders.fromOidcIssuerLocation(issuerUri).
What is happening is that the keys are being fetched at the startup of your application and the application caches them in order to perform the validation after. With this in mind, even if you turn off Keycloak the JWT will still be validated because the keys are still cached.
The JWT tokens are been cached in your springboot application, this is the default cache store. In order to delete this token from your springboot app use should use some custom caches like redis cache to be configured in your app instead of default. There is no possible way to delete the tokens stored in default caches. The token will automatically get invalidate only after the timeout that's been set inside token
JWTs are meant to be validated offline, and it is what usually happens. The receiving application (consumer of JWT), does not need constant access to the Authorization Server in order to be able to validate a JWT. Even though Spring can't talk to your Keycloak, it does not mean that tokens are not validated. As others pointed out, Spring caches the keys used to validate JWTs' signature and will use the cache if it can.
If, for some reason, you want your service / API to validate the JWT online (maybe because you want to implement a mechanism to revoke tokens), you could switch to using opaque tokens with Token Introspection. On every request your service will have to call Keycloak to exchange the opaque token for a JWT. Mind that this solution will use much more resources, and you should use it only if you have strong reasons for it.

Spring security - oauth2 resource server tests

I am having some problems when testing an oauth2 resource server using #WebMvcTest and the POST HTTP method.
I always receive a 403 status code when I don't send the csrf token, even though the token is not required when I am using a bearer token.
Here is the POST method that I want to test.
#PostMapping("/message")
public String createMessage(#RequestBody String message) {
return String.format("Message was created. Content: %s", message);
}
Here is my security config:
http.authorizeRequests(authorizeRequests -> authorizeRequests
.antMatchers("/message/**")
.hasAuthority("SCOPE_message:read")
.anyRequest().authenticated()
).oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.jwt(withDefaults())
);
I am following the tests provided in the samples of spring-security.
The following test was supposed to pass but it fails because the csrf token is not sent in the request.
mockMvc.perform(post("/message").content("Hello message")
.with(jwt(jwt -> jwt.claim("scope", "message:read")))
.andExpect(status().isOk())
.andExpect(content().string(is("Message was created. Content: Hello message")));
When I add the csrf token to the request, the test passes:
mockMvc.perform(post("/message").content("Hello message")
.with(jwt(jwt -> jwt.claim("scope", "message:read")))
.with(csrf()))
.andExpect(status().isOk())
.andExpect(content().string(is("Message was created. Content: Hello message")));
When I run the application, there is no need to send a csrf token in the POST request.
I have forked the Spring Security GitHub repository and the project with this failing test is available at this link.
Is there a way for me to configure my tests so I don't need to send the csrf token in the POST request?
In order for the CSRF filter to detect that you are using a JWT token, you will need to include the JWT token in your request as an Authorization header, or as a request parameter.
The tests that you have mentioned have a mock JwtDecoder, which means you can use any string as your token and mock the decoded value.
Your test would then become:
Jwt jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("scope", "message:read")
.build();
when(jwtDecoder.decode(anyString())).thenReturn(jwt);
mockMvc.perform(post("/message")
.content("Hello message")
.header("Authorization", "Bearer " + jwt.getTokenValue()))
.andExpect(status().isOk())
.andExpect(content().string(is("Message was created. Content: Hello message")));
If you are not mocking the JwtDecoder then you would need to retrieve a valid bearer token and pass that in the Authorization header.
First with Bearer access-token, you might be able to disable sessions (STATELESS session-management), and, as so, CSRF protection (CSRF attack vector is session).
Second there is a .csrf() MockMvc request post-processor which would simulate the CSRF token if you want to keep cessions (and CSRF protection).
Last, what jwt() request post-processor does is configuring directly the security context with a JwtAuthenticationToken instance, not creating a valid JWT token set as Bearer access-token. With MockMvc, Authorization header is not decoded nor converted to an Authentication instance. => What you should set before MockMvc request execution for your test to pass are authorities, not scope claim:
.with(jwt(jwt -> jwt.authorities(List.of(new SimpleGrantedAuthority("SCOPE_message:read")))))
Note that you could also simply decorate your test function with:
#WithMockJwtAuth("SCOPE_message:read")
From this lib I maintain

Redirecting user to oauth2 authorization server to get token Spring Boot

I have 2 apps running, one is resource server where I have the info that needs authentication to view the text. Then I have authorization server that gives tokens. Right now I can use postman or Insomnia, add the auth_url, token_url, client_id, client_secret and I get the token. I add the token to header and i get do a get request to my resource server using header, and it works just fine.
Now i have no idea how to implement redirection from my resource server directly. Like when I go to
localhost:9000/home
I'd like to get redirected to:
localhost:9001/login
where I login with my inmemory user then it redirects me back to localhost:9000/home and I see the message.
What would be the best way to implement a way for user to access information on localhost:9000/home. You go to localhost:9000/home, it goes to authorization server on localhost:9001, you log in with username and password. Approve the grant, and it puts you back to localhost:9000/home and then you can see the text, what was previously protected, because you didn't have token to access it.
ResourceServer.java
#SpringBootApplication
#RestController
#EnableResourceServer
#EnableOAuth2Client
public class SampleResourceApplication extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**").hasRole("user")
.anyRequest().authenticated();
}
#Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
public static void main(String[] args) {
SpringApplication.run(SampleResourceApplication.class, args);
}
#RequestMapping("/home")
public String home() {
return "this is home";
}
}
and my properties looks like:
server:
port: 900
security:
oauth2:
client:
client-id: foo
client-secret: foosecret
access-token-uri: http://localhost:9001/auth/oauth/token
user-authorization-uri: http://localhost:9001/auth/oauth/authorize
grant-type: USER
auto-approve-scopes: true
resource:
user-info-uri: http://localhost:9001/auth/user
Let's separate the agents: You have the user (i.e. you, also know as the resource owner), the authorization server, the resource server and the client (the application that access your urls, i.e. your browser).
Normally, this happens in your situation:
When your client access the resource server, it receives a 401. Depending of your implementation, you could also directly redirect the client to your AS (using a simple redirect response).
Your AS prompts you for credentials. After validating them, it issues a token for you. You can then use this token to access the RS.
What you're trying to get (if I understand correctly) is to redirect with the token automatically. To achieve this, you can simply pass the url you tried to reach (i.e. localhost:9000/home) when you redirect to your AS at the end of step 1. Your AS hten prompts the user for credentials, generate the token, stores it as a cookie (in the case of a browser), and redirects you to the url he received (localhost:9000/home).
EDIT: what's the resulting code for the redirection.
When you get to the configure, you first check if the user is authenticated. If he is, then all's fine, but if he isn't, you must catch this event and start your redirection. This can be done using the exceptionHandling method of the chaining http:
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**").hasRole("user")
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
}
private AuthenticationEntryPoint authenticationEntryPoint() {
return new AuthenticationEntryPoint() {
// You can use a lambda here
#Override
public void commence(HttpServletRequest aRequest, HttpServletResponse aResponse,
AuthenticationException aAuthException) throws IOException, ServletException {
aResponse.sendRedirect(MY_AS_URL + "?redirect_uri=localhost:9001/home");
}
};
}
Unfortunately, I am not familiar with the Spring framework, but hopefully this helps anyways:
OAuth is an authorization protocol. It does not handle authentication (see also: "What is the difference between authentication and authorization?" on ServerFault).
If I understand you correctly, you want users to be redirected to /login when they go to /home and aren't already logged-in. This step has nothing to do with OAuth, it must be part of your application's security / firewall setup.
Please also note that there is a difference between logging in (authenticating) on the authorization server and actually granting the application the right to access your resources (authorization). First you have to prove who you are and only then can you give access to your stuff. These are two separate steps.

Categories