Spring Security oauth2Login OAuth2UserService not executed after authentication - spring-boot

I have a Spring Boot Admin app secured by Keycloak, I defined a user having a realm role ACTUATOR.
The problem is that after authentication Spring Security does not have access to realm roles. I can see the granted authorities: Granted Authorities: ROLE_USER, SCOPE_actuator_access, SCOPE_profile'
Looking at the doc I found this section: Delegation-based strategy with OAuth2UserService and this is my configuration:
This is my configuration:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure( HttpSecurity http ) throws Exception {
http.csrf().disable().cors().disable();
http.authorizeRequests()
.antMatchers( "/error","/instances", "/**/*.css", "/**/img/**", "/**/third-party/**", "/*.js" )
.permitAll()
.anyRequest().hasRole( "ACTUATOR" )
.and()
.oauth2Login( oauth2 -> oauth2.userInfoEndpoint(
userInfo -> userInfo.oidcUserService( this.oidcUserService() ) ) );
}
// I just copy-paste the doc's code to play with it...
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return ( userRequest ) -> {
OidcUser oidcUser = delegate.loadUser( userRequest );
//TODO find a way to extract realm roles
OAuth2AccessToken accessToken = userRequest.getAccessToken();
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
oidcUser = new DefaultOidcUser( mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo() );
return oidcUser;
};
}
I hoped use the method oidcUserService() to extract Keycloak realm roles from the token but the method is not executed at all. I mean after successful authentication from Keycloak and redirected to the application, oidcUserService() method is not executed. It seems only executed at application startup, which is strange.
The question is how can I retrieve realm roles in this scenario?
EDIT
I added a sample project here: https://github.com/akuma8/sba-keycloak-spring-security
The security configurations are in application.yml and in SecurityConfig class

I found why the oidcUserService() method was not invoked after authentication. The reason is the type of the token, I have dumbly copied the code from Spring Security documentation without making attention about that information.
In the documentation it's about OidcUserRequest whereas in my case it's OAuth2UserRequest, so it's a clash between OAUTH2 vs OIDC. Changing the method to:
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
// code extracting authorities from JWT here
}
Solved my issue. I am now able to get Keycloak realm roles from the access token.

I can't tell why this oidcUserService isn't invoked. You can use JwtAuthenticationConverter instead and map you JWT to whatever role you need.
It's a bit tricky so here are two examples:
https://dev.to/toojannarong/spring-security-with-jwt-the-easiest-way-2i43 (up-to-date but a bit too broad, look at the "custom claim" part)
https://stackoverflow.com/a/58234971/1722236 (outdated)
Note I don't know if you should check the JWT signature yourself before parsing it or if Spring OAuth will do it. That'd be worth checking.

Related

Spring oauth2login oidc grant access based on user info

I'm trying to set up Authentication based on this tutorial: https://www.baeldung.com/spring-security-openid-connect part 7 specifically.
I have filled properties and configured filter chain like this:
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.anyRequest().authenticated())
.oauth2Login(oauthLogin -> oauthLogin.permitAll());
return http.build();
}
which works, but now all users from oidc can connect log in. I want to restrict access based on userinfo. E.g. add some logic like:
if(principal.getName() == "admin") {
//allow authentication
}
are there any way to do it?
I tried to create customer provider like suggested here: Add Custom AuthenticationProvider to Spring Boot + oauth +oidc
but it fails with exception and says that principal is null.
You can retrieve user info when authentication is successful and do further checks based user info.
Here is sample code that clears security context and redirects the request:
#Component
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if(authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
// OidcUser or OAuth2User
// OidcUser user = (OidcUser) token.getPrincipal();
OAuth2User user = token.getPrincipal();
if(!user.getName().equals("admin")) {
SecurityContextHolder.getContext().setAuthentication(null);
SecurityContextHolder.clearContext();
redirectStrategy.sendRedirect(request, response, "login or error page url");
}
}
}
}
Are you sure that what you want to secure does not include #RestController or #Controller with #ResponseBody? If so, the client configuration you are referring to is not adapted: you need to setup resource-server configuration for this endpoints.
I wrote a tutorial to write apps with two filter-chains: one for resource-server and an other one for client endpoints.
The complete set of tutorials the one linked above belongs to explains how to achieve advanced access-control on resource-server. Thanks to the userAuthoritiesMapper configured in resource-server_with_ui, you can write the same security expressions based on roles on client controller methods as I do on resource-server ones.

Can I use both introspection server and local check for authorize token? Spring Boot - Security

I want to
introspect JWT token on remote server
and then check locally if scope/aud/iss/exp are correct
How can this be done most easily in Spring Boot?
As I understand first case is something similar to opauqeToken functionality (but I have normal JWT) and second case is more like using jwt
Spring Security only supports JWTs or Opaque Tokens, not both at the same time.
If I use opaqueToken, then validation on remote server is done without any effort (even if that's JWT)
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/api/**").hasAuthority("SCOPE_" + scope)
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaque -> opaque
.introspectionUri(this.introspectionUri)
.introspectionClientCredentials(this.clientId, this.clientSecret)
));
return http.build();
I have scope verified. Now I want to check iss, aud, exp. Is that doable with opaqueToken?
Or should I use jwt auth instead?
IMHO opaqueToken can be JWT, so now the question is how to verify and inspect it locally after remote introspection?
It's kind of hybrid of two different approaches, but hopefully you know the simple way how to do it.
Ok, I think I have my answer. I created my own introspector which is implementing OpaqueTokenIntrospector
public class JwtOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector(
"introspect-url",
"client-id",
"client-secret"
);
#Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal introspected = this.delegate.introspect(token);
// SOME LOGIC
}
}
and I added it as a #Bean
#Bean
public OpaqueTokenIntrospector tokenIntrospector() {
return new JwtOpaqueTokenIntrospector();
}

Getting the Spring Security "JWT Login Sample" to work with roles

I'm trying to rewrite a previous example with JWT's built with a custom JWT Filter into a simplified version based on Springs new authorization server and this example:
https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/jwt/login
The example sets up an InMemoryUserDetailsManager with a single user → user,password and an "app" authority so I assume it is designed to handle roles/authorities?
Everything works fine (as explained in the examples README) if I use the provided SecurityFilterChain
But if I change this:
...
http.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
Into this
...
http.authorizeHttpRequests((authorize) -> authorize
.antMatchers("/").hasRole("app")
//.antMatchers("/").hasAuthority("app")
.anyRequest().authenticated()
)
I get a 403 Status back
The authority gets added to the JWT as expected like this:
..
"scope": "app"
}
Apart from the antMatchers given above, my code is exactly as clone from the Spring Security example
What am I missing here?
OK, read the specs ;-)
Accoring to https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/jwt.html
Authorities gets prefixed with a SCOPE_
So this partly fixes the problem
.antMatchers("/").hasAuthority("SCOPE_app")
I still havent figured out how to use hasRoles?
To use hasRole, you need to have authorities which start with ROLE_. What you could do is register a converter which would read roles from JWT and add them as GrantedAuthority.
public class RolesClaimConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter wrappedConverter;
public RolesClaimConverter(JwtGrantedAuthoritiesConverter conv) {
wrappedConverter = conv;
}
#Override
public AbstractAuthenticationToken convert(#NonNull Jwt jwt) {
// get authorities from wrapped converter
var grantedAuthorities = new ArrayList<>(wrappedConverter.convert(jwt));
// get role authorities
var roles = (List<String>) jwt.getClaims().get("roles");
if (roles != null) {
for (String role : roles) {
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
}
return new JwtAuthenticationToken(jwt, grantedAuthorities);
}
}
Then register your converter in your security configuration
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(resourceServer -> resourceServer
.jwt()
.jwtAuthenticationConverter(
new RolesClaimConverter(
new JwtGrantedAuthoritiesConverter()
)
)
)
// other configuration
;
return http.build();
}
And that's it. All you need to do now is to pass a list of roles as a claim when creating JWT and you can use .antMatchers("/").hasRole("app") and #PreAuthorize("hasRole('app')") in your code.

Spring #RestController single method for anonymous and authorized users

I have a following Spring RestController:
#RestController
#RequestMapping("/v1.0/tenants")
public class TenantController {
#Autowired
private TenantService tenantService;
#RequestMapping(value = "/{tenantId}", method = RequestMethod.GET)
public TenantResponse findTenantById(#PathVariable #NotNull #DecimalMin("0") Long tenantId) {
Tenant tenant = tenantService.findTenantById(tenantId);
return new TenantResponse(tenant);
}
}
findTenantById method should be accessed by anonymous and authorized users. In case of anonymous user SecurityContextHolder.getContext().getAuthentication() must return NULL or AnonymousAuthenticationToken but in case of authorized - Authentication object.
In my application I have implemented security model with OAuth2 + JWT tokens.
This my config:
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.antMatcher("/v1.0/**").authorizeRequests()
.antMatchers("/v1.0/tenants/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(STATELESS);
// #formatter:on
}
Also, for secure endpoints I'm applying #PreAuthorize annotation where needed but not in case of findTenantById because as I said previously, I need to grant access to this endpoint for anonymous and authorized users. Inside of endpoint business logic I'll decide who will be able to proceed based on different conditions.
Right now even I have provided my accessToken for this endpoint I can't get an authenticated User object from SecurityContextHolder.getContext().getAuthentication().
How to configure this endpoint in order to be working in a way described above ?
I think I have found a solution - I have annotated my method with:
#PreAuthorize("isAnonymous() or isFullyAuthenticated()")
Please let me know if there is any better solutions.

Spring OAuth - Reload resourceIds and authorities of authentication

I just apply Spring Boot and Spring Cloud to build a microservice system. And I also apply Spring Oauth to it. Honestly, everything is perfect. Spring does a great job in it.
In this system, I have a microservice project does the job of an OAuth server, using JDBC datasource, and I using Permission based for UserDetails authorities (1 User has several Permissions). There are several microservice project does the jobs of Resource server (expose Rest api using Jersey), access security is based on Permissions of Authentication of OAuth bearer token.
Resource Server OAuth config class is something like this
#Configuration
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/restservice/object/list")
.hasAuthority("PERMISSION_VIEW_OBJECT_LIST");
// ...
}
#Override
public void configure(ResourceServerSecurityConfigurer resources)
throws Exception {
resources.resourceId("abc-resource-id")
.tokenStore(new JdbcTokenStore(dataSource()));
}
#Bean
#ConfigurationProperties(prefix = "oauth2.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
}
Everything is great! But I encounter 2 problems:
If I add a new microservice project as a new resourceId, and I append resourceId value to RESOURCE_IDS in table OAUTH_CLIENT_DETAILS of the OAuth client, all requests to Rest API of new resource service return error something like this
{"error":"access_denied","error_description":"Invalid token does not contain resource id (xyz-resource-id)"}
This happens even when user logout and re-login to obtain new access token. It only works if I go to delete records of the Access token and Refresh token int table OAUTH_ACCESS_TOKEN and OAUTH_REFRESH_TOKEN in database.
If at runtime, Permission of a User is changed, the authorities of authentication is not reloaded, I see that AUTHENTICATION value of the Access Token in table OAUTH_ACCESS_TOKEN still contains old Authorities before Permission is changed. In this case, User must logout and re-login to obtain new Access Token with changed authorities.
So, are there any ways to fix these 2 problems.
I'm using Spring Cloud Brixton.SR4 and Spring Boot 1.3.5.RELEASE.
If you are using the default Spring JdbcTokenStore, then the users authentication is serialised and stored with the access/refresh token when the user authenticates and retrieves their token for the first time.
Each time the token is used to authenticate, it is this stored authentication that is loaded which is why changes to the user permissions or the addition of extra resources is not reflected in the users permissions.
In order to add in some checking on this, you can extend DefaultTokenServices and override the loadAuthentication(String accessTokenValue) method to perform your own checks once the users authentication is loaded from the token store.
This may not be the ideal way of doing this, but it is the only way we've found of doing it so far.
To override DefaultTokenServices, add the follwoing bean method to you AuthorizationServerConfigurerAdapter config class:
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Bean
public AuthorizationServerTokenServices authorizationServerTokenServices() throws Exception {
// Where YourTokenServices extends DefaultTokenServices
YourTokenServices tokenServices = new YourTokenServices();
tokenServices.setTokenStore(tokenStore);
tokenServices.setClientDetailsService(clientDetailsService);
return tokenServices;
}
}
I resolved reload problem this way.
#Bean
public ClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}

Resources