#RolesAllowed annotation always throw 403 error with Keycloak - spring

I'm a new one in Spring boot and have maybe a stupid question.
I have a simple spring boot rest api application with spring security and oauth2. Outh2 broker is Keycloak
So my security filter looks like this
#Configuration
#EnableWebSecurity
public class WebSecurityAdditionalConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.authorizeRequests()
.antMatchers("/api/**")
.authenticated()
.and()
.oauth2ResourceServer().jwt();
http.csrf().disable().sessionManagement().sessionCreationPolicy(
SessionCreationPolicy.STATELESS);
}
}
Also I enable global method security
#Configuration
#EnableGlobalMethodSecurity(jsr250Enabled = true, securedEnabled = true, prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}
But when I try to add #RolesAllowed('admin') to my controller method I always get 403 forbidden error
Without annotations all works fine, without token I get 401 and if token expired 403.
This is example of my jwt
{
"realm_access": {
"roles": [
"admin"
]
},
"resource_access": {
"edge_client": {
"roles": [
"cli_admin"
]
}
},
"scope": "profile web-origins email",
"email_verified": false,
"name": "John Spartan",
"groups": [
"admin"
],
"preferred_username": "test_admin",
"given_name": "John",
"family_name": "Spartan"
}

I think this class is responsible for getting authorities. By default it is looking for scope or scp claims in jwt. In your case you have "scope": "profile web-origins email". After that it prefixes every authority with DEFAULT_AUTHORITY_PREFIX equals to SCOPE_. I think when you will debug your Authentication object from SecurityContextHolder.getContext().getAuthentication() authorities returned by its getAuthorities() will be equals to SCOPE_profile, SCOPE_web-origins and SCOPE_email. You should change your code to:
.oauth2ResourceServer()
.jwt(customizer -> {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
//write your own Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter
//and override Collection<GrantedAuthority> convert(Jwt jwt) to get roles from
//realm_access.roles
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
customizer.jwtAuthenticationConverter(jwtAuthenticationConverter)
})
or use Keycloak Adapter for Spring instead of oauth2ResourceServer()

Related

Springboot with Keycloak always return 403

I have created a Springboot application with Keycloak by following this tutorial Baeldung
When I try to enter /api/foos it always returns 403 without any error messages.
// SecurityConfig
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
class SecurityConfig(
private val unauthorizedHandler: JwtAuthenticationEntryPoint
) : WebSecurityConfigurerAdapter() {
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.cors()
.and()
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**", "/api/foos")
.hasAnyRole("free_user")
.antMatchers(HttpMethod.POST, "/api/foos")
.hasAnyRole("free_user")
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.jwt()
}
}
// Controller
#RestController
#RequestMapping(value = ["/api/foos"])
class SecurityTestController() {
#GetMapping(value = ["/{id}"])
fun findOne(#PathVariable id: Long?): String {
return "fineOne with id $id"
}
#GetMapping
fun findAll(): Message {
return Message("findAll")
}
}
// application.properties
# Resource server config
rest.security.issuer-uri=http://localhost:8081/auth/realms/dev
spring.security.oauth2.resourceserver.jwt.issuer-uri=${rest.security.issuer-uri}
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${rest.security.issuer-uri}/protocol/openid-connect/certs
// build.gradle (app)
plugins {
id("org.springframework.boot")
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter- oauth2-resource-server")
implementation("org.springframework.boot:spring-boot-starter-security")
testImplementation("org.springframework.security:spring-security-test")
}
Here is my user on Keycloak
Postman result:
What I have already tried
disable csrf - Not working
Comment SecurityConfig class - Working without security
It looks like "free_user" is a client role.
Two solutions :
Either remove the current "free_user" client role, create a "free_user" realm role in the global Roles tab, and assign it to your user.
Or, add the property keycloak.use-resource-role-mappings=true to your Spring boot configuration to allow spring security to map your current client role.
I found the answer here LINK
We need to create a custom mapping between Spring security and Keycloak roles.
public class KeycloakRealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
#Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
final Map<String, Object> realmAccess = (Map<String, Object>) jwt.getClaims().get("realm_access");
return ((List<String>)realmAccess.get("roles")).stream()
.map(roleName -> "ROLE_" + roleName) // prefix to map to a Spring Security "role"
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}

How can I get the OAuth2AccessToken with Spring-Boot?

I am using Spring-Boot and Spring Security with an OAuth2 login from a third party.
The SSO provider has an accesstoken end point which returns the following JSON
{
"access_token": "CGjok",
"refresh_token": "TSHO6E",
"scope": "openid profile ",
"id_token": "eyJ0eXAiOiJKV1QiLCg",
"token_type": "Bearer",
"expires_in": 7199,
"nonce": "ImplicitFlowTest"
}
The login is working with the #EnableOAuth2Sso annotation as follows:
#EnableOAuth2Sso
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/restapi/**").hasAuthority("Mitarbeiter")
.antMatchers("/login", "/static/**", "/", "/actuator/prometheus","/error**","/logout").permitAll()
.and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).invalidateHttpSession(true)
.deleteCookies("SMSESSION", "JSESSIONID", "XSRF-TOKEN").logoutSuccessUrl("/");
http
// CSRF Token
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
We are able to logout of the application but we also want to send a request to the Authorization Server. To do so I need to access the token info endpoint.
Within my controllers I am able to see the Principal is getting the correct information from the user endpoint but where in Spring Boot is the information from the accessToken endpoint stored. I have found the class OAuth2AccessToken but cannot figure out how to read it in Spring Controller. I can access the OAuth2Authentication by casting the Principal as expected.
The SSO authorization server has the following endpoint that I need to call:
/oauth2/connect/endSession?id_token_hint=<oidc-token>&post_logout_redirect_uri=<post-logout-redirect-uri>
The refers to the value in the JSON from the accesstoken endpoint. How can I access these values given my setup?
Read token value from Security Context
String tokenValue = null;
final Authentication authenticationObject = SecurityContextHolder.getContext().getAuthentication();
if (authenticationObject != null) {
final Object detailObject = authenticationObject.getDetails();
if (detailObject instanceof OAuth2AuthenticationDetails) {
final OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) detailObject;
tokenValue = details.getTokenValue();
} else if (detailObject instanceof OAuth2AccessToken) {
final OAuth2AccessToken token = (OAuth2AccessToken) detailObject;
tokenValue = token.getValue();
} else {
tokenValue = null;
}
}

"status": 403, "error": "Forbidden", "message": "Forbidden", "path": "/post/create"

I see this response when I try to add new post after authorization by admin.
I have Basic authorization which based on spring boot security:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
//...declared fields
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.passwordEncoder(passwordEncoder())
.withUser("user")
.password("userpass")
.roles("USER")
.and()
.withUser("admin")
.password("adminpass")
.roles("ADMIN", "USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/logout").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.and().logout().permitAll()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()
.and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login");
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
I get this message when try to add new post after authorization:
{
"timestamp": "2018-07-04T12:19:25.638+0000",
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/post/create"
}
in my controller:
#RestController
public class PostController {
#Autowired
private PostDAO postDAO;
#GetMapping("/posts")
public Page<Post> getAllPosts(Pageable pageable) {
return postDAO.findAll(pageable);
}
#PostMapping("/post/create")
public Post createPost(#Valid #RequestBody Post post) {
return postDAO.save(post);
}
//other end-points........
}
However, read operations from my controller work well but to CRUD operation I haven't access.
There are my dependencies:
dependencies {
compile ('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.hibernate:hibernate-core')
compile('org.springframework.boot:spring-boot-starter-security')
runtime('mysql:mysql-connector-java')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.security:spring-security-test')
testCompile('junit:junit')
}
Any idea?
Thanks in advance!
This is due to CSRF enabled. CSRF protection is enabled by default in the Java configuration. We can still disable CSRF using the configuration given below.
http .csrf().disable() .authorizeRequests() .anyRequest().permitAll();
Starting from Spring Security 4.x – the CSRF protection is enabled by default in the XML configuration as well; we can of course still disable it if we need to:
<http>
...
<csrf disabled="true"/>
</http>
Note : CSRF is an attack which forces an end user to execute unwanted
actions in a web application in which is currently authenticated.
here's why:
csrf is automatically enabled in spring security,and I recommended you do not disable csrf.
normally your html form tag should include a hidden field which generates csrf token, however, thymeleaf automaticlly do that for you, you should check your html tag to see whether or not a "th:" was included, if not, include a "th:" before "action" in form tag, do this, thymeleaf generates csrf token invisibablly.

Spring boot OAuth2 custom roles on Resource server

TL;DR: How to assign users custom roles/authorities on Resource server side (that means without JWT) based on their access_token?
The whole story: I have a working Auth server and a client (which is SPA), which can obtain access_token from the Auth server. With that access_token the client can request data on my Resource server (which is separated from Auth server). The Resource server can get username from Auth server using the access_token.
I can access the username in code by injection Authentication object into method like this:
#RequestMapping("/ping")
fun pingPong(auth: Authentication): String = "pong, " + auth.name
My question is how to add my custom roles or authorities (auth.authorities - there is only USER_ROLE) to this object which would be managed on the Resource server, not Auth server, based on the username.
I have tried several ways to do it but none has helped. The most promising was this:
#Configuration
#EnableWebSecurity
#EnableResourceServer
class ResourceServerConfigurer(val userDetailsService: MyUserDetailsService) : ResourceServerConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.userDetailsService(userDetailsService) // userDetailsService is autowired
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers("/", "/index.html").permitAll()
.anyRequest().authenticated()
}
}
And my custom UserDetailsService:
#Service
class UserDetailsService : org.springframework.security.core.userdetails.UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
return org.springframework.security.core.userdetails.User(username, "password", getAuthorities(username))
}
private fun getAuthorities(user: String): Set<GrantedAuthority> {
val authorities = HashSet<GrantedAuthority>()
authorities.addAll(listOf(
SimpleGrantedAuthority("ROLE_ONE"), //let's grant some roles to everyone
SimpleGrantedAuthority("ROLE_TWO")))
return authorities
}
}
Everything worked (I mean I was successfully authenticated) except that I still had only ROLE_USER. Next what I tried was providing a custom implementation of AbstractUserDetailsAuthenticationProvider:
#Bean
fun authenticationProvider(): AbstractUserDetailsAuthenticationProvider {
return object : AbstractUserDetailsAuthenticationProvider() {
override fun retrieveUser(username: String, authentication: UsernamePasswordAuthenticationToken): UserDetails {
return User(username, "password", getAuthorities(username))
}
private fun getAuthorities(user: String): Set<GrantedAuthority> {
val authorities = HashSet<GrantedAuthority>()
authorities.addAll(listOf(
SimpleGrantedAuthority("ROLE_ONE"),
SimpleGrantedAuthority("ROLE_TWO")))
return authorities
}
override fun additionalAuthenticationChecks(userDetails: UserDetails, authentication: UsernamePasswordAuthenticationToken?) {
}
}
}
with same result, only the ROLE_USER was present.
I would really appreciate any ideas from you guys how add some roles to the Authentication object after the access_token was validated and username obtained from Auth server.
Solution by OP.
First of all I needed to provide custom PrincipalExtractor and AuthoritiesExtractor implementations. But to make Spring use them it is necessary in configuration NOT to use security.oauth2.resource.token-info-uri but security.oauth2.resource.user-info-uri instead (I really didn't expect this to be one of the roots of my problem).
Finally the security config must be done in ResourceServerConfigurerAdapter, not in WebSecurityConfigurerAdapter.
The final code looks like this:
#SpringBootApplication
#RestController
class MyApplication {
#RequestMapping("/ping")
fun pingPong(user: Authentication): String {
return "pong, " + user.name + " - " + user.authorities.joinToString()
}
}
#Configuration
#EnableWebSecurity
#EnableResourceServer
class ResourceServerConfigurer : ResourceServerConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers("/", "/index.html").permitAll()
.anyRequest().authenticated()
}
#Bean
fun principalExtractor() = PrincipalExtractor {
return#PrincipalExtractor it["name"]
}
#Bean
fun authoritiesExtractor() = AuthoritiesExtractor {
return#AuthoritiesExtractor AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ONE,ROLE_TWO")
}
}
fun main(args: Array<String>) {
SpringApplication.run(MyApplication::class.java, *args)
}

"An Authentication object was not found in the SecurityContext" - UserDetailsService Setup in Spring Boot

In a Spring Boot application, I have an in-memory Spring Security setup. And it works as desired.
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("kevin").password("password1").roles("USER").and()
.withUser("diana").password("password2").roles("USER", "ADMIN");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/foos").hasRole("ADMIN")
.antMatchers(HttpMethod.PUT, "/foos/**").hasRole("ADMIN")
.antMatchers(HttpMethod.PATCH, "/foos/**").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/foos/**").hasRole("ADMIN")
.and()
.csrf().disable();
}
}
Now, I convert it to a database based approach with the following code.
#Entity
class Account {
enum Role {ROLE_USER, ROLE_ADMIN}
#Id
#GeneratedValue
private Long id;
private String userName;
// #JsonIgnore
private String password;
#ElementCollection(fetch = FetchType.EAGER)
Set<Role> roles = new HashSet<>();
...
}
The repository:
#RepositoryRestResource
interface AccountRepository extends CrudRepository<Account, Long>{
#PreAuthorize("hasRole('USER')")
Optional<Account> findByUserName(#Param("userName") String userName);
}
The UserDetailsService:
#Component
class MyUserDetailsService implements UserDetailsService {
private AccountRepository accountRepository;
MyUserDetailsService(AccountRepository accountRepository){
this.accountRepository = accountRepository;
}
#Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
Optional<Account> accountOptional = this.accountRepository.findByUserName(name);
if(!accountOptional.isPresent())
throw new UsernameNotFoundException(name);
Account account = accountOptional.get();
return new User(account.getUserName(), account.getPassword(),
AuthorityUtils.createAuthorityList(account.getRoles().stream().map(Account.Role::name).toArray(String[]::new)));
}
}
And the modification of the WebSecurityConfigurerAdapter configuration:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private MyUserDetailsService userDetailsService;
SecurityConfiguration(MyUserDetailsService userDetailsService){
this.userDetailsService = userDetailsService;
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService); // <-- replacing the in-memory anthentication setup
}
...
}
When I send the same request with a pair of username and password as the basic authentication as for the in-memory version, I, however, get the 401 error:
{
"timestamp": 1489430818803,
"status": 401,
"error": "Unauthorized",
"message": "An Authentication object was not found in the SecurityContext",
"path": "/foos"
}
After reading through some related document and sample code, I can't see the cause of the error. What the error message says is that the user isn't in the Spring Security context. The line of AuthenticationManagerBuilder usage in the userDetailsService(userDetailsService) shall take care of setting up these users in the SecurityContext, isn't it?
The Spring Boot version is 1.4.3.RELEASE.
remove this preAuthorize annotation from the crud repository. this method is used when ever you are trying to login but with the preauthorize annotation it expects a user to be logged in.
#PreAuthorize("hasRole('USER')")
Optional<Account> findByUserName(#Param("userName") String userName);
and i made some changes to the configure method in websecurityconfigureadapter
you might need to allow access to the sign in and login urls in the configure method without login permission.
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeUrls()
.antMatchers("/signup","/about").permitAll() // #4
.antMatchers("/admin/**").hasRole("ADMIN") // #6
.anyRequest().authenticated() // 7
.and()
.formLogin() // #8
.loginUrl("/login") // #9
.permitAll(); // #5
}
be careful when requiring admin permission to /foo/** these kind of urls this tells that all urls begin with foo are only allowed for admins.
One problem is, that your repository finder is annotated with #PreAuthorize("hasRole('USER')"). This method is invoked before a request can be (fully) authenticated, so it can never pass the check and the exception is raised even if you pass the proper credentials.
Another point is, do you really want to expose your user account data as ReST resources? At the moment a request like curl http://localhost:8080/accounts will return all your user data as JSON+HAL response, even to anonymous users.
It would return something like:
{
"_embedded" : {
"accounts" : [ {
"userName" : "admin",
"password" : "admin",
"roles" : [ "ROLE_USER", "ROLE_ADMIN" ],
"_links" : {
"self" : {
"href" : "http://localhost:8080/accounts/1"
},
"account" : {
"href" : "http://localhost:8080/accounts/1"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/accounts"
},
"profile" : {
"href" : "http://localhost:8080/profile/accounts"
},
"search" : {
"href" : "http://localhost:8080/accounts/search"
}
}
}
So #RepositoryRestResource is dangerous here, using #Repository is the way to go if you just want to use the database table to auth your requests.
For details see the documentation.
Even if you removed the passwords from the responses as your code suggests (with the commented #JsonIgnore annotation in your Account class) you would expose security related information like usernames and roles. Beside that it's a bad practice to "abuse" your Entity to shape your response object. Better use Projections instead.
If you want to keep your /accounts endpoint you should also secure it by maybe at least adding .antMatchers("/accounts/**").hasRole("ADMIN") to your config.
Your real problem lies in the UserDetailService.loadUseByUsername - there you call:
Optional<Account> accountOptional = this.accountRepository.findByUserName(name);
and it is being done during te authorization process. But the accountRepository method is annotated with PreAuthorize - and can be called only from context containing Authentication Object.
Do a simple test to understand it:
#RepositoryRestResource
interface AccountRepository extends CrudRepository<Account, Long>{
#PreAuthorize("hasRole('USER')")
Page<Account> findAll(Pageable pageable);
Optional<Account> findByUserName(#Param("userName") String userName);
}
Then check, that you can query for all users correctly. The only problem is in the recursive nature of findByUserName method.
Cheers!

Resources