I'm using Spring Boot (2.3.4.RELEASE) to implement a webservice acting as a OAuth2 resource server. So far I'm able to secure all endpoints and ensure that a valid token is present. In the next step I want to use Spring Method Security. The third step would be to populate custom user details (via UserDetailsService
).
I'm not able to enable Spring Method Security (correctly). I have entities saved in database and also set the permissions via MutableAclService. Creating new resource is no problem.
I get the following error on reading the entity
o.s.s.acls.AclPermissionEvaluator : Checking permission 'OWNER' for object 'org.springframework.security.acls.domain.ObjectIdentityImpl[Type: io.mvc.webserver.repository.entity.ProjectEntity; Identifier: my-second-project]'
o.s.s.acls.AclPermissionEvaluator : Returning false - no ACLs apply for this principal
o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter@120d62d, returned: -1
o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.access.vote.RoleVoter@429b9eb9, returned: 0
o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.access.vote.AuthenticatedVoter@65342bae, returned: 0
o.s.web.servlet.DispatcherServlet : Failed to complete request: org.springframework.security.access.AccessDeniedException: Zugriff verweigert
o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is not anonymous); delegating to AccessDeniedHandler
I use the following Expression:
@PreAuthorize("hasPermission(#projectKey, 'io.mvc.webserver.repository.entity.ProjectEntity', 'OWNER')")
ProjectEntity findByKey(String projectKey);
As far as I understand Spring Security sets the SecurityContext accordingly to the authenticated user (by OAuth2 JWT). I want to set a custom user object (principal) based on the identified user from the token. But just providing a Bean of type UserDetailsService
does not seem to work. My UserDetailsService is never invoked...
Security configuration
@Configuration
@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.httpBasic().disable()
.formLogin().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests(authorize -> authorize
.antMatchers("/actuator/**").permitAll() // TODO: Enable basic auth for actuator
.anyRequest().authenticated()
)
.oauth2ResourceServer().jwt();
}
}
ACL configuration
@Configuration
public class AclConfiguration {
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(PermissionEvaluator permissionEvaluator) {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
@Bean
public PermissionEvaluator permissionEvaluator(PermissionFactory permissionFactory, AclService aclService) {
AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService);
permissionEvaluator.setPermissionFactory(permissionFactory);
return permissionEvaluator;
}
@Bean
public PermissionFactory permissionFactory() {
return new DefaultPermissionFactory(MvcPermission.class);
}
@Bean
public MutableAclService aclService(LookupStrategy lookupStrategy, AclCache aclCache, AclRepository aclRepository) {
return new MongoDBMutableAclService(aclRepository, lookupStrategy, aclCache);
}
@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(
new SimpleGrantedAuthority("ROLE_ADMIN"));
}
@Bean
public PermissionGrantingStrategy permissionGrantingStrategy() {
return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
}
@Bean
public AclCache aclCache(PermissionGrantingStrategy permissionGrantingStrategy,
AclAuthorizationStrategy aclAuthorizationStrategy,
EhCacheFactoryBean ehCacheFactoryBean) {
return new EhCacheBasedAclCache(
ehCacheFactoryBean.getObject(),
permissionGrantingStrategy,
aclAuthorizationStrategy
);
}
@Bean
public EhCacheFactoryBean aclEhCacheFactoryBean(EhCacheManagerFactoryBean ehCacheManagerFactoryBean) {
EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
ehCacheFactoryBean.setCacheManager(ehCacheManagerFactoryBean.getObject());
ehCacheFactoryBean.setCacheName("aclCache");
return ehCacheFactoryBean;
}
@Bean
public EhCacheManagerFactoryBean aclCacheManager() {
EhCacheManagerFactoryBean cacheManagerFactory = new EhCacheManagerFactoryBean();
cacheManagerFactory.setShared(true);
return cacheManagerFactory;
}
@Bean
public LookupStrategy lookupStrategy(MongoTemplate mongoTemplate,
AclCache aclCache,
AclAuthorizationStrategy aclAuthorizationStrategy) {
return new BasicMongoLookupStrategy(
mongoTemplate,
aclCache,
aclAuthorizationStrategy,
new ConsoleAuditLogger()
);
}
}
dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
If you want to use a custom user object, you need to implement and configer Converter<Jwt,? extends AbstractAuthenticationToken>
.
@Bean
public SecurityWebFilterChain filterChain(
ServerHttpSecurity http,
MyConverter converter
) {
http.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(converter);
return http.build();
}
If you have an UserDetailsService you should consider to make use of details instead of principal. Create a new JwtAuthenticationToken from the provided, parse in your authorities and use jwt.setDetails(myUserDetails)
.
If you'd need to implement UserDetailsService yourself, it might be easier to just implement the converter and use your own variant of AbstractAuthenticationToken instead of replacement of the principal.
Jwt is a stateless authentication mechanism. Your converter will be used on every authenticated request. Your be careful about side effects like api or db lookup. If you want to query additional user information from a database or api, consider to create a new token to encrypt everything that your application requires. You can also consider caching. Session cache can be a easy, worthy start here, since caching is mostly about performance boosts. It's a bit of a gray area with regards to statelessness.
There are various issues with the accepted answer. The biggest issue is, that it replaces the whole spring resource server jwt logic with a custom authentication filter. The answer even mentions, that you'd need to connect various logic like jwt parsing, that requires deeper understanding of either jwt or spring internals, so it only makes sense, if you need strong control over Authentication Header processing, but not if you just want to use your custom authentication class.