I'm using Spring Boot 3.0. The authorization just works as expected but when it hit SomeException like MethodArgumentNotValidException it just only show 403 Forbidden Access with empty body. Before I'm using Spring Boot 3.0 everything just work as I'm expected when hitting Exception like they give me the exception JSON result.
SecurityConfiguration
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
var secret = System.getProperty("app.secret");
var authorizationFilter = new AuthorizationFilter(secret);
var authenticationFilter = new AuthenticationFilter(secret, authenticationManager);
authenticationFilter.setFilterProcessesUrl("/login");
authenticationFilter.setPostOnly(true);
return http
.cors().and()
.csrf((csrf) -> csrf.disable())
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/login/**", "/trackers/camera/**").permitAll()
.requestMatchers("/sites/**").hasAnyRole(Role.OWNER.name())
.anyRequest().authenticated()
)
.addFilter(authenticationFilter)
.addFilterBefore(authorizationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
var config = new CorsConfiguration();
var all = Arrays.asList("*");
config.setAllowedOrigins(all);
config.setAllowedHeaders(all);
config.setAllowedMethods(all);
config.setExposedHeaders(all);
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
AuthenticationFilter
#RequiredArgsConstructor
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final String secretToken;
private final AuthenticationManager authenticationManager;
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username, password;
try {
var requestMap = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class);
username = requestMap.getUsername();
password = requestMap.getPassword();
} catch (Exception e) {
throw new AuthenticationServiceException(e.getMessage(), e);
}
var token = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(token);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
var user = (UserDetails) authResult.getPrincipal();
var algorithm = Algorithm.HMAC512(secretToken.getBytes());
var token = JWT.create()
.withSubject(user.getUsername())
.withIssuer(request.getRequestURL().toString())
.withClaim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.sign(algorithm);
var jsonMap = new HashMap<String, String>();
jsonMap.put("token", token);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), jsonMap);
response.flushBuffer();
}
}
AuthorizationFilter
#RequiredArgsConstructor
public class AuthorizationFilter extends OncePerRequestFilter {
private final String secretToken;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
var authentication = request.getHeader(HttpHeaders.AUTHORIZATION);
if(authentication != null) {
if(authentication.startsWith("Bearer")) {
var token = authentication.substring("Bearer ".length());
var algorithm = Algorithm.HMAC512(secretToken.getBytes());
var verifier = JWT.require(algorithm).build();
var message = verifier.verify(token);
var subject = message.getSubject();
var roles = message.getClaim("roles").asArray(String.class);
var authorities = new ArrayList<SimpleGrantedAuthority>();
Arrays.stream(roles).forEach(role -> authorities.add(new SimpleGrantedAuthority(role)));
var authenticationToken = new UsernamePasswordAuthenticationToken(subject, token, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else if(authentication.startsWith("Basic")) {
var token = authentication.substring("Basic ".length());
var bundle = new String(Base64.getDecoder().decode(token)).split(":", 2);
if(bundle.length == 2 && bundle[0].equals(System.getProperty("app.camera.username")) && bundle[1].equals(System.getProperty("app.camera.password"))) {
var authenticationToken = new UsernamePasswordAuthenticationToken("camera1", null, Arrays.asList(new SimpleGrantedAuthority(Role.USER.getAuthority())));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request, response);
}
}
If I understood you correctly, you want the exception-message shown in the return body of the request.
I solved this problem by implementing a (global) exception handler.
(Optional) Create a custom exception, extending some sort of other exception.
public class ApiException extends RuntimeException {
// Not really needed here, as Throwable.java has a message too
// I added it for better readability
#Getter
private String message;
public ApiException() {
super();
}
public ApiException(String message) {
this.message = message;
}
public ApiException(String message, Throwable cause) {
super(message, cause);
}
}
(Optional) A Wrapper, with custom information. (This is the object returned in the body).
// I've used a record, as the wrapper simply has to store data
public record ApiError(String message, HttpStatus status, Throwable cause, ZonedDateTime timestamp) {}
The handler
To create the handler, you simply have to create a custom class, which extends the ResponseEntityExceptionHandler.java
#ControllerAdvice
#Order(Ordered.HIGHEST_PRECEDENCE)
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
// The annotation's value can be replaced by any exception.
// Use Throwable.class to handle **all** exceptions.
// For this example I used the previously created exception.
#ExceptionHandler(value = { ApiException.class })
#ResponseBody
#ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<Object> handleApiRequestException(ApiException e) {
// At this point, you can create the exception wrapper to create a
// formatted JSON-response, but you could also just get the info
// required from the exception and return that.
ApiError error = new ApiError(
e.getMessage(),
HttpStatus.BAD_REQUEST,
null,
ZonedDateTime.now(ZoneId.of("Z"))
);
return new ResponseEntity<>(error, error.status());
}
}
Also: To handle different kinds of exceptions differently, like e.g. you want a ApiException to return a 403 and a FooException to return a 404, just create another method inside of the handler and adjust it to your likings.
I hope this helped!
Cheers
Related
I'm trying to customize a thrown exception, but it's not in the advice control.
The TokenExpiredException exception
should be handled in controller advice, but returns a common, unhandled error.
JWTValidarFilter:
public class JWTValidarFilter extends BasicAuthenticationFilter{
private static final String HEADER_ATRIBUTO = "Authorization";
private static final String ATRIBUTO_PREFIXO = "Bearer ";
public JWTValidarFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String atributo = request.getHeader(HEADER_ATRIBUTO);
if(atributo == null) {
chain.doFilter(request, response);
return;
}
if(!atributo.startsWith(ATRIBUTO_PREFIXO)) {
chain.doFilter(request, response);
return;
}
String token = atributo.replace(ATRIBUTO_PREFIXO, "");
UsernamePasswordAuthenticationToken authenticationToken = getAuthenticationToken(token);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthenticationToken(String token) {
try {
String usuario = JWT.require(Algorithm.HMAC512(JWTAutenticarFilter.TOKEN_SENHA))
.build()
.verify(token)
.getSubject();
if(usuario == null) {
return null;
}
return new UsernamePasswordAuthenticationToken(usuario, null, new ArrayList<>());
} catch (TokenExpiredException e) {
throw new TokenExpiredException("Token expirado!");
}
}
}
CustomizeResponseEntityExceptionHandler:
#ControllerAdvice
#RestController
public class CustomizeResponseEntityExceptionHandler extends ResponseEntityExceptionHandler{
#ExceptionHandler(Exception.class)
public final ResponseEntity<ExceptionResponse> handleAllExcepetions(Exception ex, WebRequest request){
ExceptionResponse exceptionResponse =
new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
#ExceptionHandler(InvalidJwtAuthenticationException.class)
public final ResponseEntity<ExceptionResponse> invalidJwtAuthenticationException(Exception ex, WebRequest request){
ExceptionResponse exceptionResponse =
new ExceptionResponse(new Date(),
ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<>(exceptionResponse, HttpStatus.BAD_REQUEST);
}
#ExceptionHandler(TokenExpiredException.class)
public final ResponseEntity<ExceptionResponse> TokenExpiredException(TokenExpiredException ex, WebRequest request){
ExceptionResponse exceptionResponse =
new ExceptionResponse(new Date(),
ex.getMessage(),
request.getDescription(true));
return new ResponseEntity<>(exceptionResponse, HttpStatus.UNAUTHORIZED);
}
#ExceptionHandler(HttpClientErrorException.class)
public ResponseEntity<String> handleException(HttpClientErrorException ex) throws HttpClientErrorException {
System.out.println("*******Exception Occured: *************" + ex);
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(" -----DD------ Exception: " + ex.getLocalizedMessage());
}
}
2021-09-28 10:13:38.729 ERROR 25385 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
com.auth0.jwt.exceptions.TokenExpiredException: Token expirado!
Try building a custom AuthenticationFailureHandler as follows:
public class CustomAuthenticationFailureHandler
implements AuthenticationFailureHandler {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
Throwable cause = exception.getCause();
ExceptionResponse exceptionResponse = null;
if (cause instanceOf InvalidJwtAuthenticationException) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
exceptionResponse = new ExceptionResponse(new Date(),
cause.getMessage(),
request.getDescription(false));
} else if (cause instanceOf TokenExpiredException) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
exceptionResponse = new ExceptionResponse(new Date(),
ex.getMessage(),
request.getDescription(true));
} else {
// additional logic here
}
response.getOutputStream().println(objectMapper.writeValueAsString(exceptionResponse));
}
}
Then you need to register it in a #Configuration class:
#Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}
You can't catch authentication exceptions using controller advice (at least not as simple :-). Authentication exceptions happen before the whole Spring exception handler intialized. Here ist similar discussion how to workaround related issues:
Spring MVC (or Spring Boot). Custom JSON response for security related exceptions like 401 Unauthorized or 403 Forbidden)
Authentication Exception can not be handled using such #ExceptionHandler, because it just happens before the Spring MVC dispatcher servlet were entered and all the MVC handlers were initialized. So you need to create a Class that inherit interface AuthenticationEntryPoint, override the commence() method and handle the exception there as you need. Here is the example:
#Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
#Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e)
throws IOException, ServletException {
LOGGER.error("Responding with unauthorized error. Message - {}", e.getMessage());
ObjectMapper mapper = new ObjectMapper();
UnauthorizedException exception = new UnauthorizedException("Please provide auth token");
String responseMsg = mapper.writeValueAsString(exception);
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpServletResponse.getWriter().write(responseMsg);
}
}
Add add below lines to your Spring Security Config that extends WebSecurityConfigurerAdapter:
http.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
and it becomes like this:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true)
public class SecutiryConfig extends WebSecurityConfigurerAdapter {
private final CustomUserDetailsServiceImpl customUserDetailsService;
private final JwtAuthenticationEntryPoint unauthorizedHandler;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
#Autowired
public SecutiryConfig(UserRepository userRepository,
CustomUserDetailsServiceImpl customUserDetailsService,
JwtAuthenticationEntryPoint unauthorizedHandler,
JwtAuthenticationFilter jwtAuthenticationFilter)
{
this.customUserDetailsService = customUserDetailsService;
this.unauthorizedHandler = unauthorizedHandler;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler) // this line
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
}
I have following metod in controller:
#PostMapping(path = "/api/users/login", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
#ResponseStatus(OK)
public TokenResponse login(#RequestBody LoginUserRequest loginUserRequest, Principal principal) {
return new TokenResponse().setAccessToken("token");
}
here is a WebSecurityConfigurerAdapter
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.formLogin().disable()
.authorizeRequests().antMatchers("/api/users/login").permitAll()
.and()
.authorizeRequests().antMatchers("/api/**").authenticated()
.and()
.addFilterBefore(mobileAuthenticationFilter(objectMapper), UsernamePasswordAuthenticationFilter.class)
.addFilter(new JwtAuthorizationFilter(authenticationManager(), super.userDetailsService()));
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("SELECT login, pass, active FROM users WHERE login = ?")
.authoritiesByUsernameQuery("SELECT login, 'ROLE_USER' FROM users WHERE login = ?")
.passwordEncoder(new CustomPasswordEncoder());
}
#Bean
public MobileAuthenticationFilter mobileAuthenticationFilter(ObjectMapper objectMapper) throws Exception {
MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter(objectMapper);
mobileAuthenticationFilter.setAuthenticationManager(authenticationManager());
mobileAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
System.out.println(request);
});
return mobileAuthenticationFilter;
}
MobileAuthenticationFilter is reading from json body and prepare UsernamePasswordAuthenticationToken
public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private final ObjectMapper objectMapper;
public MobileAuthenticationFilter(ObjectMapper objectMapper) {
super(new AntPathRequestMatcher("/api/users/login"));
this.objectMapper = objectMapper;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
BufferedReader reader = request.getReader();
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
reader.mark(0);
LoginUserRequest loginUserRequest = objectMapper.readValue(sb.toString(), LoginUserRequest.class);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginUserRequest.getLogin(), loginUserRequest.getPassword());
return getAuthenticationManager().authenticate(token);
} catch (IOException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
}
this code works fine but is one thing which I want to archive.
After successfully authentication, response is produced immediately by the:
mobileAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
System.out.println(request);
});
Here ofcourse I can return something to client (in body), but there is any possibility to invoke controller method public TokenResponse login and that method should return a response (based on method contract and annotations for http code)?
This method in controller in that scenario is never called.
Would there be a formLogin, you could have used the successHandler(...) to redirect to the page you want. Note that you have to also think about error responses.
Since you have explicitly disabled formLogin, I recommend if users call /api/users/login instead of authenticating them in attemptAuthentication(...).
So, as you have put it ..addFilterBefore(mobileAuthenticationFilter(objectMapper), UsernamePasswordAuthenticationFilter.class), your filter will be triggered populating the resulting response.
Your controller will look like something like this:
public TokenResponse login(#Valid #RequestBody LoginUserRequest loginUserRequest) {
//may be check for AuthenticationException
try{
...
generateToken(loginUserRequest.getUserName(), loginUserRequest.getPassword());
...
} catch(AuthenticationException ex){
// status = HttpStatus.UNAUTHORIZED;
} catch (Exception ex) {
//status = HttpStatus.INTERNAL_SERVER_ERROR;
}
}
public String generateToken(String name, String password) {
try {
// check for null or empty
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(name, password, new ArrayList<>());
Authentication authentication = authenticationManager.authenticate(upToken);
// do whatever operations you need and return token
} catch (Exception ex) {
throw ex;
}
}
I have implemented authentication for my APIs and it works as expected . The user first access the auth api to get a token by passing username and password. This api returns a token. The user then calls the secure apis by passing the tokens.
This issue is when the user passes an invalid token or does not pass a token the default error object is returned from Spring Boot. I wanna customize this object and for this, I wrote a custom exception handler extending ResponseEntityExceptionHandler but this is not getting triggered as the exception is thrown before the controller kicks in.
#ExceptionHandler(value = {InsufficientAuthenticationException.class})
public final ResponseEntity<Object>
authenticationException(InsufficientAuthenticationException ex) {
List<String> details = new ArrayList<>();
details.add("Authentication is required to access this resource");
ErrorResponse error = new ErrorResponse("error", "Unauthorized", details);
return new ResponseEntity(error, HttpStatus.FORBIDDEN);
}
The AuthenticationProvider is responsible to find user based on the authentication token sent by the client in the header. This is how our Spring based token authentication provider looks like:
#Component
public class AuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
#Autowired
CustomerService customerService;
#Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
//
}
#Override
protected UserDetails retrieveUser(String userName, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
Object token = usernamePasswordAuthenticationToken.getCredentials();
return Optional
.ofNullable(token)
.map(String::valueOf)
.flatMap(customerService::findByToken)
.orElseThrow(() -> new UsernameNotFoundException("Cannot find user with authentication token=" + token));
}
The token authentication filter is responsible to get the authentication filter from the header and call the authentication manager for authentication. This is how the authentication filter looks like:
public class AuthenticationFilter extends AbstractAuthenticationProcessingFilter {
AuthenticationFilter(final RequestMatcher requiresAuth) {
super(requiresAuth);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
Optional tokenParam = Optional.ofNullable(httpServletRequest.getHeader(AUTHORIZATION)); //Authorization: Bearer TOKEN
String token= httpServletRequest.getHeader(AUTHORIZATION);
token= StringUtils.removeStart(token, "Bearer").trim();
Authentication requestAuthentication = new UsernamePasswordAuthenticationToken(token, token);
return getAuthenticationManager().authenticate(requestAuthentication);
}
#Override
protected void successfulAuthentication(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain, final Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
Spring security configuration looks like:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PROTECTED_URLS = new OrRequestMatcher(
new AntPathRequestMatcher("/api/**")
);
AuthenticationProvider provider;
public SecurityConfiguration(final AuthenticationProvider authenticationProvider) {
super();
this.provider = authenticationProvider;
}
#Override
protected void configure(final AuthenticationManagerBuilder auth) {
auth.authenticationProvider(provider);
}
#Override
public void configure(final WebSecurity webSecurity) {
webSecurity.ignoring().antMatchers("/token/**");
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.and()
.authenticationProvider(provider)
.addFilterBefore(authenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.requestMatchers(PROTECTED_URLS)
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable();
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
}
#Bean
AuthenticationFilter authenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationManager(authenticationManager());
//filter.setAuthenticationSuccessHandler(successHandler());
return filter;
}
#Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(HttpStatus.FORBIDDEN);
}
#Autowired
private HandlerExceptionResolver handlerExceptionResolver;
public AuthenticationEntryPoint authenticationEntryPoint() {
log.error("in authenticationEntryPoint");
return new AuthenticationEntryPoint() {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.error("in commence");
try {
log.error(authException.getLocalizedMessage());
handlerExceptionResolver.resolveException(request, response, null, authException);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new ServletException(e);
}
}
};
}
}
P.S.: Refer to https://www.javadevjournal.com/spring/securing-a-restful-web-service-with-spring-security/
Since you are customising AbstractAuthenticationProcessingFilter , you can also customise its AuthenticationFailureHandler which will be invoked when attemptAuthentication() throw AuthenticationException. You can then handle the error at there.
An example is :
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler{
#Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
//create your custom error object
CustomError error = xxxxx;
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// Format the custom error object as JSON string , for example using Jackson :
ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString(error));
}
}
And configure to use it:
#Bean
AuthenticationFilter authenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
return filter;
}
I want to generate a JWT with expiration date for people to access the system without have to register and create a user. Is this posible? I have tried with JwtTokenProvider but it needs a LoginRequest to work also with Jwts.builder() also needs a user.
if you want to use spring security you can create security configration and extends WebSecurityConfigurerAdapter. Then important point is custom provider.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
#Autowired
private JWTConfigurer securityConfigurerAdapter;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//you can write customAuth provider
auth.authenticationProvider(customAuthenticationProvider);
}
#Override
public void configure(WebSecurity web) throws Exception {
//Some ignore etc.
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf()
.disable().and()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//important here
.antMatchers("/api/v1/authentication/**").permitAll()
.anyRequest().authenticated()
.and()
.apply(securityConfigurerAdapter);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return this.authenticationManager();
}
}
This is Filter class which extends genericFilterBean. Every request is monitored in this class
You will check to it is right token
I create token TokenProvider class and depend into JWTFilter then use valideToken method.
if token is sended and not validate then throw exception
if token is not sended then go super method so the flow is continue and works auth.authenticationProvider. Spring knows to start customAuthenticationProvider behind the scene becouse of you set into SecurityConfiguration class
#Component
public class JWTFilter extends GenericFilterBean {
private final Logger log = LoggerFactory.getLogger(JWTFilter.class);
#Autowired
private TokenProvider tokenProvider;
#Autowired
private MessageSource msgSource;
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
//Resolve method is optional what you want to use
String jwt = resolveToken(httpServletRequest);
if (StringUtils.hasText(jwt)) {
//token validation is important becouse of expires date into token
// and you will check expired date
if (this.tokenProvider.validateToken(jwt)) {
String jwtMd5 = DigestUtils.md5Hex(jwt);
MDC.put("jwt",jwtMd5);
Authentication authentication = this.tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(servletRequest, servletResponse);
}catch(Exception ex){
handleException((HttpServletResponse) servletResponse,ex);
}
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(JWTConfigurer.AUTHENTICATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
String jwt = bearerToken.substring(7, bearerToken.length());
return jwt;
}
String jwt = request.getParameter(JWTConfigurer.AUTHENTICATION_TOKEN);
if (StringUtils.hasText(jwt)) {
return jwt;
}
return null;
}
}
You can use this class for create token or validate token
you define expire date for token expiration into create method.
#Component public class TokenProvider {
private final Logger log = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
private static final String WTS_USER_ID = "wtsUserId";
private static final String CHANNEL_PERMISSIONS = "channelPermissions";
private static final String APP_ROLES = "appRoles";
private String secretKey;
private long tokenValidityInSeconds;
#Autowired private ApplicationProperties applicationProperties;
#PostConstruct public void init() {
this.tokenValidityInSeconds = 1000;
}
public String createToken(Authentication authentication, Boolean rememberMe) { List<String> authorities = authentication.getAuthorities().stream().map(authority -> authority.getAuthority())
.collect(Collectors.toList());
//Token creation format is this
// token will be three part important parts are claims and sign
// claims refers to body to use datas
// sign will use to validation
return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, authorities)
.claim(WTS_USER_ID, ((JWTAuthentication) authentication).getWtsUserId())
.claim(CHANNEL_PERMISSIONS, ((JWTAuthentication) authentication).getChannelPermissions())
.claim(APP_ROLES, ((JWTAuthentication) authentication).getAppRoles())
.signWith(SignatureAlgorithm.HS512, secretKey).setExpiration(tokenValidityInSeconds).compact(); }
#SuppressWarnings("unchecked") public Authentication getAuthentication(String token) { Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
List<String> list = (List<String>) claims.get(AUTHORITIES_KEY); Collection<? extends GrantedAuthority> authorities = list.stream()
.map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList()); Integer wtsUserId = (Integer) claims.get(WTS_USER_ID); List<String> appRoles = (List<String>) claims.get(APP_ROLES);
ObjectMapper objectMapper = new ObjectMapper(); List<ChannelPermission> channelPermissions = objectMapper.convertValue(claims.get(CHANNEL_PERMISSIONS),
new TypeReference<List<ChannelPermission>>() {
});
return new JWTAuthentication(token, wtsUserId, claims.getSubject(), authorities, channelPermissions, appRoles); }
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
log.info("Invalid JWT signature: " + e.getMessage());
return false;
} } }
This is controller who anonymous people get a JWT token .You can give a new JWT token all request and this JWT has expires date becouse of you set a expiration date into provider class.
#RequestMapping(value = "/login", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse login(#RequestBody #Validated AuthenticationRequestDTO authenticationRequest) {
Authentication authentication = this.authenticationManager.authenticate(new JWTAuthentication(
RandomUid, RandomPwd, "anonymous"));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = tokenProvider.createToken(authentication, false);
return new ApiResponse(ApiResponseStatus.SUCCESS, new AuthenticationResponseDTO(token));
}
I have Spring Boot application with configured SpringSecurity. It uses token generated by UUID.randomUUID().toString(), returned by method login in UUIDAuthenticationService class in AuthUser object. Authorized users are kept in LoggedInUsers class. When I'm sending request to API token is verified by method findByToken in UUIDAuthenticationService class.
Lastly I added timeout for token verification. Now I want to add ip address verification. If user is logged in from address X.X.X.X (which is kept in AuthUser object) he should be authorized with his token only form address X.X.X.X. How to do it?
My SecurityConfig.java:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#FieldDefaults(level = PRIVATE, makeFinal = true)
class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
new AntPathRequestMatcher("/api/login/login"),
);
private static final RequestMatcher PROTECTED_URLS = new NegatedRequestMatcher(PUBLIC_URLS);
TokenAuthenticationProvider provider;
SecurityConfig(final TokenAuthenticationProvider provider) {
super();
this.provider = requireNonNull(provider);
}
#Override
protected void configure(final AuthenticationManagerBuilder auth) {
auth.authenticationProvider(provider);
}
#Override
public void configure(final WebSecurity web) {
web.ignoring()
.requestMatchers(PUBLIC_URLS);
web.httpFirewall(defaultHttpFirewall());
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(STATELESS)
.and()
.exceptionHandling()
// this entry point handles when you request a protected page and you are not yet
// authenticated
.defaultAuthenticationEntryPointFor(forbiddenEntryPoint(), PROTECTED_URLS)
.and()
.authenticationProvider(provider)
.addFilterBefore(restAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN")
.antMatchers("/api/application/**").hasAnyAuthority("ROLE_ADMIN", "ROLE_EMPLOYEE", "ROLE_PORTAL")
.antMatchers("/api/rezerwacja/**").hasAnyAuthority("ROLE_ADMIN", "ROLE_EMPLOYEE")
.anyRequest()
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable();
}
#Bean
TokenAuthenticationFilter restAuthenticationFilter() throws Exception {
final TokenAuthenticationFilter filter = new TokenAuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(successHandler());
return filter;
}
#Bean
SimpleUrlAuthenticationSuccessHandler successHandler() {
final SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
successHandler.setRedirectStrategy(new NoRedirectStrategy());
return successHandler;
}
/**
* Disable Spring boot automatic filter registration.
*/
#Bean
FilterRegistrationBean disableAutoRegistration(final TokenAuthenticationFilter filter) {
final FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
#Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(FORBIDDEN);
}
#Bean
public HttpFirewall defaultHttpFirewall() {
return new DefaultHttpFirewall();
}
}
AbstractAuthenticationProcessingFilter.java:
#FieldDefaults(level = PRIVATE, makeFinal = true)
public final class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String BEARER = "Bearer";
public TokenAuthenticationFilter(final RequestMatcher requiresAuth) {
super(requiresAuth);
}
#Override
public Authentication attemptAuthentication(
final HttpServletRequest request,
final HttpServletResponse response) {
final String param = ofNullable(request.getHeader(AUTHORIZATION))
.orElse(request.getParameter("t"));
final String token = ofNullable(param)
.map(value -> removeStart(value, BEARER))
.map(String::trim)
.orElseThrow(() -> new BadCredentialsException("Missing Authentication Token"));
final Authentication auth = new UsernamePasswordAuthenticationToken(token, token);
return getAuthenticationManager().authenticate(auth);
}
#Override
protected void successfulAuthentication(
final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain chain,
final Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
}
TokenAuthenticationProvider/java:
#Component
#AllArgsConstructor(access = PACKAGE)
#FieldDefaults(level = PRIVATE, makeFinal = true)
public final class TokenAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
#NonNull
UserAuthenticationService auth;
#Override
protected void additionalAuthenticationChecks(final UserDetails d, final UsernamePasswordAuthenticationToken auth) {
// Nothing to do
}
#Override
protected UserDetails retrieveUser(final String username, final UsernamePasswordAuthenticationToken authentication) {
final Object token = authentication.getCredentials();
return Optional
.ofNullable(token)
.map(String::valueOf)
.flatMap(auth::findByToken)
.orElseThrow(() -> new UsernameNotFoundException("Cannot find user with authentication token=" + token));
}
}
UUIDAuthenticationService.java:
#Service
#AllArgsConstructor(access = PACKAGE)
#FieldDefaults(level = PRIVATE, makeFinal = true)
public final class UUIDAuthenticationService implements UserAuthenticationService {
private static final Logger log = LoggerFactory.getLogger(UUIDAuthenticationService.class);
#NonNull
UserCrudService users;
#Autowired
LoginManager loginMgr;
#Override
public AuthUser login(final String username, final String password) throws Exception { //throws Exception {
AuthUser user = loginMgr.loginUser(username, password);
if (user != null) {
users.delete(user);
users.save(user);
log.info("Zalogowano użytkownika {}, przydzielono token: {}", user.getUsername(), user.getUuid());
}
return Optional
.ofNullable(user)
.orElseThrow(() -> new RuntimeException("Błędny login lub hasło"));
}
#Override
public Optional<AuthUser> findByToken(final String token) {
AuthUser user = users.find(token).orElse(null); // get();
if (user != null) {
Date now = Date.from(OffsetDateTime.now(ZoneOffset.UTC).toInstant());
int ileSekund = Math.round((now.getTime() - user.getLastAccess().getTime()) / 1000); // timeout dla tokena
if (ileSekund > finals.tokenTimeout) {
log.info("Token {} dla użytkownika {} przekroczył timeout", user.getUuid(), user.getUsername());
users.delete(user);
user = null;
}
else {
user.ping();
}
}
return Optional.ofNullable(user); //users.find(token);
}
#Override
public void logout(final AuthUser user) {
users.delete(user);
}
}
I thought about creating method findByTokenAndIp in UUIDAuthenticationService, but I don't know how to find ip address of user sending request and how to get ip address while logging in login method in UUIDAuthenticationService (I need it while I'm creating AuthUser object).
You had access to HttpServletRequest request in your filter so you can extract the IP from it.
See https://www.mkyong.com/java/how-to-get-client-ip-address-in-java/
After having the IP, you can deny the request anyway that you want!
I would briefly do the following steps:
save the IP in the UUIDAuthenticationService. You can add HttpServletRequest request as a param, if you're using a controller/requestmapping, because it's auto-injected:
#RequestMapping("/login")
public void lgin(#RequestBody Credentials cred, HttpServletRequest request){
String ip = request.getRemoteAddr();
//...
}
Within the authentication filter, use the IP as the "username" for the UsernamePasswordAuthenticationToken and the token as the "password". There is also already the HttpServletRequest request that gives you the IP by getRemoteAddr().
It's also possible to create an own instance of AbstractAuthenticationToken or even UsernamePasswordAuthenticationToken, which explictly holds an IP or even the request for the authentication-manager.
Then, you just need to adapt the changes to your retrieveUser method.
I modified controller to get ip address with HttpServletRequest parameter and add parameter ipAddress to login method.
#PostMapping("/login")
public AuthUser login(InputStream inputStream, HttpServletRequest request) throws Exception {
final String ipAddress = request.getRemoteAddr();
if (ipAddress == null || ipAddress.equals("")) {
throw new Exception("Nie udało się ustalić adresu IP klienta");
}
Login login = loginMgr.prepareLogin(inputStream);
return authentication
.login(login.getUsername(), login.getPasword(), ipAddress);
}
And modified method retrieveUser in TokenAuthenticationProvider
#Override
protected UserDetails retrieveUser(final String username, final UsernamePasswordAuthenticationToken authentication) {
System.out.println("Verification: "+authentication.getPrincipal()+" => "+authentication.getCredentials());
final Object token = authentication.getCredentials();
final String ipAddress= Optional
.ofNullable(authentication.getPrincipal())
.map(String::valueOf)
.orElse("");
return Optional
.ofNullable(token)
.map(String::valueOf)
.flatMap(auth::findByToken)
.filter(user -> user.ipAddress.equals(ipAddress)) // weryfikacja adresu ip
.orElseThrow(() -> new UsernameNotFoundException("Cannot find user with authentication token=" + token));
}
And it works. Thans for help.