Springboot Webflux+Security Custom implementation calling ReactiveAuthenticationManager authenticate method twice - spring-security

I have a springboot webflux project with reactive security enabled
For some reason the project seems to be calling the authenticate method of ReactiveAuthenticationManager twice (once in the default handler and once the request reaches the controller).
Here is my sample code
WebSecurityConfig.class
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class WebSecurityConfig {
static final String[] AUTH_WHITELIST = {
"/swagger-resources/**",
"/swagger-ui.html",
"/v2/api-docs",
"/v3/api-docs",
"/webjars/**",
"/swagger-ui/**",
"/v1/healthcheck"
};
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final AuthenticationManager authenticationManager;
private final SecurityContextRepository securityContextRepository;
public WebSecurityConfig(
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
AuthenticationManager authenticationManager,
SecurityContextRepository securityContextRepository) {
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.authenticationManager = authenticationManager;
this.securityContextRepository = securityContextRepository;
}
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http.securityMatcher(
new NegatedServerWebExchangeMatcher(
ServerWebExchangeMatchers.pathMatchers(AUTH_WHITELIST)))
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(new HttpStatusServerAccessDeniedHandler(HttpStatus.BAD_REQUEST))
.and()
.csrf()
.disable()
.formLogin()
.disable()
.httpBasic()
.disable()
.logout()
.disable()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS)
.permitAll()
.anyExchange()
.authenticated()
.and()
.build();
}
ServerSecurityContextRepository.class
#Slf4j
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
private final AuthenticationService authenticationService;
private final AuthenticationManager authenticationManager;
public SecurityContextRepository(
AuthenticationService authenticationService, AuthenticationManager authenticationManager) {
this.authenticationService = authenticationService;
this.authenticationManager = authenticationManager;
}
#Override
public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) {
throw new UnsupportedOperationException("Not supported yet.");
}
#Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
log.info("Parsing Authorization token from Request");
AuthToken authToken =
authenticationService.parseRequestToken(authenticationService.getHeaders(request));
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, null);
return this.authenticationManager
.authenticate(auth)
.map(authentication -> (SecurityContext) new SecurityContextImpl(authentication))
ReactiveAuthenticationManager.class
#Slf4j
#Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
final AuthenticationService authenticationService;
#Value("${app.auth_enable}")
private boolean isAuthEnabled;
public AuthenticationManager(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
AuthToken token = (AuthToken) authentication.getPrincipal();
if (Objects.isNull(token)) {
log.error("Jwt token not provided");
return Mono.error(new AuthorizeException("Jwt token not provided"));
}
if (isAuthEnabled) {
return authenticationService
.verifyRequestToken(token)
.map(
aBoolean -> {
if (!aBoolean) {
log.warn("Jwt token not valid");
return null;
}
log.info("Jwt token is valid");
return new UsernamePasswordAuthenticationToken(token, null, null);
});
}
return Mono.just(new UsernamePasswordAuthenticationToken(token, null, null));
}
JwtAuthenticationEntryPoint.class
#Slf4j
#Component
public class JwtAuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint
implements Serializable {
private final ObjectMapper objectMapper;
public JwtAuthenticationEntryPoint(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
#Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
log.info("Commencing AuthenticationEntryPoint...");
ServerHttpResponse response = exchange.getResponse();
JwtAuthenticationError error =
new JwtAuthenticationError(JwtExceptionContext.getExceptionContext());
JwtExceptionContext.clearExceptionContext();
byte[] bytes = new byte[0];
try {
bytes = objectMapper.writeValueAsString(error).getBytes(StandardCharsets.UTF_8);
} catch (JsonProcessingException e) {
log.error("JsonProcessingException on commence function : {}", e.getMessage(), e);
}
DataBuffer buffer = response.bufferFactory().wrap(bytes);
response.setStatusCode(HttpStatus.valueOf(Integer.parseInt(error.getStatusCode())));
log.warn(
"Authentication Failed: {} -> {}",
value("errorMsg", error),
keyValue(
AppConstants.STATUS_CODE, HttpStatus.valueOf(Integer.parseInt(error.getStatusCode()))));
return response.writeWith(Mono.just(buffer));
}
Sample logs
2022-01-18 15:30:25.203 DEBUG 9308 --- [cTaskExecutor-1] o.s.web.client.RestTemplate : Reading to [java.lang.String] as "application/json"
2022-01-18 15:30:25.209 INFO 9308 --- [cTaskExecutor-1] c.s.p.r.n.h.s.AuthenticationServiceImpl : Validation Response 200
2022-01-18 15:30:25.209 INFO 9308 --- [oundedElastic-1] c.s.p.r.n.h.s.AuthenticationManager : **Jwt token is valid**
2022-01-18 15:30:25.211 DEBUG 9308 --- [oundedElastic-1] o.s.s.w.s.a.AuthorizationWebFilter : Authorization successful
2022-01-18 15:30:25.217 DEBUG 9308 --- [oundedElastic-1] s.w.r.r.m.a.RequestMappingHandlerMapping : [1fc32c7d-1, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:54225] Mapped to MyController#getCount(Boolean, String, UsernamePasswordAuthenticationToken)
2022-01-18 15:30:25.255 INFO 9308 --- [oundedElastic-1] c.s.p.r.n.h.s.AuthenticationServiceImpl : Validation Response 200
2022-01-18 15:30:25.256 INFO 9308 --- [oundedElastic-2] c.s.p.r.n.h.s.AuthenticationManager : **Jwt token is valid**
Any suggestions or pointers are appreciated.
Thanks in advance

Related

H2 access denied

#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers()
.frameOptions().disable()
.and()
.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/h2/**").permitAll()
.requestMatchers("/api/v1/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
I have this config class. I placed .requestMatchers("/h2/**").permitAll() in the code but when I visit the http://localhost:8080/h2 I take:
Access to localhost was denied
the JwtAuthenticationFilter:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
#Override
protected void doFilterInternal(
#NonNull HttpServletRequest request,
#NonNull HttpServletResponse response,
#NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
if (authHeader == null ||!authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
The doFilterInternal method is the main method of the filter. It checks if the incoming request has an "Authorization" header with a JWT starting with "Bearer ", and if so, extracts the user email from the JWT using the JwtService and loads the user details using the UserDetailsService. If the JWT is valid and the user details are successfully loaded, an Authentication object is created and set in the SecurityContextHolder. The filter chain is then continued with the filterChain.doFilter(request, response) call.

How to skip oncePerRequest filter when using httpservletRequest.login()

I am trying to login using httpServletRequest.login(), after registration for automatic login. BUT when I do this, I hit the OncePerRequest.doFilterInternal which checks the headers for an authorization header and kicks me out.
I dont think I am supposed to hit this for login, right now when I hit the route "api/v1/auth/login" I never hit the filter OncePerRequest.doFilterInternal. And its 200 response includes authorization jwt token as expected.
Am I not using the OncePerRequest.doFilterInternal correctly? I want to be able to login and set an authorization header in the response when a user registers.
Relevant code:
public class JwtTokenVerifier extends OncePerRequestFilter {
private final SecretKey secretKey;
private final JwtConfig jwtConfig;
public JwtTokenVerifier(SecretKey secretKey,
JwtConfig jwtConfig) {
this.secretKey = secretKey;
this.jwtConfig = jwtConfig;
}
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader(jwtConfig.getAuthorizationHeader());
if (Strings.isNullOrEmpty(authorizationHeader) || !authorizationHeader.startsWith(jwtConfig.getTokenPrefix())) {
filterChain.doFilter(request, response);
return;
}
String token = authorizationHeader.replace(jwtConfig.getTokenPrefix(), "");
try {
Jws<Claims> claimsJws = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
Claims body = claimsJws.getBody();
String username = body.getSubject();
var authorities = (List<Map<String, String>>) body.get("authorities");
Set<SimpleGrantedAuthority> simpleGrantedAuthorities = authorities.stream()
.map(m -> new SimpleGrantedAuthority(m.get("authority")))
.collect(Collectors.toSet());
Authentication authentication = new UsernamePasswordAuthenticationToken(
username,
null,
simpleGrantedAuthorities
);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (JwtException e) {
throw new IllegalStateException(String.format("Token %s cannot be trusted:", token));
}
filterChain.doFilter(request, response);
}
Config:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class ApplicationSecurityConfig extends
WebSecurityConfigurerAdapter {
private final AuthUserServiceImpl authUserServiceImpl;
private final IUserRepository iUserRepository;
private final PasswordEncoder passwordEncoder;
private final SecretKey secretKey;
private final JwtConfig jwtConfig;
private final AccessDeniedFilter accessDeniedFilter;
private final AuthenticationEntryPointFilter authenticationEntryPointFilter;
#Autowired
public ApplicationSecurityConfig(PasswordEncoder passwordEncoder,
AuthUserServiceImpl authUserServiceImpl,
SecretKey secretKey,
JwtConfig jwtConfig,
AccessDeniedFilter accessDeniedFilter,
AuthenticationEntryPointFilter authenticationEntryPointFilter,
IUserRepository iUserRepository) {
this.passwordEncoder = passwordEncoder;
this.authUserServiceImpl = authUserServiceImpl;
this.secretKey = secretKey;
this.jwtConfig = jwtConfig;
this.accessDeniedFilter = accessDeniedFilter;
this.authenticationEntryPointFilter = authenticationEntryPointFilter;
this.iUserRepository = iUserRepository;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/", "index", "/css/*", "/js/*", "/api/v1/auth/register").permitAll()
.antMatchers("/api/**").hasAnyRole(ADMIN.name(), USER.name())
.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedFilter)
.authenticationEntryPoint(authenticationEntryPointFilter)
.and()
.addFilter(jwtAuthorizationFilter())
.addFilterAfter(new JwtTokenVerifier(secretKey, jwtConfig), JwtUsernameAndPasswordJwtFilter.class);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider());
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("whatever.com));
configuration.setAllowedMethods(ImmutableList.of("HEAD", "GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(ImmutableList.of("Authorization", "Cache-Control", "Content-Type", "access-control-allow-origin"));
configuration.setExposedHeaders(Arrays.asList("Authorization"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(authUserServiceImpl);
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
public JwtUsernameAndPasswordJwtFilter jwtAuthorizationFilter() throws Exception {
JwtUsernameAndPasswordJwtFilter jwtAuthenticationFilter = new JwtUsernameAndPasswordJwtFilter(authenticationManager(), jwtConfig, secretKey, iUserRepository);
jwtAuthenticationFilter.setFilterProcessesUrl("/api/v1/auth/login");
return jwtAuthenticationFilter;
}
}
#PostMapping("/register")
public ResponseEntity<?> registerUserAccount(
#Valid #RequestBody AuthUserRequest authUserRequest,
Errors errors) throws NullPointerException {
String password = authUserRequest.getPassword();
if (errors.hasErrors()) {
return ResponseEntity.badRequest().body(errors.getFieldError());
}
try {
iAuthUser.registerNewUserAccount(authUserRequest);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
try {
httpServletRequest.login(authUserRequest.getUsername(), password);
} catch (ServletException e) {
System.out.println("Servlet Error: " + e);
}
return ResponseEntity.ok().build();
}

JWT Bearer token not received?

I want to use Spring security to perform authentication and authorization on 2 separated server.
I've authenticated successfully and received a JWT token on the first server.
Now I am sending a request to the second server with the JWT authorization token, but the server can't see it; basically getHeader() from getJwtFromRequest(HttpServletRequest request) returns null.
This is the server code:
//imports...
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
CustomUserDetailsService customUserDetailsService;
#Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
#Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
#Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
#Bean(BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js")
.permitAll()
.antMatchers("/api/auth/**")
.permitAll()
.antMatchers("/api/user/checkUsernameAvailability", "/api/user/checkEmailAvailability")
.permitAll()
.antMatchers(HttpMethod.GET, "/api/polls/**", "/api/users/**")
.permitAll()
.anyRequest()
.authenticated();
// Add our custom JWT security filter
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
//imports...
public class JwtAuthenticationFilter extends OncePerRequestFilter {
#Autowired
private JwtTokenProvider tokenProvider;
#Autowired
private CustomUserDetailsService customUserDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
// bearerToken prints null!
System.out.println("AUTHORIZATION: "+bearerToken);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
But the token is present on the request:
The authorization header was stripped on the edge server

I can not return access token in JWT SPRINGBOOT V2.2.0 OAuth2

I have a hard time on a personal study project. Difficulty implementing oauth2, whenever I try to get access token it returns me
2019-11-19 22:01:35.398 ERROR 4705 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.StackOverflowError] with root cause
java.lang.StackOverflowError: null
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$UserDetailsServiceDelegator.loadUserByUsername(WebSecurityConfigurerAdapter.java:448) ~[spring-security-config-5.2.0.RELEASE.jar:5.2.0.RELEASE]
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$UserDetailsServiceDelegator.loadUserByUsername(WebSecurityConfigurerAdapter.java:449) ~[spring-security-config-5.2.0.RELEASE.jar:5.2.0.RELEASE]
2019-11-19 22:01:35.426 WARN 4705 --- [nio-8080-exec-2] o.s.web.servlet.PageNotFound : No mapping for POST /error
my project uses Version Spring Boot v2.2.0.RELEASE and Java 1.8
Application.propeties
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=test
spring.datasource.password=test123
spring.jpa.hibernate.ddl-auto=update
spring.datasource.testWhileIdle = true
spring.datasource.timeBetweenEvictionRunsMillis = 3600000
spring.datasource.validationQuery = SELECT 1
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
spring.jpa.show-sql=true
AuthorizationServerConfig
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private PasswordEncoder passwordEncoder;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("angular")
.secret(passwordEncoder.encode("angular"))
.scopes("read", "write")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(1800)
.refreshTokenValiditySeconds(3600 * 24)
.and()
.withClient("admin")
.secret("admin")
.scopes("read")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(1800)
.refreshTokenValiditySeconds(3600 * 24);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
.reuseRefreshTokens(false)
.authenticationManager(authenticationManager);
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setSigningKey("financas");
return accessTokenConverter;
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
}
ResourceServerConfig
#Configuration
#EnableWebSecurity
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/pagamento").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.csrf().disable();
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.stateless(true);
}
#Bean
public static PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
#Bean
public MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}
SecurityConfig
#EnableWebSecurity
#EnableAuthorizationServer
#EnableResourceServer
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
#Bean
#Override
public UserDetailsService userDetailsService() {
return super.userDetailsService();
}
}
now my refresh token classes
RefreshTokenPostProcessor
#ControllerAdvice
public class RefreshTokenPostProcessor implements ResponseBodyAdvice<OAuth2AccessToken> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.getMethod().getName().equals("postAccessToken");
}
#Override
public OAuth2AccessToken beforeBodyWrite(OAuth2AccessToken body,
MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass,
ServerHttpRequest request, ServerHttpResponse response) {
HttpServletRequest req = ((ServletServerHttpRequest)request).getServletRequest();
HttpServletResponse resp = ((ServletServerHttpResponse)response).getServletResponse();
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) body;
String refreshToken = body.getRefreshToken().getValue();
adicionaRefreshTokenNoCookie(refreshToken, req, resp);
removerRefreshTokenDoBody(token);
return body;
}
private void removerRefreshTokenDoBody(DefaultOAuth2AccessToken token) {
token.setRefreshToken(null);
}
private void adicionaRefreshTokenNoCookie(String refreshToken, HttpServletRequest req, HttpServletResponse resp) {
Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setSecure(false); //TODO: change in production
refreshTokenCookie.setPath(req.getContextPath() + "/oauth/token");
refreshTokenCookie.setMaxAge(2592000);
resp.addCookie(refreshTokenCookie);
}
}
RefreshTokenPreProcessorFilter
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class RefreshTokenPreProcessorFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
if ("/oauth/token".equalsIgnoreCase(req.getRequestURI())
&& "refresh_token".equals(req.getParameter("grant_type"))
&& req.getCookies() != null) {
for (Cookie cookie : req.getCookies()) {
if (cookie.getName().equals("refreshToken")) {
String refreshToken = cookie.getValue();
req = new MyServletRequestWrapper(req, refreshToken);
}
}
}
chain.doFilter(req, response);
}
#Override
public void destroy() {
}
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
static class MyServletRequestWrapper extends HttpServletRequestWrapper {
private String refreshToken;
public MyServletRequestWrapper(HttpServletRequest request, String refreshToken) {
super(request);
}
#Override
public Map<String, String[]> getParameterMap() {
ParameterMap<String, String[]> map = new ParameterMap<>(getRequest().getParameterMap());
map.put("refresh_token", new String[] {refreshToken});
map.setLocked(true);
return map;
}
}
}
SpringBootApplication
#SpringBootApplication
#WebAppConfiguration
public class FinancaApplication {
public static void main(String[] args) {
SpringApplication.run(FinancaApplication.class, args);
}
}
Remembering that my authentication is in the database and I am using PasswordEncoder on the database password and authentication passwords, I would like some help setting up the server and where my error is because I cannot generate an access token, the error returned is just this one in the console. thankful
I like your code Felipe - it's a very nicely written API - the problem is that it attempts to issue tokens also, which an API should not do.
I'd recommend walking through my tutorial, to understand the roles of API, UI and Authorization Server:
https://authguidance.com/2017/09/24/basicspa-overview/
Once you've done that I think you'll be able to adapt your API and fix your own problems - feel free to post back any follow up questions though.
In the real world almost all companies use a third party (cloud) system as the Authorization Server:
https://authguidance.com/2019/09/15/developer-domain-setup/
You will learn OAuth much faster this way - and it is free.
You then only need to focus on integrating your APIs and UIs with standards compliant endpoints like this:
https://authguidance.com/2019/03/24/java-spring-boot-api-oauth-coding/
Happy to answer follow up questions if it helps ..

Spring Security Access Denied Handler is called when the HttpServletResponse is already committed

I have an HTTP API, protected with Spring Security and JWT.
I get a 401 when I'm trying to access a protected resource.
I get the resource if I'm authenticated (JWT is valid) and I have the correct role. The resource is protected with #PreAuthorize("hasRole('USER')").
The issue I have is that when I don't have the correct role I'd like to return a 403 (in the following code it is a 401 for the sake of testing).
But right know I get a 500 because of the AccessDeniedException which is thrown when the role is incorrect.
The weird thing is that it goes to my JwtAccessDeniedHandler custom code but the response is already committed (isCommitted() == true) so whenever I try to set the status etc it does nothing.
Do you have any ideas about what could be misconfigured or missing?
Config:
#Slf4j
#EnableWebSecurity
#EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ObjectMapper objectMapper;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(
jwtAuthenticationFilter(joseHelper(jsonWebKey())),
UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.accessDeniedHandler(new JwtAccessDeniedHandler());
}
#Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(JoseHelper joseHelper) {
return new JwtAuthenticationFilter(joseHelper);
}
#Bean
public JoseHelper joseHelper(PublicJsonWebKey key) {
return new JoseHelper(key);
}
#Bean
public PublicJsonWebKey jsonWebKey() throws IOException, JoseException {
return RsaJwkGenerator.generateJwk(2048);
}
private void sendUnauthorized(HttpServletResponse httpServletResponse) throws IOException {
httpServletResponse.setContentType("application/json");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ApiError apiError = ApiError.builder()
.code(HttpStatus.UNAUTHORIZED.name())
.message(HttpStatus.UNAUTHORIZED.getReasonPhrase())
.httpStatus(HttpStatus.UNAUTHORIZED)
.build();
httpServletResponse.getWriter().print(objectMapper.writeValueAsString(apiError));
}
private class JwtAccessDeniedHandler implements AccessDeniedHandler {
#Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
log.info("accessDeniedHandler", e);
sendUnauthorized(httpServletResponse);
}
}
private class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
sendUnauthorized(httpServletResponse);
}
}
}
Filter:
#Slf4j
#Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String BEARER = "Bearer ";
private JoseHelper joseHelper;
#Autowired
public JwtAuthenticationFilter(JoseHelper joseHelper) {
this.joseHelper = joseHelper;
}
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String header = httpServletRequest.getHeader("Authorization");
if (header == null || !header.startsWith(BEARER)) {
log.error("JWT token is not valid");
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
final String encryptedToken = header.substring(BEARER.length());
try {
final String decryptedJwt = joseHelper.decryptJwt(encryptedToken);
final String verifiedJwt = joseHelper.verifyJwt(decryptedJwt);
final JwtClaims jwtClaims = joseHelper.parse(verifiedJwt);
List<SimpleGrantedAuthority> authorities = jwtClaims.getStringListClaimValue("userRoles")
.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(jwtClaims, null, authorities);
SecurityContextHolder.getContext().setAuthentication(jwtAuthenticationToken);
filterChain.doFilter(httpServletRequest, httpServletResponse);
} catch (JoseException | InvalidJwtException | MalformedClaimException e) {
log.error("JWT token is not valid", e);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
}
The issue was because I use Jersey apparently. I didn't really had time to investigate the why right now.
Once I registered an exception mapper in my JerseyConfig I was able to capture and handle the AccessDeniedException correctly.
And from that point the access denied handler is not called anymore and becomes useless.
A bit weird, but there is probably a good reason.

Resources