Keycloak- Angular-Rest Integration - Roles are always empty - spring-boot

When I read about integrating keycloak with Angular+REST application, mostly I see an approach having two clients, one public and one bearer-only. Is this the best solution or can I use a single confidential client for both application. I read that using confidential client for javascript is not the best way to do as there is no way to keep the secret hidden in javascript.
Also, after integrating keycloak to both rest and UI project using the two clients approach, authentication seems to be working. But I am not getting any roles in the rest side. I am using spring security adapter and springboot 1.5.18 for the backend. My keycloak server version is 3.4.12 and keycloak spring adapter version is 3.4.3. Keycloak configuration files are also provided below.
keycloak.json (Angular project)
{
"realm": "dev",
"auth-server-url": "https://<keycloakserver> /auth",
"resource": "frontend-dev",
"public-client": true,
"use-resource-role-mappings": true,
"confidential-port": 0,
"ssl-required": "external",
"disable-trust-manager": true
}
application.properties (springboot)
keycloak.realm=dev
keycloak.bearer-only=true
keycloak.auth-server-url=https:// <keycloakserver> /auth
keycloak.resource= backend-dev
keycloak.use-resource-role-mappings=true
keycloak.credentials.secret=222-3333-4444-5555
#development only properties
keycloak.ssl-required=external
keycloak.disable-trust-manager=true
Keycloak Java configuration
#KeycloakConfiguration
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
/**
* Defines the session authentication strategy.
*/
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Bean
public FilterRegistrationBean
keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
#Bean
public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(KeycloakPreAuthActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public AccessToken accessToken() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
return ((KeycloakSecurityContext)((KeycloakAuthenticationToken) request.getUserPrincipal()).getCredentials())
.getToken();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests().antMatchers("/**").permitAll();
}
}
To protect a rest resource used the annotation
#RolesAllowed(“Name of the role”)
Even after assigning a client role to the user it was throwing 403-Access denied error
I also tried to get the roles manually using the code
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.getAuthentication().getAuthorities();
But it was always returning an empty array.

I was able to finally solve the issue. Problem was with the missing Scope configuration in the frontend keycloak client.
For all the clients full scope was turned off due to security reason. Because of that unless we set explicitly in the client scope configuration of the frontend client to include the backend client roles, it wont be part of the token.

Related

Get Keycloak AccessToken in controller

I am trying to get the access token after a successfully login, and after a lot of researched I got to this post, how to get Keycloak access token and store it in db for spring boot?, where it's said to make a Keycloak login manually, but I don't know how. The link to the document in the comments doesn't work anymore.
I also tired to get it thought the headers, but no Authorization header is sent.
String authHeader = servletRequest.getHeader("Authorization"); //returns null
if (authHeader != null
&& !authHeader.isEmpty()
&& authHeader.startsWith("Bearer")) {
String accessToken = authHeader.substring("Bearer".length()).trim();
if (accessToken != null) {
return new ResponseEntity(true, HttpStatus.OK);
} else {
return new ResponseEntity(false, HttpStatus.UNAUTHORIZED);
}
} else {
log.error("Invalid authorization header. ");
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
I tried also to get it throught the Principal, but I get an error:
java.lang.ClassCastException: class org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken cannot be cast to class org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder
.currentRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
KeycloakAuthenticationToken userPrincipal = (KeycloakAuthenticationToken) request.getUserPrincipal();
SimpleKeycloakAccount userPrincipalDetails = (SimpleKeycloakAccount) userPrincipal.getDetails();
return userPrincipalDetails
.getKeycloakSecurityContext()
.getToken();
The same error is displayed if I try:
KeycloakAuthenticationToken authenticatication = (KeycloakAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
I can login/logout, but I cannot get the accessToken...
#KeycloakConfiguration
#EnableWebSecurity
#Order(1)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
private final KeycloakLogoutHandler keycloakLogoutHandler;
public SecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) {
this.keycloakLogoutHandler = keycloakLogoutHandler;
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/somepage/*").permitAll()
.anyRequest().authenticated();
http.oauth2Login()
.and()
.logout()
.addLogoutHandler(keycloakLogoutHandler)
.logoutSuccessUrl("/");
}
}
Any ideas?
First, do not use Keycloak libs for Spring: it is (very) deprecated. Instead use:
spring-boot-starter-oauth2-resource-server if your app is a REST API. Instruction in the first of this series of tutorials.
spring-boot-starter-oauth2-client if your app serves UI (with thymeleaf or whatever)
The exact type of Authentication returned by SecurityContextHolder.getContext().getAuthentication() depends on your app being a client or a resource-server and it being configured with JWT decoder or token introspection, but all expose the Bearer access-token string. Just get it from there.
In the case your app is a resource-server (REST API), you might use one of the spring-boot starters I maintain for spring-boot-starter-oauth2-resource-server auto-configuration from properties. This quite simplifies the configuration compared to the first tutorial linked before:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<!-- replace "webmvc" with "weblux" if your app is reactive -->
<!-- replace "jwt" with "introspecting" to use token introspection instead of JWT decoding -->
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<!-- this version is to be used with spring-boot 3.0.0-RC1, use 5.x for spring-boot 2.6.x or before -->
<version>6.0.4</version>
</dependency>
#EnableMethodSecurity
public static class WebSecurityConfig { }
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,ressource_access.some-client.roles
com.c4-soft.springaddons.security.cors[0].path=/some-api
The Authentication for authorized requests will the be JwtAuthenticationToken:
#RestController
#RequestMapping("/demo")
public class DemoController {
#GetMapping("/access-token")
#PreAuthorize("isAuthenticated()")
public String getAccessToken(JwtAuthenticationToken auth) {
return auth.getToken().getTokenValue();
}
}
Disclaimer: be carefull with what you do with access-tokens and who you expose it to. If it leaks, it might be used for identity usurpation.

ReactiveSpringSecurity role mapping

Based on this example https://blog.jdriven.com/2019/11/spring-cloud-gateway-with-openid-connect-and-token-relay I'm trying to use Spring Gateway, the latest version of Spring Security and Keycloak. Behind the Gateway there is a static application that I want to limit access to. I managed to configure everything so that authentication works. However, I am unable to validate the role correctly. Spring Security does not read it from the token and always assigns ROLE_USER. Token contains proper role and other parameters like username or scopes are read correctly. How to map a roles using ReactiveSecurity. Below is my configuration.
#Configuration
public class SecurityConfig {
#Bean
public OidcClientInitiatedServerLogoutSuccessHandler logoutSuccessHandler(#Value("${postLogoutRedirectUrl}") URI postLogoutRedirectUrl, ReactiveClientRegistrationRepository clientRegistrationRepository) {
OidcClientInitiatedServerLogoutSuccessHandler logoutSuccessHandler = new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository);
logoutSuccessHandler.setPostLogoutRedirectUri(postLogoutRedirectUrl);
return logoutSuccessHandler;
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(#Value("${securedPaths}") String[] securedPaths, ServerHttpSecurity http,
ReactiveClientRegistrationRepository clientRegistrationRepository,
OidcClientInitiatedServerLogoutSuccessHandler logoutSuccessHandler) {
http.oauth2Login();
http.logout(logout -> logout.logoutSuccessHandler(new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
http.authorizeExchange()
.pathMatchers(securedPaths).hasRole("admin")
.anyExchange().permitAll();
http.logout().logoutSuccessHandler(logoutSuccessHandler);
http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler());
http.csrf().disable();
return http.build();
}
}

Storing JWT tokens on OAuth2 web client using Spring Security

I'm implementing an OAuth2 web application Client using Spring Boot 2.1.3 and Spring Security 5.1.3 that is obtaining JWT tokens from an authorization server through authorization code grant type and calls a protected resource server.
This is how the implementation looks up till now:
Security configuration and a restTemplate bean used to call the protected resource:
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
.and()
.oauth2Client()
.and().logout().logoutSuccessUrl("/");
}
#Bean
public RestTemplate restTemplate(OAuth2AuthorizedClientService clientService) {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
interceptors.add(new AuthorizationHeaderInterceptor(clientService));
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
The interceptor that adds the authorization header (from the framework's InMemoryOAuth2AuthorizedClientService) in the restTemplate:
public class AuthorizationHeaderInterceptor implements ClientHttpRequestInterceptor {
private OAuth2AuthorizedClientService clientService;
public AuthorizationHeaderInterceptor(OAuth2AuthorizedClientService clientService) {
this.clientService = clientService;
}
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String accessToken = null;
if (authentication != null && authentication.getClass().isAssignableFrom(OAuth2AuthenticationToken.class)) {
OAuth2AuthenticationToken auth = (OAuth2AuthenticationToken) authentication;
String clientRegistrationId = auth.getAuthorizedClientRegistrationId();
OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(clientRegistrationId, auth.getName());
accessToken = client.getAccessToken().getTokenValue();
request.getHeaders().add("Authorization", "Bearer " + accessToken);
}
return execution.execute(request, bytes);
}
}
And the controller that calls the protected resource server:
#Controller
#RequestMapping("/profile")
public class ProfileController {
#Autowired
private RestTemplate restTemplate;
#Value("${oauth.resourceServerBase}")
private String resourceServerBase;
#GetMapping
public String getProfile(Model model) {
Profile profile = restTemplate.getForEntity(resourceServerBase + "/api/profile/", Profile.class).getBody();
model.addAttribute("profile", profile);
return "profile";
}
}
The OAuth2 client configuration is directly in the application.yml:
spring:
security:
oauth2:
client:
registration:
auth-server:
client-id: webClient
client-secret: clientSecret
scope: read,write
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8081/client/login/oauth2/code/auth-server
provider:
auth-server:
authorization-uri: http://localhost:8080/auth-server/oauth/authorize
token-uri: http://localhost:8080/auth-server/oauth/token
user-info-uri: http://localhost:8082/resource-server/users/info
user-name-attribute: user_name
After doing some debugging I've observed that at the end of a successful authentication flow through OAuth2LoginAuthtenticationFilter the framework is storing the obtained access and refresh JWT tokens under OAuth2AuthorizedClient model in memory through the provided InMemoryOAuth2AuthorizedClientService.
I am trying to find out how to override this behaviour so that the tokens can remain available after a server restart. And also keep the user logged in based on this.
Should I just provide a custom OAuth2AuthorizedClientService implementation? How could I configure Spring Security to use it? And should this custom implementation store the tokens in a cookie?
Should I just provide a custom OAuth2AuthorizedClientService
implementation?
I think yes, for solving your use case
How could I configure Spring Security to use it?
From spring doc:
If you would like to provide a custom implementation of
AuthorizationRequestRepository that stores the attributes of
OAuth2AuthorizationRequest in a Cookie, you may configure it as shown
in the following example:
#EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Client()
.authorizationCodeGrant()
.authorizationRequestRepository(this.cookieAuthorizationRequestRepository())
...
}
private AuthorizationRequestRepository<OAuth2AuthorizationRequest> cookieAuthorizationRequestRepository() {
return new HttpCookieOAuth2AuthorizationRequestRepository();
}
}

Spring Boot Keycloak- Bearer-only - for backend service not working

I am trying to secure my rest api and frontend using Keycloak.
Frontend is based on Angular 4.
Backend is rest api built using Spring boot.
I have created two client in keycloak admin console,in same realm(testRealm).
front-end client's ACCESS-TYPE is "Public".
Backend client(backendService) ACCESS-TYPE is "Bearer-only".
Front-End is working fine and displays tabs based on roles.
When I try to hit/access backend service call, it gives me below error in browser console-
Failed to load http://localhost:9099/api/transaction
Response for preflight has invalid HTTP status code 401.
Below is the config I am using to secure rest api.
application.properties
server.contextPath=/test
server.port=9090
keycloak.realm: testRealm
keycloak.bearer-only: true
keycloak.auth-server-url: http://localhost:8080/auth
keycloak.ssl-required: external
keycloak.resource: backendService
keycloak.use-resource-role-mappings: true
keycloak.confidential-port: 0
keycloak.credentials.secret=dc04c236-d2b9-560e-b6b2-efa2064b2386
ApiSecurityConfig.java(Spring boot security config for keycloak adapter)
#Configuration
#EnableWebSecurity
#ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class ApiSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
/**
* Api request URI and their mapping roles and access are configured in this
* method.This is method from spring security web configuration Override this
* method to configure the HttpSecurity.
*
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeRequests()
.antMatchers("/api/*")
.hasRole("admin")
.anyRequest()
.permitAll();
}
}
I have verified the keclaok properties in application.properties file,by cross checking the installation JSON file from Keycloak server,which looks like below.
Installation JSON file taken from the Keycloak server
{
"realm": "testRealm",
"bearer-only": true,
"auth-server-url": "http://localhost:8080/auth",
"ssl-required": "external",
"resource": "backendService",
"use-resource-role-mappings": true,
"confidential-port": 0
}
I have checked keycloak.credentials.secret and its exactly the same what I am using,still it is not allowing access to backend and giving
Unauthorized, status=401
Not sure,If I am missing something,any help is much appreciated..

Fill sessionRegistry and session control

Good evening to everyone.
I'm trying to set up a simple MVC site without using xml configurations but only java code.
The site has public and private contents and is managed with spring security.
I don't want to have cookies so I wrote
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
#Override
protected Set<SessionTrackingMode> getSessionTrackingModes() {
return EnumSet.of(SessionTrackingMode.SSL);
}
}
When I configure the MVC I define the beans
#Configuration
#EnableWebMvc
#ComponentScan(basePackages = "com.csbab.spring")
public class SpringWebConfiguration extends WebMvcConfigurerAdapter {
#Bean(name = "sessionRegistry")
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
#Bean(name = "httpSessionEventPublisher")
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
In the security class I have
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
#Qualifier("customUserDetailsService")
UserDetailsService userDetailsService;
#Autowired
private SessionRegistry sessionRegistry;
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
...
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests = http.authorizeRequests();
authorizeRequests.and()
.sessionManagement()
.maximumSessions(1) // How many session the same user can have? This can be any number you pick
.maxSessionsPreventsLogin(true)
.expiredUrl("/login?expired")
.sessionRegistry(sessionRegistry);
...
When the user log in this code is called
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
If I try to access an
#Autowired
private SessionRegistry sessionRegistry;
is always empty (even if the the user is logged and the ssl session seems active) and the security check of number of session fails...
Any suggestions?
thanks.
going on (noone has replied in the while...)
I added
#Bean(name = "sessionAuthenticationStrategy")
public SessionAuthenticationStrategy sessionAuthenticationStrategy(){
List<SessionAuthenticationStrategy> delegateStrategies=new ArrayList<SessionAuthenticationStrategy>();
ConcurrentSessionControlAuthenticationStrategy concurrent = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
concurrent.setMaximumSessions(1);
concurrent.setExceptionIfMaximumExceeded(true);
delegateStrategies.add(concurrent);
delegateStrategies.add(new SessionFixationProtectionStrategy());
delegateStrategies.add(new RegisterSessionAuthenticationStrategy(sessionRegistry));
return new CompositeSessionAuthenticationStrategy(delegateStrategies);
}
I removed
authorizeRequests.and()
.sessionManagement()...
and I changed the login code to
SecurityContextHolder.getContext().setAuthentication(authentication);
sessionAuthenticationStrategy.onAuthentication(authentication, request, response);
return determineTargetUrl(authentication);
the sessionRegistry has started :) and i made first positive test opening explorer and chrome (the second login is refused)
but when I tried to login from a new window inside chrome (or another instance of chrome) I'm able to make two cuncurrent login (sharing the same session) and I have this message in the log:
Your servlet container did not change the session ID when a new session was created. You will not be adequately protected against session-fixation attacks
any idea?
nosce te ipsum...it is only code...
To do what I want in my scenario (that maybe is not usual) I have to do something different...
Reading the code I realized I'm not interested at all in session fixation, I want a single session per user, If another session is requested it's a security problem or an error...so I wrote...
#Bean(name = "sessionAuthenticationStrategy")
public SessionAuthenticationStrategy sessionAuthenticationStrategy(){
List<SessionAuthenticationStrategy> delegateStrategies=new ArrayList<SessionAuthenticationStrategy>();
ConcurrentSessionControlAuthenticationStrategy concurrent = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
concurrent.setMaximumSessions(1);
concurrent.setExceptionIfMaximumExceeded(true);
delegateStrategies.add(concurrent);
delegateStrategies.add(new SessionSingleUseProtectionStrategy(sessionRegistry));
delegateStrategies.add(new RegisterSessionAuthenticationStrategy(sessionRegistry));
return new CompositeSessionAuthenticationStrategy(delegateStrategies);
}
that's all I have to say about that quote :)
Where does the sessionRegistry instance come from when instantiating
new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry)
Make sure this is the same instance as your sessionRegistry() returns (beware that calling that method multiple times will create multiple instances!)
Also, I don't see any code where the sessionAuthenticationStrategy is bound to session management:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionAuthenticationStrategy(sessionAuthenticationStrategy());
}

Resources