Saturday, September 17, 2022

SAML 2.0 With Spring Boot 2 - Security and Keycloak 18.0.1

SAML V2.0

SAML version 2.0 was approved as an OASIS Standard in March 2005. Approved Errata for SAML V2.0 was last produced by the SSTC on 1 May 2012. In addition to the normative errata document, the following non-normative "errata composite" documents have been provided that combine the prescribed corrections with the original specification text, illustrating the changes with margin change bars, struck-through original text, and highlighted new text.

What is SAML?

Security Assertion Markup Language. It is an XML-based open-standard for transferring identity data between two parties: an identity provider (IdP) and a service provider (SP).

Identity Provider 

Performs authentication and passes the user's identity and authorization level to the service provider. in this post we our Identity provider (IdP) realm was created on keycloak.

Service Provider 

Trusts the identity provider and authorizes the given user to access the requested resource. in this post we our service provider was created on Spring boot and spring Security.

For more details I suggest you watch the Demystifying SAML Using Spring Security.



     [Image]. SpringOne Platform


Downloading and installing Keycloak 


docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:18.0.1 start-dev

This will start Keycloak exposed on the local port 8080. It will also create an initial admin user with username admin and password admin.

Setting Up a keycloack server.


Open a browser  http://localhost:8080 or http://127.0.0.1:8080 result this.














We can now proceed to the Administrative Console.






















Creating a Realm

For default the console opens up Master realm, Let's navigate left corner Add realm button.













For this example I created realm IDP_REALM. we'll click the button create.











For check your SAML 2.0 Identity Provider Metadata Click on















Result this:











This URL we need later for configuration in Spring boot.

http://localhost:8080/realms/IDP_REALM/protocol/saml/descriptor


Creating a Client









Add a new client with the name spring-boot-keycloak with Client Protocol  saml. click button save.











Only add next values and then click button save.

Client ID: spring-boot-keycloak
Client Protocol: saml
Signature Algorithm: RSA_SHA256
SAML Signature Key Name: KEY_ID
Canonicalization Method: EXCLUSIVE
Name ID Format : username
Root URL: http://localhost:8081
Valid Redirect URIs: /saml/*
Base URL: /

**Fine Grain SAML Endpoint Configuration***
Assertion Consumer Service POST Binding URL: /saml/SSO
Logout Service POST Binding URL: /saml/logout





















Go to Tab Keys, Click on Export












Export SAML Key: Type next Key Password and  Store Password -> store123
This password we need later for configuration in Spring boot.














Download and save. Later replace file in Spring Boot resources/saml folder keystore.jks







Creating a Users



Add next user: user1 password user1

































                                 



Creating a Spring Boot Application


We're integration SAML 2.0, Spring Boot, Security and keycloack.

  • Configuration SAML 2.0, Spring Boot, Security and keycloack.
  • Spring Controllers  


Technology

  • Spring Boot 2.7.3
  • SAML 2.0
  • Keycloak 18.0.1
  • Thymeleaf 
  • Java 17 (Zulu)
  • Docker
  • Maven
  • IntelliJ IDEA 

Project Structure






















Configuration Spring Boot Keycloak project 


pom.xml


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.henry</groupId>
<artifactId>saml2-sso-springboot-keycloak</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>saml2-sso-springboot-keycloak</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.extensions</groupId>
<artifactId>spring-security-saml2-core</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
</dependencies>

<repositories>
<repository>
<id>Shibboleth</id>
<name>Shibboleth</name>
<url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
</plugin>
</plugins>
</build>

</project>



application.yml:


server:
port: 8081
servlet:
context-path: /
spring:
main:
allow-circular-references: true

keycloak:
client: spring-boot-keycloak

saml:
keystore:
path: classpath:/saml/keystore.jks
password: store123
alias: spring-boot-keycloak
url:
descriptor: http://localhost:8080/realms/IDP_REALM/protocol/saml/descriptor


Stereotypes


package com.henry.stereotypes;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUser {}

Authentication



package com.henry.authentication;

import java.security.Principal;

import com.henry.stereotypes.CurrentUser;
import org.springframework.core.MethodParameter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
public class CurrentUserHandlerMethodArgumentResolver implements
HandlerMethodArgumentResolver {

public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(CurrentUser.class) != null
&& methodParameter.getParameterType().equals(User.class);
}

public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
if (this.supportsParameter(methodParameter)) {
Principal principal = (Principal) webRequest.getUserPrincipal();
return (User) ((Authentication) principal).getPrincipal();
} else {
return WebArgumentResolver.UNRESOLVED;
}
}
}


package com.henry.authentication;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class MvcConfig implements WebMvcConfigurer {

@Autowired
CurrentUserHandlerMethodArgumentResolver currentUserHandlerMethodArgumentResolver;

@Override
public void addViewControllers(ViewControllerRegistry registry) {

}

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!registry.hasMappingForPattern("/static/**")) {
registry.addResourceHandler("/static/**")
.addResourceLocations("/static/");
}
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers){
argumentResolvers.add(currentUserHandlerMethodArgumentResolver);
}

}


package com.henry.authentication;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.security.saml.userdetails.SAMLUserDetailsService;
import org.springframework.stereotype.Service;

@Service
public class SAMLUserDetailsServiceImpl implements SAMLUserDetailsService {

// Logger
private static final Logger LOG = LoggerFactory.getLogger(SAMLUserDetailsServiceImpl.class);

public Object loadUserBySAML(SAMLCredential credential)
throws UsernameNotFoundException {

// The method is supposed to identify local account of user referenced by
// data in the SAML assertion and return UserDetails object describing the user.

String userID = credential.getNameID().getValue();

LOG.info(userID + " is logged in");
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
authorities.add(authority);

// In a real scenario, this implementation has to locate user in a arbitrary
// dataStore based on information present in the SAMLCredential and
// returns such a date in a form of application specific UserDetails object.
return new User(userID, "<abc123>", true, true, true, true, authorities);
}

}



Config



package com.henry.config;

import com.henry.authentication.SAMLUserDetailsServiceImpl;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.velocity.app.VelocityEngine;
import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.xml.parse.ParserPool;
import org.opensaml.xml.parse.StaticBasicParserPool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.security.saml.*;
import org.springframework.security.saml.context.SAMLContextProviderImpl;
import org.springframework.security.saml.key.JKSKeyManager;
import org.springframework.security.saml.key.KeyManager;
import org.springframework.security.saml.log.SAMLDefaultLogger;
import org.springframework.security.saml.metadata.CachingMetadataManager;
import org.springframework.security.saml.metadata.ExtendedMetadata;
import org.springframework.security.saml.metadata.ExtendedMetadataDelegate;
import org.springframework.security.saml.processor.*;
import org.springframework.security.saml.util.VelocityFactory;
import org.springframework.security.saml.websso.*;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;

import java.util.*;

@Configuration
public class SamlSecurityConfig {

@Value("${saml.keystore.path}")
private String samlKeystorePath;

@Value("${saml.keystore.password}")
private String samlKeystorePassword;

@Value("${saml.keystore.alias}")
private String samlKeystoreAlias;

@Value("${saml.url.descriptor}")
private String samlUrlDescriptor;

@Autowired
private SAMLUserDetailsServiceImpl samlUserDetailsServiceImpl;

private MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager;

// Initialization of OpenSAML library
@Bean
public static SAMLBootstrap sAMLBootstrap() {
return new SAMLBootstrap();
}

// Initialization of the velocity engine
@Bean
public VelocityEngine velocityEngine() {
return VelocityFactory.getEngine();
}

@Bean(initMethod = "initialize")
public StaticBasicParserPool parserPool() {
return new StaticBasicParserPool();
}

@Bean
public HttpClient httpClient() {
this.multiThreadedHttpConnectionManager = new MultiThreadedHttpConnectionManager();
return new HttpClient(this.multiThreadedHttpConnectionManager);
}

@Bean
public SAMLAuthenticationProvider samlAuthenticationProvider() {
SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
samlAuthenticationProvider.setUserDetails(samlUserDetailsServiceImpl);
samlAuthenticationProvider.setForcePrincipalAsString(false);
return samlAuthenticationProvider;
}

// Provider of default SAML Context
@Bean
public SAMLContextProviderImpl contextProvider() {
return new SAMLContextProviderImpl();
}

// Logger for SAML messages and events
@Bean
public SAMLDefaultLogger samlLogger() {
return new SAMLDefaultLogger();
}

// SAML 2.0 WebSSO Assertion Consumer
@Bean
public WebSSOProfileConsumer webSSOprofileConsumer() {
return new WebSSOProfileConsumerImpl();
}

// SAML 2.0 Holder-of-Key Web SSO profile
@Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
return new WebSSOProfileConsumerHoKImpl();
}

// SAML 2.0 Web SSO profile
@Bean
public WebSSOProfile webSSOprofile() {
return new WebSSOProfileImpl();
}

// SAML 2.0 Holder-of-Key WebSSO Assertion Consumer
@Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() {
return new WebSSOProfileConsumerHoKImpl();
}

// SAML 2.0 ECP profile
@Bean
public WebSSOProfileECPImpl ecpprofile() {
return new WebSSOProfileECPImpl();
}

@Bean
public SingleLogoutProfile logoutprofile() {
return new SingleLogoutProfileImpl();
}

// Central storage of cryptographic keys
@Bean
public KeyManager keyManager() {
DefaultResourceLoader loader = new DefaultResourceLoader();
Resource storeFile = loader
.getResource(samlKeystorePath);
String storePass = samlKeystorePassword;
Map<String, String> passwords = new HashMap<String, String>();
passwords.put(samlKeystoreAlias, samlKeystorePassword);
String defaultKey = samlKeystoreAlias;
return new JKSKeyManager(storeFile, storePass, passwords, defaultKey);
}

@Bean
public WebSSOProfileOptions defaultWebSSOProfileOptions() {
WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
webSSOProfileOptions.setIncludeScoping(false);
return webSSOProfileOptions;
}

@Bean
public SAMLEntryPoint samlEntryPoint() {
SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions());
return samlEntryPoint;
}

@Bean
public ExtendedMetadata extendedMetadata() {
ExtendedMetadata extendedMetadata = new ExtendedMetadata();
extendedMetadata.setIdpDiscoveryEnabled(false);
extendedMetadata.setSignMetadata(false);
return extendedMetadata;
}

/**
* Access metadata by URL.
* */

@Bean
@Qualifier("idp-keycloak")
public ExtendedMetadataDelegate keycloakExtendedMetadataProvider()
throws MetadataProviderException {
String idpSSOKeycloakMetadataURL = samlUrlDescriptor;

Timer timer = new Timer("saml-metadata");

HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(
timer, httpClient(), idpSSOKeycloakMetadataURL);
httpMetadataProvider.setParserPool(parserPool());
ExtendedMetadataDelegate extendedMetadataDelegate =
new ExtendedMetadataDelegate(httpMetadataProvider, extendedMetadata());
extendedMetadataDelegate.setMetadataTrustCheck(true);
extendedMetadataDelegate.setMetadataRequireSignature(false);
return extendedMetadataDelegate;
}

/**
* Access metadata XML FILE.
* */
/*
@Bean
@Qualifier("idp-keycloak")
public ExtendedMetadataDelegate keycloakExtendedMetadataProvider() throws MetadataProviderException {

org.opensaml.util.resource.Resource resource = null;

try {
resource = new ClasspathResource("/saml/descriptor.xml");
} catch (ResourceException e) {
e.printStackTrace();
}

Timer timer = new Timer("saml-metadata");
ResourceBackedMetadataProvider provider = new ResourceBackedMetadataProvider(timer,resource);
provider.setParserPool(parserPool());
return new ExtendedMetadataDelegate(provider, extendedMetadata());
}
*/
@Bean
@Qualifier("metadata")
public CachingMetadataManager metadata() throws MetadataProviderException {
List<MetadataProvider> providers = new ArrayList<MetadataProvider>();
providers.add(keycloakExtendedMetadataProvider());
return new CachingMetadataManager(providers);
}

@Bean
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler =
new SavedRequestAwareAuthenticationSuccessHandler();
successRedirectHandler.setDefaultTargetUrl("/landing");
return successRedirectHandler;
}

@Bean
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
SimpleUrlAuthenticationFailureHandler failureHandler =
new SimpleUrlAuthenticationFailureHandler();
failureHandler.setUseForward(true);
failureHandler.setDefaultFailureUrl("/error");
return failureHandler;
}

@Bean
public SimpleUrlLogoutSuccessHandler successLogoutHandler() {
SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
successLogoutHandler.setDefaultTargetUrl("/");
return successLogoutHandler;
}

@Bean
public SecurityContextLogoutHandler logoutHandler() {
SecurityContextLogoutHandler logoutHandler =
new SecurityContextLogoutHandler();
logoutHandler.setInvalidateHttpSession(true);
logoutHandler.setClearAuthentication(true);
return logoutHandler;
}

@Bean
public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
return new SAMLLogoutProcessingFilter(successLogoutHandler(),
logoutHandler());
}

@Bean
public SAMLLogoutFilter samlLogoutFilter() {
return new SAMLLogoutFilter(successLogoutHandler(),
new LogoutHandler[]{logoutHandler()},
new LogoutHandler[]{logoutHandler()});
}

@Bean
public HTTPPostBinding httpPostBinding() {
return new HTTPPostBinding(parserPool(), velocityEngine());
}

@Bean
public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() {
return new HTTPRedirectDeflateBinding(parserPool());
}

// Bindings
private ArtifactResolutionProfile artifactResolutionProfile() {
final ArtifactResolutionProfileImpl artifactResolutionProfile =
new ArtifactResolutionProfileImpl(httpClient());
artifactResolutionProfile.setProcessor(new SAMLProcessorImpl(soapBinding()));
return artifactResolutionProfile;
}

@Bean
public HTTPArtifactBinding artifactBinding(ParserPool parserPool, VelocityEngine velocityEngine) {
return new HTTPArtifactBinding(parserPool, velocityEngine, artifactResolutionProfile());
}

@Bean
public HTTPSOAP11Binding soapBinding() {
return new HTTPSOAP11Binding(parserPool());
}

@Bean
public HTTPSOAP11Binding httpSOAP11Binding() {
return new HTTPSOAP11Binding(parserPool());
}

@Bean
public HTTPPAOS11Binding httpPAOS11Binding() {
return new HTTPPAOS11Binding(parserPool());
}

// Processor
@Bean
public SAMLProcessorImpl processor() {
Collection<SAMLBinding> bindings = new ArrayList<SAMLBinding>();
bindings.add(httpRedirectDeflateBinding());
bindings.add(httpPostBinding());
bindings.add(artifactBinding(parserPool(), velocityEngine()));
bindings.add(httpSOAP11Binding());
bindings.add(httpPAOS11Binding());
return new SAMLProcessorImpl(bindings);
}

}


package com.henry.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.saml.*;
import org.springframework.security.saml.key.KeyManager;
import org.springframework.security.saml.metadata.ExtendedMetadata;
import org.springframework.security.saml.metadata.MetadataDisplayFilter;
import org.springframework.security.saml.metadata.MetadataGenerator;
import org.springframework.security.saml.metadata.MetadataGeneratorFilter;
import org.springframework.security.saml.parser.ParserPoolHolder;
import org.springframework.security.saml.trust.httpclient.TLSProtocolConfigurer;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.util.ArrayList;
import java.util.List;



@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

@Value("${keycloak.client}")
private String keycloakClient;
@Autowired
private SAMLEntryPoint samlEntryPoint;
@Autowired
private ExtendedMetadata extendedMetadata;
@Autowired
private SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler;
@Autowired
private SAMLLogoutProcessingFilter samlLogoutProcessingFilter;
@Autowired
private SimpleUrlAuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private SAMLLogoutFilter samlLogoutFilter;
@Autowired
private SAMLAuthenticationProvider samlAuthenticationProvider;

@Autowired
private KeyManager keyManager;

@Bean(name = "parserPoolHolder")
public ParserPoolHolder parserPoolHolder() {
return new ParserPoolHolder();
}


// Setup TLS Socket Factory
@Bean
public TLSProtocolConfigurer tlsProtocolConfigurer() {
return new TLSProtocolConfigurer();
}

@Bean
public SAMLDiscovery samlIDPDiscovery() {
SAMLDiscovery idpDiscovery = new SAMLDiscovery();
return idpDiscovery;
}

@Bean
public MetadataGenerator metadataGenerator() {
MetadataGenerator metadataGenerator = new MetadataGenerator();
metadataGenerator.setEntityId(keycloakClient);
metadataGenerator.setExtendedMetadata(extendedMetadata);
metadataGenerator.setIncludeDiscoveryExtension(false);
metadataGenerator.setKeyManager(keyManager);
return metadataGenerator;
}

@Bean
public MetadataDisplayFilter metadataDisplayFilter() {
return new MetadataDisplayFilter();
}

@Bean
public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter(HttpSecurity http) throws Exception {
SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter = new SAMLWebSSOHoKProcessingFilter();
samlWebSSOHoKProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler);
samlWebSSOHoKProcessingFilter.setAuthenticationManager(authenticationManagerBean(http));
samlWebSSOHoKProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
return samlWebSSOHoKProcessingFilter;
}

@Bean
public SAMLProcessingFilter samlWebSSOProcessingFilter(HttpSecurity http) throws Exception {
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManagerBean(http));
samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler);
samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
return samlWebSSOProcessingFilter;
}

@Bean
public MetadataGeneratorFilter metadataGeneratorFilter() {
return new MetadataGeneratorFilter(metadataGenerator());
}

@Bean
public FilterChainProxy samlFilter(HttpSecurity http) throws Exception {
List<SecurityFilterChain> chains = new ArrayList<SecurityFilterChain>();
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
samlEntryPoint));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
samlLogoutFilter));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
metadataDisplayFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
samlWebSSOProcessingFilter(http)));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSOHoK/**"),
samlWebSSOHoKProcessingFilter(http)));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
samlLogoutProcessingFilter));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"),
samlIDPDiscovery()));
return new FilterChainProxy(chains);
}

@Bean
public AuthenticationManager authenticationManagerBean(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.authenticationProvider(samlAuthenticationProvider);
return authenticationManagerBuilder.build();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

http
.httpBasic()
.authenticationEntryPoint(samlEntryPoint);
http
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
.addFilterAfter(samlFilter(http), BasicAuthenticationFilter.class)
.addFilterBefore(samlFilter(http), CsrfFilter.class);
http
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/saml/**").permitAll()
//.antMatchers("/css/**").permitAll()
//.antMatchers("/img/**").permitAll()
//.antMatchers("/js/**").permitAll()
.anyRequest().authenticated();
http
.logout()
.disable(); // The logout procedure is already handled by SAML filters.
return http.build();
}

}


Controller


package com.henry.controller;

import com.henry.stereotypes.CurrentUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.saml.metadata.MetadataManager;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;

@Controller
public class IndexController {

// Logger
private static final Logger LOG = LoggerFactory
.getLogger(IndexController.class);

@Autowired
private MetadataManager metadata;

@RequestMapping("/")
public String index(HttpServletRequest request, Model model) {
return "index";
}

@RequestMapping("/landing")
public String landing(@CurrentUser User user, Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null)
LOG.debug("Current authentication instance from security context is null");
else
LOG.debug("Current authentication instance from security context: "
+ this.getClass().getSimpleName());
model.addAttribute("username", user.getUsername());
return "landing";
}

}

Replace keystore.jks with your keystore.

Note: There are two ways to access ExtendedMetadataDelegate: By Url or XML File in both cases you need to access by IDP_REALM was created on keycloak. 

http://localhost:8080/realms/IDP_REALM/protocol/saml/descriptor

In this post we used a URL, however if you want to XML File then download from URL and save with the name descriptor.xml and it will replace the Spring boot resources. and uncomment the method in the class SamlSecurityConfig -> ExtendedMetadataDelegate, however just one method should be enabled.









Run & Test


Run Spring Boot application with command: mvn spring-boot:run. by console or IntelliJ etc.









Open a browser  http://localhost:8081 result this.











Click on Login button, result this.

Set username user1 and password user1 , keycloak suggest to you to change the password,
In this example I set the same password.






























And resul this.


















Global logout back the login.



Source Code


Here on Github.


References:

https://stackoverflow.com/questions/71605941/spring-security-global-authenticationmanager-without-the-websecurityconfigurera
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
https://www.baeldung.com/spring-deprecated-websecurityconfigureradapter
https://github.com/eugenp/tutorials/tree/master/spring-security-modules/spring-security-saml
https://www.baeldung.com/spring-security-saml
https://blog.codecentric.de/en/2019/03/secure-spring-boot-app-saml-keycloak/
https://github.com/thomasdarimont/spring-boot-security-saml-sample
https://www.youtube.com/watch?v=SvppXbpv-5k
https://www.youtube.com/watch?v=PruyokKaJWw&t=586s
https://codetinkering.com/spring-security-saml2-service-provider-example/
https://codetinkering.com/saml2-spring-security-5-2-tutorial/
https://auth0.com/blog/how-saml-authentication-works/
https://github.com/fhanik/springone2018
https://www.youtube.com/watch?v=JBAKnJ9Obvw&t=21s


















Thursday, August 18, 2022

Multiple Data Sources in Spring Boot 3 with Java 21


In this blog post, we'll explore the configuration and setup for a Spring Boot 3 application with Java 21 that uses multiple data sources. This setup is useful when dealing with databases of different types, such as MySQL, Oracle, and PostgreSQL. 






Technology


  • Spring Boot 3.2.0
  • Java 21 (Zulu)
  • Docker Compose
  • Maven 
  • IntelliJ IDEA  2023.3.1 Community 
  • Postman


Project Structure


multiple-data-sources-jpa
|-- src
|   |-- main
|       |-- java
|           |-- com
|               |-- henry
|                   |-- configuration
|                   |-- model
|                   |-- repository
|                   |-- service
|                   |-- controller
|-- src
|   |-- test
|       |-- java
|           |-- com
|               |-- henry
|                   |-- ...
|-- pom.xml


Application.yaml Configuration



The application file contains configuration deatils for each data source, including URLs, usernames, passowrds, and driver class names.


server:
  port: 9000
  servlet:
    context-path: /

spring:
  datasource:
    mysql:
      url: jdbc:mysql://localhost:3306/test_db?allowPublicKeyRetrieval=true
      username: test
      password: test_pass
      driverClassName: com.mysql.cj.jdbc.Driver
      ddlAuto: create-drop
    postgres:
      url: jdbc:postgresql:postgre_test
      username: postgre_test
      password: postgre_test
      driverClassName: org.postgresql.Driver
      ddlAuto: create-drop
    oracle:
      url: jdbc:oracle:thin:@//localhost:1521/xe
      username: system
      password: oracle
      driverClassName: oracle.jdbc.OracleDriver
      ddlAuto: update


Configuration Classes


We've created configuration classes for each data source - MysqlConfig, PostgreConfig, and OracleConfig:


package com.henry.configuration;

import com.henry.record.OracleRecord;
import com.henry.record.PostgreRecord;
import com.henry.record.MysqlRecord;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Data;

@Component
@ConfigurationProperties("spring.datasource")
@Data
public class DataSourceProperties {
   private MysqlRecord mysql;
   private PostgreRecord postgres;
   private OracleRecord oracle;

}


package com.henry.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.naming.NamingException;
import javax.sql.DataSource;
import java.util.Properties;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.henry.repository.user",
        entityManagerFactoryRef = "userEntityManager",
        transactionManagerRef = "userTransactionManager"
)
public class MysqlConfig {

    @Autowired
    private DataSourceProperties dsProperties;

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean userEntityManager()
            throws NamingException {
        LocalContainerEntityManagerFactoryBean em
                = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(userDataSource());
        em.setPackagesToScan("com.henry.model.user");

        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(userHibernateProperties());

        return em;
    }

    @Bean
    @Primary
    public DataSource userDataSource() throws IllegalArgumentException, NamingException {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(dsProperties.getMysql().driverClassName());
        dataSource.setUrl(dsProperties.getMysql().url());
        dataSource.setUsername(dsProperties.getMysql().username());
        dataSource.setPassword(dsProperties.getMysql().password());

        return dataSource;
    }

    private Properties userHibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.hbm2ddl.auto",dsProperties.getMysql().ddlAuto());

        return properties;
    }

    @Primary
    @Bean
    public PlatformTransactionManager userTransactionManager() throws NamingException {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(userEntityManager().getObject());
        return transactionManager;
    }
}

package com.henry.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.naming.NamingException;
import javax.sql.DataSource;
import java.util.Properties;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.henry.repository.brand",
        entityManagerFactoryRef = "brandEntityManager",
        transactionManagerRef = "brandTransactionManager"
)
public class OracleConfig {

    @Autowired
    private DataSourceProperties dsProperties;

    @Bean
    public LocalContainerEntityManagerFactoryBean brandEntityManager()
            throws NamingException {
        LocalContainerEntityManagerFactoryBean em
                = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(brandDataSource());
        em.setPackagesToScan("com.henry.model.brand");

        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(brandHibernateProperties());

        return em;
    }

    @Bean
    public DataSource brandDataSource() throws IllegalArgumentException, NamingException {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(dsProperties.getOracle().driverClassName());
        dataSource.setUrl(dsProperties.getOracle().url());
        dataSource.setUsername(dsProperties.getOracle().username());
        dataSource.setPassword(dsProperties.getOracle().password());

        return dataSource;
    }

    private Properties brandHibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.hbm2ddl.auto", dsProperties.getOracle().ddlAuto());

        return properties;
    }


    @Bean
    public PlatformTransactionManager brandTransactionManager() throws NamingException {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(brandEntityManager().getObject());
        return transactionManager;
    }
}

package com.henry.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.naming.NamingException;
import javax.sql.DataSource;
import java.util.Properties;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.henry.repository.company",
        entityManagerFactoryRef = "companyEntityManager",
        transactionManagerRef = "companyTransactionManager"
)
public class PostgreConfig {

    @Autowired
    private DataSourceProperties dsProperties;

    @Bean
    public LocalContainerEntityManagerFactoryBean companyEntityManager()
            throws NamingException {
        LocalContainerEntityManagerFactoryBean em
                = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(companyDataSource());
        em.setPackagesToScan("com.henry.model.company");

        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(companyHibernateProperties());

        return em;
    }

    @Bean
    public DataSource companyDataSource() throws IllegalArgumentException, NamingException {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(dsProperties.getPostgres().driverClassName());
        dataSource.setUrl(dsProperties.getPostgres().url());
        dataSource.setUsername(dsProperties.getPostgres().username());
        dataSource.setPassword(dsProperties.getPostgres().password());

        return dataSource;
    }

    private Properties companyHibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.hbm2ddl.auto", dsProperties.getPostgres().ddlAuto());

        return properties;
    }

    @Bean
    public PlatformTransactionManager companyTransactionManager() throws NamingException {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(companyEntityManager().getObject());
        return transactionManager;
    }
}


Record Classes


To encapsulate the configuration properties for each data source, we've defined record classes - MysqlRecord, PostgreRecord, and OracleRecord:


package com.henry.record;

public record MysqlRecord(String url, String username, String password, String driverClassName,String ddlAuto) {
}

package com.henry.record;

public record OracleRecord(String url, String username, String password, String driverClassName,String ddlAuto) {
}


package com.henry.record;

public record PostgreRecord(String url, String username, String password, String driverClassName,String ddlAuto) {
}

Repositories


Repository interfaces extend the CrudRepository for each entity (Brand, Company, User):


package com.henry.repository.brand;

import com.henry.model.brand.Brand;
import org.springframework.data.repository.CrudRepository;

public interface BrandRepository extends CrudRepository<Brand,Long> {
}

package com.henry.repository.company;

import com.henry.model.company.Company;
import org.springframework.data.repository.CrudRepository;

public interface CompanyRepository extends CrudRepository<Company,Long> {
}

package com.henry.repository.user;

import com.henry.model.user.User;
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User,Long> {
}

Services


Service classes (BrandServiceImpl, CompanyServiceImpl, UserServiceImpl) implement a common interface (DefaultService) for generic CRUD operations:

package com.henry.service;

public sealed interface DefaultService<T, G> permits UserServiceImpl, CompanyServiceImpl, BrandServiceImpl {

    T save(T obj);
    Iterable<T>  findAll();
    T findById(G id);
}

package com.henry.service;

import com.henry.model.brand.Brand;
import com.henry.repository.brand.BrandRepository;
import org.springframework.stereotype.Service;

@Service
public final class BrandServiceImpl implements DefaultService<Brand, Long> {

    private final BrandRepository brandRepository;

    public BrandServiceImpl(BrandRepository brandRepository) {
        this.brandRepository = brandRepository;
    }


    @Override
    public Brand save(Brand obj) {
        return brandRepository.save(obj);
    }

    @Override
    public Iterable<Brand> findAll() {
        return brandRepository.findAll();
    }

    @Override
    public Brand findById(Long id) {
        return brandRepository.findById(id).get();
    }
}

package com.henry.service;

import com.henry.model.company.Company;
import com.henry.repository.company.CompanyRepository;
import org.springframework.stereotype.Service;

@Service
public final class CompanyServiceImpl implements DefaultService<Company, Long> {

    private final CompanyRepository companyRepository;

    public CompanyServiceImpl(CompanyRepository companyRepository) {
        this.companyRepository = companyRepository;
    }

    @Override
    public Company save(Company obj) {
        return companyRepository.save(obj);
    }

    @Override
    public Iterable<Company> findAll() {
        return companyRepository.findAll();
    }

    @Override
    public Company findById(Long id) {
        return companyRepository.findById(id).get();
    }
}

package com.henry.service;

import com.henry.model.user.User;
import com.henry.repository.user.UserRepository;
import org.springframework.stereotype.Service;

@Service
public final class UserServiceImpl implements DefaultService<User,Long> {

    private final UserRepository userRepository;

    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public User save(User obj) {
        return userRepository.save(obj);
    }

    @Override
    public Iterable<User> findAll() {
        return userRepository.findAll();
    }

    @Override
    public User findById(Long id) {
        return userRepository.findById(id).get();
    }
}

REST Controllers


REST controllers (BrandController, CompanyController, UserController) handle HTTP requests for creating entities:


package com.henry.controller;

import com.henry.model.brand.Brand;
import com.henry.service.DefaultService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v3")
public class BrandController {

    private final DefaultService<Brand, Long> defaultService;

    public BrandController(DefaultService<Brand, Long> defaultService) {
        this.defaultService = defaultService;
    }

    @PostMapping("/brands")
    public Brand createEmployee(@RequestBody Brand brand) {
        return defaultService.save(brand);
    }

}

package com.henry.controller;

import com.henry.model.company.Company;
import com.henry.service.DefaultService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v2")
public class CompanyController {

    private final DefaultService<Company,Long> defaultService;

    public CompanyController(DefaultService<Company, Long> defaultService) {
        this.defaultService = defaultService;
    }

    @PostMapping("/companies")
    public Company createEmployee(@RequestBody Company company) {
        return defaultService.save(company);
    }
}

package com.henry.controller;

import com.henry.model.user.User;
import com.henry.service.DefaultService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1")
public class UserController {

    private final DefaultService<User,Long> defaultService;

    public UserController(DefaultService<User, Long> defaultService) {
        this.defaultService = defaultService;
    }

    @PostMapping("/users")
    public User createEmployee(@RequestBody User user) {
        return defaultService.save(user);
    }
}


Test Classes


JUnit test classes (BrandServiceTest, CompanyServiceTest, UserServiceTest) use Mockito for mocking dependencies:


package com.henry;

import com.google.common.collect.Iterables;
import com.henry.model.brand.Brand;
import com.henry.repository.brand.BrandRepository;
import com.henry.service.BrandServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class BrandServiceTest {

    @Mock
    private BrandRepository brandRepository;

    @InjectMocks
    private BrandServiceImpl brandService;

    @Test
    void testSaveBrand() {
        Brand brand = Brand.builder()
                .id(1L)
                .name("Test Brand")
                .build();

        when(brandRepository.save(any())).thenReturn(brand);

        Brand saved = brandService.save(brand);

        assertEquals(brand, saved);
        verify(brandRepository).save(any());
    }

    @Test
    void testFindAllBrands() {
        Brand brand1 = Brand.builder()
                .id(1L)
                .name("Test Brand")
                .build();
        Brand brand2 = Brand.builder()
                .id(2L)
                .name("Demo Brand")
                .build();

        when(brandRepository.findAll()).thenReturn(Arrays.asList(brand1, brand2));

        Iterable<Brand> result = brandService.findAll();

        assertEquals(2, Iterables.size(result));
        verify(brandRepository).findAll();
    }

    @Test
    void testFindBrandById() {
        Brand brand = Brand.builder()
                .id(1L)
                .name("Demo Brand")
                .build();

        when(brandRepository.findById(1L)).thenReturn(Optional.of(brand));

        Brand result = brandService.findById(1L);

        assertEquals(brand, result);
        verify(brandRepository).findById(1L);
    }
}
package com.henry;

import com.google.common.collect.Iterables;
import com.henry.model.company.Company;
import com.henry.repository.company.CompanyRepository;
import com.henry.service.CompanyServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class CompanyServiceTest {

    @Mock
    private CompanyRepository companyRepository;

    @InjectMocks
    private CompanyServiceImpl companyService;

    @Test
    void testSaveCompany() {
        Company company =
                Company.builder()
                        .id(1L)
                        .name("Test Corp")
                        .build();

        when(companyRepository.save(any())).thenReturn(company);

        Company saved = companyService.save(company);

        assertEquals(company, saved);
        verify(companyRepository).save(any());
    }

    @Test
    void testFindAllCompanies() {
        Company comp1 = Company.builder()
                .id(1L)
                .name("Test Corp")
                .build();
        Company comp2 = Company.builder()
                .id(2L)
                .name("Demo Corp")
                .build();

        when(companyRepository.findAll()).thenReturn(Arrays.asList(comp1, comp2));

        Iterable<Company> result = companyService.findAll();


        assertEquals(2, Iterables.size(result));
        verify(companyRepository).findAll();
    }

    @Test
    void testFindCompanyById() {
        Company company = Company.builder()
                .id(1L)
                .name("Test Corp")
                .build();

        when(companyRepository.findById(1L)).thenReturn(Optional.of(company));

        Company result = companyService.findById(1L);

        assertEquals(company, result);
        verify(companyRepository).findById(1L);
    }
}
package com.henry;

import com.google.common.collect.Iterables;
import com.henry.model.user.User;
import com.henry.repository.user.UserRepository;
import com.henry.service.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserServiceImpl userService;

    @Test
    public void saveUser_shouldPersistUser() {
/**

 int x = 10;
 int y = 20;
 String result = STR."\{x} + \{y} = \{x + y}";
 string templates are a preview feature and are disabled by default.
 Are string templates supported yet? - IntelliJ support - JetBrains
 https://intellij-support.jetbrains.com/hc/en-us/community/posts/13451540436114-Are-string-templates-supported-yet-
 **/

        // Create a User
        User user = User.builder()
                .id(1L)
                .name("John")
                .lastName("Doe")
                .build();

        when(userRepository.save(any())).thenReturn(user);

        User saved = userService.save(user);

        assertEquals(user, saved);
        verify(userRepository).save(any());
    }

    @Test
    void testFindAllUsers() {
        User user1 = User.builder()
                .id(1L)
                .name("John")
                .lastName("Doe")
                .build();
        User user2 = User.builder()
                .id(2L)
                .name("John")
                .lastName("Doe")
                .build();

        when(userRepository.findAll()).thenReturn(Arrays.asList(user1, user2));

        Iterable<User> result = userService.findAll();

        assertEquals(2, Iterables.size(result));
        verify(userRepository).findAll();
    }

    @Test
    void testFindUserById() {
        User user = User.builder()
                .id(1L)
                .name("John")
                .lastName("Doe")
                .build();

        when(userRepository.findById(1L)).thenReturn(Optional.of(user));

        User result = userService.findById(1L);

        assertEquals(user, result);
        verify(userRepository).findById(1L);
    }
}


Maven Configuration


The pom.xml file manages project dependencies and build configuration:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.henry</groupId>
	<artifactId>multiple-data-sources-jpa</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>multiple-data-sources-jpa</name>
	<description>Demo project multiple data sources jpa</description>
	<properties>
		<java.version>21</java.version>
	</properties>
	<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<exclusions>
				<!-- Exclude the Tomcat dependency -->
				<exclusion>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-starter-tomcat</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<!-- Use Jetty instead -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jetty</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<!-- MARIADB Connector -->

		<dependency>
			<groupId>org.mariadb.jdbc</groupId>
			<artifactId>mariadb-java-client</artifactId>
		</dependency>

		<!-- Mysql Connector -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.30</version>
		</dependency>

		<!-- ORACLE database driver -->
		<!-- https://mvnrepository.com/artifact/oracle/ojdbc6 -->
		<dependency>
			<groupId>com.oracle</groupId>
			<artifactId>ojdbc6</artifactId>
			<version>11.2.0.4</version>
		</dependency>

		<!-- POSTGRESQL database driver -->
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>

		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>31.0.1-jre</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.antlr/stringtemplate
		<dependency>
			<groupId>org.antlr</groupId>
			<artifactId>stringtemplate</artifactId>
			<version>4.0.2</version>
		</dependency>
		-->

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>21</source>
					<target>21</target>
					<compilerArgs>--enable-preview</compilerArgs>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>


Environment Setup with Docker Compose


To simplify the setup of your application's environment, you can use Docker Compose. Below is a docker-compose-multiple-db.yml file that defines services for Oracle, PostgreSQL, and MySQL databases:

version: '3'
services:
  oracle:
    image: pengbai/docker-oracle-xe-11g-r2
    ports:
      - "1521:1521"
      - "8080:8080"
    shm_size: 1g

  postgres:
    image: postgres:14.1
    container_name: postgre_test
    environment:
      POSTGRES_USER: postgre_test
      POSTGRES_PASSWORD: postgre_test
      POSTGRES_DB: postgre_test
    ports:
      - "5432:5432"

  mysql:
    image: mysql/mysql-server:latest
    container_name: mysql-docker-container
    environment:
      MYSQL_ROOT_PASSWORD: mypass
      MYSQL_DATABASE: test_db
      MYSQL_USER: test
      MYSQL_PASSWORD: test_pass
    ports:
      - "3306:3306"

Run command:

 
docker-compose -f docker-compose-multiple-db.yml up -d



Only for oracle:

mvn install:install-file "-Dfile=path/to/your/ojdbc6.jar" "-DgroupId=com.oracle" "-DartifactId=ojdbc6" "-Dversion=11.2.0.4" "-Dpackaging=jar" "-DgeneratePom=true"


Run & Test


Run Spring Boot application with command: by console, IntelliJ etc.


mvn test -Dtest=UserServiceTest

mvn test -Dtest=BrandServiceTest

mvn test -Dtest=CompanyServiceTest







Run locally: mvn spring-boot:run





POST
http://localhost:9000/api/v1/users

{
    "name": "henry",
    "lastName": "x"
}







POST
http://localhost:9000/api/v2/companies
{
    "name": "company1"
}











POST
http://localhost:9000/api/v3/brands

{
    "name": "Demo Brand"
}












Source Code


Here on GitHub.










🔐AWS → Google Cloud Workload Identity Federation with Terraform

  Eliminating Service Account Keys for Cross-Cloud Workloads Managing identities across cloud providers is often more challenging than manag...