-1

Keycloak spring-boot spring-security spring-security-oauth2

Schema: EurekaServer - Spring Cloud Gateway - Microservices

Hello,

I am doing a project using springBoot 2.7.11 and Keycloak 18.0.0 for security and role management, spring cloud version 2021.0.1, keycloak-adapter-bom 21.1.1. However, currently testing the services by going through the Gateway I am getting an Unauthorized 401 Error.

Requests: Keycloack - post - http://localhost/realms/EcommerceRealm/protocol/openid-connect/token

Service from gateway- get - http://localhost:8765/api/articles/prot/search/listArticles

Below I add the code:

application

eureka:
  instance:
    instance-id: ${server.port}-${spring.application.name}
    hostname: localhost


keycloak:
  auth-server-url: http://localhost:8080
  bearer-only: true
  confidential-port: 0
  cors: true
  principal-attribute: preferred_username
  realm: EcommerceRealm
  resource: api-services
  ssl-required: none
  use-resource-role-mappings: false
management:
  endpoints:
    web:
      exposure:
        include: '*'
server:
  port: 8765
spring:
  h2:
    console:
      enabled: true
  jpa:
    hibernate:
      ddl-auto: validate
  main:
    allow-bean-definition-overriding: true
    banner-mode: console
    web-application-type: reactive
  application:
    name: tsaTechAPIGetaway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
       
      routes:
      - id: articleModule
        order: 0
        predicates:
        - Path=/api/articles/**
        uri: lb://tsatechArticlesService
      - id: articleProt
        order: 1
        predicates:
        - Path=/api/articles/prot/**
        uri: lb://tsaTechArticlesServiceProt
      

# ==============================================================
# = Impostazioni Client
# ==============================================================
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
     defaultZone: ${EUREKA_URL:http://localhost:8761/eureka/}
    #time out di connessione in secondi al server Eureka (def 5 sec)
    eureka-server-connect-timeout-seconds: 8

#==============================================================
# = Logs Parameters
# ==============================================================
logging:
  level:
    '[org.springframework.cloud]': DEBUG
    '[org.springframework.security]': DEBUG

ApiGetawayApplication

@SpringBootApplication
@EnableEurekaClient
public class ApiGetawayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGetawayApplication.class, args);
    }

}

KeycloakSecurityConfig

@KeycloakConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    private final KeycloakClientRequestFactory keycloakClientRequestFactory;

    public KeycloakSecurityConfig(KeycloakClientRequestFactory keycloakClientRequestFactory) {
        this.keycloakClientRequestFactory = keycloakClientRequestFactory;

        // to use principal and authentication together with @async
        SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
    }

    /**
     * If you don't want to use the keycloak.json file, then uncomment this bean.
     */
    /**
     * Use properties in application.properties instead of keycloak.json
     */
    @Bean
    @Primary
    public KeycloakConfigResolver keycloakConfigResolver(KeycloakSpringBootProperties properties) {
        return new CustomKeycloakSpringBootConfigResolver(properties);
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public KeycloakRestTemplate keycloakRestTemplate() {
        return new KeycloakRestTemplate(keycloakClientRequestFactory);
    }
    
    public SimpleAuthorityMapper grantedAuthority() {
        SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();
        mapper.setConvertToUpperCase(true);
        return mapper;
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthority());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    /**
     * Use NullAuthenticatedSessionStrategy for bearer-only tokens. Otherwise, use
     * RegisterSessionAuthenticationStrategy.
     */
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new NullAuthenticatedSessionStrategy();
    }

    /**
     * Secure appropriate endpoints
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        super.configure(http);
        http.headers().frameOptions().sameOrigin();

        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http.cors() //
                .and() //
                .csrf().disable() //
//                .anonymous().disable() //
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //
                .and() //
                .authorizeRequests();

        expressionInterceptUrlRegistry = expressionInterceptUrlRegistry.antMatchers("/api/articles/prot/**").hasRole("ADMIN");
        expressionInterceptUrlRegistry = expressionInterceptUrlRegistry.antMatchers("/api/articles/**").hasRole("USER");

        expressionInterceptUrlRegistry.anyRequest().permitAll();
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter filter) {

        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(KeycloakPreAuthActionsFilter filter) {

        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(KeycloakAuthenticatedActionsFilter filter) {

        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    public FilterRegistrationBean keycloakSecurityContextRequestFilterBean(KeycloakSecurityContextRequestFilter filter) {

        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    @Override
    @ConditionalOnMissingBean(HttpSessionManager.class)
    protected HttpSessionManager httpSessionManager() {
        return new HttpSessionManager();
    }

    @Bean
    public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
        return new ServletListenerRegistrationBean<>(new HttpSessionEventPublisher());
    }

}

CustomKeycloakSpringBootConfigResolver

@Configuration
public class CustomKeycloakSpringBootConfigResolver extends KeycloakSpringBootConfigResolver {

    private final KeycloakDeployment keycloakDeployment;

    public CustomKeycloakSpringBootConfigResolver(KeycloakSpringBootProperties properties) {
        keycloakDeployment = KeycloakDeploymentBuilder.build(properties);
    }

    @Override
    public KeycloakDeployment resolve(HttpFacade.Request facade) {
        return keycloakDeployment;
    }

} 

The above code refers to the Gateway project.

Below I add the code for the service to be invoked through the gateway:

application

#==========================================================
#= Article Web Service - Base Version
#==========================================================
server:
  port: 5052
  
spring:
  application:
    name: tsaTechProtArticlesService
    
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8080/realms/EcommerceRealm

  management:
    endpoints:
     web:
      exposure:
        include: '*'
                  
#==========================================================
#= PARAMETER DBMS MySQL 
#==========================================================
  sql:
    init:
      mode: always
      platform: mysql
  datasource:
    password: ********
    url: jdbc:mysql://localhost:3306/DBName
    username: root
  session:
    jdbc:
      initialize-schema: always
  jpa:
    hibernate:
      ddl-auto: update
      generate-ddl: true
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5Dialect
    show-sql: true
    
# ==============================================================
# = Eureka Properties  
# ==============================================================
eureka:
   client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
     defaultZone: ${EUREKA_URL:http://localhost:8761/eureka/}
     
#==============================================================
# = Logs Parameters
# ==============================================================
logging:
  level:
    '[org.springframework.cloud]': DEBUG
    '[org.springframework.security]': DEBUG

SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity security) throws Exception {
        security.authorizeRequests(authorize -> authorize.anyRequest().authenticated())
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    }
}

ArticlesProtServiceApplication

@SpringBootApplication
@EnableEurekaClient
public class ArticlesProtServiceApplication {

    @GetMapping("/")
    String home() {
        return "Spring is here!";
    }

    public static void main(String[] args) {
        SpringApplication.run(ArticlesProtServiceApplication.class, args);
    }
}

Testing everything via Postman, then calling keycloak to get the token and subsequently invoking the service via OAUTH2 adding the token, I always get the 401 unauthorized error. I hope someone can help me find a solution. Thank you

LOG ERROR: 2023-05-31 16:06:52.716 DEBUG 74219 --- [nio-8765-exec-6] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/login', method=POST} 2023-05-31 16:06:52.716 DEBUG 74219 --- [nio-8765-exec-6] athPatternParserServerWebExchangeMatcher : Request 'GET /api/articles/search/listArticles' doesn't match 'POST /login' 2023-05-31 16:06:52.716 DEBUG 74219 --- [nio-8765-exec-6] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found 2023-05-31 16:06:52.717 DEBUG 74219 --- [nio-8765-exec-6] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/login', method=GET} 2023-05-31 16:06:52.717 DEBUG 74219 --- [nio-8765-exec-6] athPatternParserServerWebExchangeMatcher : Request 'GET /api/articles/search/listArticles' doesn't match 'GET /login' 2023-05-31 16:06:52.717 DEBUG 74219 --- [nio-8765-exec-6] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found 2023-05-31 16:06:52.717 DEBUG 74219 --- [nio-8765-exec-6] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/logout', method=GET} 2023-05-31 16:06:52.717 DEBUG 74219 --- [nio-8765-exec-6] athPatternParserServerWebExchangeMatcher : Request 'GET /api/articles/search/listArticles' doesn't match 'GET /logout' 2023-05-31 16:06:52.717 DEBUG 74219 --- [nio-8765-exec-6] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found 2023-05-31 16:06:52.719 DEBUG 74219 --- [ parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/logout', method=POST} 2023-05-31 16:06:52.720 DEBUG 74219 --- [ parallel-2] athPatternParserServerWebExchangeMatcher : Request 'GET /api/articles/search/listArticles' doesn't match 'POST /logout' 2023-05-31 16:06:52.720 DEBUG 74219 --- [ parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found 2023-05-31 16:06:52.720 DEBUG 74219 --- [ parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using EndpointRequestMatcher includes=[health], excludes=[], includeLinks=false 2023-05-31 16:06:52.720 DEBUG 74219 --- [ parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/actuator/health/', method=null} 2023-05-31 16:06:52.720 DEBUG 74219 --- [ parallel-2] athPatternParserServerWebExchangeMatcher : Request 'GET /api/articles/search/listArticles' doesn't match 'null /actuator/health/' 2023-05-31 16:06:52.720 DEBUG 74219 --- [ parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found 2023-05-31 16:06:52.721 DEBUG 74219 --- [ parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found 2023-05-31 16:06:52.721 DEBUG 74219 --- [ parallel-2] a.DelegatingReactiveAuthorizationManager : Checking authorization on '/api/articles/search/listArticles' using org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager@5392b123 2023-05-31 16:06:52.721 DEBUG 74219 --- [ parallel-2] ebSessionServerSecurityContextRepository : No SecurityContext found in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession@585916dd' 2023-05-31 16:06:52.721 DEBUG 74219 --- [ parallel-2] o.s.s.w.s.a.AuthorizationWebFilter : Authorization failed: Access Denied 2023-05-31 16:06:52.721 DEBUG 74219 --- [ parallel-2] ebSessionServerSecurityContextRepository : No SecurityContext found in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession@585916dd' 2023-05-31 16:06:52.721 DEBUG 74219 --- [ parallel-2] DelegatingServerAuthenticationEntryPoint : Trying to match using MediaTypeRequestMatcher [matchingMediaTypes=[text/html], useEquals=false, ignoredMediaTypes=[/]] 2023-05-31 16:06:52.722 DEBUG 74219 --- [ parallel-2] .s.u.m.MediaTypeServerWebExchangeMatcher : httpRequestMediaTypes=[/] 2023-05-31 16:06:52.722 DEBUG 74219 --- [ parallel-2] .s.u.m.MediaTypeServerWebExchangeMatcher : Processing / 2023-05-31 16:06:52.722 DEBUG 74219 --- [ parallel-2] .s.u.m.MediaTypeServerWebExchangeMatcher : Ignoring 2023-05-31 16:06:52.722 DEBUG 74219 --- [ parallel-2] .s.u.m.MediaTypeServerWebExchangeMatcher : Did not match any media types 2023-05-31 16:06:52.722 DEBUG 74219 --- [ parallel-2] DelegatingServerAuthenticationEntryPoint : Trying to match using OrServerWebExchangeMatcher{matchers=[org.springframework.security.config.web.server.ServerHttpSecurity$HttpBasicSpec$$Lambda$1003/0x00000008007dd040@4ad21437, AndServerWebExchangeMatcher{matchers=[NegatedServerWebExchangeMatcher{matcher=MediaTypeRequestMatcher [matchingMediaTypes=[text/html], useEquals=false, ignoredMediaTypes=[]]}, MediaTypeRequestMatcher [matchingMediaTypes=[application/atom+xml, application/x-www-form-urlencoded, application/json, application/octet-stream, application/xml, multipart/form-data, text/xml], useEquals=false, ignoredMediaTypes=[/]]]}]} 2023-05-31 16:06:52.722 DEBUG 74219 --- [ parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using org.springframework.security.config.web.server.ServerHttpSecurity$HttpBasicSpec$$Lambda$1003/0x00000008007dd040@4ad21437 2023-05-31 16:06:52.722 DEBUG 74219 --- [ parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using AndServerWebExchangeMatcher{matchers=[NegatedServerWebExchangeMatcher{matcher=MediaTypeRequestMatcher [matchingMediaTypes=[text/html], useEquals=false, ignoredMediaTypes=[]]}, MediaTypeRequestMatcher [matchingMediaTypes=[application/atom+xml, application/x-www-form-urlencoded, application/json, application/octet-stream, application/xml, multipart/form-data, text/xml], useEquals=false, ignoredMediaTypes=[/]]]} 2023-05-31 16:06:52.722 DEBUG 74219 --- [ parallel-2] .s.s.w.s.u.m.AndServerWebExchangeMatcher : Trying to match using NegatedServerWebExchangeMatcher{matcher=MediaTypeRequestMatcher [matchingMediaTypes=[text/html], useEquals=false, ignoredMediaTypes=[]]} 2023-05-31 16:06:52.722 DEBUG 74219 --- [ parallel-2] .s.u.m.MediaTypeServerWebExchangeMatcher : httpRequestMediaTypes=[/] 2023-05-31 16:06:52.722 DEBUG 74219 --- [ parallel-2] .s.u.m.MediaTypeServerWebExchangeMatcher : Processing / 2023-05-31 16:06:52.722 DEBUG 74219 --- [ parallel-2] .s.u.m.MediaTypeServerWebExchangeMatcher : text/html .isCompatibleWith / = true 2023-05-31 16:06:52.722 DEBUG 74219 --- [ parallel-2] .w.s.u.m.NegatedServerWebExchangeMatcher : matches = false 2023-05-31 16:06:52.723 DEBUG 74219 --- [ parallel-2] .s.s.w.s.u.m.AndServerWebExchangeMatcher : Did not match 2023-05-31 16:06:52.723 DEBUG 74219 --- [ parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found 2023-05-31 16:06:52.723 DEBUG 74219 --- [ parallel-2] DelegatingServerAuthenticationEntryPoint : No match found. Using default entry point org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint@344b11cb 2023-05-31 16:06:52.723 DEBUG 74219 --- [ parallel-2] DelegatingServerAuthenticationEntryPoint : Trying to match using org.springframework.security.config.web.server.ServerHttpSecurity$HttpBasicSpec$$Lambda$1003/0x00000008007dd040@4ad21437 2023-05-31 16:06:52.723 DEBUG 74219 --- [ parallel-2] DelegatingServerAuthenticationEntryPoint : No match found. Using default entry point org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint@47d7881c

Jeff Cook
  • 7,956
  • 36
  • 115
  • 186
Andreat
  • 1
  • 2

1 Answers1

0

Keycloack adapters for Spring are deprecated

It was announced more than a year ago (early 2022). Don't use it, even with Boot 2.7: it is using WebSecurityConfigurerAdapter which is already deprecated in Boot 2.7 and was removed in Boot 3 (Security 6). It is not maintained enough and will be an obstacle to your upcoming versions upgrades.

Two alternatives detailed in this other answer:

  • spring-boot-starter-oauth2-resource-server and spring-boot-starter-oauth2-client
  • my starters, which are just thin wrappers around the two "official" ones listed just above, pushing auto-configuration from application properties one step further

What you should change

First, as already written, remove your dependency on Keycloak adapters.

Then, I strongly recommand that you correctly configure the REST APIs behind the Gateway as OAuth2 resource servers. This will enable you to include accessed resources in security rules: it is very likely that you'll need more than Role Based Access Control (for instance to allow a customer to access only the orders he passed), and it is none of Gateway business to know about the structure of entities manipulated by each downstream service => this kind of access check should be performed (and unit-tested) in each service. In preceding sample, only the "orders" API should know how an order is related to users and decide which user can access which order, as well as what a given user can do with a given order (based on roles, on who passed this order, on the order status, etc.).

If that was already your intention, your configuration is missing at least some authorities mapping (Keycloak does not put roles in scope claim, gateway tries to configure a SimpleAuthorityMapper but does not read from realm_access.roles claim and downstream service does nothing at all ...).

With APIs configured as resource servers, you can completely remove security from the gateway if front-ends are OAuth2 clients:

  • development tools like Postman
  • mobile or Javascript based (Angular, React, Vue, ...) applications configured as OAuth2 "public" clients
  • web apps with content rendered on the server (Thymeleaf, JSF, etc.) and configured as OAuth2 "confidential" clients (with a client-secret)

If front-ends are not OAuth2 clients (and it is preferable for security that Javascript based and mobile apps are not configured as OAuth2 clients), then the gateway should be configured as Backend For Frontend: as an OAuth2 client (like it seems to be right now) and with the TokenRelay filter (which seems to be missing).

  • Front-ends will be secured with sessions
  • Gateway will
    • redirect to login requests within a session without an authorized client
    • store tokens (in session on the server)
    • thanks to TokenRelay filter, replace session cookie with the access token in session before forwarding a request to resource server.

More background and ready to use solution

See my tutorials

ch4mp
  • 6,622
  • 6
  • 29
  • 49