spring boot OAuth resource server JWT AudienceValidator 401 message empty - spring-boot

I'm using the Spring Boot OAuth Resource Server (JOSE) in order to validate the JWT from Auth0.
The flow is running fine except when I check the audience claim from JWT, if it's fail the 401 response status is return, but the message body is empty.
I customised the exceptionHandling to send a message body and it works fine using authenticationEntryPoint and accessDeniedHandler, but these handlings were not trigged for OAuth2TokenValidator.
I just want to receive the 401 and my customized message body for 401 when the audience validation error occurs.
Here is the code that I used:
public static class AudienceValidator implements OAuth2TokenValidator<Jwt> {
private Logger logger = LoggerFactory.getLogger(AudienceValidator.class);
private final String audience;
OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
public AudienceValidator(String audience) {
this.audience = audience;
}
public OAuth2TokenValidatorResult validate(Jwt jwt) {
logger.debug("validating audience: " + jwt.getAudience());
if (jwt.getAudience().contains(audience)) {
logger.debug("audience success");
return OAuth2TokenValidatorResult.success();
} else {
logger.debug("invalid audience");
return OAuth2TokenValidatorResult.failure(error);
}
}
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(grantedAuthoritiesExtractor())
)
);
logger.debug("authenticatedOnly:"+authenticatedOnly);
if (authenticatedOnly) http.authorizeRequests().anyRequest().authenticated();
else http.authorizeRequests().anyRequest().permitAll();
http
.exceptionHandling()
.authenticationEntryPoint((request, response, e) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.getWriter().write("{\"status\":401,\"error\":\"You are not authenticated.\",\"message\":\"\"}");
})
.accessDeniedHandler((request, response, e) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json");
response.getWriter().write("{\"status\":403,\"error\":\"You are not authorized.\",\"message\":\"\"}");
})
;
}

Related

Spring Boot Authentication succeeds with embedded Tomcat, but returns 403 with Open/WAS Liberty

I use Spring Security to authenticate/authorize against Active Directory. Below code works just fine if I run it in Spring embedded Tomcat.
But when I switch to Open/WAS Liberty server, I get 403 on authenticate (/auth endpoint):
My WebSecurityConfiguration class looks like:
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Value("${active.dir.domain}")
private String domain;
#Value("${active.dir.url}")
private String url;
#Value("${active.dir.userDnPattern}")
private String userDnPattern;
private final Environment environment;
public WebSecurityConfiguration(Environment environment) {
this.environment = environment;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(activeDirectoryAuthenticationProvider()).eraseCredentials(false);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf().disable()
.authorizeRequests()
.antMatchers("/auth").permitAll()
.anyRequest()
.authenticated()
.and()
.addFilter(getAuthenticationFilter())
.addFilter(new AuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
public AuthenticationProvider activeDirectoryAuthenticationProvider() {
String adSearchFilter = "(&(sAMAccountName={1})(objectClass=user))";
ActiveDirectoryLdapAuthenticationProvider ad = new ActiveDirectoryLdapAuthenticationProvider(domain, url, userDnPattern);
ad.setConvertSubErrorCodesToExceptions(true);
ad.setUseAuthenticationRequestCredentials(true);
ad.setSearchFilter(adSearchFilter);
return ad;
}
//CORS configuration source
#Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://some.url"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("*"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
//Customize the Spring default /login url to overwrite it with /auth.
private AuthenticationFilter getAuthenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
filter.setFilterProcessesUrl("/auth");
return filter;
}
}
Here is my AuthorizationFilter class:
public class AuthorizationFilter extends BasicAuthenticationFilter {
public AuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
//Extracts username from Jwt token
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null) {
token = token.replace("Bearer ", "");
String username = Jwts.parser()
.setSigningKey("somesecret")
.parseClaimsJws(token)
.getBody()
.getSubject();
if (username != null) {
return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
}
}
return null;
}
}
Here is my AuthenticationFilter class:
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
UserLoginRequestModel userLoginRequestModel = extractCredentials(request);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
userLoginRequestModel.getUsername()
, userLoginRequestModel.getPassword()
, new ArrayList<>());
return authenticationManager.authenticate(token);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication auth) throws IOException, ServletException {
String userId = ((UserDetails)auth.getPrincipal()).getUsername();
Instant now = Instant.now();
String jwtToken = Jwts.builder()
.setSubject(userId)
.setIssuer("me")
.setAudience("myapp")
.setId(UUID.randomUUID().toString())
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plus(30000)))
.signWith(SignatureAlgorithm.HS512, SecurityConstants.getTokenSecret())
.compact();
response.addHeader("Authorization", "Bearer " + jwtToken);
response.addHeader("Access-Control-Expose-Headers", accessControlHeaders.toString());
}
private UserLoginRequestModel extractCredentials(HttpServletRequest request) {
UserLoginRequestModel userLoginRequestModel = new UserLoginRequestModel();
String authorizationHeader = request.getHeader("Authorization");
try {
if (authorizationHeader != null && authorizationHeader.toLowerCase().startsWith("basic")) {
String base64Credentials = authorizationHeader.substring("Basic".length()).trim();
byte[] decodedCredentials = Base64.getDecoder().decode(base64Credentials);
String headerCredentials = new String(decodedCredentials, StandardCharsets.UTF_8);
final String[] credentialsValues = headerCredentials.split(":", 2);
userLoginRequestModel.setUsername(credentialsValues[0]);
userLoginRequestModel.setPassword(credentialsValues[1]);
} else {
userLoginRequestModel = new ObjectMapper().readValue(request.getInputStream(), UserLoginRequestModel.class);
}
return userLoginRequestModel;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
In Postman, I call:
POST: http://localhost/myapi/v1/auth
And I pass it BasicAuth with username and password.
I get 403 Forbidden back if I run this on Open/WAS Liberty. Same code, with no change whatsoever, runs just fine in embedded Tomcat that comes with Spring and I get 200 OK.
The reason I was experiencing this was that in my Liberty server.xml, I was missing defined context-path. As it looks like, Liberty does not consider context-path set up in your application.properties file.
Below is the context-path I have in my application.properties file.
Unfortunatelly, Liberty does not read (or considers) it and just uses the app name as the context-path instead of using the setting in application.properties or application.yml file:
server.servlet.context-path=/myapi/v1
As a result, the above context-path will work just fine if deployment in Spring Boot embedded Tomcat container but not in Liberty container.
When you deploy it to OpenLiberty/WASLiberty, you might find that your endpoints will stop working and you get 403 and/or 404 errors.
In my example above, I have getAuthenticationFilter() method, in my WebSecurityConfiguration class. Below, I added little bit more comments to it to explain:
//Customize the /login url to overwrite the Spring default provided /login url.
private AuthenticationFilter getAuthenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
// This works fine on embedded tomcat, but not in Liberty where it returns 403.
// To fix, in server.xml <appllication> block, add
// <application context-root="/myapi/v1" ... and then both
// auth and other endpoints will work fine in Liberty.
filter.setFilterProcessesUrl("/auth");
// This is temporary "fix" that creates rather more issues, as it
// works fine with Tomcat but fails in Liberty and all other
// endpoints still return 404
//filter.setFilterProcessesUrl("/v1/auth");
return filter;
}
Based on the above context-path, on Tomcat, it becomes /myapi/v1/auth while on Liberty, it ends up being just /myapi/auth which is wrong. I think what Liberty does, it will just take the name of the api and add to it the endpoint, therefore ignoring the versioning.
As a result of this, AntPathRequestMatcher class matches() method will result in a non-matching /auth end point and you will get 403 error. And the other endpoints will result in 404 error.
SOLUTION
In your application.properties, leave:
server.servlet.context-path=/myapi/v1
, this will be picked up by embedded Tomcat and your app will continue to work as expected.
In your server.xml configuration for Open/WAS Liberty, add
matching context-root to the section like:
<application context-root="/myapi/v1" id="myapi" location="location\of\your\myapi-0.0.1.war" name="myapi" type="war">
, this will be picked up by Open/WASLiberty and your app will continue to work as expected on Liberty container as well.

Extend Keycloak token in Spring boot

I'm using Keycloak to secure my Spring boot backend.
Dependencies:
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-2-adapter</artifactId>
<version>12.0.3</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-tomcat7-adapter-dist</artifactId>
<version>12.0.3</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
<version>12.0.3</version>
</dependency>
Security config:
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http.cors()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests();
expressionInterceptUrlRegistry = expressionInterceptUrlRegistry.antMatchers("/iam/accounts/promoters*").hasRole("PROMOTER");
expressionInterceptUrlRegistry.anyRequest().permitAll();
}
Everything work fine!
But now I add a new section in keycloak token "roles" and I need to somehow extend keycloak jwt class in my Spring boot and write some code to parse and store the roles information to SecurityContext. Could you Guy please tell me how to archive the goal?
First, extends keycloak AccessToken:
#Data
static class CustomKeycloakAccessToken extends AccessToken {
#JsonProperty("roles")
protected Set<String> roles;
}
Then:
#KeycloakConfiguration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Override
protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
return new KeycloakAuthenticationProvider() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (String role : ((CustomKeycloakAccessToken)((KeycloakPrincipal)token.getPrincipal()).getKeycloakSecurityContext().getToken()).getRoles()) {
grantedAuthorities.add(new KeycloakRole(role));
}
return new KeycloakAuthenticationToken(token.getAccount(), token.isInteractive(), new SimpleAuthorityMapper().mapAuthorities(grantedAuthorities));
}
};
}
/**
* Use NullAuthenticatedSessionStrategy for bearer-only tokens. Otherwise, use
* RegisterSessionAuthenticationStrategy.
*/
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(authenticationManagerBean());
filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
filter.setRequestAuthenticatorFactory(new SpringSecurityRequestAuthenticatorFactory() {
#Override
public RequestAuthenticator createRequestAuthenticator(HttpFacade facade,
HttpServletRequest request, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort) {
return new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, sslRedirectPort) {
#Override
protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() {
return new BearerTokenRequestAuthenticator(deployment) {
#Override
protected AuthOutcome authenticateToken(HttpFacade exchange, String tokenString) {
log.debug("Verifying access_token");
if (log.isTraceEnabled()) {
try {
JWSInput jwsInput = new JWSInput(tokenString);
String wireString = jwsInput.getWireString();
log.tracef("\taccess_token: %s", wireString.substring(0, wireString.lastIndexOf(".")) + ".signature");
} catch (JWSInputException e) {
log.errorf(e, "Failed to parse access_token: %s", tokenString);
}
}
try {
TokenVerifier<CustomKeycloakAccessToken> tokenVerifier = AdapterTokenVerifier.createVerifier(tokenString, deployment, true, CustomKeycloakAccessToken.class);
// Verify audience of bearer-token
if (deployment.isVerifyTokenAudience()) {
tokenVerifier.audience(deployment.getResourceName());
}
token = tokenVerifier.verify().getToken();
} catch (VerificationException e) {
log.debug("Failed to verify token");
challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "invalid_token", e.getMessage());
return AuthOutcome.FAILED;
}
if (token.getIssuedAt() < deployment.getNotBefore()) {
log.debug("Stale token");
challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.STALE_TOKEN, "invalid_token", "Stale token");
return AuthOutcome.FAILED;
}
boolean verifyCaller;
if (deployment.isUseResourceRoleMappings()) {
verifyCaller = token.isVerifyCaller(deployment.getResourceName());
} else {
verifyCaller = token.isVerifyCaller();
}
surrogate = null;
if (verifyCaller) {
if (token.getTrustedCertificates() == null || token.getTrustedCertificates().isEmpty()) {
log.warn("No trusted certificates in token");
challenge = clientCertChallenge();
return AuthOutcome.FAILED;
}
// for now, we just make sure Undertow did two-way SSL
// assume JBoss Web verifies the client cert
X509Certificate[] chain = new X509Certificate[0];
try {
chain = exchange.getCertificateChain();
} catch (Exception ignore) {
}
if (chain == null || chain.length == 0) {
log.warn("No certificates provided by undertow to verify the caller");
challenge = clientCertChallenge();
return AuthOutcome.FAILED;
}
surrogate = chain[0].getSubjectDN().getName();
}
log.debug("successful authorized");
return AuthOutcome.AUTHENTICATED;
}
};
}
};
}
});
return filter;
}
}
I didn't understand why do you need extend Keycloak Token. The roles already there are in Keycloak Token. I will try explain how to access it, the Keycloak have two levels for roles, 1) Realm level and 2) Application (Client) level, by default your Keycloak Adapter use realm level, to use application level you need setting the propertie keycloak.use-resource-role-mappings with true in your application.yml
How to create roles in realm
enter image description here
How to creare roles in client
enter image description here
User with roles ADMIN (realm) and ADD_USER (application)
enter image description here
To have access roles you can use KeycloakAuthenticationToken class in your Keycloak Adapter, you can try invoke the following method:
...
public ResponseEntity<Object> getUsers(final KeycloakAuthenticationToken authenticationToken) {
final AccessToken token = authenticationToken.getAccount().getKeycloakSecurityContext().getToken();
final Set<String> roles = token.getRealmAccess().getRoles();
final Map<String, AccessToken.Access> resourceAccess = token.getResourceAccess();
...
}
...
To protect any router using Spring Security you can use this annotation,  example below:
#PreAuthorize("hasRole('ADMIN')")
#GetMapping("/users")
public ResponseEntity<Object> getUsers(final KeycloakAuthenticationToken token) {
return ResponseEntity.ok(service.getUsers());
}
Obs: The  keycloak.use-resource-role-mappings set up using #PreAuthorize Annotation.  If set to true, #PreAuthorize checks roles in token.getRealmAccess().getRoles(), if false it checks roles in token.getResourceAccess().
If you want add any custom claim in token, let me know that I can explain better.
I put here how I set up my Keycloak Adapter and the properties in my  application.yml:
SecurityConfig.java
...
#KeycloakConfiguration
#EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Value("${project.cors.allowed-origins}")
private String origins = "";
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(this.authenticationManagerBean());
filter.setSessionAuthenticationStrategy(this.sessionAuthenticationStrategy());
filter.setAuthenticationFailureHandler((request, response, exception) -> {
response.addHeader("Access-Control-Allow-Origin", origins);
if (!response.isCommitted()) {
response.sendError(401, "Unable to authenticate using the Authorization header");
} else if (200 <= response.getStatus() && response.getStatus() < 300) {
throw new RuntimeException("Success response was committed while authentication failed!", exception);
}
});
return filter;
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http.csrf()
.disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "**").permitAll()
.antMatchers("/s/**").authenticated()
.anyRequest().permitAll();
}
}
application.yml
..
keycloak:
enabled: true
auth-server-url: http://localhost:8080/auth
resource: myclient
realm: myrealm
bearer-only: true
principal-attribute: preferred_username
use-resource-role-mappings: true
..

How to response custom json body on unauthorized requests while implementing custom authentication manager in webflux

I was trying to implement custom JWT token authentication while i am also handling global exceptions to customize response body for each type of exceptions. Everything is working fine except I would like to return custom json response when an unauthorized request is received instead of just 401 status code.
Below is my implementation for JwtServerAuthenticationConverter and JwtAuthenticationManager.
#Component
public class JwtServerAuthenticationConverter implements ServerAuthenticationConverter {
private static final String AUTH_HEADER_VALUE_PREFIX = "Bearer ";
#Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return Mono.justOrEmpty(exchange)
.flatMap(serverWebExchange -> Mono.justOrEmpty(
serverWebExchange
.getRequest()
.getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION)
)
)
.filter(header -> !header.trim().isEmpty() && header.trim().startsWith(AUTH_HEADER_VALUE_PREFIX))
.map(header -> header.substring(AUTH_HEADER_VALUE_PREFIX.length()))
.map(token -> new UsernamePasswordAuthenticationToken(token, token))
;
}
}
#Component
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
private final JWTConfig jwtConfig;
private final ObjectMapper objectMapper;
public JwtAuthenticationManager(JWTConfig jwtConfig, ObjectMapper objectMapper) {
this.jwtConfig = jwtConfig;
this.objectMapper = objectMapper;
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.map(auth -> JWTHelper.loadAllClaimsFromToken(auth.getCredentials().toString(), jwtConfig.getSecret()))
.onErrorResume(throwable -> Mono.error(new JwtException("Unauthorized")))
.map(claims -> objectMapper.convertValue(claims, JWTUserDetails.class))
.map(jwtUserDetails ->
new UsernamePasswordAuthenticationToken(
jwtUserDetails,
authentication.getCredentials(),
jwtUserDetails.getGrantedAuthorities()
)
)
;
}
}
And below is my global exception handling which is working absolutely fine except the case where webflux return 401 from JwtServerAuthenticationConverter convert method.
#Configuration
#Order(-2)
public class ExceptionHandler implements WebExceptionHandler {
#Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
exchange.getResponse().getHeaders().set("Content-Type", MediaType.APPLICATION_JSON_VALUE);
return buildErrorResponse(ex)
.flatMap(
r -> r.writeTo(exchange, new HandlerStrategiesResponseContext(HandlerStrategies.withDefaults()))
);
}
private Mono<ServerResponse> buildErrorResponse(Throwable ex) {
if (ex instanceof RequestEntityValidationException) {
return ServerResponse.badRequest().contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse(ex.getMessage())),
ErrorResponse.class
);
} else if (ex instanceof ResponseStatusException) {
ResponseStatusException exception = (ResponseStatusException) ex;
if (exception.getStatus().value() == 404) {
return ServerResponse.status(HttpStatus.NOT_FOUND).contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse("Resource not found - 404")),
ErrorResponse.class
);
} else if (exception.getStatus().value() == 400) {
return ServerResponse.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse("Unable to parse request body - 400")),
ErrorResponse.class
);
}
} else if (ex instanceof JwtException) {
return ServerResponse.status(HttpStatus.UNAUTHORIZED).contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse(ex.getMessage())),
ErrorResponse.class
);
}
ex.printStackTrace();
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse("Internal server error - 500")),
ErrorResponse.class
);
}
}
#RequiredArgsConstructor
class HandlerStrategiesResponseContext implements ServerResponse.Context {
private final HandlerStrategies handlerStrategies;
#Override
public List<HttpMessageWriter<?>> messageWriters() {
return this.handlerStrategies.messageWriters();
}
#Override
public List<ViewResolver> viewResolvers() {
return this.handlerStrategies.viewResolvers();
}
}
#Configuration
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http,
ReactiveAuthenticationManager jwtAuthenticationManager,
ServerAuthenticationConverter jwtAuthenticationConverter
) {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(jwtAuthenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(jwtAuthenticationConverter);
return http
.authorizeExchange()
.pathMatchers("/auth/login", "/auth/logout").permitAll()
.anyExchange().authenticated()
.and()
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.httpBasic()
.disable()
.csrf()
.disable()
.formLogin()
.disable()
.logout()
.disable()
.build();
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
So when i am hitting it with an invalid JWT token in header. This got handled by my ExceptioHandler class and I got below output which is great.
But when i hit it with empty jwt token I got this.
Now i would like to return the same body which i am returning in the case of invalid JWT token. but the problem is when empty token is provided its not even falling in handle method of ExceptionHandler class. thats why its not in my control like i did for JwtException in the same class. How could i do that please help?
I sort it out myself.
webflux provides ServerAuthenticationFailureHandler to handle custom response for that but unfortunately ServerAuthenticationFailureHandler not works and its a known issue so i created a failure route and write my custom response in it and setup login page.
.formLogin()
.loginPage("/auth/failed")
.and()
.andRoute(path("/auth/failed").and(accept(MediaType.APPLICATION_JSON)), (serverRequest) ->
ServerResponse
.status(HttpStatus.UNAUTHORIZED)
.body(
Mono.just(new ErrorResponse("Unauthorized")),
ErrorResponse.class
)
);

Spring reactive security

I am trying for reactive security and the unauthenticated calls are not going to auth manager.
#Configuration
#EnableWebFluxSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig{
#Autowired
private WebAuthenticationManager authenticationManager;
#Autowired
private ServerSecurityContextRepository securityContextRepository;
private static final String[] AUTH_WHITELIST = {
"/login/**",
"/logout/**",
"/authorize/**",
"/favicon.ico",
};
#Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
return http.exceptionHandling().authenticationEntryPoint((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
}).and().csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(authenticationManager)
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authorizeExchange().pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers(AUTH_WHITELIST).permitAll()
.anyExchange().authenticated().and().build();
}
#Bean
public PBKDF2Encoder passwordEncoder() {
return new PBKDF2Encoder();
}
}
WebAuthentication Manager,
#Component
public class WebAuthenticationManager implements ReactiveAuthenticationManager {
#Autowired
private JWTUtil jwtUtil;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
String authToken = authentication.getCredentials().toString();
String username;
try {
username = jwtUtil.getUsernameFromToken(authToken);
} catch (Exception e) {
username = null;
}
if (username != null && jwtUtil.validateToken(authToken)) {
Claims claims = jwtUtil.getAllClaimsFromToken(authToken);
List<String> rolesMap = claims.get("role", List.class);
List<Role> roles = new ArrayList<>();
for (String rolemap : rolesMap) {
roles.add(Role.valueOf(rolemap));
}
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username,
null,
roles.stream().map(authority -> new SimpleGrantedAuthority(authority.name())).collect(Collectors.toList())
);
return Mono.just(auth);
} else {
return Mono.empty();
}
}
}
Here, I have registered my WebAuthentication manager in Securityconfig. But, still the unauthenticated calls are not going to the WebAuthenticationManager.
It is expected to go to AuthenticationManager when the protected URL's are hit. For ex,
http://localhost:8080/api/v1/user.
Not sure, why the calls are not going to AuthManager.
In non reactive, we have OncePerRequestFilter and the auth is being taken care over there. Not sure, how to implement the same for reactive.
You disabled all authentication mechanisms hence there is nothing calling your authentication manager. As you mentioned, you can implement authentication flow through filters.
Sample implementation of authentication filter:
#Bean
public AuthenticationWebFilter webFilter() {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(tokenAuthenticationConverter());
authenticationWebFilter.setRequiresAuthenticationMatcher(serverWebExchangeMatcher());
authenticationWebFilter.setSecurityContextRepository(NoOpServerSecurityContextRepository.getInstance());
return authenticationWebFilter;
}
Then add this filter to ServerHttpSecurity: http.addFilterBefore(webFilter(),SecurityWebFiltersOrder.HTTP_BASIC)
Then finally your authentication manager will be called.
You must provide few additional things to make it working.
Matcher to check if Authorization header is added to request:
#Bean
public ServerWebExchangeMatcher serverWebExchangeMatcher() {
return exchange -> {
Mono<ServerHttpRequest> request = Mono.just(exchange).map(ServerWebExchange::getRequest);
return request.map(ServerHttpRequest::getHeaders)
.filter(h -> h.containsKey(HttpHeaders.AUTHORIZATION))
.flatMap($ -> ServerWebExchangeMatcher.MatchResult.match())
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
};
}
Token converter responsible for getting token from request and preparing basic AbstractAuthenticationToken
#Bean
public ServerAuthenticationConverter tokenAuthenticationConverter() {
return exchange -> Mono.justOrEmpty(exchange)
.map(e -> getTokenFromRequest(e))
.filter(token -> !StringUtils.isEmpty(token))
.map(token -> getAuthentication(token));
}
I intentionally omitted implementation of getTokenFromRequest and getAuthentication because there is a lot of examples available.

How to add a custom OpenId Filter in a Spring boot application?

I am trying to implement the backend side of an OpenId Connect authentication. It is a stateless API so I added a filter that handles the Bearer token.
I have created the OpenIdConnect Filter that handles the Authentication and added it in a WebSecurityConfigurerAdapter.
public class OpenIdConnectFilter extends
AbstractAuthenticationProcessingFilter {
#Value("${auth0.clientId}")
private String clientId;
#Value("${auth0.issuer}")
private String issuer;
#Value("${auth0.keyUrl}")
private String jwkUrl;
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
public OpenIdConnectFilter() {
super("/connect/**");
setAuthenticationManager(new NoopAuthenticationManager());
}
#Bean
public FilterRegistrationBean registration(OpenIdConnectFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
try {
Authentication authentication = tokenExtractor.extract(request);
String accessToken = (String) authentication.getPrincipal();
String kid = JwtHelper.headers(accessToken)
.get("kid");
final Jwt tokenDecoded = JwtHelper.decodeAndVerify(accessToken, verifier(kid));
final Map<String, Object> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
verifyClaims(authInfo);
Set<String> scopes = new HashSet<String>(Arrays.asList(((String) authInfo.get("scope")).split(" ")));
int expires = (Integer) authInfo.get("exp");
OpenIdToken openIdToken = new OpenIdToken(accessToken, scopes, Long.valueOf(expires), authInfo);
final OpenIdUserDetails user = new OpenIdUserDetails((String) authInfo.get("sub"), "Test", openIdToken);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (final Exception e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
public void verifyClaims(Map claims) {
int exp = (int) claims.get("exp");
Date expireDate = new Date(exp * 1000L);
Date now = new Date();
if (expireDate.before(now) || !claims.get("iss").equals(issuer) || !claims.get("azp").equals(clientId)) {
throw new RuntimeException("Invalid claims");
}
}
private RsaVerifier verifier(String kid) throws Exception {
JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
Jwk jwk = provider.get(kid);
return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}
Here is security configuration:
#Configuration
#EnableWebSecurity
public class OpenIdConnectWebServerConfig extends
WebSecurityConfigurerAdapter {
#Bean
public OpenIdConnectFilter myFilter() {
final OpenIdConnectFilter filter = new OpenIdConnectFilter();
return filter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.antMatcher("/connect/**").authorizeRequests()
.antMatchers(HttpMethod.GET, "/connect/public").permitAll()
.antMatchers(HttpMethod.GET, "/connect/private").authenticated()
.antMatchers(HttpMethod.GET, "/connect/private-
messages").hasAuthority("read:messages")
.antMatchers(HttpMethod.GET, "/connect/private-
roles").hasAuthority("read:roles")
.and()
.addFilterBefore(myFilter(),
UsernamePasswordAuthenticationFilter.class);
}
Rest endpoints looks like following:
#RequestMapping(value = "/connect/public", method = RequestMethod.GET,
produces = "application/json")
#ResponseBody
public String publicEndpoint() throws JSONException {
return new JSONObject()
.put("message", "All good. You DO NOT need to be authenticated to
call /api/public.")
.toString();
}
#RequestMapping(value = "/connect/private", method = RequestMethod.GET,
produces = "application/json")
#ResponseBody
public String privateEndpoint() throws JSONException {
return new JSONObject()
.put("message", "All good. You can see this because you are
Authenticated.")
.toString();
}
If I remove completely the filter for configuration and also the #Bean definition, the configuration works as expected: /connect/public is accessible, while /connect/private is forbidden.
If I keep the #Bean definition and add it in filter chain the response returns a Not Found status for requests both on /connect/public and /connect/private:
"timestamp": "18.01.2019 09:46:11",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/
When debugging I noticed that filter is processing the token and returns an implementation of Authentication.
Is the filter properly added in filter chain and in correct position?
Why is the filter invoked also on /connect/public path when this is supposed to be public. Is it applied to all paths matching super("/connect/**") call?
Why is it returning the path as "/" when the request is made at /connect/private
Seems that is something wrong with the filter, cause every time it is applied, the response is messed up.

Resources