Spring Security JWT REST API returning 401 - spring-boot

I have a relatively simple setup using Spring Boot 2, Spring Security, and I'm using JWT to essentially keep users logged in.
The full project is here: http://github.com/mikeycoxon/spring-boot-2-security-jwt
I have two filters, one, does authentication, the other authorization.
I have an AuthNFilter:
public class AuthNFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public AuthNFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
User creds = new ObjectMapper()
.readValue(req.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
creds.getRoles())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
String token = Jwts.builder()
.setSubject(((User) auth.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET.getBytes())
.compact();
res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
}
}
This verifies the user against a data store and adds a custom header to the response with the token.
and an AuthZFilter:
public class AuthZFilter extends BasicAuthenticationFilter {
public AuthZFilter(AuthenticationManager authManager) {
super(authManager);
}
#Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if (token != null) {
// parse the token.
String user = Jwts.parser()
.setSigningKey(SECRET.getBytes())
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody()
.getSubject();
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
Which replaces the BasicAuthenticationFilter so that we can read the JWT and set up the user in the SecurityContext.
For this to apply, I set up a WebSecurityConfigurerAdapter so that we could override spring security's defaults:
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
private UserDetailsServiceImpl userDetailsService;
private BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurity(UserDetailsServiceImpl userDetailsServiceImpl, BCryptPasswordEncoder bCryptPasswordEncoder) {
this.userDetailsService = userDetailsServiceImpl;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(SIGN_UP_URL).permitAll()
.antMatchers(LOGIN_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new AuthNFilter(authenticationManager()))
.addFilter(new AuthZFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
SIGNUP_URL = /api/user and is a POST
LOGIN_URL = spring's own /login endpoint
Basically, the problem comes up in the test:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ActiveProfiles("embedded")
#AutoConfigureMockMvc
public class AccessControllerFunctionalTest {
#Autowired
private WebApplicationContext context;
#Autowired
private MockMvc mvc;
#MockBean
private UserRepository userRepository;
#Autowired
private PasswordEncoder passwordEncoder;
#Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
#Test
public void doSignup() throws Exception {
String requestString = "{\"username\": \"mike#gmail.com\",\"password\": \"password\"}";
mvc.perform(post("/api/user").contentType(APPLICATION_JSON)
.content(requestString))
.andDo(print()).andExpect(status().isOk());
}
#Test
public void doLoginFailsWithUserNotExists() throws Exception {
String requestString = "{\"username\": \"mike#gmail.com\",\"password\": \"password\"}";
mvc.perform(post("/login").contentType(APPLICATION_JSON)
.content(requestString))
.andDo(print())
.andExpect(status().isUnauthorized());
}
#Test
public void doLoginSuccessWithUserExists() throws Exception {
String requestString = "{\"username\": \"rmjcoxon#gmail.com\",\"password\": \"password\"}";
mvc.perform(post("/login").contentType(APPLICATION_JSON)
.content(requestString))
.andDo(print())
.andExpect(status().isOk())
.andExpect(header().exists(HEADER_STRING));
}
}
The first two tests pass, the third one fails, which is unexpected. It always returns with:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /login
Parameters = {}
Headers = {Content-Type=[application/json]}
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 401
Error message = Unauthorized
Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
2018-05-27 19:56:24.868 INFO 8949 --- [ Test worker] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet ''
2018-05-27 19:56:24.868 INFO 8949 --- [ Test worker] o.s.t.web.servlet.TestDispatcherServlet : FrameworkServlet '': initialization started
2018-05-27 19:56:24.872 INFO 8949 --- [ Test worker] o.s.t.web.servlet.TestDispatcherServlet : FrameworkServlet '': initialization completed in 4 ms
MockHttpServletRequest:
HTTP Method = POST
Request URI = /login
Parameters = {}
Headers = {Content-Type=[application/json]}
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 401
Error message = Unauthorized
Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
Status expected:<200> but was:<401>
Expected :200
Actual :401
I'm not sure where the /login endpoint comes from, but I'm pretty sure that it shouldn't be getting authenticated like it is, otherwise how does anyone login?
I assume that my lack of understanding of Spring Security is at fault her, can anyone see what I've done wrong?
I asked a similar question before on a different setup - there was little in the way on answers, so I'm trying again.

Per default spring generates a basic form login. You need to disable it in the Websecurity like so:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(SIGN_UP_URL).permitAll()
.antMatchers(LOGIN_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new AuthNFilter(authenticationManager()))
.addFilter(new AuthZFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().formLogin().disable();
}
EDIT:
After some debugging I found the errors.
You have mocked the UserRepository but not the method so
findByUsername will always return null. I have removed it to
use the real repository against the hsql.
The user is always locked.
#Override
public boolean isAccountNonLocked() {
return false; //changed it to true
}
The password encoder only support version $2a$ from bcyrpt and not $2y$.
After changing these the test runs without error.

Related

Json authentication in spring boot with flutter front end

I am trying to make a log in page in spring boot with a flutter front end by sending the username and password through JSON. The IP sends the request from my android emulator to localhost.
The following flutter code runs when a submit button is pressed:
Future<String> sendLogin(
String username, String password, BuildContext context) async {
var url = "http://10.0.2.2:8080/login";
var response = await http.post(Uri.parse(url),
headers: <String, String>{"Content-Type": "application/json"},
body: jsonEncode(<String, String>{
"username": username,
"password": password,
}));
url = "http://10.0.2.2:8080/protected-resource";
response = await http.get(Uri.parse(url));
return response.body;
}
My spring boot back-end looks like this
-Security configuration:
#Configuration
#EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private MyAuthenticationProvider authProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
CustomFilter mupaf = new CustomFilter();
mupaf.setAuthenticationManager(authenticationManager());
http
.csrf().disable()
.addFilterAt(
mupaf,
UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/h2/**")
.permitAll()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/protected-resource").authenticated()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
http.authenticationProvider(authProvider);
}
}
The custom filter:
public class CustomFilter extends AbstractAuthenticationProcessingFilter {
protected CustomFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
#Override
public Authentication attemptAuthentication(
HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username, password;
try {
Map<String, String> requestMap = new ObjectMapper().readValue(request.getInputStream(), Map.class);
username = requestMap.get("username");
password = requestMap.get("password");
System.out.println(username+ " "+password+" Tried to log in");
} catch (IOException e) {
throw new AuthenticationServiceException(e.getMessage(), e);
}
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
Authentication provider:
#Component
#RequiredArgsConstructor
public class MyAuthenticationProvider implements AuthenticationProvider {
private final UserRepository userRepository;
#Override
public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
final UsernamePasswordAuthenticationToken upAuth = (UsernamePasswordAuthenticationToken) authentication;
final String name = (String) authentication.getPrincipal();
final String password = (String) upAuth.getCredentials();
final String storedPassword = userRepository.findByUsername(name).map(User::getPassword)
.orElseThrow(() -> new BadCredentialsException("illegal id or passowrd"));
if (Objects.equals(password, "") || !Objects.equals(password, storedPassword)) {
throw new BadCredentialsException("illegal id or passowrd");
}
final Object principal = authentication.getPrincipal();
final UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
Collections.emptyList());
result.setDetails(authentication.getDetails());
return result;
}
#Override
public boolean supports(Class<?> authentication) {
return true;
}
}
When I send a username and password through postman I am able to log in and access protected resources. But when I do the same through the flutter front end, the log in is successful but the protected resource can't be accessed (Error 403).
What am I missing here that causes the issue?

spring security - jwt request filter validates and authenticate session but principal is null

After updating my spring boot from 2.0.3 to 2.2.1 somehow my Spring Security configuration stop working.
setup is as follow - I would like to have all my request processed by request filter where JWT token is validated and UserDetails are created. So config is pretty easy:
#Configuration
#EnableWebSecurity
public class NoAuthConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated();
}
}
and filter class
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
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 ){}
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");
}
}
if (username != null ) {
UserDetails userDetails = null;
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);
}
} else {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
chain.doFilter(request, response);
}
#Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String[] AUTH_WHITELIST = {
"/v2/auth/",
"/api/packetdiscovery"
};
String path = request.getServletPath();
return (StringUtils.startsWithAny(path, AUTH_WHITELIST));
}
}
and by debugging I can see that everything work smoothly until request reach Controller where NullPointerException Is thrown on Principal
#PreAuthorize("hasAuthority('ROLE_USER')")
#GetMapping(value = "/userinfo")
public ResponseEntity<SessionOwner> getSesstionOwner(Principal user) {
return dashboardService.getSessionOwner(user.getName());
}
Could anyone give me advice on how to handle it?

Access Deny and Allow Functionality using Spring and Spring security

Currently I am trying to implement authentication example using spring MVC and spring boot with spring security. In my sample application what I am trying to do is - I am sending one authentication token in header of one URL. I need to take this authentication token from URL and decode. If username and password is matching , then only need to transfer the control to end point "api/getStudent/v1" or something like this. Otherwise from there only need to give the response that denying.
For this Currently I tried with authentication provider from spring security. But it is not suitable for taking the token from header of request. Here my confusion is that , from spring security which method I have to implement here ? Can anyone suggest a standard way of implementation ? Or Any documentation for this type of implementation?
All you need to do is create a custom security filter and plug this filter before spring security BasicAuthenticationFilter. Sample code -
public class CustomAuthenticationFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeaders("Authorization");
//Decode the authHeader
//Validate the authHeader with your username & password
if(invalid) {
//throw exception and abort processing
}
filterChain.doFilter(request, response);
}
}
Now either you can create the bean OR make this as #component so that spring picks it up and creates bean for you.
In your security configuration, add following -
#Configuration
public class CustomWebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAfter(new CustomAuthenticationFilter(), BasicAuthenticationFilter.class);
}
}
You can try out the following. I have used JWT authentication here. And as per your problem you can preauthorize your end point "api/getStudent/v1" with spring's #Preauthorize annotation.
Following is the end point where user will be directed upon the signin.
#PostMapping("/signin")
public ResponseEntity<?> authenticateUser(#Valid #RequestBody LoginForm loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtProvider.generateJwtToken(authentication);
UserPrinciple userPrinciple = (UserPrinciple) authentication.getPrincipal();
String name = userRepo.findById(userPrinciple.getId()).get().getName();
return ResponseEntity.ok(new JwtResponse(jwt, userPrinciple.getUsername(),
userPrinciple.getAuthorities(),name,userPrinciple.getGender()));
}
Following is the WebSecurityConfig class
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(
prePostEnabled = true
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
UserDetailsServiceImpl userDetailsService;
#Autowired
private JwtAuthEntryPoint unauthorizedHandler;
#Bean
public JwtAuthTokenFilter authenticationJwtTokenFilter() {
return new JwtAuthTokenFilter();
}
#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 AuthorizationRequestRepository<OAuth2AuthorizationRequest> customAuthorizationRequestRepository() {
return new HttpSessionOAuth2AuthorizationRequestRepository();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
Following JWTProvider class includes the method to generate the JWT token.(note: I have set the email of each user as the username. You can do it according to your wish)
#Component
public class JwtProvider {
#Autowired
UserRepository userRepo;
private static final Logger logger = LoggerFactory.getLogger(JwtProvider.class);
public String generateJwtToken(Authentication authentication) {
UserPrinciple userPrincipal = (UserPrinciple) authentication.getPrincipal();
String name = userRepo.findById(userPrincipal.getId()).get().getName();
return Jwts.builder()
.setSubject((userPrincipal.getUsername())) //getUsername returns the email
.claim("id",userPrincipal.getId() )
.claim("name",name)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public String generateJwtToken(UserPrinciple userPrincipal) {
String name = userRepo.findById(userPrincipal.getId()).get().getName();
return Jwts.builder()
.setSubject((userPrincipal.getUsername())) //getUsername returns the email
.claim("id",userPrincipal.getId() )
.claim("name",name)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
logger.error("Invalid JWT signature -> Message: {} ", e);
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token -> Message: {}", e);
} catch (ExpiredJwtException e) {
logger.error("Expired JWT token -> Message: {}", e);
} catch (UnsupportedJwtException e) {
logger.error("Unsupported JWT token -> Message: {}", e);
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty -> Message: {}", e);
}
return false;
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody().getSubject();
}
}
Following is the JWTAuthTokenFilter class which is initiated in WebSecurityConfig class. Here is where it decodes the token from the rquest and checks whether the token is valid or not
public class JwtAuthTokenFilter extends OncePerRequestFilter {
#Autowired
private JwtProvider tokenProvider;
#Autowired
private UserDetailsServiceImpl userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthTokenFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwt(request);
if (jwt != null && tokenProvider.validateJwtToken(jwt)) {
String email = tokenProvider.getUserNameFromJwtToken(jwt);//returns the email instead of username
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Can NOT set user authentication -> Message: {}", e);
}
filterChain.doFilter(request, response);
}
private String getJwt(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.replace("Bearer ", "");
}
return null;
}
}
Following is the JWTAuthEntryPoint . Check WebSecurityConfig class for the use of this class
#Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthEntryPoint.class);
#Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e)
throws IOException, ServletException {
logger.error("Unauthorized error. Message - {}", e.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error -> Unauthorized");
}
}
Following is the class I created for the constraints
public class SecurityConstraints {
public static final String SECRET = "********";//add any secret you want
public static final long EXPIRATION_TIME = 864_000_000L;
}
Seem like you are working with REST API, you can use JWT and Custom Filter similar to this (https://medium.com/#hantsy/protect-rest-apis-with-spring-security-and-jwt-5fbc90305cc5)
I am sending one authentication token in header of one URL. I need to
take this authentication token from URL and decode. If username and
password is matching...
Usually, the goal of using tokens for authentication is to get rid of username and password check.
Basic HTTP authentication that is supported by Spring Security out of the box assumes passing base64 encoded username and password in the HTTP header: e.g. Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l (base64 encoded Aladdin:OpenSesame).
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/public").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
}
}
If you still need to extract username and password from a token in a different way, consider the following example.
Considering you have the following REST controller:
#RestController
public class TestRestController {
#GetMapping("/api/getStudent/v1")
public String helloWorld() {
return "Hello, World!";
}
#GetMapping("/info")
public String test() {
return "Test";
}
}
In order to make endpoint /api/getStudent/v1 protected and /info public, and extract principal and credentials from the HTTP request header you need to implement custom AbstractAuthenticationProcessingFilter:
public class HeaderUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public HeaderUsernamePasswordAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
super(requiresAuthenticationRequestMatcher);
setAuthenticationSuccessHandler((request, response, authentication) -> {
});
setAuthenticationFailureHandler((request, response, exception) ->
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, exception.getMessage()));
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String token = request.getHeader("token");
String username = token; //get username from token
String password = token; //get password from token
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(authenticationToken);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
}
This filter must extract principal and credentials from the token passed in header and attempt an authentication with Spring Security.
Next, you have to create an instance of this custom filter and configure Spring Security to add the filter in the security filter chain (.addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)):
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public HeaderUsernamePasswordAuthenticationFilter authenticationFilter() throws Exception {
HeaderUsernamePasswordAuthenticationFilter authenticationFilter =
new HeaderUsernamePasswordAuthenticationFilter(new AntPathRequestMatcher("/api/**"));
authenticationFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationFilter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.addFilterBefore(
authenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
}
//...
}
It is important to make the filter aware of the Spring Security authenticationManagerBean: authenticationFilter.setAuthenticationManager(authenticationManagerBean());.
You can configure what endpoints to protect with aunthentication by passing a RequestMatcher: e.g. new AntPathRequestMatcher("/api/**").
For testing, you can create in-memory UserDetailsService and test user with username test, password test and authority admin:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//...
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("test")
.password(passwordEncoder().encode("test"))
.authorities("admin");
}
}
Run the application and try to access the public endpoint without an authentication:
curl -i http://localhost:8080/info
HTTP/1.1 200
Test
the protected endpoint without an authentication:
curl -i http://localhost:8080/api/getStudent/v1
HTTP/1.1 401
the protected endpoint without an invalid token:
curl -i http://localhost:8080/api/getStudent/v1 -H 'token: not_valid'
HTTP/1.1 401
and finally the protected endpoint with a valid token:
curl -i http://localhost:8080/api/getStudent/v1 -H 'token: test'
HTTP/1.1 200
Hello, World!

Configuring security in a Spring Boot application

I'm upgrading an application to Spring Boot 2.0.3.
But my login request is unauthorized:
curl -H "Accept:application/json" -H "Content-Type: application/json" "http://localhost:8080/api/users/login" -X POST -d "{ \"email\" : \"myemail#somedomain.com\", \"password\" : \"xxxxx\" }" -i
The response is a 401 Unauthorized access. You failed to authenticate.
It is given by my custom entry point:
#Component
public final class RESTAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
private static Logger logger = LoggerFactory.getLogger(RESTAuthenticationEntryPoint.class);
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
logger.debug("Security - RESTAuthenticationEntryPoint - Entry point 401");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized access. You failed to authenticate.");
}
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("User REST");
super.afterPropertiesSet();
}
}
The debugger shows the authenticate method of my CustomAuthenticationProvider is not called as I expect it to be:
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
CredentialsService credentialsService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String email = authentication.getName();
String password = authentication.getCredentials().toString();
List<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<SimpleGrantedAuthority>();
User user = null;
try {
user = credentialsService.findByEmail(new EmailAddress(email));
} catch (IllegalArgumentException e) {
throw new BadCredentialsException("The login " + email + " and password could not match.");
}
if (user != null) {
if (credentialsService.checkPassword(user, password)) {
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
return new UsernamePasswordAuthenticationToken(email, password, grantedAuthorities);
} else {
throw new BadCredentialsException("The login " + user.getEmail() + " and password could not match.");
}
}
throw new BadCredentialsException("The login " + authentication.getPrincipal() + " and password could not match.");
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
But the filter is exercised and a null token is found:
#Component
public class AuthenticationFromTokenFilter extends OncePerRequestFilter {
#Autowired
private TokenAuthenticationService tokenAuthenticationService;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
tokenAuthenticationService.authenticateFromToken(request);
chain.doFilter(request, response);
}
}
#Service
public class TokenAuthenticationServiceImpl implements TokenAuthenticationService {
private static Logger logger = LoggerFactory.getLogger(TokenAuthenticationServiceImpl.class);
private static final long ONE_WEEK = 1000 * 60 * 60 * 24 * 7;
private static final String TOKEN_URL_PARAM_NAME = "token";
#Autowired
private ApplicationProperties applicationProperties;
#Autowired
private UserDetailsService userDetailsService;
public void addTokenToResponseHeader(HttpHeaders headers, String username) {
String token = buildToken(username);
headers.add(CommonConstants.AUTH_HEADER_NAME, token);
}
public void addTokenToResponseHeader(HttpServletResponse response, Authentication authentication) {
String username = authentication.getName();
if (username != null) {
String token = buildToken(username);
response.addHeader(CommonConstants.AUTH_HEADER_NAME, token);
}
}
private String buildToken(String username) {
String token = null;
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
Date expirationDate = new Date(System.currentTimeMillis() + ONE_WEEK);
token = CommonConstants.AUTH_BEARER + " " + Jwts.builder().signWith(HS256, getEncodedPrivateKey()).setExpiration(expirationDate).setSubject(userDetails.getUsername()).compact();
}
return token;
}
public Authentication authenticateFromToken(HttpServletRequest request) {
String token = extractAuthTokenFromRequest(request);
logger.debug("The request contained the JWT token: " + token);
if (token != null && !token.isEmpty()) {
try {
String username = Jwts.parser().setSigningKey(getEncodedPrivateKey()).parseClaimsJws(token).getBody().getSubject();
if (username != null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
logger.debug("Security - The filter authenticated fine from the JWT token");
}
} catch (SignatureException e) {
logger.info("The JWT token " + token + " could not be parsed.");
}
}
return null;
}
private String extractAuthTokenFromRequest(HttpServletRequest request) {
String token = null;
String header = request.getHeader(CommonConstants.AUTH_HEADER_NAME);
if (header != null && header.contains(CommonConstants.AUTH_BEARER)) {
int start = (CommonConstants.AUTH_BEARER + " ").length();
if (header.length() > start) {
token = header.substring(start - 1);
}
} else {
// The token may be set as an HTTP parameter in case the client could not set it as an HTTP header
token = request.getParameter(TOKEN_URL_PARAM_NAME);
}
return token;
}
private String getEncodedPrivateKey() {
String privateKey = applicationProperties.getAuthenticationTokenPrivateKey();
return Base64.getEncoder().encodeToString(privateKey.getBytes());
}
}
My security configuration is:
#Configuration
#EnableWebSecurity
#ComponentScan(nameGenerator = PackageBeanNameGenerator.class, basePackages = { "com.thalasoft.user.rest.security", "com.thalasoft.user.rest.filter" })
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private AuthenticationFromTokenFilter authenticationFromTokenFilter;
#Autowired
private SimpleCORSFilter simpleCORSFilter;
#Autowired
private RESTAuthenticationEntryPoint restAuthenticationEntryPoint;
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.authenticationProvider(new CustomAuthenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(restAuthenticationEntryPoint)
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(simpleCORSFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(authenticationFromTokenFilter, UsernamePasswordAuthenticationFilter.class)
.headers().cacheControl().disable().frameOptions().disable()
.and()
.userDetailsService(userDetailsService)
.authorizeRequests()
.antMatchers(RESTConstants.SLASH + UserDomainConstants.USERS + RESTConstants.SLASH + UserDomainConstants.LOGIN).permitAll()
.antMatchers(RESTConstants.SLASH + RESTConstants.ERROR).permitAll()
.antMatchers("/**").hasRole(UserDomainConstants.ROLE_ADMIN).anyRequest().authenticated();
}
}
The user details service is:
#Component
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
private CredentialsService credentialsService;
#Override
#Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (username != null && !username.isEmpty()) {
User user = credentialsService.findByEmail(new EmailAddress(username));
if (user != null) {
return new UserDetailsWrapper(user);
}
}
throw new UsernameNotFoundException("The user " + username + " was not found.");
}
}
Why is the custom authentication provider not authenticating the username and password ?
UPDATE:
I read something interesting and puzzling in this guide
Note that the AuthenticationManagerBuilder is #Autowired into a method in a #Bean - that is what makes it build the global (parent) AuthenticationManager. In contrast if we had done it this way (using an #Override of a method in the configurer) then the AuthenticationManagerBuilder is only used to build a "local" AuthenticationManager, which is a child of the global one. In a Spring Boot application you can #Autowired the global one into another bean, but you can’t do that with the local one unless you explicitly expose it yourself.
So, is there anything wrong with my usage of the configure method for setting up the authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider); ?
Instead of the above configuration, I tried the following configuration:
#Autowired
public void initialize(AuthenticationManagerBuilder authenticationManagerBuilder) {
authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider);
}
But it still didn't exercise the custom authentication provider upon a request.
I also tried to have the filter after as in:
http.addFilterAfter(authenticationFromTokenFilter, UsernamePasswordAuthenticationFilter.class);
instead of addFilterBefore but it didn't change anything to the issue.
In WebSecurityConfiguration inside configure(HttpSecurity http) method:
http.authorizeRequests().antMatchers("/api/users/login").permitAll();
http.authorizeRequests().anyRequest().authenticated();
Add in the same order.
Explanation: Login and logout requests should be permitted without any authentication
A sample configure method that works is:
http.formLogin().disable().logout().disable().httpBasic().disable();
http.authorizeRequests().antMatchers("/logout", "/login", "/").permitAll();
http.authorizeRequests().anyRequest().authenticated();
http.addFilterBefore(new SomeFilter(), SecurityContextHolderAwareRequestFilter.class);
http.addFilterBefore(new CORSFilter(env), ChannelProcessingFilter.class);
http.addFilterBefore(new XSSFilter(),CORSFilter.class);
According to me when we implement our own ApplicationFilter by implementing GenericFilterBean we need to check if the token received from the request is valid or not. If it is not valid then we need to dump the token into the security context (for the authentication-provider to pick up). I haven't gone through your filter class. But this worked for me :
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httprequset=(HttpServletRequest)request;
String uname=request.getParameter("username");
String pwd=request.getParameter("password");
String role=request.getParameter("role");
List<GrantedAuthority> l = new ArrayList<>();
l.add( new SimpleGrantedAuthority(role.toUpperCase()) );
UsernamePasswordAuthenticationToken token=new UsernamePasswordAuthenticationToken(uname,pwd,l);
token.setAuthenticated(false);
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(httprequset, response);
}

Spring/Spock - integration test of authentication

I want to test protected endpoints in my application and I would like login before each test, get token and use this token in next tests.
#ContextConfiguration
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#Stepwise
class IntegrationSpec extends Specification {
#Autowired
private TestRestTemplate rest
#Autowired
private String token
#Shared
private HttpEntity request
def setup() {
UserData userData = new UserData("Username", "Password");
def response = rest.postForEntity('/login', userData, Object)
String tempToken = response.getHeaders().get('Authorization')
// remove prefix of token
this.token = tempToken.substring(tempToken.lastIndexOf(" ") + 1)
String loginToken = token.substring(token.lastIndexOf(" ") + 1)
final MultiValueMap<String, String> headers = new LinkedMultiValueMap<>()
headers.add('Authorization', loginToken)
this.request = new HttpEntity(headers)
}
def 'protected resource test'() {
when:
def response = rest.exchange('/protected/resource', HttpMethod.GET, request, new ParameterizedTypeReference<String>() {})
then:
response.getStatusCode() == HttpStatus.OK
}
}
but I get an error :
io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:354) ~[jjwt-0.7.0.jar:0.7.0]
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:481) ~[jjwt-0.7.0.jar:0.7.0]
at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:541) ~[jjwt-0.7.0.jar:0.7.0]
at mypackages.TokenAuthenticationService.getAuthentication(TokenAuthenticationService.java:39) ~[classes/:na]
at mypackages.filters.JWTAuthenticationFilter.doFilter(JWTAuthenticationFilter.java:23) ~[classes/:na]
my TokenService looks following:
public class TokenAuthenticationService {
static final long EXPIRATION_TIME = 10000000;
static final String SECRET = "secret_word";
static final String TOKEN_PREFIX = "Bearer";
static final String HEADER_STRING = "Authorization";
public static void addAuthentication(HttpServletResponse res, String username) {
String JWT = Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT);
}
public static Authentication getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if (token != null) {
String user = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody()
.getSubject();
return user != null ?
new UsernamePasswordAuthenticationToken(user, null, new ArrayList<GrantedAuthority>()) :
null;
}
return null;
}
}
JWTAuthenticationFilter:
public class JWTAuthenticationFilter extends GenericFilterBean {
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
Authentication authentication = TokenAuthenticationService.getAuthentication((HttpServletRequest) servletRequest);
SecurityContextHolder.getContext()
.setAuthentication(authentication);
filterChain.doFilter(servletRequest, servletResponse);
}
}
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public UserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.antMatchers(HttpMethod.POST, "/register").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JWTLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService());
}
}

Resources