How generate token for entire spring boot application - java

I have a REST API which consumes an external API. I am using WebClient, but I have to pass a token with each request to the external API. The token is generated through an addAuthentication POST call. I want to use the same token within the entire application, until it gets expired as; this is an application specific token. But I am not sure how to generate the token to be available in my entire application, for use in all http requests.
I tried to instantiate a connector class and used in #PostConstruct in my spring boot application class; but I am not sure how to access this instance within all the application.
#Component
public class AppConnector {
#Value("${some.dev.url}")
private String baseUrl;
#Value("${some.dev.appid}")
private String appId;
#Value("${some.dev.username}")
private String username;
#Value("${some.dev.password}")
private String password;
private String token;
private boolean isConnected;
private final WebClient webClient;
#Autowired
public AppConnector(WebClient webClient) {
this.webClient = WebClient.builder()
.baseUrl("http://localhost:8080/api")
.defaultHeader("application-id", "client-01")
.defaultHeader("username", username)
.defaultHeader("password", password)
.build();
}
/***
* Method to add Authentication
* #return Session_Token: String
*/
public String addAuthentication() {
AddAuthenticationOutputVO response = null;
response = this.webClient.post()
.uri("/authentication?version=1.0")
.retrieve()
.bodyToMono(AddAuthenticationOutputVO.class).block();
this.token = response.getSession_token();
return response.getSession_token();
}
}

One solution is to use JWT signed and issued by the main application, and both system must have the main key secret. The verification can be a simple token verification, and you can find the userId or userName inside the JWT. The expiration is inside the JWT so every time the token is verified the system can know if the token has expired or not and you can answer to the client as expired token.
This solution with Spring is implement two filters, Authentication to generate the token, and Authorization to verify the token.
Here is the sample code.
Authentication:
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final String maxSizeToUpload;
public JWTAuthenticationFilter(AuthenticationManager authenticationManger, String maxSizeToUpload) {
this.authenticationManager = authenticationManger;
this.maxSizeToUpload = maxSizeToUpload;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
try {
AuthenticationRequest authRequest = new ObjectMapper().readValue(request.getInputStream(),
AuthenticationRequest.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
authRequest.getUsername(), authRequest.getPassword(), new ArrayList<>()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication auth) throws IOException, ServletException {
// Los tokens son generados hasta el final del día.
Date expirationDate = DateUtil.setZeroHour(DateUtil.getDateAddDays(new Date(), 1));
String token = Jwts.builder().setIssuedAt(new Date()).setIssuer(WebSecurity.ISSUER)
.setSubject(((BuserDetails)auth.getPrincipal()).getUsername())
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, HardCodeUtil.JWT_KEY).compact();
response.addHeader(WebSecurity.HEADER_AUTHORIZATION, WebSecurity.PREFIX_JWT + token);
response.addHeader(WebSecurity.HEADER_JWT_EXPIRATION_DATE, String.valueOf(expirationDate.getTime()));
ObjectMapper mapper = new ObjectMapper();
ExtraParams extraParams = new ExtraParams(
((BuserDetails)auth.getPrincipal()).getBuser().getBusiness().getCurrency(),
Byte.parseByte(maxSizeToUpload.replace("MB", "")));
String body = mapper.writeValueAsString(new LoginResponse(((BuserDetails)auth.getPrincipal()).getBuser(),
extraParams));
response.setContentType("application/json");
response.getWriter().write(body);
response.getWriter().flush();
response.getWriter().close();
}
}
Authorization:
This must be used in both system and you can find the JWT_KEY as I used, you need to share that value in both systems.
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
private static final Logger log = Logger.getLogger(JWTAuthorizationFilter.class.getName());
public JWTAuthorizationFilter(AuthenticationManager authManager) {
super(authManager);
}
#Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
String header = req.getHeader(WebSecurity.HEADER_AUTHORIZATION);
if (header == null || !header.startsWith(WebSecurity.PREFIX_JWT)) {
chain.doFilter(req, res);
return;
}
try {
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}catch (SignatureException ex) {
log.log(Level.SEVERE, "FIRMA INVALIDA DEL JWT ENVIADO");
}catch (MalformedJwtException ex) {
log.log(Level.SEVERE, "ESTRUCTURA INVALIDA DEL JWT ENVIADO");
}catch (ExpiredJwtException ex) {
GeneralResponse jwtInvalidResponse = new GeneralResponse(ErrorsEnum.JWT_EXPIRED);
ObjectMapper mapper = new ObjectMapper();
String body = mapper.writeValueAsString(jwtInvalidResponse);
res.setContentType("application/json");
res.getWriter().write(body);
res.getWriter().flush();
res.getWriter().close();
}catch (UnsupportedJwtException ex) {
log.log(Level.SEVERE, "NO SOPORTADO JWT ENVIADO");
}catch (IllegalArgumentException ex) {
log.log(Level.SEVERE, "ILLEGAL ARGUMENT JWT ENVIADO");
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(WebSecurity.HEADER_AUTHORIZATION);
if (token != null) {
String user = Jwts.parser()
.setSigningKey(HardCodeUtil.JWT_KEY)
.parseClaimsJws(token.replace(WebSecurity.PREFIX_JWT, ""))
.getBody()
.getSubject();
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
}
return null;
}
}
In the example I have implemented with this library:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

Related

Spring Rest Forbidden Access when hit Exception

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

How to call controller method after authentication

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;
}
}

Spring security configuration - addFilterBefore is not working for PUT, PATCH and DELETE

I have prepared configuration for Spring security for API calls. It should verify JWT token provided in the request.
http.csrf().disable().authorizeRequests()
.antMatchers("/v2/api/**/*").authenticated().and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
and my controller method
#PreAuthorize("hasAuthority('ROLE_USER')")
#PutMapping(value = "/v2/api/dashboard/projects")
public List<Projects> getProjects(Principal principal) {
return dashboardService.getProjects();
}
and by executing request I get
Resolved
[org.springframework.web.HttpRequestMethodNotSupportedException:
Request method 'PUT' not supported]
When I change it to GetMapping request is being handled properly.
After settings logs from logging.level.org.springframework.web=DEBUG I can see that PUT not supported is not returned from /v2/api/dashboard/projects but from '/forbidden' which for obvious reasons doesn't support such methods.
Further investigation with debuging jwtRequestFilter showed that filter is not even executed on PUT, PATCH or DELETE methods.
code of it:
#Component
public class JwtRequestFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(JwtRequestFilter.class);
#Autowired
private JwtUserDetailsService jwtUserDetailsService;
#Autowired
private JwtUtils jwtTokenUtil;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String requestTokenHeader = "";
try{
requestTokenHeader = WebUtils.getCookie(request, "token").getValue();
} catch (NullPointerException ex ){}
String username = null;
String jwtToken = null;
if (requestTokenHeader != null && requestTokenHeader.contains(".")) {
jwtToken = requestTokenHeader;
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
log.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
log.error("JWT Token has expired");
}
} else {
logger.warn("JWT Token does not look like token");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
Can anyone give me a hint how to make it works so PUT and PATCH methods would be filtered with given class?

Auditable Entity In Microservice Architecture

I'm trying to implement Abstract Auditable Entity in my current Microservice architecture. It is working fine for a single module but I'm confused on how to pass the SecurityContext across multiple modules.
I've already tried by transferring the token as a header from my zuul-service (auth-server) to other core modules and the value is always null.
Also, I tried passing the SecurityContext using feign client but it didn't work for me either.
Cannot get JWT Token from Zuul Header in Spring Boot Microservice Module
Audit Logging in Spring Microservices
Session Management in microservices
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtConfig jwtConfig;
public JwtTokenAuthenticationFilter(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
}
private static final int FILTER_ORDER = 0;
private static final boolean SHOULD_FILTER = true;
private static final Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request1, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String header = request1.getHeader(jwtConfig.getHeader());
if (header == null || !header.startsWith(jwtConfig.getPrefix())) {
chain.doFilter(request1, response);
return;
}
/* new token getting code*/
String token = header.replace(jwtConfig.getPrefix(), "");
try {
Claims claims = Jwts.parser()
.setSigningKey(jwtConfig.getSecret().getBytes())
.parseClaimsJws(token)
.getBody();
String username = claims.getSubject();
System.out.println(username);
if (username != null) {
#SuppressWarnings("unchecked")
List<String> authorities = (List<String>) claims.get("authorities");
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
username,
null, authorities.stream().map(
SimpleGrantedAuthority::new
).collect(Collectors.toList()));
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
SecurityContextHolder.clearContext();
}
System.out.println(String.format("%s request to %s", request1.getMethod(), request1.getRequestURL().toString()));
/* return null;*/
request1.setAttribute("header",token);
chain.doFilter(request1, response);
}
}

Why BadCredentialsException can not be thrown or handled in my customized AuthenticationProvider?

I try to use JWT to secure my resource based on Spring Security to achieve the following:
1. Token invalid or expired, return 401.
2. Successfully authorized but have no right to reach some controllers. Then return 403.
Now there is something wrong with it. I throw BadCredentialsException in my customized AuthenticationProvider (named TokenAuthenticationProvider) while user fails to be authenticated. But it finally returns 403. What can I do to handle the exception and return 403 http code.
I tried to implement AuthenticationEntryPoint but it doesn't work.
And one another way to handle the exception is using customized filter to catch the Exception. But this way definitely doesn't work because even the http response doesn't show 500 BadCredentialsException. So there must be a place already catching this Exception and I can't understand.
TokenAuthenticationProvider.class
public class TokenAuthenticationProvider implements AuthenticationProvider {
UserService userService;
public TokenAuthenticationProvider(UserService userService) {
this.userService = userService;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
throw new BadCredentialsException("hello");
}
#Override
public boolean supports(Class<?> aClass) {
System.out.println(aClass);
TokenAuthenticationProvider.class.isAssignableFrom(aClass);
return true;
}
}
WebSecurity.class
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Autowired
UserService userService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/**")
.addFilterAfter(new TokenAuthenticationFilter(), BasicAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().hasRole("API");
}
#Override
protected void configure(AuthenticationManagerBuilder auth){
auth.authenticationProvider(new TokenAuthenticationProvider(userService));
}
}
TokenAuthenticationFilter.class
public class TokenAuthenticationFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication(new TokenAuthentication("hello"));
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
The above code has been simplified. Instead of following a normal process, I directly throw the BadCredentialsException. What can I do to handle this Exception and return 401 http code.
You need to implement two filters to control the JWT generated.
First Filter is to authenticate and send the JWT to the client when the authentication is successful.
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManger) {
this.authenticationManager = authenticationManger;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
try {
AuthenticationRequest authRequest = new ObjectMapper().readValue(request.getInputStream(),
AuthenticationRequest.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
authRequest.getUsername(), authRequest.getPassword(), new ArrayList<>()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication auth) throws IOException {
Date expirationDate = DateUtil.getDateAddDays(new Date(), 1);
String token = Jwts.builder().setIssuedAt(new Date()).setIssuer(WebSecurity.ISSUER)
.setSubject(((ClientDetails)auth.getPrincipal()).getUsername())
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, HardCodeUtil.JWT_KEY).compact();
response.addHeader(WebSecurity.HEADER_AUTHORIZATION, WebSecurity.PREFIX_JWT + token);
response.addHeader(WebSecurity.HEADER_JWT_EXPIRATION_DATE, String.valueOf(expirationDate.getTime()));
ObjectMapper mapper = new ObjectMapper();
ClientExtraParams extraParams = new ClientExtraParams((byte)1);
String body = mapper.writeValueAsString(new ClientLoginResponse(((ClientDetails)auth.getPrincipal()).getClient(),
extraParams));
response.setContentType("application/json");
response.getWriter().write(body);
response.getWriter().flush();
response.getWriter().close();
}
}
The second Filter is to validate every JWT before access to the resources:
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
private static final Logger log = Logger.getLogger(JWTAuthorizationFilter.class.getName());
public JWTAuthorizationFilter(AuthenticationManager authManager) {
super(authManager);
}
#Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
String header = req.getHeader(WebSecurity.HEADER_AUTHORIZATION);
if (header == null || !header.startsWith(WebSecurity.PREFIX_JWT)) {
chain.doFilter(req, res);
return;
}
try {
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}catch (SignatureException ex) {
log.log(Level.SEVERE, "JWT SIGNING INVALID");
}catch (MalformedJwtException ex) {
log.log(Level.SEVERE, "JWT STRUCTURE INVALID");
}catch (ExpiredJwtException ex) {
log.log(Level.SEVERE, "JWT EXPIRED");
GeneralResponse jwtInvalidResponse = new GeneralResponse(ErrorsEnum.JWT_EXPIRED);
ObjectMapper mapper = new ObjectMapper();
String body = mapper.writeValueAsString(jwtInvalidResponse);
res.setContentType("application/json");
res.getWriter().write(body);
res.getWriter().flush();
res.getWriter().close();
}catch (UnsupportedJwtException ex) {
log.log(Level.SEVERE, "JWT UNSUPPORTED");
}catch (IllegalArgumentException ex) {
log.log(Level.SEVERE, "ILLEGAL ARGUMENT JWT ENVIADO");
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(WebSecurity.HEADER_AUTHORIZATION);
if (token != null) {
String user = Jwts.parser()
.setSigningKey(HardCodeUtil.JWT_KEY)
.parseClaimsJws(token.replace(WebSecurity.PREFIX_JWT, ""))
.getBody()
.getSubject();
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
}
return null;
}
}
In your Spring configuration of HttpSecurity add these filters:
.and().addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
I implemented this using this library:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

Categories