I am trying to build Websecurity filter, which will authorize any calls to microservices.
Pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
WebSecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
// securedEnabled = true,
// jsr250Enabled = true,
prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Autowired
private AuthEntryPointJwt unauthorizedHandler;
@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@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().authorizeRequests().antMatchers("/api/v1/auth/**").permitAll().anyRequest()
.authenticated().and().exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
AuthEntryPointJwt
@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
logger.error("Unauthorized error: {}", authException.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
final Map<String, Object> body = new HashMap<>();
body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
body.put("error", "Unauthorized");
body.put("message", authException.getMessage());
body.put("path", request.getServletPath());
final ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), body);
}
}
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsServiceImpl userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, username,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e.getMessage());
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7, headerAuth.length());
}
return null;
}
}
@Component
public class JwtUtils {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationMs}")
private int jwtExpirationMs;
public String generateJwtToken(Authentication authentication) {
return generateTokenFromUsername(String.valueOf(authentication.getPrincipal()));
}
public String generateTokenFromUsername(String username) {
return Jwts.builder().setSubject(username).setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)).signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
logger.error("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
What i am doing here in below is, call another api/endpoint to get the user validated using HttpClient
public class UserDetailsImpl implements UserDetails {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserDetailsImpl(Long id, String username, String email, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserDetailsImpl build(AccessDetailsResponse user) {
List<GrantedAuthority> authorities = user.getRoleName().stream().map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
return new UserDetailsImpl(user.getId(), user.getEmail(), user.getEmail(), user.getEmail(), authorities);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
String validateUserURL = null;
private Properties myProperties;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
myProperties = new Properties();
InputStream input = AuthTokenFilter.class.getClassLoader().getResourceAsStream("config.properties");
myProperties.load(input);
String authUrl = myProperties.getProperty("api.auth.url");
validateUserURL = authUrl + "/api/v1/auth/validateUserAccess";
AccessDetailsResponse userDetails = validateAccess(validateUserURL, username, "/api/loan/");
return UserDetailsImpl.build(userDetails);
} catch (IOException e) {
return null;
}
}
/**
*
* @param uri
* @param userName
* @param apiEndPoint
* @return
*/
private AccessDetailsResponse validateAccess(String uri, String userName, String apiEndPoint) {
AccessDetailsResponse accessResponse = null;
try {
HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10)).build();
Map<Object, Object> data = new HashMap();
data.put("userName", userName);
data.put("apiName", apiEndPoint);
HttpRequest request = HttpRequest.newBuilder().POST(ofFormData(data)).uri(URI.create(uri))
.header("Content-Type", "application/json").build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
Gson jsonObj = new Gson();
accessResponse = jsonObj.fromJson(response.body(), AccessDetailsResponse.class);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return accessResponse;
}
/**
*
* @param data
* @return
*/
private static HttpRequest.BodyPublisher ofFormData(Map<Object, Object> data) {
var builder = new StringBuilder();
for (Map.Entry<Object, Object> entry : data.entrySet()) {
if (builder.length() > 0) {
builder.append("&");
}
builder.append(URLEncoder.encode(entry.getKey().toString(), StandardCharsets.UTF_8));
builder.append("=");
builder.append(URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8));
}
return HttpRequest.BodyPublishers.ofString(builder.toString());
}
}
The JAR build from above is included as dependency - pom.xml in all microservices, expecting to get the all apis in microservices getting authorized before access. Also token is generated and sent when the user login, and token will be send in header as 'Bearer ' while making any further calls. But I dont see any logs in validateUserAccess API, getting called. But with dependency added i am getting below response and removing the dependency it is working fine.
timestamp": "2022-02-03T03:50:16.146+00:00",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/api/loan/191"
Update: Above unauthorized message is not from JWTentrypoint class, it is from the microservice where the Custom auth JAR i built is included. I added WebsecurityConfig class in my microservice and permitting all in it, i got response:200.
I referred this https://www.bezkoder.com/spring-boot-jwt-authentication/ and started building it as a standalone jar, then included it as dependency in microservice. But auth jar has been never called.
Whatever I tried so far in https://github.com/patsub/spring-security-jwt
Below is the trace:
[2022-02-27 19:42:59.573[ [3 INFO[ [16696[ [---[ [[on(3)-127.0.0.1][ [36mo.a.c.c.C.[Tomcat].[localhost].[/] [ [:[ Initializing Spring DispatcherServlet 'dispatcherServlet'
[2022-02-27 19:42:59.574[ [3 INFO[ [16696[ [---[ [[on(3)-127.0.0.1][ [36mo.s.web.servlet.DispatcherServlet [ [:[ Initializing Servlet 'dispatcherServlet'
[2022-02-27 19:42:59.577[ [3 INFO[ [16696[ [---[ [[on(3)-127.0.0.1][ [36mo.s.web.servlet.DispatcherServlet [ [:[ Completed initialization in 3 ms
[2022-02-27 19:43:03.385[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@3b2c3f9d, org.springframework.security.web.context.SecurityContextPersistenceFilter@37b33187, org.springframework.security.web.header.HeaderWriterFilter@7a59322f, org.springframework.web.filter.CorsFilter@477318f9, org.springframework.security.web.csrf.CsrfFilter@6c015fbf, org.springframework.security.web.authentication.logout.LogoutFilter@511f5f80, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4df3000d, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@9aa80ac, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@6750dc5d, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5f3fe785, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7c015a23, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@1889b178, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@41edbebb, org.springframework.security.web.session.SessionManagementFilter@3552bcee, org.springframework.security.web.access.ExceptionTranslationFilter@d438e1b, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@fdcdbe8]] (1/1)
[2022-02-27 19:43:03.386[ [3DEBUG[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Securing GET /api/valuation/details/id/38
[2022-02-27 19:43:03.387[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking WebAsyncManagerIntegrationFilter (1/16)
[2022-02-27 19:43:03.390[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking SecurityContextPersistenceFilter (2/16)
[2022-02-27 19:43:03.393[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mw.c.HttpSessionSecurityContextRepository[ [:[ Did not find SecurityContext in HttpSession 2C0701A217A4019C38B63040C5632635 using the SPRING_SECURITY_CONTEXT session attribute
[2022-02-27 19:43:03.395[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mw.c.HttpSessionSecurityContextRepository[ [:[ Created SecurityContextImpl [Null authentication]
[2022-02-27 19:43:03.400[ [3DEBUG[ [16696[ [---[ [[nio-8082-exec-1][ [36ms.s.w.c.SecurityContextPersistenceFilter[ [:[ Set SecurityContextHolder to empty SecurityContext
[2022-02-27 19:43:03.400[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking HeaderWriterFilter (3/16)
[2022-02-27 19:43:03.401[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking CorsFilter (4/16)
[2022-02-27 19:43:03.413[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking CsrfFilter (5/16)
[2022-02-27 19:43:03.416[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.csrf.CsrfFilter [ [:[ Did not protect against CSRF since request did not match CsrfNotRequired [TRACE, HEAD, GET, OPTIONS]
[2022-02-27 19:43:03.416[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking LogoutFilter (6/16)
[2022-02-27 19:43:03.416[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.s.w.a.logout.LogoutFilter [ [:[ Did not match request to Ant [pattern='/logout', POST]
[2022-02-27 19:43:03.416[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking UsernamePasswordAuthenticationFilter (7/16)
[2022-02-27 19:43:03.416[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mw.a.UsernamePasswordAuthenticationFilter[ [:[ Did not match request to Ant [pattern='/login', POST]
[2022-02-27 19:43:03.416[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking DefaultLoginPageGeneratingFilter (8/16)
[2022-02-27 19:43:03.416[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking DefaultLogoutPageGeneratingFilter (9/16)
[2022-02-27 19:43:03.417[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36m.w.a.u.DefaultLogoutPageGeneratingFilter[ [:[ Did not render default logout page since request did not match [Ant [pattern='/logout', GET]]
[2022-02-27 19:43:03.417[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking BasicAuthenticationFilter (10/16)
[2022-02-27 19:43:03.417[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.s.w.a.www.BasicAuthenticationFilter [ [:[ Did not process authentication request since failed to find username and password in Basic Authorization header
[2022-02-27 19:43:03.417[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking RequestCacheAwareFilter (11/16)
[2022-02-27 19:43:03.418[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.s.w.s.HttpSessionRequestCache [ [:[ Removing DefaultSavedRequest from session if present
[2022-02-27 19:43:03.418[ [3DEBUG[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.s.w.s.HttpSessionRequestCache [ [:[ Loaded matching saved request http://localhost:8082/api/valuation/details/id/38
[2022-02-27 19:43:03.420[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking SecurityContextHolderAwareRequestFilter (12/16)
[2022-02-27 19:43:03.423[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking AnonymousAuthenticationFilter (13/16)
[2022-02-27 19:43:03.425[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.s.w.a.AnonymousAuthenticationFilter [ [:[ Set SecurityContextHolder to AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=2C0701A217A4019C38B63040C5632635], Granted Authorities=[ROLE_ANONYMOUS]]
[2022-02-27 19:43:03.426[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking SessionManagementFilter (14/16)
[2022-02-27 19:43:03.426[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking ExceptionTranslationFilter (15/16)
[2022-02-27 19:43:03.426[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.security.web.FilterChainProxy [ [:[ Invoking FilterSecurityInterceptor (16/16)
[2022-02-27 19:43:03.432[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36medFilterInvocationSecurityMetadataSource[ [:[ Did not match request to org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest$EndpointRequestMatcher@703b28c8 - [permitAll] (1/2)
[2022-02-27 19:43:03.433[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.s.w.a.i.FilterSecurityInterceptor [ [:[ Did not re-authenticate AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=2C0701A217A4019C38B63040C5632635], Granted Authorities=[ROLE_ANONYMOUS]] before authorizing
[2022-02-27 19:43:03.433[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.s.w.a.i.FilterSecurityInterceptor [ [:[ Authorizing filter invocation [GET /api/valuation/details/id/38] with attributes [authenticated]
[2022-02-27 19:43:03.440[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.s.w.a.expression.WebExpressionVoter [ [:[ Voted to deny authorization
[2022-02-27 19:43:03.441[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.s.w.a.i.FilterSecurityInterceptor [ [:[ Failed to authorize filter invocation [GET /api/valuation/details/id/38] with attributes [authenticated] using AffirmativeBased [DecisionVoters=[org.springframework.security.web.access.expression.WebExpressionVoter@2de672fd], AllowIfAllAbstainDecisions=false]
[2022-02-27 19:43:03.456[ [3TRACE[ [16696[ [---[ [[nio-8082-exec-1][ [36mo.s.s.w.a.ExceptionTranslationFilter [ [:[ Sending AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=2C0701A217A4019C38B63040C5632635], Granted Authorities=[ROLE_ANONYMOUS]] to authentication entry point since access is denied
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:73) ~[spring-security-core-5.5.2.jar:5.5.2]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.attemptAuthorization(AbstractSecurityInterceptor.java:238) ~[spring-security-core-5.5.2.jar:5.5.2]