We have an API service which has multiple APIs exposed, and there are multiple personas who/which can access our service.
Users - Who needs to have an account in our system -> Needs to be
authenticated with our Identity Provider Service (Keycloak) with JWT
token.
Regulated System - Which needs to be authenticated with central
authority maintained by some party.
Internal service to service communication -> authentication with same
Keycloak.
Temporary JWT token issued by the same service before creating the user
account when the user digitally verified the mobile number.
I was trying to have AuthenticationWebFilter for each authentication type, and configure with Pathmatchers, though it was getting authenticated by the right authentication web filter, the request keeps flowing through the other authentication filter, and ends up resulting as unauthorized.
Snippet of configuration:
public class Configuration {
#Bean
#Order(1)
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity httpSecurity,
#Qualifier("userCreationFilter")
AuthenticationWebFilter userCreationFilter) {
final String[] WHITELISTED_URLS = {"/**.json",
"/users/verify",
"/users/permit",
"/sessions",
"/internal/xxxxx/**",
"/**.html",
"/**.js",
"/**.yaml",
"/**.css",
"/**.png"};
httpSecurity.authorizeExchange().pathMatchers(WHITELISTED_URLS).permitAll();
httpSecurity.addFilterBefore(userCreationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.authorizeExchange()
.pathMatchers("/users")
.authenticated();
httpSecurity.httpBasic().disable().formLogin().disable().csrf().disable().logout().disable();
return httpSecurity.build();
}
#Bean
#Order(2)
public SecurityWebFilterChain securityWebFilterChain2(ServerHttpSecurity httpSecurity,
#Qualifier("managerFilter")
AuthenticationWebFilter managerFilter) {
httpSecurity.addFilterBefore(managerFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.authorizeExchange()
.pathMatchers("/xxxxx/**",
"/providers",
"/xxxxx/**/approve",
"/xxxx/**/xxxxx").authenticated();
return httpSecurity.build();
}
}
Right now there are no roles as such we have.
I tried keeping all configuration in single SecurityWebFilterChain Bean, and tried addWebFilterAt, but no luck.
What am I missing? Should I do it different way?
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.
I'm trying to persist user in http session and verify authentication request inside Gateway by using a custom filter. I found a similar question too:
SecurityConfig:
#Configuration
public class SecurityConfig {
#Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http,
MyAuthenticationFilter myAuthenticationFilter
) {
http
.csrf()
.disable()
.authorizeExchange()
.pathMatchers("/**")
.permitAll()
.and()
.addFilterAt(myAuthenticationFilter, SecurityWebFiltersOrder.FIRST); // custom filter
return http.build();
}
MyAuthenticationFilter:
#Component
public class MyAuthenticationFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
exchange.getSession().map(
session -> {
session.getAttributes().put("userId", "id123");
// It does not print anything
System.out.println("userId in session: " + session.getAttribute("userId"));
return session;
}
);
return chain.filter(exchange);
}
}
By adding a custom filter, and attempting to read/write session attribute, as I observed in debug mode, the function inside map() never gets executed, and nothing gets print out in the terminal. (Unsurprisingly, downstream service cannot read userId from session even though both gateway and service share the same session).
Why is it not working? Here's a minimal reproduced version: Github repo, please take a look.
Found a workaround:
By setting up a servlet application as a downstream service that does authorization, and then reading/writing session inside the "authorization service" will be relatively easier (since the gateway and the direct downstream service will be sharing the same session).
As to the whole authentication/authorization part in this microservice architecture, I found out that using JWT is more preferable, and it's recommended that it should be stateless in between services, while gateway can be stateful, so as to share session with the said "authorization service".
A great answer explaining JWT implementation visually.
In a multi-tenancy Spring boot web app (with spring security 5), I need to make http requests to externals services secured by oauth2.
Each client using the same app can has multiple oauth2 config depending on external services it can access.
There is how I instantiate my singleton WebClient:
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository = new HttpSessionOAuth2AuthorizedClientRepository();
// oAuth2ClientRegistrationDAO is my custom bean implements ClientRegistrationRepository defined below
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(oAuth2ClientRegistrationDAO, oAuth2AuthorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
ServletOAuth2AuthorizedClientExchangeFilterFunction filter = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
authorizedClientManager);
// WebClient to be used to communicate thought oauth2 secured webservices
WebClient webClient = WebClient.builder().apply(filter.oauth2Configuration()).build();
This is my custom bean implements ClientRegistrationRepository used to provide right Oauth2 parameters depending on registrationId value:
#Repository
public class OAuth2ClientRegistrationDAO implements ClientRegistrationRepository {
#Override
public ClientRegistration findByRegistrationId(String registrationId) {
// Problem: auth object is NULL!
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println(auth); // print: null
// TODO get ClientRegistration from right database
// (depending logged tenant, fetched from Authentication object) and return them
}
}
The problem is when I do a request like this:
// Here, auth is not null: Ok!
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
webClient.get()
.uri("[External webservice url secured with oauth2]")
.attributes(
ServerOAuth2AuthorizedClientExchangeFilterFunction
.clientRegistrationId("[registrationId]"))
.retrieve()
.bodyToMono(String.class)
.block();
My Authentication object of logged user is null into OAuth2ClientRegistrationDAO because on asynchronous implementation of WebClient, method findByRegistrationId() is called on another thread than this used by Authenticated spring app client.
Edit: I found a potential solution using SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); Authentication context is still present on other thread with this configuration
I have a api gateway to my rest api micro service. the gateway is implemented using the spring cloud gateway project. I want to enable CSRF on the api gateway. I used the below code provided in the documentation to enable it.
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()));
return http.build();
}
To log in to my app, the GUI makes a POST api request to my rest web service, which goes through the api gateway. This call is blocked with the message "An expected CSRF token cannot be found".
So I wanted to permit only the login request and hence made the changes as below.
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
.authorizeExchange().pathMatchers("/login")
.permitAll();
return http.build();
}
now when I restart my application, it does not go to my landing page, instead provides its own log in page.
Below is my entire configuration. I have angular running my GUI.
#Configuration
#EnableWebFluxSecurity
public class NettyConfiguration implements
WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
#Value("${server.max-initial-line-length:65536}")
private int maxInitialLingLength;
#Value("${server.max-http-header-size:65536}")
private int maxHttpHeaderSize;
public void customize(NettyReactiveWebServerFactory container) {
container.addServerCustomizers(
httpServer -> httpServer.httpRequestDecoder(
httpRequestDecoderSpec -> {
httpRequestDecoderSpec.maxHeaderSize(maxHttpHeaderSize);
httpRequestDecoderSpec.maxInitialLineLength(maxInitialLingLength);
return httpRequestDecoderSpec;
}
)
);
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
.authorizeExchange().pathMatchers("/login")
.permitAll();
return http.build();
}
}
Check Disable authentication and csrf for a given path in Spring Weblux?.
The gist of it is that authorizeRequests() does not care about csrf. You should use requireCsrfProtectionMatcher instead to which urls would be subject to CORS verification
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.