View Javadoc
1   /*
2    * Copyright 2026 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.bremersee.spring.boot.autoconfigure.keycloak;
18  
19  import java.util.ArrayList;
20  import java.util.List;
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  import org.bremersee.exception.feign.FeignClientExceptionErrorDecoder;
24  import org.bremersee.exception.webclient.DefaultWebClientErrorDecoder;
25  import org.bremersee.keycloak.api.webflux.AdminApi;
26  import org.bremersee.keycloak.api.webflux.AdminApiMock;
27  import org.bremersee.keycloak.api.webflux.KeycloakAdminClient;
28  import org.bremersee.keycloak.api.webflux.KeycloakClientFactory;
29  import org.springframework.beans.factory.ObjectProvider;
30  import org.springframework.boot.autoconfigure.AutoConfiguration;
31  import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
32  import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
33  import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
34  import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
35  import org.springframework.boot.context.event.ApplicationReadyEvent;
36  import org.springframework.boot.context.properties.EnableConfigurationProperties;
37  import org.springframework.context.annotation.Bean;
38  import org.springframework.context.event.EventListener;
39  import org.springframework.http.HttpStatusCode;
40  import org.springframework.util.Assert;
41  import org.springframework.util.ClassUtils;
42  import reactivefeign.client.statushandler.ReactiveStatusHandlers;
43  import reactivefeign.webclient.WebClientFeignCustomizer;
44  
45  /**
46   * The reactive keycloak client autoconfiguration.
47   *
48   * @author Christian Bremer
49   */
50  @ConditionalOnWebApplication(type = Type.REACTIVE)
51  @EnableConfigurationProperties(KeycloakProperties.class)
52  @ConditionalOnClass(name = {"org.bremersee.keycloak.api.webflux.KeycloakClientFactory"})
53  @AutoConfiguration
54  public class ReactiveKeycloakClientAutoConfiguration {
55  
56    private static final Log log = LogFactory.getLog(ReactiveKeycloakClientAutoConfiguration.class);
57  
58    private final KeycloakProperties properties;
59  
60    /**
61     * Instantiates a new reactive keycloak client autoconfiguration.
62     *
63     * @param properties the properties
64     */
65    public ReactiveKeycloakClientAutoConfiguration(KeycloakProperties properties) {
66      this.properties = properties;
67    }
68  
69    /**
70     * Init.
71     */
72    @EventListener(ApplicationReadyEvent.class)
73    public void init() {
74      log.info(String.format("""
75              
76              *********************************************************************************
77              * %s
78              *********************************************************************************
79              * properties = %s
80              *********************************************************************************""",
81          ClassUtils.getUserClass(getClass()).getSimpleName(), properties));
82      if (properties.getAdminClient().isEnabled()) {
83        Assert.hasText(properties.getBaseUri(), "Keycloak Base URI is required.");
84        Assert.hasText(properties.getAdminClient().getLoginRealm(),
85            "Keycloak Login Realm is required.");
86        Assert.hasText(properties.getAdminClient().getClientId(), "Keycloak Client ID is required.");
87        Assert.hasText(properties.getAdminClient().getUsername(),
88            "Keycloak Client Username is required.");
89        Assert.hasText(properties.getAdminClient().getPassword(),
90            "Keycloak Client Password is required.");
91      }
92    }
93  
94    /**
95     * Creates keycloak admin api.
96     *
97     * @param webClientCustomizers the web client customizers
98     * @param keycloakAdminApiCustomizers the keycloak admin api customizers
99     * @param errorDecoderProvider the error decoder provider
100    * @param feignErrorDecoderProvider the feign error decoder provider
101    * @return the admin api
102    */
103   @ConditionalOnMissingBean
104   @Bean
105   public AdminApi keycloakAdminApi(
106       List<WebClientFeignCustomizer> webClientCustomizers,
107       List<KeycloakAdminApiCustomizer> keycloakAdminApiCustomizers,
108       ObjectProvider<DefaultWebClientErrorDecoder> errorDecoderProvider,
109       ObjectProvider<FeignClientExceptionErrorDecoder> feignErrorDecoderProvider) {
110 
111     if (!properties.getAdminClient().isEnabled()) {
112       log.warn("Keycloak Admin Client is disabled. Creating admin api MOCK!");
113       return new AdminApiMock();
114     }
115 
116     List<WebClientFeignCustomizer> webClientExtendedCustomizers = new ArrayList<>();
117     errorDecoderProvider.ifAvailable(decoder -> webClientExtendedCustomizers
118         .add(builder -> builder.defaultStatusHandler(HttpStatusCode::isError, decoder)));
119     webClientExtendedCustomizers.addAll(webClientCustomizers);
120 
121     List<KeycloakAdminApiCustomizer> keycloakAdminApiExtendedCustomizers = new ArrayList<>();
122     feignErrorDecoderProvider.ifAvailable(decoder -> keycloakAdminApiExtendedCustomizers
123         .add(builder -> builder.statusHandler(ReactiveStatusHandlers.errorDecoder(decoder))));
124     keycloakAdminApiExtendedCustomizers.addAll(keycloakAdminApiCustomizers);
125 
126     KeycloakClientFactory factory = createKeycloakClientFactory();
127 
128     factory.setWebClientBuilderCustomizer(builder -> webClientExtendedCustomizers
129         .forEach(c -> c.accept(builder)));
130     factory.setAdminApiCustomizer(builder -> keycloakAdminApiExtendedCustomizers
131         .forEach(c -> c.customize(builder)));
132 
133     return factory.newClient();
134   }
135 
136   private KeycloakClientFactory createKeycloakClientFactory() {
137     return new KeycloakClientFactory(
138         properties.getBaseUri(),
139         properties.getAdminClient().getLoginRealm(),
140         properties.getAdminClient().getClientId(),
141         properties.getAdminClient().getUsername(),
142         properties.getAdminClient().getPassword());
143   }
144 
145   /**
146    * Creates keycloak admin client.
147    *
148    * @param adminApi the admin api
149    * @return the keycloak admin client
150    */
151   @ConditionalOnMissingBean
152   @Bean
153   public KeycloakAdminClient keycloakAdminClient(AdminApi adminApi) {
154     return new KeycloakAdminClient(adminApi);
155   }
156 
157 }