Webflux with different authentication schemes - spring

I just got a project I need to maintain and I need to add support for an extra authentication scheme in a resource server. Something like besides regular Authentication: Bearer <jwt.token> to use a custom one: Authentication: Custom <other.jwt.token>. Both should work and handled differently.
Yes, I know spring can handle multiple providers, I know I can use a ReactiveAuthenticationManager but I am stuck in how to deal with the Custom prefix for the opaque token.
Just to make it clear, I need both to work - and, of course, to be handled differently:
GET /
Authorization: Bearer x.y.z
and
GET /
Authorization: Custom a.b.c
If possible, I'd like also to return the list of supported authentication protocols in WWW-Authorization header (i.e. Bearer, Custom).
Any hints? Googling only points me to regular stuff, with Bearer and whatever I try, spring automatically rejects me with 401 (of course, token is not handled).
Thanks.

What I did:
I implemented different ReactiveAuthenticationManager, one for each protocol I needed. Something like BearerReactiveAuthenticationManager and CustomReactiveAuthenticationManager and made them #Components;
I also implemented ServerSecurityContextRepository and injected both authentication managers from previous point. In the body I had something like:
#Override
public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
ServerHttpRequest request = serverWebExchange.getRequest();
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader.startsWith("Bearer ")) {
String authToken = authHeader.substring(7);
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
return this.bearerReactiveAuthenticationManager.authenticate(auth)
.map(SecurityContextImpl::new);
} else if (authHeader.startsWith("Custom ")) { {
String authToken = authHeader.substring(7);
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
return this.customReactiveAuthenticationManager.authenticate(auth)
.map(SecurityContextImpl::new);
} else {
log.debug("Could not identify the authentication header");
return Mono.empty();
}
}
And my SecurityConfig bean looked like this:
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
#Slf4j
public class SecurityConfig {
private final ServerSecurityContextRepository serverSecurityContextRepository;
#Autowired
public SecurityConfig(ServerSecurityContextRepository serverSecurityContextRepository) {
this.serverSecurityContextRepository = serverSecurityContextRepository;
}
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable()
.securityContextRepository(serverSecurityContextRepository)
.exceptionHandling()
.authenticationEntryPoint((swe, e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)))
.accessDeniedHandler((swe, e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN)))
.and().authorizeExchange();
return http.build();
}
}

Related

Basic Auth allowing users to pass through without credentails

I have written a Spring Security Class. But somehow it is not working as expected. I am trying to hit the Rest APIs via a Postman by selecting the Basic Auth method. And here is the scenario.
Correct username and password --> Works (I get 200 responses)
Incorrect username/password --> Works (I get 401 responses)
Select No Auth in Postman --> Doesn't Work (I should get 401, but it allows the request to pass through)
Now for #1 and #2 it works fine. Its the #3 that is the troublesome part. My Security code is written like:
#Configuration
#EnableWebSecurity
class SecurityConfig {
#Value("\${spring.security.user.name}")
private val userName : String? = null
#Value("\${spring.user.password}")
private val password : String? = null
#Autowired
lateinit var appAuthenticationEntryPoint: AppAuthenticationEntryPoint
#Bean
fun passwordEncoder(): PasswordEncoder {
return MyPasswordDelegation().createDelegatingPasswordEncoder()
}
#Bean
#Throws(Exception::class)
fun userDetailsService(): InMemoryUserDetailsManager? {
val userDetails : UserDetails = User.withUsername(userName).password(passwordEncoder().encode(password)).roles("USER").build()
return InMemoryUserDetailsManager(userDetails)
}
#Throws(Exception::class)
#Bean
fun filterChain(httpSecurity : HttpSecurity): SecurityFilterChain {
httpSecurity.csrf().disable()
// Allow only HTTPS Requests
httpSecurity.requiresChannel {
channel -> channel.anyRequest().requiresSecure()
}.authorizeRequests {
authorize -> authorize.antMatchers("/app-download/**").fullyAuthenticated()
.and()
.httpBasic()
.and()
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
}
return httpSecurity.build()
}
}
Can you please tell me what am I doing wrong here?

Getting the Spring Security "JWT Login Sample" to work with roles

I'm trying to rewrite a previous example with JWT's built with a custom JWT Filter into a simplified version based on Springs new authorization server and this example:
https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/jwt/login
The example sets up an InMemoryUserDetailsManager with a single user → user,password and an "app" authority so I assume it is designed to handle roles/authorities?
Everything works fine (as explained in the examples README) if I use the provided SecurityFilterChain
But if I change this:
...
http.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
Into this
...
http.authorizeHttpRequests((authorize) -> authorize
.antMatchers("/").hasRole("app")
//.antMatchers("/").hasAuthority("app")
.anyRequest().authenticated()
)
I get a 403 Status back
The authority gets added to the JWT as expected like this:
..
"scope": "app"
}
Apart from the antMatchers given above, my code is exactly as clone from the Spring Security example
What am I missing here?
OK, read the specs ;-)
Accoring to https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/jwt.html
Authorities gets prefixed with a SCOPE_
So this partly fixes the problem
.antMatchers("/").hasAuthority("SCOPE_app")
I still havent figured out how to use hasRoles?
To use hasRole, you need to have authorities which start with ROLE_. What you could do is register a converter which would read roles from JWT and add them as GrantedAuthority.
public class RolesClaimConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter wrappedConverter;
public RolesClaimConverter(JwtGrantedAuthoritiesConverter conv) {
wrappedConverter = conv;
}
#Override
public AbstractAuthenticationToken convert(#NonNull Jwt jwt) {
// get authorities from wrapped converter
var grantedAuthorities = new ArrayList<>(wrappedConverter.convert(jwt));
// get role authorities
var roles = (List<String>) jwt.getClaims().get("roles");
if (roles != null) {
for (String role : roles) {
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
}
return new JwtAuthenticationToken(jwt, grantedAuthorities);
}
}
Then register your converter in your security configuration
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(resourceServer -> resourceServer
.jwt()
.jwtAuthenticationConverter(
new RolesClaimConverter(
new JwtGrantedAuthoritiesConverter()
)
)
)
// other configuration
;
return http.build();
}
And that's it. All you need to do now is to pass a list of roles as a claim when creating JWT and you can use .antMatchers("/").hasRole("app") and #PreAuthorize("hasRole('app')") in your code.

Spring Boot REST API disable Form Login redirect

I have been banging my head against the wall for hours trying to figure out something that I would expect to work out of the box these days.
I am building an API with Spring Boot backend and I will create a react front end.
I only have one server so I dont need to use tokens. I want the same normal server side sessions and cookies.
I managed to get the Authentication to work but for some reason it keeps redirecting after success to the default / endpoint.
I really do not want this to happen and can't figure out why this is the case. I also can't find any decent resources on the internet of people that have encountered this issue.
I have seen a few videos where I have seen people handling the login in a Rest Controller end point rather than using filters. I assume this could work but then how would I implement session management?
Here is the code so far:
#Configuration
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Autowired
private AuthUserService authUserService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(authUserService);
}
#Bean
public CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/api/**").hasRole("AUTH_USER")
.mvcMatchers("/**").permitAll();
http.cors();
http.addFilterAfter(new CsrfHandlerFilter(), CsrfFilter.class);
AuthenticationFilter filter = new AuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
Authentication Filter:
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public AuthenticationFilter(){
super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("Custom Authentication Filter fired!");
ObjectMapper mapper = new ObjectMapper();
Login login = new Login();
try {
login = mapper.readValue(request.getInputStream(), Login.class);
} catch (StreamReadException e) {
e.printStackTrace();
} catch (DatabindException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
login.getUsername(),
login.getPassword()
);
return this.getAuthenticationManager().authenticate(token);
}
}
Login Model class:
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Login {
private String username;
private String password;
}
I want a normal server side session. I am not using JWT just because it is a JavaScript client. But all I want is for it to not redirect. Is this possible?
Any advice would be appreciated
There are a few ways to approach this, depending on your preference.
Certainly, you can stand up your own Spring MVC endpoint and set the SecurityContext yourself. Spring Security's SecurityContextPersistenceFilter will store the SecurityContext in an HttpSessionSecurityContextRepository by default, which induces the container to write a JSESSIONID session cookie that can be used on subsequent requests.
The main reason to go this route is if you want to have access to the MVC feature set when writing this endpoint.
One downside of this route is that Spring Security 6 will no longer save the security context for you when it comes to custom MVC endpoints, so you would need to be aware of that when upgrading.
HTTP Basic
That said, it doesn't seem like your requirements are so sophisticated that you can't use Spring Security's OOTB behavior.
One way to do this is with HTTP Basic. Note that for simplicity, I'll publish the SecurityFilterChain as a #Bean instead of using the now-deprecated WebSecurityConfigurerAdapter:
#Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.mvcMatchers("/api/**").hasRole("AUTH_USER")
.mvcMatchers("/**").permitAll()
)
.httpBasic(Customizer.withDefaults())
.cors(Customizer.witHDefaults())
.addFilterAfter(new CsrfHandlerFilter(), CsrfFilter.class);
return http.build();
}
This will allow you to send the username/password using the Authorization: Basic header. There's no need in this case for you to stand up anything custom. The filter chain will store the security
context in the session, and your Javascript can call endpoints using the JSESSIONID or by resending the username/password creds.
AuthenticationSuccessHandler
If for some reason you want to use form login (what your sample is customizing right now), instead of creating a custom filter, you can configure the existing form login filter with an AuthenticationSuccessHandler that does not redirect:
#Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.mvcMatchers("/api/**").hasRole("AUTH_USER")
.mvcMatchers("/**").permitAll()
)
.formLogin((form) -> form
.successHandler((request, response, authentication) ->
response.setStatusCode(200)
)
)
.cors(Customizer.witHDefaults())
.addFilterAfter(new CsrfHandlerFilter(), CsrfFilter.class);
return http.build();
}
Once again, the filter chain will save the subsequent UsernamePasswordAuthenticationToken to the session and issue a JSESSIONID for subsequent requests.

How to by pass Authorizatoin header for non secured endpoints in Spring

I am using spring webflux and security. I have 3 services A, B, C and two endpoints in service C as below
health - secured
status - shouldn't be secured
below is my webflux configuration
#Configuration
#EnableWebFluxSecurity
public class SecureConfiguration {
#Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("john")
.password("{noop}" + "password")
.roles("")
.build();
return new MapReactiveUserDetailsService(user);
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.csrf().disable()
.authorizeExchange()
.pathMatchers("/test/health")
.authenticated()
.pathMatchers("/**").permitAll()
.and().httpBasic();
return http.build();
}
}
Below scenarios are working
health endpoint with valid Authorization header
status endpoint without Authorization header
status endpoint with valid Authorization header
But when i access status endpoint with invalid Authorization header it's failing with Anuauthorized
How to avoid this? because the status call will be originated from Service A, there is different auth required for Service A to Service B which is being passed to Service C because of that it's failing but for Service B to Service C no need any auth for status endpoint.
I know we can do by create a fresh request without auth header but i want to know why spring security is not ignoring Authorization header for non secure endpoints.
When you use httpBasic() method, Spring Security configure an AuthenticationWebFilter with a default anyExchange matcher, which means that every request that comes to your spring application is going to go through this filter. Also, AuthenticationWebFilter has precedence over AuthorizationWebFilter (see Spring Security source code to know the order).
In some point the request reach this method (see Spring Security source code):
#Override
#Deprecated
public Mono<Authentication> apply(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (!StringUtils.startsWithIgnoreCase(authorization, "basic ")) {
return Mono.empty();
}
String credentials = authorization.length() <= BASIC.length() ?
"" : authorization.substring(BASIC.length(), authorization.length());
byte[] decodedCredentials = base64Decode(credentials);
String decodedAuthz = new String(decodedCredentials);
String[] userParts = decodedAuthz.split(":", 2);
if (userParts.length != 2) {
return Mono.empty();
}
String username = userParts[0];
String password = userParts[1];
return Mono.just(new UsernamePasswordAuthenticationToken(username, password));
}
Depending on which invalid header do you send, the result of this method could be:
An empty mono, which means the chain continues (this explains your 2 case).
An exception, here AuthenticationWebFilter is going to execute the ServerAuthenticationFailureHandler, this explains your failure case (see AuthenticationWebFilter):
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return this.requiresAuthenticationMatcher.matches(exchange)
.filter( matchResult -> matchResult.isMatch())
.flatMap( matchResult -> this.authenticationConverter.convert(exchange))
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.flatMap( token -> authenticate(exchange, chain, token))
.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler
.onAuthenticationFailure(new WebFilterExchange(exchange, chain), e));
}
By default this handler is going to ask again for the http basic headers.
The easiest way to bypass this, is implementing your own AuthenticationWebFilter, this will allow you to change the behavior.

Spring boot authorization returns 403 for any authorization request using #RolesAllowed, #Secured or #PreAuthorize

I've been working from this article (and a few other similar ones): https://medium.com/omarelgabrys-blog/microservices-with-spring-boot-authentication-with-jwt-part-3-fafc9d7187e8
The client is an Angular 8 app which acquires a Jwt from an independent microservice. Trying to add filter(s) to a different microservice to require specific authorization via jwt roles.
Consistently receiving 403 errors.
Security Config:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true,
securedEnabled = true,
jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurityConfig() {}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and().csrf().disable()
// make sure we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// Add a filter to validate the tokens with every request
.addFilterAfter(new JwtAuthorizationFilter2(), UsernamePasswordAuthenticationFilter.class)
// authorization requests config
.authorizeRequests()
// Any other request must be authenticated
.anyRequest().authenticated();
}
}
Filter:
public class JwtAuthorizationFilter2 extends OncePerRequestFilter {
private final String HEADER = "Authorization";
private final String PREFIX = "Bearer ";
private final String SECRET = "foo";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String token = request.getHeader(SecurityConstants.HEADER_STRING);
if (token != null) {
// parse the token.
DecodedJWT decoded = JWT.require(Algorithm.HMAC512(SecurityConstants.SECRET.getBytes()))
.build()
.verify(token.replace(SecurityConstants.TOKEN_PREFIX, ""));
String user = decoded.getSubject();
List<SimpleGrantedAuthority> sgas = Arrays.stream(
decoded.getClaim("roles").asArray(String.class))
.map( s -> new SimpleGrantedAuthority(s))
.collect( Collectors.toList());
if (sgas != null) {
sgas.add(new SimpleGrantedAuthority("FOO_Admin"));
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
user,
null,
sgas);
SecurityContextHolder.getContext().setAuthentication(auth);
}
else {
SecurityContextHolder.clearContext();
}
chain.doFilter(request, response);
}
}
}
This code works fine without any authorization requirements defined, but if an authorization is defined in WebSecurityConfig, or by decorating a controller method, http 403 is received for all requests in scope.
Examples:
.authorizeRequests().antMatchers("/**").hasRole("FOO_Admin")
// or any of these
#PreAuthorize("hasRole('FOO_Admin')")
#RolesAllowed({"FOO_Admin"})
#Secured({"FOO_Admin"})
Device get(#PathVariable String id) {
// some code
}
When code is halted at SecurityContextHolder.getContext().setAuthentication(auth),
auth.authenticated = true
and
auth.authorities includes a SimpleGrantedAuthority for "FOO_Admin"
So I'm wondering whether:
The FilterChain needs an Authentication Filter (or does authentication occur in JwtAuthorizationFilter2?)?
There is not a spelling or formatting or capitalization difference to role name.
I'm stupefied. Any help would be greatly appreciated.
#PreAuthorize("hasRole('FOO_Admin')) expects the user has an authority ROLE_FOO_Admin, which will be prefixed by ROLE_. However, the user only has the authority FOO_Admin , hence it fails to access the method.
You have several options:
(1) Change the prefix by declaring a GrantedAuthorityDefaults bean:
#Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("FOO");
}
And use #PreAuthorize(hasRole('Admin')) to secure the method.
(2) Or more simpler is to use #PreAuthorize("hasAuthority('FOO_Admin')") , which will directly check if the user has the authority FOO_Admin , without adding any prefix to it.
P.S JwtAuthorizationFilter2 only verifies if an user is valid and get the related user information which prepare for the authorization user later. It is an authentication and I would rename it to JwtAuthenticationFilter2 to describe more exactly what it does actually.

Resources