I have a small Spring Boot 2.1.6 webapp with JWT authententication. Call flow is as follows:
User enters username and password and sends a POST request to /authenticate
A filter is watching this URL (setFilterProcessesUrl), when a request comes, it hashes the password and checks it against the hash stored in DB
If matches, and user is not locked, it creates a JWT with username and granted roles, and returns it in response
User must include this JWT in all further requests
Also, CSRF is disabled in the WebSecurityConfigurerAdapter.
The solution itself is working fine, but I have to create unit tests as well. I ended up with the following test case:
#RunWith(SpringRunner.class)
#WebMvcTest
#ContextConfiguration(classes = { ConfigReaderMock.class })
public class ControllerSecurityTest {
private static final String VALID_USERNAME = "username";
private static final String VALID_PASSWORD = "password";
#Autowired
private MockMvc mockMvc;
private String createAuthenticationBody(String username, String passwordHash) {
return "username=" + URLEncoder.encode(username, StandardCharsets.UTF_8) + "&password="
+ URLEncoder.encode(passwordHash, StandardCharsets.UTF_8);
}
#Test
public void testValidLogin() throws Exception {
MvcResult result = mockMvc
.perform(MockMvcRequestBuilders.post("/authenticate")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.content(createAuthenticationBody(VALID_USERNAME, VALID_PASSWORD)).accept(MediaType.ALL))
.andExpect(status().isOk()).andReturn();
String authHeader = result.getResponse().getHeader(SecurityConstants.TOKEN_HEADER);
mockMvc.perform(MockMvcRequestBuilders.get("/main?" + SecurityConstants.TOKEN_QUERY_PARAM + "="
+ URLEncoder.encode(authHeader, StandardCharsets.UTF_8))).andExpect(status().isOk());
}
}
What I expect, is that the server accepts the username and password provided, and returns the JWT, which I can use in the subsequent request to access the next page (the same is implemented in the front end). Instead I get HTTP 403 from the authentication filter:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /authenticate
Parameters = {username=[username], password=[password]}
Headers = [Content-Type:"application/x-www-form-urlencoded", Accept:"*/*"]
Body = <no character encoding set>
Session Attrs = {org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN=org.springframework.security.web.csrf.DefaultCsrfToken#4ac0fdc7}
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 = 403
Error message = Forbidden
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 = []
I noticed it is sending a CSRF token for some reason in the Session Attributes. Further checking the logs, I can see the belo messages:
2019-07-29 08:09:17,438 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration'
2019-07-29 08:09:17,443 DEBUG o.s.s.c.a.a.c.AuthenticationConfiguration$EnableGlobalAuthenticationAutowiredConfigurer [main] Eagerly initializing {org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration=org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration$$EnhancerBySpringCGLIB$$236da03c#4e68aede}
2019-07-29 08:09:17,444 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'inMemoryUserDetailsManager'
2019-07-29 08:09:17,445 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration'
2019-07-29 08:09:17,454 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'spring.security-org.springframework.boot.autoconfigure.security.SecurityProperties'
2019-07-29 08:09:17,457 DEBUG o.s.b.f.s.ConstructorResolver [main] Autowiring by type from bean name 'inMemoryUserDetailsManager' via factory method to bean named 'spring.security-org.springframework.boot.autoconfigure.security.SecurityProperties'
2019-07-29 08:09:17,462 INFO o.s.b.a.s.s.UserDetailsServiceAutoConfiguration [main]
Using generated security password: 963b2bac-d953-4793-a8cd-b3f81586823e
...
2019-07-29 08:09:17,783 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository [main] No HttpSession currently exists
2019-07-29 08:09:17,784 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository [main] No SecurityContext was available from the HttpSession: null. A new one will be created.
2019-07-29 08:09:17,794 DEBUG o.s.s.w.c.CsrfFilter [main] Invalid CSRF token found for http://localhost/authenticate
2019-07-29 08:09:17,795 DEBUG o.s.s.w.h.w.HstsHeaderWriter [main] Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher#1c15a6aa
2019-07-29 08:09:17,796 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository$SaveToSessionResponseWrapper [main] SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2019-07-29 08:09:17,799 DEBUG o.s.s.w.c.SecurityContextPersistenceFilter [main] SecurityContextHolder now cleared, as request processing completed
So it seems like Spring Security is creating it's own security configuration, instead of using the class I created, extending WebSecurityConfigurerAdapter. Question is, why? And how can I force it to use my security config, as I'm dependent on it with the database login?
Update: added WebSecurityConfigurerAdapter
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AICAuthenticationService authenticationService;
#Autowired
private AICUserDetailsService aicUserDetailsService;
#Autowired
private AICLogoutSuccessHandler aicLogoutSuccessHandler;
#Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and()
.authorizeRequests()
.antMatchers("/resources/**", "/login", "/").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(aicLogoutSuccessHandler)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "error");
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(aicUserDetailsService);
}
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return authenticationService;
}
#Bean
public AuthenticationManager custromAuthenticationManager() throws Exception {
return authenticationManager();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(aicUserDetailsService);
}
I was able to get it done with TestRestTemplate, like this:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ControllerSecurityTest {
private static final String VALID_USERNAME = "username";
private static final String VALID_PASSWORD = "password";
#LocalServerPort
private int port;
#Autowired
private TestRestTemplate restTemplate;
#Test
public void testValidLogin() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setAccept(Arrays.asList(MediaType.ALL));
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("username", VALID_USERNAME);
map.add("password", VALID_PASSWORD);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
ResponseEntity<String> tokenResponse = restTemplate
.postForEntity("http://localhost:" + port + "/authenticate", request, String.class);
assertEquals(200, tokenResponse.getStatusCodeValue());
String authHeader = tokenResponse.getHeaders().getFirst(SecurityConstants.TOKEN_HEADER);
assertNotNull(authHeader);
ResponseEntity<String> mainResponse = restTemplate.getForEntity("http://localhost:" + port + "/main?"
+ SecurityConstants.TOKEN_QUERY_PARAM + "=" + URLEncoder.encode(authHeader, StandardCharsets.UTF_8),
String.class);
assertEquals(200, mainResponse.getStatusCodeValue());
}
}
Related
I'm running into a weird situation.
Here's what I know:
In order to get it to log in, I need to send the credentials as a x-www-form-urlencoded POST request. User provides correct credentials, it passes and provides the user with an access token and a refresh token, if not, it fails and does not provide any of those tokens - which is what one would expect.
Now, once logged in and I try to access a resource - in this case it's the list of users I have - it fails with a 403 forbidden error. I'm currently using PostMan to test the API. The initial POST request I made after login is with a Bearer Token authorisation where in I use the access token. It fails. After debugging my code on Spring Boot:
#Slf4j
public class AuthorizationFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
if(request.getServletPath().equals("api/login") || request.getServletPath().equals("/api/token/refresh")) {
filterChain.doFilter(request, response);
} else {
String authorizationHeader = request.getHeader(AUTHORIZATION);
if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
try {
String token = authorizationHeader.substring("Bearer ".length());
// TODO: Refactor this to a utility class.
Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = verifier.verify(token);
String username = decodedJWT.getSubject();
String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
stream(roles).forEach(role -> {
authorities.add(new SimpleGrantedAuthority(role));
});
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
} catch (Exception exception) {
log.error("Error logging in: " + exception.getMessage());
Map<String, String> error = new HashMap<>();
error.put("error_message", exception.getMessage());
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), error);
}
} else {
filterChain.doFilter(request, response);
}
}
}
}
I found that token returns null. OK... let's try sending it as x-www-form-urlencoded instead. token picks up the token sent... Great! stepped in all the way through the code after filterChain.doFilter(request, response); and get to requiresAuthentication() in Spring Boots code... this returns false which is good... This is looking really good.
However, return to Postman - Status: 403 Forbidden. Oh - how? Looking at the logs for Spring Boot I picked out the following which is related to my url entrypoint:
2022-04-16 08:36:47.267 DEBUG 21000 --- [nio-8102-exec-4] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(1683533175<open>)] after transaction
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] o.s.s.a.dao.DaoAuthenticationProvider : Failed to authenticate since no credentials provided
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] o.a.c.c.C.[Tomcat].[localhost] : Processing ErrorPage[errorCode=0, location=/error]
2022-04-16 08:36:47.285 DEBUG 21000 --- [nio-8102-exec-4] o.s.security.web.FilterChainProxy : Securing GET /error
Being new to this, I'm not sure how I should proceed to fix this.
EDIT:
This is my current security configuration:
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Lazy
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.cors();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers("/login/**", "/api/login/**", "/api/token/refresh/**").permitAll();
http.authorizeRequests().antMatchers(HttpMethod.GET, "/api/users/**").hasAnyAuthority("ROLE_USER");
http.authorizeRequests().antMatchers(HttpMethod.POST, "/api/user/save/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().antMatchers(HttpMethod.POST, "/api/role/assign-to-user/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().anyRequest().authenticated();
http.addFilter(new AuthenticationFilter(authenticationManagerBean()));
http.addFilterBefore(new AuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}
#Bean
protected CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedMethods(List.of("*"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.applyPermitDefaultValues();
source.registerCorsConfiguration("/**", config);
return source;
}
}
AuthenticationFilter:
#Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response
) throws AuthenticationException {
String username = request.getParameter("username");
String password = request.getParameter("password");
log.info("Username: \"" + username + "\", password: \"" + password + "\"");
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(authenticationToken);
}
#Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authentication
) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
// TODO: Generate secret key that is a little more secure than this.
Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
String accessToken = JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + 10 * 60 * 1000))
.withIssuer(request.getRequestURL().toString())
.withClaim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.sign(algorithm);
String refreshToken = JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
.withIssuer(request.getRequestURL().toString())
.sign(algorithm);
Map<String, String> tokens = new HashMap<>();
tokens.put("access_token", accessToken);
tokens.put("refresh_token", refreshToken);
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), tokens);
}
}
I noticed that AuthorizationFilter creates authorities but then does not set them in the UsernamePasswordAuthenticationToken. Your security matchers require an authority.
When a POST request to a non-existing end-point in the application is sent, the server returns 405 instead of 404. A similar problem for Requests with an existing endpoint occurs, the status code returns 200 whenever everything goes right, but when an internal server error occurs (for example User not found), the http response becomes 405 (instead of 500). With GET requests everything works as it should.
The weird thing is, if I put on the debugger, and I follow the process of the error to be thrown, it is handling a 500 error. But apparently somewhere in the end something goes wrong and I get a 405 returned.
My web security config:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
#Autowired
private UserDetailsService jwtUserDetailsService;
#Autowired
private JwtRequestFilter jwtRequestFilter;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// configure AuthenticationManager so that it knows from where to load
// user for matching credentials
// Use BCryptPasswordEncoder
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Value("${allowedOrigin}")
private String origin = "http://localhost:4200";
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
//You can enforce the use of HTTPS when your app is running on Heroku by adding
// the following configuration to your Spring Boot app.
httpSecurity.requiresChannel()
.requestMatchers(r - > r.getHeader("X-Forwarded-Proto") != null)
.requiresSecure();
httpSecurity
.cors()
.and().csrf()
.disable()
// dont authenticate this particular request
.authorizeRequests()
.antMatchers("/api/authenticate")
.permitAll()
// all other requests for /api need to be authenticated
.antMatchers("/api/**", "/admin/**")
.authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
UPDATE:
I do not have any ControllerAdivce and there is no Global Exception handler written.
The "Allow" header in the 405 response reads "GET, HEAD", even when the POST request actually entered the POST endpoint.
Based on your HttpSecurity configuration you only allowing Post requests, you will get 405 (method not allowed) for other HTTP methods like GET, PUT....
antMatchers(HttpMethod.POST).permitAll();
It turned out an ErrorController with an unimplemented method for the "/error" path was causing the problem. Whenever an exception or an error was thrown it was resolved to "/error" and picked up by the ErrorController, which for some reason resolved it to a 405. After implementing the method the HTTP statuses are returned correctly.
#RestController
public class RoutingController implements ErrorController {
private static final String PATH = "/error";
#RequestMapping(value = PATH)
public String handleError(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
return String.format("<html><body><h2>Error Page</h2><div>Status code: <b>%s</b></div>" +
"<div>Exception Message: <b>%s</b></div><body></html>",
statusCode, exception == null ? "N/A" : exception.getMessage());
}
public String getErrorPath() {
return PATH;
}
}
How to correctly get the users's session oauth2 token ?
I implemented an OAuth2 Authorization/Resource server using spring-security-oauth2-autoconfigure.
I implemented a client app, that uses the authorization server to login the user and gets his access token. The login phase is working perfectly and so the retreive of the login data (using the access token by the oauth2 filters). The Principal in the client app requests correctly shows all authorities filled by the authorization server.
I'd like to use the client app as a proxy to send Rest Request using the given Access Token of the user that requested the call.
I already tried to use #EnableOAuth2Client but that does not work. The OAuth2RestTemplate is null when tried to be autowired.
I had to reimplement a request scoped bean of a RestTemplate which get the tokenValue from the SecurityContext. This works, but I do not find this clean. This behavior is quite common, so I should miss something.
application.yml
spring:
application.name: client
security:
oauth2:
client:
registration:
myclient:
client-id: client-id
client-secret: client-secret
redirect-uri: http://localhost:8081/login/oauth2/code/
authorization-grant-type: authorization_code
provider:
myclient:
authorization-uri: http://localhost:8090/oauth/authorize
token-uri: http://localhost:8090/oauth/token
user-info-uri: http://localhost:8090/me
user-name-attribute: name
SecurityConfiguration
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.authorizeRequests()
.antMatchers("/", "/login").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and().oauth2Login()
.and().oauth2Client()
;
// #formatter:on
}
#Bean
#Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
#Qualifier("oauth2RestTemplate")
public RestTemplate oauth2RestTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new ClientHttpRequestInterceptor() {
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && auth instanceof OAuth2AuthenticationToken) {
#SuppressWarnings("unchecked")
Map<String, Object> details = (Map<String, Object>) ((OAuth2AuthenticationToken) auth).getPrincipal().getAttributes().get("details");
String tokenValue = (String) details.get("tokenValue");
if (tokenValue != null) {
request.getHeaders().add("Authorization", "Bearer " + tokenValue);
}
}
return execution.execute(request, body);
}
});
return restTemplate;
}
}
In WebController
private #Autowired #Qualifier("oauth2RestTemplate") RestTemplate oauth2RestTemplate;
#GetMapping("/remote")
public Map<String, Object> remote() {
#SuppressWarnings("unchecked")
Map<String, Object> resp = oauth2RestTemplate.getForObject(URI.create("http://localhost:8090/api/test"), Map.class);
return resp;
}
It works, but I do not think I should configure the RestTemplate myself.
Unfortunately, you have to define OAuth2RestTemplate. However, this is a more clean implementation.
#Bean
public OAuth2RestTemplate oauth2RestTemplate() {
ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
resourceDetails.setAccessTokenUri(format("%s/oauth/token", authServerUrl));
resourceDetails.setClientId("client_id");
resourceDetails.setClientSecret("client_secret");
resourceDetails.setGrantType("client_credentials");
resourceDetails.setScope(asList("read", "write"));
DefaultOAuth2ClientContext clientContext = new DefaultOAuth2ClientContext();
return new OAuth2RestTemplate(resourceDetails, clientContext);
}
In this case, your Resource server will communicate with the authorization server on your behalf using its own credentials.
I'm dwelling with this problem... I have a Spring Boot application wit a S2S communication. I have a #RestController method which should accept POST request.
This is the controller
#RestController
public class PaymentRestController {
#PostMapping("/util/paymentResponse")
public void savePaymentResponse(#RequestParam boolean transaction_status, #RequestParam String usedToken,
#RequestParam String transaction_message, #RequestParam String authCode,
#RequestParam String transactionCode, #RequestParam String orderId, HttpServletRequest request) {
//business logic
}
}
If i hit this link i get a 405 error, method not allowed
At first time i found that the request was blocked by the CSFR Filter which is enabled on the web application, so I have configured my security in this way
#Configuration
#ComponentScan("it.besmart")
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
#Autowired
#Qualifier("customUserDetailsService")
UserDetailsService userDetailsService;
#Autowired
CustomSuccessHandler customSuccessHandler;
#Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
#Autowired
DataSource dataSource;
private final static Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
#Autowired
public void configureGlobalService(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public SwitchUserFilter switchUserFilter() {
SwitchUserFilter filter = new SwitchUserFilter();
filter.setUserDetailsService(userDetailsService);
filter.setSuccessHandler(customSuccessHandler);
filter.setFailureHandler(customAuthenticationFailureHandler);
return filter;
}
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Webapp security configured");
http
.authorizeRequests()
.antMatchers("/", "/home", "/contacts", "/faq", "/privacy", "/register", "/registrationConfirm", "/util/**", "/resendRegistrationToken","/park**", "/oauth/authorize", "/error")
.permitAll()
.antMatchers("/profile**", "/edit**","/payment**", "/plate**","/notification**", "/addPaymentMethod**", "/logout/impersonate**")
.access("hasRole('USER') or hasRole('NOPAYMENT')")
.antMatchers("/book**", "/manage**")
.access("hasRole('USER')")
.antMatchers("/admin**", "/login/impersonate**").access("hasRole('ADMIN')")
.antMatchers("/updatePassword").hasAuthority("CHANGE_PASSWORD_PRIVILEGE")
.and().formLogin().loginPage("/?login=login").loginProcessingUrl("/") .successHandler(customSuccessHandler).failureHandler(customAuthenticationFailureHandler).usernameParameter("email").passwordParameter("password").and().rememberMe().rememberMeParameter("remember-me").tokenRepository(persistentTokenRepository()).tokenValiditySeconds(86400).and().exceptionHandling().accessDeniedPage("/accessDenied")
.and().csrf().ignoringAntMatchers( "/util**")
.and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/?logout=true").permitAll()
.and().addFilterAfter(switchUserFilter(), FilterSecurityInterceptor.class);
}
In this way i'm not getting the CSRF token exception, but still getting the 405 error.
It's not even a problem of POST because if i change to GET the request and the mapping, i still take the 405 error... And if i try to send a POST, i see in the header response that the Allowed method is POST, if i send it in GET i see allowed method POST... weird
I don't know where to see...
In my case the endpoint had ssl on i.e. it was https
In Postman I by mistake was using http
http will work fine for GETs but for POSTs it returns a 405 method not allowed. It has to be https if your endpoint expects it to be.
If you have request and response logging turned on in Spring the POST endpoint in the above situation will log as follows:
[2021-02-26T10:40:07+02:00] (my-app/fffffa343226e) 2021-02-26T08:40:07,915Z (UTC+0) [http-nio-80-exec-6] DEBUG o.s.w.f.CommonsRequestLoggingFilter - Before request [GET /api/v1/my-app, client=1.2.3.4, user=aUser]
[2021-02-26T10:40:07+02:00] (my-app/fffffa343226e) 2021-02-26T08:40:07,915Z (UTC+0) [http-nio-80-exec-6] WARN o.s.w.s.m.s.DefaultHandlerExceptionResolver - Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' not supported]
[2021-02-26T10:40:07+02:00] (my-app/fffffa343226e) 2021-02-26T08:40:07,916Z (UTC+0) [http-nio-80-exec-6] DEBUG o.s.w.f.CommonsRequestLoggingFilter - After request [GET /api/v1/my-app, client=1.2.3.4, user=aUser]
So the problem was that one of the parameter was null.
It has been solved adding required=null at the request parameter annotation, like that:
#RequestParam(value = "yourParamName", required = false)
this cause a 405, as defined here:
6.5.5. 405 Method Not Allowed
The 405 (Method Not Allowed) status code indicates that the method
received in the request-line is known by the origin server but not
supported by the target resource. The origin server MUST generate an
Allow header field in a 405 response containing a list of the target
resource's currently supported methods.
A 405 response is cacheable by default; i.e., unless otherwise
indicated by the method definition or explicit cache controls (see
Section 4.2.2 of [RFC7234]).
when the "target resource" are defined here:
In my case I a mapping in my controller in the following way:
#RequestMapping(name = "/fetch", method = RequestMethod.POST)
public Long createFetch() throws IOException {
return fetchService.doFetch();
}
If you notice, the above mapping is to name, but the requests work with this. Once I apply the same at the #Controller level and at method level, I started seeing this error. Setting the path to value resolved this.
Also remember to check the protocol. In my case it had to be https instead of http
The error message could certainly be more descriptive!
I am trying to build a #Restcontroller with some basic function and authenticate/authorize thru spring security 4 with an ldap backend, all in java config. I have some questions:
storing passwords in ldap are realworld scenarios or they should be somewhere in a database and that involves a custom userDetailService implementation with dao functions?
If passwords should be stored in ldap then PasswordComparisonAuthenticator is how performance optimized(speed, constant connection toward ldap)?
When calling the #RestController how should the caller authenticate and what java config needs to be done there?
My current implementation works with simple httpBasic authentication against the #RestController but when changing the authenticationProvider to ldap I get some error(I see the bad credentials error but i don't know why I gets it), am I missing some conceptual thing?:
Basic Authentication Authorization header found for user 'jfryer'
[DEBUG] [http-bio-8080-exec-3 04:06:02] (ProviderManager.java:authenticate:162) Authentication attempt using org.springframework.security.ldap.authentication.LdapAuthenticationProvider
[DEBUG] [http-bio-8080-exec-3 04:06:02] (AbstractLdapAuthenticationProvider.java:authenticate:67) Processing authentication request for user: jfryer
[DEBUG] [http-bio-8080-exec-3 04:06:02] (BasicAuthenticationFilter.java:doFilterInternal:196) Authentication request for failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
[DEBUG] [http-bio-8080-exec-3 04:06:02] (DelegatingAuthenticationEntryPoint.java:commence:78) Trying to match using RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]
[DEBUG] [http-bio-8080-exec-3 04:06:02] (DelegatingAuthenticationEntryPoint.java:commence:91) No match found. Using default entry point org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint#56f05ca6
[DEBUG] [http-bio-8080-exec-3 04:06:02] (SecurityContextPersistenceFilter.java:doFilter:105) SecurityContextHolder now cleared, as request processing completed
And the java config:
#Configuration
#EnableWebSecurity
#ComponentScan("my packages")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public DefaultSpringSecurityContextSource contextSource() {
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource("ldap://localhost:11389/o=sevenSeas");
contextSource.setUserDn("uid=admin,ou=system");
contextSource.setPassword("admin");
return contextSource;
}
#Bean
public PasswordComparisonAuthenticator ldapAuthenticator(DefaultSpringSecurityContextSource contextSource) {
PasswordComparisonAuthenticator authenticator = new PasswordComparisonAuthenticator(contextSource);
String[] userDn = {"cn={0},ou=people"};
authenticator.setUserDnPatterns(userDn);
authenticator.setPasswordAttributeName("userPassword");
authenticator.setPasswordEncoder(new LdapShaPasswordEncoder());
return authenticator;
}
#Bean
public DefaultLdapAuthoritiesPopulator authoritiesPopulator(DefaultSpringSecurityContextSource contextSource) {
DefaultLdapAuthoritiesPopulator populator = new DefaultLdapAuthoritiesPopulator(contextSource, "ou=groups");
populator.setGroupRoleAttribute("cn");
return populator;
}
#Bean
public LdapAuthenticationProvider ldapAuthenticationProvider(DefaultSpringSecurityContextSource contextSource) {
return new LdapAuthenticationProvider(this.ldapAuthenticator(contextSource), authoritiesPopulator(contextSource));
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, DefaultSpringSecurityContextSource contextSource) throws Exception {
//auth.userDetailsService(userDetailsService);
auth.authenticationProvider(ldapAuthenticationProvider(contextSource));
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests().antMatchers("/hello/**").hasRole("ADMIN").antMatchers("/date/**").hasRole("USER").and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().csrf().disable().exceptionHandling()
.authenticationEntryPoint(new AuthenticationHandler()).accessDeniedHandler(new PermissionHandler());
}
}
The calling code:
final String helloUri = "http://localhost:8080/Security/hello/Zoltan";
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
String auth = "jfryer:alma";
byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(Charset.forName("US-ASCII")));
String authHeader = "Basic " + new String(encodedAuth);
headers.add("Authorization", authHeader);
HttpEntity<String> entity = new HttpEntity<String>("parameters", headers);
ResponseEntity<String> result = restTemplate.exchange(helloUri, HttpMethod.POST, entity, String.class);