I've been following a guide but I can't get Spring Security to work.
It looks like it is authenticating but not authorizing or viceversa, or not redirecting to the login successful page. Maybe it is a stupid mistake but I can't see it.
My spring security config:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private Environment env;
#Autowired
private UserSecurityService userSecurityService;
private static final String[] PUBLIC_MATCHERS = {
"/webjars/**",
"/css/**",
"/js/**",
"/images/**",
"/",
"/about/**",
"/contact/**",
"/error/**/*",
"/h2-console/**"
};
#Override
protected void configure(HttpSecurity http) throws Exception {
List<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
// Required by h2 console to work
if(activeProfiles.contains("dev")) {
http.csrf().disable();
http.headers().frameOptions().disable();
}
http
.authorizeRequests()
.antMatchers(PUBLIC_MATCHERS).permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").defaultSuccessUrl("/payload")
.failureUrl("/login?error").permitAll()
.and()
.logout().permitAll();
}
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userSecurityService);
}
}
The application-dev.properties
spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.username=sa
spring.datasource.password=
hibernate.dialect=org.hibernate.dialect.H2Dialect
The logs:
DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#2dafa81d: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#2cd90: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 0D60174BBA25377F65443D95DB72F713; Granted Authorities: ROLE_ANONYMOUS
DEBUG o.s.s.access.vote.AffirmativeBased - Voter: org.springframework.security.web.access.expression.WebExpressionVoter#7a27baf6, returned: 1
DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Authorization successful
DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - RunAsManager did not change Authentication object
DEBUG o.s.security.web.FilterChainProxy - /js/scripts.js reached end of additional filter chain; proceeding with original chain
DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository - SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
DEBUG o.s.s.w.a.ExceptionTranslationFilter - Chain processed normally
DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed
DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository - SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
DEBUG o.s.s.w.a.ExceptionTranslationFilter - Chain processed normally
DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed
During authentication the application throws the following error:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
This exception is thrown, because the (plain text) password provided is missing the password-encoder {id}-prefix. Spring Security 5 now stores passwords using the following format (this was not the case for previous versions of spring security):
{id}encodedPassword
So that means for plain-text passwords, the {noop} id tells spring to match passwords using a NoOpPasswordEncoder (which basically handles passwords as plain-text).
However, storing plain-text passwords is highly discouraged (although it might be useful for automated testing).
Use a password encoder instead
Use of a BCryptPasswordEncoder, Pbkdf2PasswordEncoder or SCryptPasswordEncoder is highly recommended.
BCryptPasswordEncoder
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
#Configuration
class Config {
#Bean
public PasswordEncoder passwordEncoder() {
// Create an encoder with strength 31
// values from 4 .. 31 are valid; the higher the value, the more work has to be done to calculate the hash
return new BCryptPasswordEncoder(12);
}
}
Security Config
#Configuration
class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
PasswordEncoder passwordEncoder;
...
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userSecurityService)
.passwordEncoder(passwordEncoder);
}
}
Encoding the password
#Service
class UserService implements UserDetailsService {
private UserRepository userRepository;
private PasswordEncoder passwordEncoder;
UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
User createUser(String username, String password) {
// encrypt the plain-text password
String encodedPassword = passwordEncoder.encode(password);
User user = new User(username, encodedPassword));
//...
return userRepository.save(user);
}
}
Supporting more than one encoder
To support more than one encoder, one might want to look at the DelegatingPasswordEncoder and PasswordEncoderFactories.
For further details have a look at https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-storage-format
Related
I have a SpringBoot 2.4.2 application that uses JSON Web Tokens (JWT, sometimes pronounced /dʒɒt/, the same as the English word "jot"[1]) is an Internet proposed standard for creating data with optional signature and/or optional encryption whose payload holds JSON that asserts some number of claims. The tokens are signed either using a private secret or a public/private key. For example, a server could generate a token that has the claim "logged in as admin" and provide that to a client. The client could then use that token to prove that it is logged in as admin.
This is my WebSecurityConfig:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String SALT = "fd23451*(_)nof";
private final JwtAuthenticationEntryPoint unauthorizedHandler;
private final JwtTokenUtil jwtTokenUtil;
private final UserSecurityService userSecurityService;
#Value("${jwt.header}")
private String tokenHeader;
public ApiWebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler, JwtTokenUtil jwtTokenUtil,
UserSecurityService userSecurityService) {
this.unauthorizedHandler = unauthorizedHandler;
this.jwtTokenUtil = jwtTokenUtil;
this.userSecurityService = userSecurityService;
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userSecurityService)
.passwordEncoder(passwordEncoder());
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12, new SecureRandom(SALT.getBytes()));
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// we don't need CSRF because our token is invulnerable
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// Un-secure H2 Database
.antMatchers("/h2-console/**/**").permitAll()
.antMatchers("/api/v1/users").permitAll()
.anyRequest().authenticated();
// Custom JWT based security filter
JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);
httpSecurity
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity
.headers()
.frameOptions()
.sameOrigin() // required to set for H2 else H2 Console will be blank.
.cacheControl();
}
#Override
public void configure(WebSecurity web) {
// AuthenticationTokenFilter will ignore the below paths
web
.ignoring()
.antMatchers(
HttpMethod.POST,
"/api/v1/users"
);
}
}
and this is my Filter:
#Provider
#Slf4j
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
private UserDetailsService userDetailsService;
private JwtTokenUtil jwtTokenUtil;
private String tokenHeader;
public JwtAuthorizationTokenFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, String tokenHeader) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.tokenHeader = tokenHeader;
}
#Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return new AntPathMatcher().match("/api/v1/users", request.getServletPath());
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException,
IOException {
log.info("processing authentication for '{}'", request.getRequestURL());
final String requestHeader = request.getHeader(this.tokenHeader);
String username = null;
String authToken = null;
if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
authToken = requestHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (IllegalArgumentException e) {
logger.info("an error occured during getting username from token", e);
} catch (ExpiredJwtException e) {
logger.info("the token is expired and not valid anymore", e);
}
} else {
logger.info("couldn't find bearer string, will ignore the header");
}
log.info("checking authentication for user '{}'", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
logger.info("security context was null, so authorizating user");
// It is not compelling necessary to load the use details from the database. You could also store the information
// in the token and read it from it. It's up to you ;)
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// For simple validation it is completely sufficient to just check the token integrity. You don't have to call
// the database compellingly. Again it's up to you ;)
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
log.info("authorizated user '{}', setting security context", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
and
#Component
#Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
#Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
log.info("user tries to access a secured REST resource without supplying any credentials");
// This is invoked when user tries to access a secured REST resource without supplying any credentials
// We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
This is the console when I start the app:
18:02:51.974 [restartedMain] DEBUG com.agrumh.Application - Running with Spring Boot v2.4.2, Spring v5.3.3
18:02:51.974 [restartedMain] INFO com.agrumh.Application - No active profile set, falling back to default profiles: default
18:02:57.383 [restartedMain] INFO o.s.s.web.DefaultSecurityFilterChain - Will secure Ant [pattern='/api/v1/users', POST] with []
18:02:57.414 [restartedMain] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [permitAll] for Ant [pattern='/h2-console/**/**']
18:02:57.415 [restartedMain] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [permitAll] for Ant [pattern='/api/v1/users']
18:02:57.416 [restartedMain] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [authenticated] for any request
18:02:57.422 [restartedMain] INFO o.s.s.web.DefaultSecurityFilterChain - Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#24c68fed, org.springframework.security.web.context.SecurityContextPersistenceFilter#1537eb0a, org.springframework.security.web.header.HeaderWriterFilter#95de45c, org.springframework.security.web.authentication.logout.LogoutFilter#733cf550, com.dispacks.config.JwtAuthorizationTokenFilter#538a96c8, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#8d585b2, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#784cf061, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#64915f19, org.springframework.security.web.session.SessionManagementFilter#21f180d0, org.springframework.security.web.access.ExceptionTranslationFilter#2b153a28, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#4942d157]
18:02:58.619 [restartedMain] INFO com.dispacks.DispacksApplication - Started DispacksApplication in 6.974 seconds (JVM running for 7.697)
18:04:03.685 [http-nio-1133-exec-1] DEBUG o.s.security.web.FilterChainProxy - Securing POST /error
18:04:03.687 [http-nio-1133-exec-1] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Set SecurityContextHolder to empty SecurityContext
18:04:03.689 [http-nio-1133-exec-1] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
18:04:03.694 [http-nio-1133-exec-1] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Failed to authorize filter invocation [POST /error] with attributes [authenticated]
18:04:03.698 [http-nio-1133-exec-1] INFO c.d.s.JwtAuthenticationEntryPoint - user tries to access a secured REST resource without supplying any credentials
18:04:03.699 [http-nio-1133-exec-1] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Cleared SecurityContextHolder to complete request
But when I access with Postman I have this error:
22:58:33.562 [http-nio-1133-exec-2] WARN o.s.w.s.m.s.DefaultHandlerExceptionResolver - Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'text/plain' not supported]
22:58:33.579 [http-nio-1133-exec-2] INFO c.d.s.JwtAuthenticationEntryPoint - user tries to access a secured REST resource without supplying any credentials
Authorization and authentication are different
The POST /api/v1/users was allowed, because the resource POST does not need to be authorized to be accessed.
In your code,
#Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException // AuthenticationException means authentication failed, not "without supplying any credentials".
) throws IOException {
// Break point here, or print authException.
log.info("user tries to access a secured REST resource without supplying any credentials"); // Wrong message. You can say "Authentication failed.", or log.info(authException.getMessage()).
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
The authentication error actually happens when accessing /error resource.
18:04:03.694 [http-nio-1133-exec-1] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Failed to authorize filter invocation [POST /error] with attributes [authenticated]
I assume some error happened, your application is redirecting you to /error, but the /error is protected. So authenticationException happened on /error.
Add /error before .permitAll().
Breakpoint the authenticationException so I can update this answer.
What is the path that you call from Postman? If it's /api/v1/users I can see that you have this path set in the shouldNotFilter method of your filter. Doesn't that mean that you're ignoring your JWT filter for this path?
By the way, if you don't need any additional functionality you can use Spring Security's support for validating JWTs. Have a look at this tutorial to see how it's configured. This way you will not need your own filter.
If i understand you correct, you want the JWT-filter to run only for certain endpoints? I had this same problem that I couldn't get SpringSecurity to only run my JWT-filter for specified entrypoints no matter how much I tried diffrent security configs.
I solved this by overriding shouldNotFilter as you did, but mine looks something like this:
#Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return new AntPathRequestMatcher("/api/v1/users").matches(request);
}
Perhaps this could solve your problem.
I have been all over stack overflow trying to find out why this issue is happening, but cannot find an answer.
This is my setup:
SecurityConfig
#Autowired
private IUserService userService;
#Override
public void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
// #formatter:off
http.
authorizeRequests().
antMatchers("/api/**"). // if you want a more explicit mapping here
//anyRequest().
// authenticated().antMatchers("/api/users/**").
permitAll().
and().
httpBasic().
and().
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
and().csrf().disable();
// #formatter:on
}
#Bean
public PasswordEncoder passwordEncoder(){
PasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder;
}
UserService create method:
#Override
public User create(User u) {
User newUser = new User();
newUser.setUsername(u.getUsername());
newUser.setEmail(u.getEmail());
newUser.setPhoneNum(u.getPhoneNum());
newUser.setPassword(passwordEncoder.encode(u.getPassword()));
// Add default roles
Role userRole = roleService.findByName("ROLE_USER");
newUser.setRoles(Sets.<Role>newHashSet(userRole));
dao.save(newUser);
return newUser;
}
Note that User implements UserDetails and IUserService implements UserDetailsService.
Based on other articles here is some more information:
I'm not trying to do OAUTH so please don't recommend that i also encode the client secret
I checked my database, its a VARCHAR(68), so I believe there is enough room to store the encoded password.
The database does indeed store the encoded password (i looked and its not plain text)
Here is some DEBUG logs from a request that gets denied:
DEBUG o.s.s.w.a.w.BasicAuthenticationFilter - Basic Authentication Authorization header found for user 'wowz'
23:17:57.187 [http-nio-8082-exec-8] DEBUG o.s.s.authentication.ProviderManager - Authentication attempt using org.springframework.security.authentication.dao.DaoAuthenticationProvider
23:17:57.471 [http-nio-8082-exec-8] WARN o.s.s.c.bcrypt.BCryptPasswordEncoder - Encoded password does not look like BCrypt
23:17:57.472 [http-nio-8082-exec-8] DEBUG o.s.s.a.d.DaoAuthenticationProvider - Authentication failed: password does not match stored value
23:17:57.472 [http-nio-8082-exec-8] DEBUG o.s.s.w.a.w.BasicAuthenticationFilter - Authentication request for failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
23:17:57.472 [http-nio-8082-exec-8] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Trying to match using RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]
23:17:57.473 [http-nio-8082-exec-8] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - No match found. Using default entry point org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint#42da9490
23:17:57.473 [http-nio-8082-exec-8] DEBUG o.s.s.w.h.writers.HstsHeaderWriter - Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher#115f4872
23:17:57.473 [http-nio-8082-exec-8] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed
Also note that this is security for a REST API, not a MVC application
The best way to identify this problem "Encoded password does not look like BCrypt" is setup a break porint in class org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder. And then check the root cause for the warnning.
if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
logger.warn("Encoded password does not look like BCrypt");
return false;
}
I use spring boot and spring security.
In my rest controller, i have one method
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled=true)
#EnableWebSecurity
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
#Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
private RESTAuthenticationFailureHandler authenticationFailureHandler;
#Autowired
private RESTAuthenticationSuccessHandler authenticationSuccessHandler;
#Autowired
private UserDetailsService userDetailsService;
#Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(encoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/rest/**").authenticated();
http.csrf().disable();
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.formLogin().successHandler(authenticationSuccessHandler);
http.formLogin().failureHandler(authenticationFailureHandler);
http.logout().logoutUrl("/logout");
http.logout().logoutSuccessUrl("/");
// CSRF tokens handling
//http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
}
}
#RequestMapping(value = "/rest")
#RestController
public class MemberController {
#GetMapping(value = "/members/card")
public boolean hasCardIdValid(#RequestBody String cardId) {
return memberService.hasCardIdValid(cardId);
}
}
In another spring boot application, i try to call hasCreditCard method
#Autowired
public GlobalScan(RestTemplateBuilder restTemplateBuilder, #Value("${main.server.url}") String mainServerUrl, #Value("${commerce.username}") String commerceUsername, #Value("${commerce.password}")String commercePassword) {
this.restTemplate = restTemplateBuilder.basicAuthorization(commerceUsername, commercePassword).rootUri(mainServerUrl).build();
}
I do a call with this code
Map<String, String> vars = new HashMap<String, String>();
vars.put("cardId", cardId);
boolean accessAllowed = restTemplate.getForObject("/rest/members/card/" , Boolean.class, vars);
i get this message
2016-11-02 16:20:50.601 DEBUG 7139 --- [nio-8080-exec-1] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/rest/members/card/'; against '/login'
2016-11-02 16:20:50.601 DEBUG 7139 --- [nio-8080-exec-1] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/rest/members/card/'; against '/rest/**'
2016-11-02 16:20:50.601 DEBUG 7139 --- [nio-8080-exec-1] o.s.s.w.a.i.FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /rest/members/card/; Attributes: [authenticated]
2016-11-02 16:20:50.601 DEBUG 7139 --- [nio-8080-exec-1] o.s.s.w.a.i.FilterSecurityInterceptor : Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
2016-11-02 16:20:50.602 DEBUG 7139 --- [nio-8080-exec-1] o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.web.access.expression.WebExpressionVoter#3d300693, returned: -1
2016-11-02 16:20:50.602 TRACE 7139 --- [nio-8080-exec-1] ationConfigEmbeddedWebApplicationContext : Publishing event in org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext#2bdd8394: org.springframework.security.access.event.AuthorizationFailureEvent[source=FilterInvocation: URL: /rest/members/card/]
2016-11-02 16:20:50.606 DEBUG 7139 --- [nio-8080-exec-1] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.1.1.RELEASE.jar:4.1.1.RELEASE]
On my main app, i use a form login to connect to the app, like you can see in the spring security config.
From my other app how to call a ws without form login?
tried to call ws with this
final RequestConfig config = RequestConfig.custom().setConnectTimeout(timeout * 1000).setConnectionRequestTimeout(timeout * 1000).setSocketTimeout(timeout * 1000).build();
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope("http://localhost", 8080, AuthScope.ANY_REALM), new UsernamePasswordCredentials("bob", "smith"));
final CloseableHttpClient client = HttpClientBuilder.create().setDefaultRequestConfig(config).setDefaultCredentialsProvider(credentialsProvider).build();
final ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(client);
RestTemplate restTemplate = new RestTemplate(requestFactory);
ResponseEntity<MemberDto> member = restTemplate.getForEntity("http://localhost:8080/rest/members/1", MemberDto.class);
result: http://pastebin.com/psNKPUtM
The default password in spring security is configured by the following property: security.user.password=YOUR_PASSWORD
This should be done in your main app where you have security configuration and which you are trying to call.
You can change the password by providing a security.user.password.
This and other useful properties are externalized via
SecurityProperties (properties prefix "security").
So, if you didn't update the property to match the password in commerce.password spring will reject your authorization and you will get 401. By default it uses some random generated password it prints to the console during the start. documentation
You are configuring formLogin() but you try to use an http Basic Auth in your RestTemplate.
For requests via http REST I suggest that you change your configuration to use basic auth:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/rest/**").authenticated();
http.csrf().disable();
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.httpBasic();
http.logout().logoutUrl("/logout");
http.logout().logoutSuccessUrl("/");
// CSRF tokens handling
//http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
}
If you need both I think you can configure both.
Add BASIC auth to your existing configuration
#Override
protected void configure(HttpSecurity http) throws Exception {
http
....
.and()
.formLogin() // <------ Keep this
....
.and()
.httpBasic() // <------ Add BASIC Auth
.and()
.....;
}
Write a simple client using RestTemplate
public static void main(String[] args) {
RestTemplate rest = new RestTemplate(new ArrayList(Arrays.asList(new MappingJackson2HttpMessageConverter())));
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Basic YOUR_BASE64_ENCODED_CREDENTIALS");
MediaType applicationJson = new MediaType("application","json");
headers.setContentType(applicationJson);
headers.setAccept(Collections.singletonList(applicationJson));
ResponseEntity<YourResponseObject> resp = rest.exchange("http://URL/rest/yourendpoint", HttpMethod.GET, new HttpEntity<String>("parameters", headers), YourResponseObject.class);
System.out.println(resp.getBody());
}
YOUR_BASE64_ENCODED_CREDENTIALS => If use use Java 8 you can use java.util.Base64, otherwise use commons-codec to do that or something else.
Update:
Spring boot reference: http://docs.spring.io/spring-security/site/docs/current/reference/html/jc.html#jc-httpsecurity
I want to use http basic authentication for my Spring Boot application with one set of credentials and at the same time I want to configure actuator to use a different set of credentials for the management resources (health, env etc). I've read the Actucator documentation where it says that you should be able to set the username and password using the security.user.name and security.user.password properties. However when I add my custom WebSecurityConfigurerAdapter it no longer seems to be applied. My WebSecurityConfigurerAdapter looks like this:
#Configuration
#EnableWebMvcSecurity
#Order(Ordered.LOWEST_PRECEDENCE - 11)
public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String API_USER = "API";
private static final String ADMIN_USER = "ADMIN";
#NotNull
#Value("${security.user.name}")
private String managementUsername;
#NotNull
#Value("${security.user.password}")
private String managementPassword;
#NotNull
#Value("${management.context-path}")
private String managementContextPath;
public ApplicationSecurityConfig() {
super(true);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement()
.sessionCreationPolicy(STATELESS)
.and()
.securityContext().and()
.requestCache().and()
.servletApi().and()
.authorizeRequests()
.antMatchers(managementContextPath+"/**").hasRole(ADMIN_USER)
.antMatchers("/**").hasRole(API_USER)
.and()
.httpBasic();
// #formatter:on
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("apiUsername").password("apiPassword").roles(API_USER).
and().withUser(managementUsername).password(managementPassword).roles(ADMIN_USER);
}
}
I've also tried setting management.security.enabled to false but then the management resources seem to be open to all despite my effort to protect it above.
Does anyone know what I'm doing wrong and how to go about?
Update
I see that three events are emitted by Spring from my app:
2015-06-10 20:04:37.076 INFO 44081 --- [nio-8083-exec-1] o.s.b.a.audit.listener.AuditListener : AuditEvent [timestamp=Wed Jun 10 20:04:37 CEST 2015, principal=<unknown>, type=AUTHENTICATION_FAILURE, data={type=org.springframework.security.authentication.AuthenticationCredentialsNotFoundException, message=An Authentication object was not found in the SecurityContext}]
2015-06-10 20:04:39.564 INFO 44081 --- [nio-8083-exec-2] o.s.b.a.audit.listener.AuditListener : AuditEvent [timestamp=Wed Jun 10 20:04:39 CEST 2015, principal=admin, type=AUTHENTICATION_SUCCESS, data={details=org.springframework.security.web.authentication.WebAuthenticationDetails#b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null}]
2015-06-10 20:04:39.569 INFO 44081 --- [nio-8083-exec-2] o.s.b.a.audit.listener.AuditListener : AuditEvent [timestamp=Wed Jun 10 20:04:39 CEST 2015, principal=admin, type=AUTHORIZATION_FAILURE, data={type=org.springframework.security.access.AccessDeniedException, message=Access is denied}]
But there's only two from hyness sample app:
2015-06-10 19:34:10.851 INFO 42714 --- [nio-8083-exec-1] o.s.b.a.audit.listener.AuditListener : AuditEvent [timestamp=Wed Jun 10 19:34:10 CEST 2015, principal=anonymousUser, type=AUTHORIZATION_FAILURE, data={type=org.springframework.security.access.AccessDeniedException, message=Access is denied}]
2015-06-10 19:34:17.139 INFO 42714 --- [nio-8083-exec-2] o.s.b.a.audit.listener.AuditListener : AuditEvent [timestamp=Wed Jun 10 19:34:17 CEST 2015, principal=manage, type=AUTHENTICATION_SUCCESS, data={details=org.springframework.security.web.authentication.WebAuthenticationDetails#b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null}]
I guess that you would like to have different configurations for different URLs? The Multiple HttpSecurity chapter in the Spring Security reference docs suggests that you should create a security config that has multiple WebSecurityConfigurerAdapter beans (simplified snippet based on your problem and the example in the reference docs):
#Configuration
#EnableWebSecurity
public class MultiHttpSecurityConfig {
// variables omitted...
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth
.inMemoryAuthentication()
.withUser("apiUsername").password("apiPassword")
.roles(API_USER).and()
.withUser(managementUsername).password(managementPassword)
.roles(ADMIN_USER);
}
#Configuration
#Order(1)
public static class ManagementWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher(managementContextPath+"/**")
.authorizeRequests()
.anyRequest().hasRole("ADMIN_USER")
.and()
.httpBasic();
}
}
#Configuration
public static class DefaultWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().hasRole("API_USER")
.and()
.httpBasic()
}
}
}
Please read the reference docs for details.
I changed the precedence and changed the management username and password property names and it works for me. The management context is only accessible to the management user and the rest of the secured paths are only accessible to the apiUsername. The problem is there no basic logout functionality. You either need to close the browser window or use a private tab to switch users.
#Configuration
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String API_USER = "API";
private static final String ADMIN_USER = "ADMIN";
#NotNull
#Value("${management.user.name}")
private String managementUsername;
#NotNull
#Value("${management.user.password}")
private String managementPassword;
#NotNull
#Value("${management.context-path}")
private String managementContextPath;
public ApplicationSecurityConfig() {
super(true);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and().headers().and().sessionManagement()
.sessionCreationPolicy(STATELESS).and().securityContext().and()
.requestCache().and().servletApi().and().authorizeRequests()
.antMatchers(managementContextPath + "/**").hasRole(ADMIN_USER)
.antMatchers("/**").hasRole(API_USER).and().httpBasic();
// #formatter:on
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("apiUsername")
.password("apiPassword").roles(API_USER).and()
.withUser(managementUsername).password(managementPassword)
.roles(ADMIN_USER);
}
}
hyness answer worked if I changed:
..
.antMatchers(managementContextPath + "/**").hasRole(ADMIN_USER)
.antMatchers("/**").hasRole(API_USER)
to
..
.requestMatchers(request -> !request.getContextPath().startsWith(managementContextPath)).hasRole(API)
.antMatchers("/**").not().hasRole(API)
.antMatchers(managementContextPath + "/**").hasRole(ADMIN)
I have problem with Spring REST oAuth2 configuration. Springs sees and map my URLs, but after oauth2 security check (successful) claims there is no URL to match. But I have no idea why, because Spring sees it on app initialisation.
I am able to properly authenticate with /oauth/token and generate token.
I am just unable to process requests which do not need authorization with token.
Spring 4.0.6, spring-security 3.2.4, Spring-security-oauth2 2.0.1
Logs from context initialisation
2014-08-29 08:56:26.415 [Scanner-1] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/api/users/{email}],methods=[PUT],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.util.concurrent.Callable<org.springframework.http.ResponseEntity> com.example.user.UserCommandsController.update(java.lang.String)
2014-08-29 08:56:26.416 [Scanner-1] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/api/users/{email}],methods=[DELETE],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.util.concurrent.Callable<org.springframework.http.ResponseEntity> com.example.user.UserCommandsController.delete(java.lang.String)
2014-08-29 08:56:26.416 [Scanner-1] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/api/users/logout],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.util.concurrent.Callable<org.springframework.http.ResponseEntity> com.example.user.UserCommandsController.logout()
2014-08-29 08:56:26.416 [Scanner-1] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/api/users],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.util.concurrent.Callable<org.springframework.http.ResponseEntity<java.lang.Void>> com.example.user.UserCommandsController.signup(java.lang.String,java.lang.String)
After sending request
2014-08-29 09:00:58.654 [qtp1157726741-28] DEBUG o.s.s.w.u.m.AntPathRequestMatcher - Checking match of request : '/api/users'; against '/api/users'
2014-08-29 09:00:58.654 [qtp1157726741-28] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /api/users; Attributes: [permitAll]
2014-08-29 09:00:58.654 [qtp1157726741-28] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#9055c2bc: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
2014-08-29 09:00:58.654 [qtp1157726741-28] DEBUG o.s.s.a.vote.AffirmativeBased - Voter: org.springframework.security.web.access.expression.WebExpressionVoter#31b7d21c, returned: 1
2014-08-29 09:00:58.654 [qtp1157726741-28] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Authorization successful
2014-08-29 09:00:58.654 [qtp1157726741-28] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - RunAsManager did not change Authentication object
2014-08-29 09:00:58.654 [qtp1157726741-28] DEBUG o.s.s.web.FilterChainProxy - /api/users reached end of additional filter chain; proceeding with original chain
2014-08-29 09:00:58.655 [qtp1157726741-28] DEBUG o.s.w.servlet.DispatcherServlet - DispatcherServlet with name 'dispatcher' processing POST request for [/api/users]
2014-08-29 09:00:58.655 [qtp1157726741-28] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Looking up handler method for path /api/users
2014-08-29 09:00:58.655 [qtp1157726741-28] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Did not find handler method for [/api/users]
2014-08-29 09:00:58.655 [qtp1157726741-28] DEBUG o.s.s.o.p.e.FrameworkEndpointHandlerMapping - Looking up handler method for path /api/users
2014-08-29 09:00:58.655 [qtp1157726741-28] DEBUG o.s.s.o.p.e.FrameworkEndpointHandlerMapping - Did not find handler method for [/api/users]
2014-08-29 09:00:58.655 [qtp1157726741-28] WARN o.s.web.servlet.PageNotFound - No mapping found for HTTP request with URI [/api/users] in DispatcherServlet with name 'dispatcher'
And configuration
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId("sample-resource-id");
}
#Override
public void configure(final HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.requestMatchers()
.antMatchers(HttpMethod.POST, "/api/buildings/**")
.antMatchers(HttpMethod.DELETE, "/api/**")
.antMatchers(HttpMethod.PATCH, "/api/**")
.antMatchers(HttpMethod.PUT, "/api/**")
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/buildings/**").access("hasRole('ROLE_USER')")
.antMatchers(HttpMethod.DELETE, "/api/**").access("hasRole('ROLE_USER')")
.antMatchers(HttpMethod.PATCH, "/api/**").access("hasRole('ROLE_USER')")
.antMatchers(HttpMethod.PUT, "/api/**").access("hasRole('ROLE_USER')");
}
}
#Controller
#EnableWebSecurity
#Profile("default")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
/**
* By default all request need authentication. Only those which do not need it, shall be specified explicitly.
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.csrf().disable();
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/buildings/**").permitAll()//to consider anonymous()
.antMatchers(HttpMethod.POST, "/api/users").permitAll()//to consider anonymous()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/app/**","/webjars/**", "/images/**", "/oauth/uncache_approvals", "/oauth/cache_approvals");
}
#Override
#Bean(name = "authenticationManagerBean")
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Part of user controller
#RestController
#RequestMapping("/api")
public class UserCommandsController {
private final UserService userService;
private AccountRecoveryMailer accountRecoveryMailer;
private MessageSource messageSource;
#Inject
public UserCommandsController(final UserService userService, final AccountRecoveryMailer accountRecoveryMailer,
final MessageSource messageSource) {
this.userService = userService;
this.accountRecoveryMailer = accountRecoveryMailer;
this.messageSource = messageSource;
}
#RequestMapping(value = "/users", method = RequestMethod.POST)
public Callable<ResponseEntity<Void>> signup(#RequestParam String email, #RequestParam String password) {
return () -> {
//do something
};
}
}
What I want to achieve is to secure all requests and only some of them make with free access (or maybe with only Authorization header to match client_id).
Here is solution for my problem. The root of this evil thing was beans initialisation, or better to say their scopes. BTW SSL isn't needed.
Wrong configuration below, do not blindly copy-paste.
I had two #ComponentScan classes.
#Configuration
#EnableWebMvc
#ComponentScan(basePackageClasses = Application.class,
excludeFilters = #Filter({RestController.class, Controller.class, Service.class, Repository.class, Configuration.class}))
class WebMvcConfig extends WebMvcConfigurationSupport {
//some code
}
#Configuration
#ComponentScan(basePackageClasses = Application.class)
class ApplicationConfig {
//some code
}
And mine WebAppInitialization code
#Order(2)
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
#Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{ApplicationConfig.class, DataSourceConfig.class, SecurityConfig.class};
}
#Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{WebMvcConfig.class};
}
#Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
characterEncodingFilter.setEncoding("UTF-8");
characterEncodingFilter.setForceEncoding(true);
return new Filter[]{characterEncodingFilter};
}
#Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("defaultHtmlEscape", "true");
registration.setInitParameter("spring.profiles.active", "default");
}
}
As you can see, entire components class path scanning with all type of beans would be initialized in getRootConfigClasses() method, and only part of beans would be initialized in getServletConfigClasses() method, due to WebMvcConfig.class and its exclusion of some bean types in component scanning. This shall be enough in my opinion for Spring, because beans from rootContext are available for servletContext. And was, but only for web app instantiation. Spring Security oAuth2 haven't seen controller mappings.
Solution to this problem was to get rid of component scanning in WebMvcConfig, and change getServletConfigClasses() method to this:
#Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{ApplicationConfig.class, WebMvcConfig.class};
}
Thanks to eager caching of Spring beans, everything shall be fine.
You setup the server with oAuth2, that server can be accessed in secure fashion (https:) only.
If you need to provide non-secure(http:) service, you have to create another server.
Let consider that if your home's door has lock, and only the persons who have the key can enter your home, your home is secure.
If you add another door without lock to your home, your home becomes not-secure.
If you want to make door without lock, you should install that door to other hut for non-secure use.
Secure home, and non-secure hut.
These may be what you want to build on your server.