Digest authentication with spring security: 401 recieved as expected but with two WWW-Authenticate headers - spring-boot

When I send PUT request with correct username and password, it works fine.
But when I send request with wrong password, I received 401 which is ok, but in I got 2 WWW-Authenticate headers:
Response headers:
HTTP/1.1 401
WWW-Authenticate: Digest realm="NOKIA.COM", qop="auth", nonce="MTU1MjM3MDk2MDQ2MjpmOWNjYjVmNGU5ODA0ZmY0YWY0MjIxNDlhY2U2ODJiMQ=="
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
WWW-Authenticate: Digest realm="NOKIA.COM", qop="auth", nonce="MTU1MjM3MDk2MDQ2NjoxOTQ4MDhjNzBjYjkyMGI1Y2Q2YjU3OGMyMTM2NmE3OQ=="
Content-Length: 0
Date: Tue, 12 Mar 2019 06:08:20 GMT
#EnableWebSecurity
#Configuration
#Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
#Autowired
DummyUserService userDetail;
#Autowired
DigestAuthenticationFilter digestFilter;
#Autowired
DigestAuthenticationEntryPoint digestEntryPoint;
#Override
protected void configure( HttpSecurity http ) throws Exception
{
http.addFilter(digestFilter) // register digest entry point
.exceptionHandling().authenticationEntryPoint(digestEntryPoint) // on exception ask for digest authentication
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and().csrf().disable();
http.httpBasic().disable();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
#Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
#Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
};
}
}
#Bean
DigestAuthenticationFilter digestFilter( DigestAuthenticationEntryPoint digestAuthenticationEntryPoint,
UserCache digestUserCache, UserDetailsService userDetailsService )
{
DigestAuthenticationFilter filter = new DigestAuthenticationFilter();
filter.setAuthenticationEntryPoint( digestAuthenticationEntryPoint );
filter.setUserDetailsService( userDetailsService );
filter.setUserCache( digestUserCache );
return filter;
}
#Bean
UserCache digestUserCache() throws Exception
{
return new SpringCacheBasedUserCache( new ConcurrentMapCache( "digestUserCache" ) );
}
#Bean
DigestAuthenticationEntryPoint digestAuthenticationEntry()
{
DigestAuthenticationEntryPoint digestAuthenticationEntry = new DigestAuthenticationEntryPoint();
digestAuthenticationEntry.setRealmName( "XXX.COM" );
digestAuthenticationEntry.setKey( "XXX" );
digestAuthenticationEntry.setNonceValiditySeconds( 60 );
return digestAuthenticationEntry;
}
Please someone can give me some help. Many thanks!

I solved this problem by myself. For request with incorrect auth, DigestAuthenticationEntryPoint was called twice by both digestFilter and exceptionFilter.
Overwrite DigestAuthenticationEntryPoint:
public class CustomDigestAuthenticationEntryPoint extends DigestAuthenticationEntryPoint
{
#Override
public void commence( HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException )
throws IOException, ServletException
{
HttpServletResponse httpResponse = ( HttpServletResponse ) response;
String authHeader = httpResponse.getHeader( "WWW-Authenticate" );
if( authHeader != null )
{
httpResponse.sendError( HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase() );
}
else
{
super.commence( request, httpResponse, authException );
}
}
}

Related

Spring Custom Authentication Provider- how to return custom REST Http Status when authentication fails

I have custom authentication provider that works fine:
#Component
public class ApiAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
final String name = authentication.getName();
final String password = authentication.getCredentials().toString();
if (isAuthorizedDevice(name, password)) {
final List<GrantedAuthority> grantedAuths = new ArrayList<>();
grantedAuths.add(new SimpleGrantedAuthority(ApiInfo.Role.User));
final UserDetails principal = new User(name, password, grantedAuths);
return new UsernamePasswordAuthenticationToken(principal, password, grantedAuths);
} else {
return null;
}
}
But it always return 401. I would like to change it in some cases to 429 for brute force mechanism. Instead of returning null I would like to return error: f.e.: 429. I think It should not be done here. It should be done in configuration: WebSecurityConfig but I have no clue how to achieve this.
I tried already throwing exceptions like:
throw new LockedException("InvalidCredentialsFilter");
throw new AuthenticationCredentialsNotFoundException("Invalid Credentials!");
or injecting respone object and setting there status:
response.setStatus(429);
But non of it worked. It always return 401.
F.e.:
curl http://localhost:8080/api/v1.0/time --header "Authorization: Basic poaueiccrmpoawklerpo0i"
{"timestamp":"2022-08-12T20:58:42.236+00:00","status":401,"error":"Unauthorized","path":"/api/v1.0/time"}%
And body:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Fri Aug 12 22:58:17 CEST 2022
There was an unexpected error (type=Unauthorized, status=401).
Also could not find any docs or Baeldung tutorial for that.
Can You help me?
P.S My WebSecurityConfig:
#Configuration
#EnableWebSecurity
class WebSecurityConfig {
AuthenticationProvider apiAuthenticationProvider;
#Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
return http
.csrf().disable()
.formLogin().disable()
.httpBasic().and()
.authenticationProvider(apiAuthenticationProvider)
.authorizeRequests()
.antMatchers(ApiInfo.BASE_URL + "/**")
.fullyAuthenticated()
.and()
.build();
}
As I did not useful answer I will post my solution.
Generally I've added custom implementation of AuthenticationEntryPoint, which handles all unauthorized request and it is proceeded after AuthenticationProvider:
#Component
public class BruteForceEntryPoint implements AuthenticationEntryPoint {
final BruteForce bruteForce;
static final String WWW_AUTHENTICATE_HEADER_VALUE = "Basic realm=\"Access to API\", charset=\"UTF-8\"";
public BruteForceEntryPoint(BruteForce bruteForce) {
this.bruteForce = bruteForce;
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
addWwwAuthenticateHeader(request, response);
bruteForce.incrementFailures(request.getRemoteAddr());
if (bruteForce.IsBlocked(request.getRemoteAddr())) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
OutputStream responseStream = response.getOutputStream();
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(responseStream, HttpStatus.TOO_MANY_REQUESTS);
responseStream.flush();
} else {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
OutputStream responseStream = response.getOutputStream();
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(responseStream, HttpStatus.UNAUTHORIZED);
responseStream.flush();
}
}
void addWwwAuthenticateHeader(HttpServletRequest request, HttpServletResponse response) {
if (isWwwAuthenticateSupported(request)) {
response.addHeader(WWW_AUTHENTICATE, WWW_AUTHENTICATE_HEADER_VALUE);
}
}
}
Config:
#Configuration
class WebSecurityConfig {
AuthenticationProvider apiAuthenticationProvider;
AuthenticationEntryPoint customAuthenticationEntryPoint;
public WebSecurityConfig(AuthenticationProvider apiAuthenticationProvider, AuthenticationEntryPoint customAuthenticationEntryPoint) {
this.apiAuthenticationProvider = apiAuthenticationProvider;
this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
}
#Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
return
http
.httpBasic()
.authenticationEntryPoint(customAuthenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers(AapiInfo.BASE_URL + "/**").authenticated()
.and()
.authenticationProvider(apiAuthenticationProvider)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.formLogin().disable()
.logout().disable()
.build();
}

Spring Boot #CrossOrigin annotation not working

I am using Spring 2.6.6 and Reactjs for frontend. I have a POST endpoint exposed using a rest controller but is is giving CORS error when api is called
Below is the rest controller
#RestController
#CrossOrigin(origins = "http://localhost:3000", allowedHeaders = "*")
#RequestMapping("/image")
public class ImageUpload {
#Autowired
private ExtractData extractData;
#Autowired
private Gson gson;
#PostMapping("/extract")
public Map<String, ArrayList<String>> upload(
#RequestParam(value = "image") MultipartFile image,
#RequestParam(value = "selectedOptions") String selectedOptions
) throws IOException, TesseractException {
Map<String, Boolean> selectedOptionsMap = gson.fromJson(selectedOptions, Map.class);
byte[] imageData = image.getBytes();
ByteArrayInputStream bais = new ByteArrayInputStream(imageData);
try {
BufferedImage bufferedImage = ImageIO.read(bais);
Map<String, ArrayList<String>> resultSet = new HashMap<String, ArrayList<String>>();
resultSet = extractData.extractData(selectedOptionsMap, bufferedImage);
return resultSet;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Also implemented WebMvcConfigurer as follows but this also didn't work:
#Configuration
#EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/image/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST")
.allowedHeaders("*")
.allowCredentials(false).maxAge(3600);
}
}
My security configuration class
#EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService myUserDetailsService;
#Autowired
private JwtRequestFilter jwtRequestFilter;
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().antMatchers("/authenticate").permitAll()
.anyRequest().authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Request headers:
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Connection: keep-alive
Content-Length: 110239
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX8BGweBFqsM2R7hy
Host: localhost:8080
Origin: http://localhost:3000
Referer: http://localhost:3000/
sec-ch-ua: "Chromium";v="88", "Google Chrome";v="88", ";Not A Brand";v="99"
sec-ch-ua-mobile: ?0
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.146 Safari/537.36
Response headers:
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Length: 0
Date: Sun, 03 Jul 2022 13:37:57 GMT
Expires: 0
Keep-Alive: timeout=60
Pragma: no-cache
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Also adding CORS filter implementation did not work
I was missing http.cors() in the security configuration
Changing the configure method as follows solved the problem
#Override
public void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests().antMatchers("/authenticate").permitAll()
.anyRequest().authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}

Spring security basic authentication configuration

I've been trying to follow this tutorial :
https://www.baeldung.com/spring-security-basic-authentication
I have created a couple of rest endpoints like this :
#RestController
public class PostController {
#Autowired
PostCommentService postCommentService;
#Autowired
PostService postService;
#GetMapping("/comment")
public PostComment getComment(#RequestParam Long id) {
return postCommentService.findPostCommentById(id);
}
#PostMapping("/createPost")
public void createPost(#RequestBody PostDTO body){
postService.createPost(body);
}
}
Now for security I am using spring like this:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
This is the config class for spring security:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private MyBasicAuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers( "/comment").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint);
http.addFilterAfter(new CustomFilter(),
BasicAuthenticationFilter.class);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password(passwordEncoder().encode("password"))
.authorities("ROLE_USER");
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
The CustomFilter looks like this:
public class CustomFilter extends GenericFilterBean {
#Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
}
}
And this is the AuthenticationEntryPoint:
#Component
public class MyBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx)
throws IOException {
response.addHeader("WWW-Authenticate", "Basic realm= + getRealmName() + ");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.println("HTTP Status 401 - " + authEx.getMessage());
}
#Override
public void afterPropertiesSet(){
setRealmName("spring");
super.afterPropertiesSet();
}
}
Now the problem is that whenever I try to send a POST request I end up getting this error message:
HTTP Status 401 - Full authentication is required to access this resource
I have tried two approaches to send the request, one via postman
And the second one via curl:
curl -i --user admin:password --request POST --data {"text":"this is a new Post"} http://localhost:8080/createPost
I am at my wits' end here, hence the need to create this post. Any help will be much appreciated.
This is the curl response in case it might shed light on the matter:
1.1 401
Set-Cookie: JSESSIONID=6FE84B06E90BE7F2348C0935FE3DA971; Path=/; HttpOnly
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
WWW-Authenticate: Basic realm= + getRealmName() +
Content-Length: 75
Date: Thu, 10 Sep 2020 13:47:14 GMT
HTTP Status 401 - Full authentication is required to access this resource
This happens because Spring Security comes with CSRF protection enabled by default (and for a good reason). You can read about Cross Site Request Forgery here. In your case the CsrfFilter detects missing or invalid CSRF token and you're getting the 401 response. The easiest way to make your example work would be to disable csrf-ing in your security configuration but, of course, you shouldn't do this in a real application.
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.antMatchers( "/comment").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint);
http.addFilterAfter(new CustomFilter(),
BasicAuthenticationFilter.class);
}

Spring boot 2.0 HttpSecurity auth doesn't work without sent Authorization header

I have this security settings in class WebSecurity extends WebSecurityConfigurerAdapter:
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.addFilterBefore(corsFilter(), SessionManagementFilter.class)
.csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
JWTAuthenticationFilter:
class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter
{
private AuthenticationManager authenticationManager;
private Logger logger = Logger.getLogger("JWTAuthenticationFilter");
JWTAuthenticationFilter(AuthenticationManager authenticationManager)
{
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException
{
String username = req.getParameter("username");
logger.info("Login attempt with username: " + username);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, req.getParameter("password"), new ArrayList<>())
);
}
#Override
protected void successfulAuthentication(
HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth
)
{
String token = Jwts
.builder()
.setSubject(((User) auth.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
}
}
JWTAuthorizationFilter:
public class JWTAuthorizationFilter extends BasicAuthenticationFilter
{
JWTAuthorizationFilter(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)
{
String user = Jwts
.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody()
.getSubject();
if (user != null)
{
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
When I send the Authorization: Bearer "correct token" header, it's working fine.
When I send the Authorization: Bearer "expired token" header, I got the correct error message.
But If I don't send the header it won't bock the API call and I got the response without error message.
If I send the Auth header with random text instead of Bearer I got the response without error message.
What could wrong with it?
Not an expert but you can try to add your filters at a specific location with
.addFilterBefore(new JWTAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
Let me know if something changes

Spring Security JWT REST API returning 401

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.

Resources