How to use custom UserDetailService in Spring OAuth2 Resource Server? - spring

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).
How to configure Spring Method Security properly?
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);
How to provide Custom User Details Service?
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>

In your ResourceServerConfig class you should override configureGlobal and authenticationManagerBean methods, as well as providing passwordEncoderBean in order to invoke your userDeatailsService:
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoderBean());
}
#Bean
public PasswordEncoder passwordEncoderBean() {
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
The variable userDetailsService in configureGlobal should hold a reference (through dependency injection #Autowird in your class) to your implementation of org.springframework.security.core.userdetails.UserDetailsService, in the implementation you should override the method loasUserByUsername to get the actual user in your database and pass the required values to UserDetails user, this user or Principal is what will be used in the authentication manager:
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<UserFromDb> user = userRepository.findByUsername(username);
if (!user.isPresent()) {
throw new UsernameNotFoundException("User not found!");
}
return new MyUser(user.get());
}
The class MyUser should implement org.springframework.security.core.userdetails.UserDetails and pass the required values to MyUser as the example shows. How to pass the required values is up to you, here I passed the user from database and internally in the implementation I extracted whatever values are needed.
You should add the following line to the end of configure method
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
authenticationTokenFilter is of a type that implements OncePerRequestFilter, you should override the method doFilterInternal:
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, FilterChain filterChain)
throws ServletException, IOException {
final String requestTokenHeader = httpServletRequest.getHeader("Authorization");//sometime it's lowercase: authorization
String username = getUserName(requestTokenHeader);
String jwtToken = getJwtToken(requestTokenHeader);
if (username != null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (isValidToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
Of course you should write the logic of getUserName, getJwtToken and isValidToken methods, which require understanding of JWT token and http headers...

Related

Spring Boot + Keycloak via SAML - Invalid reqeuests

I've been trying to set up a Keycloak locally with docker to be able to login to our application with SAML 2.0.
Versions used:
Keyloak 19.0.3
Spring Boot 2.7.3
When I call an REST endpoint of the application, I am correctly redirected to Keycloak (redirect to http://localhost:8085/realms/PocRealm/protocol/saml). But I don't get a login form but the message: "We are sorry ... Invalid Request" .
I then see the following entry in the Docker console logs.
2022-10-07 12:22:41,972 WARN [org.keycloak.events] (executor-thread-104) type=LOGIN_ERROR, realmId=e026f301-c74b-4247-bb0a-58cdb651ae00, clientId=null, userId=null, ipAddress=172.17.0.1, error=client_not_found, reason=Cannot_match_source_hash
These are my configurations:
application.properties
# Spring Server Settings
server.port=8081
#Keycloak Settings
keycloak.auth-server-url=http://localhost:8085
keycloak.realm=PocRealm
keycloak.resource=pocApp
keycloak.principal-attribute=preferred_username
#SAML Settings
spring.security.saml2.relyingparty.registration.keycloak.signing.credentials[0].private-key-location=classpath:credentials/myKey.key
spring.security.saml2.relyingparty.registration.keycloak.signing.credentials[0].certificate-location=classpath:credentials/myCert.crt
spring.security.saml2.relyingparty.registration.keycloak.assertingparty.metadata-uri=http://localhost:8085/realms/PocRealm/protocol/saml/descriptor
KeycloakConfig.java
#Configuration
public class KeycloakConfig {
#Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
SecurityConfig.java
#KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
{
#Autowired
public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder)
{
SimpleAuthorityMapper simpleAuthorityMapper = new SimpleAuthorityMapper();
simpleAuthorityMapper.setPrefix("ROLE_");
KeycloakAuthenticationProvider keycloakAuthenticationProvider =
keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(simpleAuthorityMapper);
authenticationManagerBuilder.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy ()
{
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Bean
#Override
#ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager()
{
return new HttpSessionManager();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(groupsConverter());
// #formatter:off
httpSecurity
.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/favicon.ico").permitAll()
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.authenticationManager(new ProviderManager(authenticationProvider))
)
.saml2Logout(withDefaults());
// #formatter:on
}
private Converter<OpenSaml4AuthenticationProvider.ResponseToken, Saml2Authentication> groupsConverter() {
Converter<ResponseToken, Saml2Authentication> delegate =
OpenSaml4AuthenticationProvider.createDefaultResponseAuthenticationConverter();
return (responseToken) -> {
Saml2Authentication authentication = delegate.convert(responseToken);
Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
List<String> groups = principal.getAttribute("groups");
Set<GrantedAuthority> authorities = new HashSet<>();
if (groups != null) {
groups.stream().map(SimpleGrantedAuthority::new).forEach(authorities::add);
} else {
authorities.addAll(authentication.getAuthorities());
}
return new Saml2Authentication(principal, authentication.getSaml2Response(), authorities);
};
}
}
I don't see the problem and even after hours of research I can't get anywhere at this point and maybe someone here can help me? Maybe there is a better approach in general? (OpenID-Connect instead of SAML is unfortunately not an option)
Thank you

Extend Keycloak token in Spring boot

I'm using Keycloak to secure my Spring boot backend.
Dependencies:
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-2-adapter</artifactId>
<version>12.0.3</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-tomcat7-adapter-dist</artifactId>
<version>12.0.3</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
<version>12.0.3</version>
</dependency>
Security config:
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http.cors()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests();
expressionInterceptUrlRegistry = expressionInterceptUrlRegistry.antMatchers("/iam/accounts/promoters*").hasRole("PROMOTER");
expressionInterceptUrlRegistry.anyRequest().permitAll();
}
Everything work fine!
But now I add a new section in keycloak token "roles" and I need to somehow extend keycloak jwt class in my Spring boot and write some code to parse and store the roles information to SecurityContext. Could you Guy please tell me how to archive the goal?
First, extends keycloak AccessToken:
#Data
static class CustomKeycloakAccessToken extends AccessToken {
#JsonProperty("roles")
protected Set<String> roles;
}
Then:
#KeycloakConfiguration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Override
protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
return new KeycloakAuthenticationProvider() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (String role : ((CustomKeycloakAccessToken)((KeycloakPrincipal)token.getPrincipal()).getKeycloakSecurityContext().getToken()).getRoles()) {
grantedAuthorities.add(new KeycloakRole(role));
}
return new KeycloakAuthenticationToken(token.getAccount(), token.isInteractive(), new SimpleAuthorityMapper().mapAuthorities(grantedAuthorities));
}
};
}
/**
* Use NullAuthenticatedSessionStrategy for bearer-only tokens. Otherwise, use
* RegisterSessionAuthenticationStrategy.
*/
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(authenticationManagerBean());
filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
filter.setRequestAuthenticatorFactory(new SpringSecurityRequestAuthenticatorFactory() {
#Override
public RequestAuthenticator createRequestAuthenticator(HttpFacade facade,
HttpServletRequest request, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort) {
return new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, sslRedirectPort) {
#Override
protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() {
return new BearerTokenRequestAuthenticator(deployment) {
#Override
protected AuthOutcome authenticateToken(HttpFacade exchange, String tokenString) {
log.debug("Verifying access_token");
if (log.isTraceEnabled()) {
try {
JWSInput jwsInput = new JWSInput(tokenString);
String wireString = jwsInput.getWireString();
log.tracef("\taccess_token: %s", wireString.substring(0, wireString.lastIndexOf(".")) + ".signature");
} catch (JWSInputException e) {
log.errorf(e, "Failed to parse access_token: %s", tokenString);
}
}
try {
TokenVerifier<CustomKeycloakAccessToken> tokenVerifier = AdapterTokenVerifier.createVerifier(tokenString, deployment, true, CustomKeycloakAccessToken.class);
// Verify audience of bearer-token
if (deployment.isVerifyTokenAudience()) {
tokenVerifier.audience(deployment.getResourceName());
}
token = tokenVerifier.verify().getToken();
} catch (VerificationException e) {
log.debug("Failed to verify token");
challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "invalid_token", e.getMessage());
return AuthOutcome.FAILED;
}
if (token.getIssuedAt() < deployment.getNotBefore()) {
log.debug("Stale token");
challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.STALE_TOKEN, "invalid_token", "Stale token");
return AuthOutcome.FAILED;
}
boolean verifyCaller;
if (deployment.isUseResourceRoleMappings()) {
verifyCaller = token.isVerifyCaller(deployment.getResourceName());
} else {
verifyCaller = token.isVerifyCaller();
}
surrogate = null;
if (verifyCaller) {
if (token.getTrustedCertificates() == null || token.getTrustedCertificates().isEmpty()) {
log.warn("No trusted certificates in token");
challenge = clientCertChallenge();
return AuthOutcome.FAILED;
}
// for now, we just make sure Undertow did two-way SSL
// assume JBoss Web verifies the client cert
X509Certificate[] chain = new X509Certificate[0];
try {
chain = exchange.getCertificateChain();
} catch (Exception ignore) {
}
if (chain == null || chain.length == 0) {
log.warn("No certificates provided by undertow to verify the caller");
challenge = clientCertChallenge();
return AuthOutcome.FAILED;
}
surrogate = chain[0].getSubjectDN().getName();
}
log.debug("successful authorized");
return AuthOutcome.AUTHENTICATED;
}
};
}
};
}
});
return filter;
}
}
I didn't understand why do you need extend Keycloak Token. The roles already there are in Keycloak Token. I will try explain how to access it, the Keycloak have two levels for roles, 1) Realm level and 2) Application (Client) level, by default your Keycloak Adapter use realm level, to use application level you need setting the propertie keycloak.use-resource-role-mappings with true in your application.yml
How to create roles in realm
enter image description here
How to creare roles in client
enter image description here
User with roles ADMIN (realm) and ADD_USER (application)
enter image description here
To have access roles you can use KeycloakAuthenticationToken class in your Keycloak Adapter, you can try invoke the following method:
...
public ResponseEntity<Object> getUsers(final KeycloakAuthenticationToken authenticationToken) {
final AccessToken token = authenticationToken.getAccount().getKeycloakSecurityContext().getToken();
final Set<String> roles = token.getRealmAccess().getRoles();
final Map<String, AccessToken.Access> resourceAccess = token.getResourceAccess();
...
}
...
To protect any router using Spring Security you can use this annotation,  example below:
#PreAuthorize("hasRole('ADMIN')")
#GetMapping("/users")
public ResponseEntity<Object> getUsers(final KeycloakAuthenticationToken token) {
return ResponseEntity.ok(service.getUsers());
}
Obs: The  keycloak.use-resource-role-mappings set up using #PreAuthorize Annotation.  If set to true, #PreAuthorize checks roles in token.getRealmAccess().getRoles(), if false it checks roles in token.getResourceAccess().
If you want add any custom claim in token, let me know that I can explain better.
I put here how I set up my Keycloak Adapter and the properties in my  application.yml:
SecurityConfig.java
...
#KeycloakConfiguration
#EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Value("${project.cors.allowed-origins}")
private String origins = "";
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(this.authenticationManagerBean());
filter.setSessionAuthenticationStrategy(this.sessionAuthenticationStrategy());
filter.setAuthenticationFailureHandler((request, response, exception) -> {
response.addHeader("Access-Control-Allow-Origin", origins);
if (!response.isCommitted()) {
response.sendError(401, "Unable to authenticate using the Authorization header");
} else if (200 <= response.getStatus() && response.getStatus() < 300) {
throw new RuntimeException("Success response was committed while authentication failed!", exception);
}
});
return filter;
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http.csrf()
.disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "**").permitAll()
.antMatchers("/s/**").authenticated()
.anyRequest().permitAll();
}
}
application.yml
..
keycloak:
enabled: true
auth-server-url: http://localhost:8080/auth
resource: myclient
realm: myrealm
bearer-only: true
principal-attribute: preferred_username
use-resource-role-mappings: true
..

#oauth2.hasScope in spring boot + keycloak

I want to use #oauth2.hasScope in spring boot + keycloak, get token with grant_type = client_credentials. but it's not working. this is my code, can u pls help me .
<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.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
This is my config
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
String jwkSetUri = "http://localhost:8080/auth/realms/microservice/protocol/openid-connect/certs";
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests(authorizeRequests -> authorizeRequests
.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
#Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build();
}
}
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return getOAuth2MethodSecurityExpressionHandler();
}
/**
*A security expression handler that can handle default method security expressions plus the set provided by OAuth2SecurityExpressionMethods using the variable oauth2 to access the methods. For example, the expression #oauth2.clientHasRole('ROLE_ADMIN') would invoke OAuth2SecurityExpressionMethods.clientHasRole(java.lang.String)
*By default the OAuth2ExpressionParser is used. If this is undesirable one can inject their own ExpressionParser using AbstractSecurityExpressionHandler.setExpressionParser(ExpressionParser).
*/
#Bean
public OAuth2MethodSecurityExpressionHandler getOAuth2MethodSecurityExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}
This is my controller
hasAnyAuthority('SCOPE_phone') is ok, but #oauth2.hasScope('phone') or #oauth2.hasScope('SCOPE_phone') not working i receive 403 forbidden.
#RequestMapping(value = "/user", method = RequestMethod.GET)
#PreAuthorize("hasAnyAuthority('SCOPE_phone')")
public ResponseEntity<String> getUser() {
return ResponseEntity.ok("Hello User");
}
#RequestMapping(value = "/all-user", method = RequestMethod.GET)
#PreAuthorize("#oauth2.hasScope('phone')")
public ResponseEntity<String> getAllUser() {
return ResponseEntity.ok("Hello All User");
}
This is my token, i have phone scope.
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1MkRsOVNTMlREY0M5SkFtZmZ3ZE1BNjJkbFBreDlFMDdRSnhObF9sVDNJIn0.eyJleHAiOjE2MDM3ODQzNTMsImlhdCI6MTYwMzc4NDA1MywianRpIjoiY2Q2OWIzMDgtNDBmZi00YjJmLTljOWMtNjMyZjQxYTYyNzgwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL21pY3Jvc2VydmljZSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIxZGExM2RjMy0yNDQ1LTRlZTQtYjFhNS0zNjc2YzYyMjY4OTciLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJtb2JpbGVyZXRhaWwiLCJzZXNzaW9uX3N0YXRlIjoiMmIxMzRlN2MtNDZiMS00MGNmLWIyMmYtODA3MDcyYWFjMGU0IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsibW9iaWxlcmV0YWlsIjp7InJvbGVzIjpbInVtYV9wcm90ZWN0aW9uIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6InBob25lIHByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImNsaWVudElkIjoibW9iaWxlcmV0YWlsIiwiY2xpZW50SG9zdCI6IjEyNy4wLjAuMSIsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1tb2JpbGVyZXRhaWwiLCJjbGllbnRBZGRyZXNzIjoiMTI3LjAuMC4xIn0.acg7pPIK89AVFQT0oYrMmBwBGe4hy6PYrQCYdNbSOQA49p6FZ5ZCdqBtxKrxy2DYxgBJZNhMJ-PPZ_WrmRwTAS1H-Udo0Dj9o8VQLDyG2PsrVv8jdsCrrlSnPIg978HF6eP2bh49G4JYZjuLqzuX2h29voFWEMvtCjKGPYTwrwG24uYCKyEr_nCUV8_7Ky6hTyxl10xxnQ5qGjo1Acbhs-F4omgi6H2I2H17PUPVkKdeMKAbsVbubzbMbijgYAUf3j4KbsxEArJ6KC6ZZDsFIIB7-xMBMZD3OYjpn6-3Mt1s_QAlp4I9bdkSS2dNFtP3U6OmTnHCGyYgmScD8FTffw",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxOGM5ZmRiNy1mNzQ0LTQ2ZjktODQ4Ni0wMTFjNWVkOWNkZDIifQ.eyJleHAiOjE2MDM3ODU4NTMsImlhdCI6MTYwMzc4NDA1MywianRpIjoiMGJmOWMyZjItNjM5YS00OGVkLTgzNzUtM2M0YTE0ODc2ZGU0IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL21pY3Jvc2VydmljZSIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9hdXRoL3JlYWxtcy9taWNyb3NlcnZpY2UiLCJzdWIiOiIxZGExM2RjMy0yNDQ1LTRlZTQtYjFhNS0zNjc2YzYyMjY4OTciLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoibW9iaWxlcmV0YWlsIiwic2Vzc2lvbl9zdGF0ZSI6IjJiMTM0ZTdjLTQ2YjEtNDBjZi1iMjJmLTgwNzA3MmFhYzBlNCIsInNjb3BlIjoicGhvbmUgcHJvZmlsZSBlbWFpbCJ9.dG67rD5TPQLSQY69Fhdh9am_t_SZoiL9MEuufD6eOvU",
"token_type": "bearer",
"not-before-policy": 0,
"session_state": "2b134e7c-46b1-40cf-b22f-807072aac0e4",
"scope": "phone profile email"
}

Spring boot webflux security with JWT token

Trying to setup JWT token based auth with Spring boot webflux.
Spring boot version :- 2.3.0.BUILD-SNAPSHOT
Tech Stacks:- Angular 9, Spring boot 2.3.0.BUILD-SNAPSHOT, Spring security, Spring security JWT
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.0.RELEASE</version>
</dependency>
GUI is angular 9 based and using form based authentication.
Need JWT for call coming from angular and also call coming to the API directly.
WebSecurityConfig,
#Configuration
#EnableWebFluxSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private ServerSecurityContextRepository securityContextRepository;
#Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
return http.exceptionHandling().authenticationEntryPoint((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
}).and().csrf().disable().formLogin().disable().httpBasic().disable()
.authenticationManager(authenticationManager).securityContextRepository(securityContextRepository)
.authorizeExchange().pathMatchers(HttpMethod.OPTIONS).permitAll().pathMatchers("/login").permitAll()
.anyExchange().authenticated().and().build();
}
#Bean
public PBKDF2Encoder passwordEncoder() {
return new PBKDF2Encoder();
}
}
PBKDF2Encoder,
#Component
public class PBKDF2Encoder implements PasswordEncoder {
#Value("${springbootwebfluxjjwt.password.encoder.secret}")
private String secret;
#Value("${springbootwebfluxjjwt.password.encoder.iteration}")
private Integer iteration;
#Value("${springbootwebfluxjjwt.password.encoder.keylength}")
private Integer keylength;
/**
* More info (https://www.owasp.org/index.php/Hashing_Java)
*
* #param cs password
* #return encoded password
*/
#Override
public String encode(CharSequence cs) {
try {
byte[] result = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")
.generateSecret(
new PBEKeySpec(cs.toString().toCharArray(), secret.getBytes(), iteration, keylength))
.getEncoded();
return Base64.getEncoder().encodeToString(result);
} catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
throw new RuntimeException(ex);
}
}
#Override
public boolean matches(CharSequence cs, String string) {
return encode(cs).equals(string);
}
}
AuthenticationManager,
#Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
#Autowired
private JWTUtil jwtUtil;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
String authToken = authentication.getCredentials().toString();
String username;
try {
username = jwtUtil.getUsernameFromToken(authToken);
} catch (Exception e) {
username = null;
}
if (username != null && jwtUtil.validateToken(authToken)) {
Claims claims = jwtUtil.getAllClaimsFromToken(authToken);
List<String> rolesMap = claims.get("role", List.class);
List<Role> roles = new ArrayList<>();
for (String rolemap : rolesMap) {
roles.add(Role.valueOf(rolemap));
}
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username,
null,
roles.stream().map(authority -> new SimpleGrantedAuthority(authority.name())).collect(Collectors.toList())
);
return Mono.just(auth);
} else {
return Mono.empty();
}
}
}
SecurityContextRepository,
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
#Autowired
private AuthenticationManager authenticationManager;
#Override
public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) {
throw new UnsupportedOperationException("Not supported yet.");
}
#Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String authToken = authHeader.substring(7);
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
return this.authenticationManager.authenticate(auth).map((authentication) -> {
return new SecurityContextImpl(authentication);
});
} else {
return Mono.empty();
}
}
}
Is this the correct approach? Is there any better approach?
I've faced same problems, and after lot of research I've made a fully 100% functional demo project implementing webflux + webflux-security + others...
you can find the complete implementation here: https://github.com/eriknyk/webflux-jwt-security-demo
the implementation contains:
Spring webflux
Spring security implemented with JWT + validation layer
User register demo endpoint
User authentication endpoint
Model to dto mapping (using mapstruct)
User R2db with Postgresql repository impl
User validation in spring security layer, according to the user record in db

The AOuth2 Authorization server does not insert token in the given database

There is some configuration class in Authorization server:
#Configuration
public class AppConfig {
#Value("${spring.datasource.url}")
private String datasourceUrl;
#Value("${spring.database.driverClassName}")
private String dbDriverClassName;
#Value("${spring.datasource.username}")
private String dbUsername;
#Value("${spring.datasource.password}")
private String dbPassword;
#Bean
public DataSource dataSource() {
final DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(dbDriverClassName);
dataSource.setUrl(datasourceUrl);
dataSource.setUsername(dbUsername);
dataSource.setPassword(dbPassword);
return dataSource;
}
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource());
}
}
and the another class:
#EnableWebSecurity
#Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private AccountUserDetailsService accountUserDetailsService;
#Bean
public PasswordEncoder passwordEncoder() {
return new HashingClass();
}
#Bean
#Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return accountUserDetailsService;
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/webjars/**", "/resources/**");
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/login", "/oauth/authorize")
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServiceBean())
.passwordEncoder(passwordEncoder());
}
}
and the third class is:
#EnableAuthorizationServer
#Configuration
public class AuthServerOAuth2Config extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
private final AppConfig appConfig;
#Autowired
public AuthServerOAuth2Config(AuthenticationManager authenticationManager, AppConfig appConfig) {
this.authenticationManager = authenticationManager;
this.appConfig = appConfig;
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(appConfig.dataSource());
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
/*
* Allow our tokens to be delivered from our token access point as well as for tokens
* to be validated from this point
*/
security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");;
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(appConfig.tokenStore()); // Persist the tokens in the database
}
}
and the client application is like this:
#SpringBootApplication
#EnableOAuth2Sso
#RestController
#Configuration
public class SocialApplication extends WebSecurityConfigurerAdapter {
#Value("${security.oauth2.client.clientId}")
private String clientId;
#Value("${security.oauth2.client.clientSecret}")
private String clientSecret;
#Value("${security.oauth2.client.accessTokenUri}")
private String accessTokenUri;
#RequestMapping("/user")
public Principal user(HttpServletResponse response, Principal principal) {
return principal;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests().antMatchers("/", "/login**", "/webjars/**", "/index.html", "/getEmployees.jsp").permitAll()
.anyRequest().authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
#Bean
public OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails() {
ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
details.setClientId(clientId);
details.setClientSecret(clientSecret);
details.setAccessTokenUri(accessTokenUri);
return details;
}
public static void main(String[] args) {
SpringApplication.run(SocialApplication.class, args);
}
}
and its application.yml file is:
server:
port: 8090
security:
basic:
enabled: false
oauth2:
client:
clientId: web
clientSecret: secret
accessTokenUri: http://localhost:8081/auth/oauth/token
userAuthorizationUri: http://localhost:8081/auth/oauth/authorize
authenticationScheme: query
clientAuthenticationScheme: form
resource:
userInfoUri: http://localhost:8081/auth/user
logging:
level:
org.springframework.security: DEBUG
When I request http://localhost:8090/user (this rest is owned client) I redirected to Authorization server and I can login successfully after login again I am redirected to the client with a code, but client is not able to exchange access token with the given code. and the following exception is raised on the browser:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing
this as a fallback. Mon Dec 17 09:47:27 IRST 2018 There was an
unexpected error (type=Unauthorized, status=401). Authentication
Failed: Could not obtain access token
And I query from DB:
select * from oauth_access_token
or
select * from oauth_client_token
The two above tables are empty.
Where is wrong? I am really confused.
EDIT
The application.properties file of AOuthrization server:
server.port=8081
server.context-path=/auth
security.basic.enabled=false
# ===============================
# = DATA SOURCE
# ===============================
# Set here configurations for the database connection
# Connection url for the database w/createDatabaseIfNotExist=true
spring.datasource.url = jdbc:oracle:thin:#192.168.192.129:1521:hamed
spring.database.driverClassName = oracle.jdbc.OracleDriver
#spring.jpa.database = MySQL
#spring.datasource.platform = mysql
# Database - data initialization
spring.jpa.generate-ddl = true
# Username and password
spring.datasource.username = test
spring.datasource.password = test
# ===============================
# = JPA / HIBERNATE
# ===============================
# Use spring.jpa.properties.* for Hibernate native properties (the prefix is
# stripped before adding them to the entity manager).
# Show or not log for each sql query
spring.jpa.show-sql = true
# Transactions
spring.jpa.open-in-view = false
# Hibernate ddl auto (create, create-drop, update): with "update" the database
# schema will be automatically updated accordingly to java entities found in
# the project
spring.jpa.hibernate.ddl-auto = none
# Naming strategy
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
# spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
# Allows Hibernate to generate SQL optimized for a particular DBMS
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.Oracle12cDialect
and the its pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc7</artifactId>
<version>12.1.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>r05</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>11.0</version>
</dependency>
</dependencies>
and the version of spring boot is:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath/>
</parent>
I noticed you are using spring-boot and to me seems like you can clean up your code a little, for instance, all this DataSource config may not be necessary.
As for your problem, it could be related to the class AuthServerOAuth2Config where you are injecting AppConfig instead of DataSource and TokenStore, making this change can solve your problem.
Here you can find a sample auth service I did a while ago https://github.com/marcosbarbero/spring-security-authserver-sample
I could solve my problem by removing two lines of application.yml of client application.
These two lines are:
authenticationScheme: query
clientAuthenticationScheme: form
Why these two lines cause problem, I do not know.

Resources