August 19, 2026

GIT Rebase

1. Check out your main branch:

$ git checkout main

Reference: https://docs.gitlab.com/topics/git/git_rebase/

2. "Ensure you have the latest contents of the target branch. In this example, the target branch is main:"

git fetch origin main

3. "Check out your branch:"

git checkout my-branch

4. "Rebase against the main branch:"

git rebase origin/main

5. "Resolve the conflicts in your editor."

6. "Stage the changes:"

git add .

7. "Continue the rebase:"

git rebase --continue

8. "Force push your changes to the target branch, while protecting others’ commits:"

git push origin my-branch --force-with-lease

April 10, 2026

How to Install Docker CE on Fedora 43

Prerequisite

$ cat /etc/os-release
...
VERSION="43 (Workstation Edition)"
...

Install Docker CE (Community Edition)

https://docs.docker.com/engine/install/fedora/

Remove possible old version of Docker CE

$ sudo dnf remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-selinux \
                  docker-engine-selinux \
                  docker-engine

Install Docker CE RPM repo

$ sudo dnf config-manager addrepo --from-repofile https://download.docker.com/linux/fedora/docker-ce.repo

Install Docker CE

$ sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Start and auto start Docker CE

$ sudo systemctl enable --now docker

Test

$ sudo docker run hello-world
...
Hello from Docker!
This message shows that your installation appears to be working correctly.
...

Run the Docker Daemon as a Non-Root User (Rootless Mode).

https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user

$ sudo groupadd docker
$ sudo usermod -aG docker $USER

"Log out and log back in so that your group membership is re-evaluated."

"You can also run the following command to activate the changes to groups:"

$ newgrp docker

Verify

$ docker run hello-world
...
Hello from Docker!
This message shows that your installation appears to be working correctly.
...

April 6, 2026

Writing mTLS/Two-Way SSL/Client Certificate Authentication with Spring Boot 4 and HTTP Service Clients

Introduction

In my previous I setup client certification authentication with RestClient https://magnus-k-karlsson.blogspot.com/2026/04/writing-mtlstwo-way-sslclient.html

Here I will use new Spring Boot 4 Rest Client: HTTP Service Clients

Configuration

The configuration of the RestClient is the same.

Java Interface

package se.magnuskkarlsson.clientcert;

import org.springframework.web.service.annotation.GetExchange;

public interface HelloService {

    @GetExchange("/hello")
    public String hello();
}

Use It

    @Autowired
    RestClient restClient;

    private HelloService service;

    // https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-service-client
    @BeforeEach
    void setUp() throws Exception {
        // Using RestClient...
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
        service = factory.createClient(HelloService.class);
    }

    @Test
    void hello() throws Exception {
        System.out.println(service.hello());
    }

Writing mTLS/Two-Way SSL/Client Certificate Authentication with Spring Boot 4 and RestClient

Introduction

In my previous I setup the server side with mLTS https://magnus-k-karlsson.blogspot.com/2026/04/configure-mtlstwo-way-sslclient.html

SSL Bundle

application.properties

# https://docs.spring.io/spring-boot/reference/features/ssl.html
spring.ssl.bundle.jks.mybundle.truststore.location=file:src/test/resources/localhost.p12
spring.ssl.bundle.jks.mybundle.truststore.password=changeit

spring.ssl.bundle.jks.mybundle.keystore.location=file:src/test/resources/localhost.p12
spring.ssl.bundle.jks.mybundle.key.alias=localhost
spring.ssl.bundle.jks.mybundle.keystore.password=changeit
spring.ssl.bundle.jks.mybundle.keystore.type=PKCS12

RestClient Configuration

package se.magnuskkarlsson.clientcert;

import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.util.Timeout;
import org.springframework.boot.restclient.autoconfigure.RestClientSsl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {

    // https://dev.to/akdevcraft/never-use-spring-restclient-default-implementation-in-production-100g
    // https://docs.spring.io/spring-boot/api/java/org/springframework/boot/restclient/autoconfigure/RestClientSsl.html
    // https://docs.spring.io/spring-boot/reference/features/ssl.html
    // https://docs.spring.io/spring-boot/appendix/application-properties/index.html
    @Bean
    public RestClient restClient(RestClient.Builder restClientBuilder, RestClientSsl ssl) {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(150); // total connections
        connectionManager.setDefaultMaxPerRoute(50); // per-host connections

        RequestConfig requestConfig = RequestConfig.custom() //
                .setConnectTimeout(Timeout.ofMilliseconds(2000)) // time to establish connection (ms)
                .setResponseTimeout(Timeout.ofMilliseconds(3000)) // time waiting for server response (ms)
                .setConnectionRequestTimeout(Timeout.ofMilliseconds(1000)) // time to wait for connection from pool (ms)
                .build();

        CloseableHttpClient httpClient = HttpClients.custom() //
                .setConnectionManager(connectionManager) //
                .setDefaultRequestConfig(requestConfig) //
                .build();

        return restClientBuilder //
                .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)) //
                .apply(ssl.fromBundle("mybundle")) //
                .baseUrl("https://localhost:8443") //
                .build();
    }
}

RestClient Code

    @Autowired
    RestClient restClient;

    @Test
    void hello() {
        // https://dzone.com/articles/spring-boot-32-replace-your-resttemplate-with-rest
        var response = restClient.get() //
                .uri("/hello") //
                .retrieve() //
                .toEntity(String.class);
        System.out.println(response.getBody());
    }

Configure mTLS/Two-Way SSL/Client Certificate Authentication with Spring Boot 4 and Spring Security X.509 Authentication

Introduction

In this blog I'm configure mTLS also called Two-Way SSL or Client Certificate Authentication, with self signed certificate.

Server Certificate (self signed)

$ keytool -genkeypair -alias localhost -dname "cn=localhost, o=Magnus K Karlsson, c=SE" -validity 3650 -keyalg RSA -keysize 2048 -ext ku:c=dig,keyEnc -ext "san=dns:localhost,ip:127.0.0.1" -ext eku=serverAuth -keystore src/test/resources/localhost.p12 -storetype PKCS12 -storepass changeit -keypass changeit

Client Certificate (self signed)

For simplicity we are going to use same certificate.

Server Truststore (Which client certificate are allowed to login)

The server truststore, defines which client CA or client certificate are allowed to login. Here we simply export server certificate, which we use as client certificate as well and import into a new truststore (JKS format).

$ keytool -exportcert -rfc -alias localhost -file src/test/resources/localhost.cert.pem -keystore src/test/resources/localhost.p12 -storepass changeit -storetype PKCS12 -v

$ keytool -importcert -trustcacerts -alias localhost -file src/test/resources/localhost.cert.pem -keystore src/test/resources/truststore-client.jks -storepass changeit -storetype JKS -v

Spring Boot 4 with Spring Security X.509 Authentication

Create a new simple project with Spring Boot 4 and Java 25

<?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>4.0.5</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>se.magnuskkarlsson</groupId>
    <artifactId>client-cert</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <java.version>25</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</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-restclient</artifactId>
        </dependency>

        <!-- Apache HttpClient 5 (for advanced pooling, timeouts, etc.) -->
        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-restclient-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Create simple REST Controller

package se.magnuskkarlsson.clientcert;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello World!";
    }
}

Now create Spring Security Configuration class with X.509 Authentication.

package se.magnuskkarlsson.clientcert;

import java.security.cert.X509Certificate;

import javax.security.auth.x500.X500Principal;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final Log log = LogFactory.getLog(getClass());

    // https://docs.spring.io/spring-security/reference/servlet/authentication/x509.html
    @Bean
    DefaultSecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http //
                .authorizeHttpRequests((exchanges) -> exchanges //
                        .anyRequest().authenticated() //
                ) //
                .x509((x509) -> x509 //
                        .x509PrincipalExtractor(x509PrincipalExtractor()) //
                ) //
                .userDetailsService(userDetailsService());
        return http.build();
    }

    @Bean
    X509PrincipalExtractor x509PrincipalExtractor() {
        // org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor
        return new X509PrincipalExtractor() {
            @Override
            public Object extractPrincipal(X509Certificate cert) {
                X500Principal principal = cert.getSubjectX500Principal();
                return principal.toString();
            }
        };
    }

    @Bean
    UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                log.info("username='" + username + "'");
                // accept all mTLS verified client certificate, here you would normally lookup user from storage
                return new User(username, "", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
            }
        };
    }
}

Finally configure the application.

spring.application.name=client-cert

server.port=8443
server.ssl.enabled=true

server.ssl.key-store=src/test/resources/localhost.p12
server.ssl.key-store-password=changeit

server.ssl.trust-store=src/test/resources/truststore-client.jks
server.ssl.trust-store-password=changeit
server.ssl.client-auth=need

#logging.level.org.springframework.security=TRACE

Test

Start application with

$ mvn spring-boot:run

To test with curl you need to export client certifite to PEM

$ openssl pkcs12 -in src/test/resources/localhost.p12 -out src/test/resources/localhost.key.pem -nodes

$ cp src/test/resources/localhost.key.pem src/test/resources/localhost.cert.pem
$ curl -i --request GET \
     --url     https://localhost:8443/hello?name=FOO \
     --cacert  src/test/resources/localhost.cert.pem \
     --cert    src/test/resources/localhost.cert.pem \
     --key     src/test/resources/localhost.key.pem

August 2, 2024

Spring Boot Database Connection Pooling

 https://medium.com/@ak123aryan/mastering-database-connection-pooling-in-spring-boot-a-comprehensive-guide-with-java-96b1ae8e93db

https://www.baeldung.com/spring-boot-tomcat-connection-pool

application.properties

spring.datasource.type =

"Fully qualified name of the connection pool implementation to use. By default, it is auto-detected from the classpath."

https://docs.spring.io/spring-boot/appendix/application-properties/index.html

HikariCP

spring.datasource.type = com.zaxxer.hikari.HikariDataSource

#default is 10
spring.datasource.hikari.maximum-pool-size=10
#default is same as max pool size
spring.datasource.hikari.minimum-idle=10
#default is 30 seconds
spring.datasource.hikari.connection-timeout=30000
#default is 600000 i.e 10 minutes
spring.datasource.hikari.idle-timeout=600000
#default is 1800000 i.e 30 minutes
spring.datasource.hikari.max-lifetime=1800000 
spring.datasource.hikari.pool-name=HikariConnPool

#spring.datasource.hikari.* =

https://medium.com/javarevisited/hikari-connection-pooling-5600d765e5ae

Tomcat JDBC Connection Pool

spring.datasource.type = org.apache.tomcat.jdbc.pool.DataSource

spring.datasource.tomcat.* =

Apache DBCP2

spring.datasource.type = org.apache.commons.dbcp2.BasicDataSource

spring.datasource.dbcp2.* =

July 31, 2024

IPv4 Network Ranges

Example 192.168.0.0/30

number of IP addresses = 2 ^ (32–30)

which is 2 ^ 2 = 4

The IP addresses are thus 192.168.0.0, 192.168.0.1, 192.168.0.2 and 192.168.0.3

Example 192.168.0.0/29

number of IP addresses = 2 ^ (32–29)

which is 2 ^ 3 = 8

The IP addresses are thus 192.168.0.0, 192.168.0.1, 192.168.0.2 … 192.168.0.7

Example 192.168.0.1/28

number of IP address = 2 ^ (32–28)

which is 2 ^ 4 = 16

The IP addresses are thus 192.168.0.0, 192.168.0.1, 192.168.0.2 … 192.168.0.15

Spring RestTemplate and WebClient Improvements

 Improvements for reactive WebClient

https://medium.com/@dixitsatish34/how-to-improve-webclient-response-time-in-spring-boot-3c0c898f06b4

Improvements for blocking RestTemplate

https://medium.com/@nitinvohra/how-to-improve-performance-of-spring-resttemplate-6af37e0a0f33


RestClient vs. WebClient vs RestTemplate

https://docs.spring.io/spring-framework/reference/web/webmvc-client.html

1. RestTemplate (Spring Framework 3+) Blocking/Synchronous on Servlet Stack (org.springframework.boot:spring-boot-starter-web)


2. WebClient (Spring Framework 5+) Reactive/Asynchronous on WebFlux (org.springframework.boot:spring-boot-starter-webflux)


3. RestClient (Spring Framework 6.1+) Blocking/Synchronous on Servlet Stack (org.springframework.boot:spring-boot-starter-web)


https://digma.ai/restclient-vs-webclient-vs-resttemplate/


September 10, 2023

Keycloak, User Federation from LDAP and Lesson Learned

LDAP

We are going to use OpenLDAP and setup in previous blog OpenLDAP Image and Custom LDIF with User and Group.

$ podman run -d --name openldap \
    -e LDAP_ROOT=dc=magnuskkarlsson,dc=se \
    -e LDAP_ADMIN_USERNAME=admin \
    -e LDAP_ADMIN_PASSWORD=changeit \
    -e LDAP_CONFIG_ADMIN_ENABLED=true \
    -e LDAP_ALLOW_ANON_BINDING=false \
    -e LDAP_CUSTOM_LDIF_DIR=/ldifs \
    -p 1389:1389 \
    -p 1636:1636 \
    -v ./ldifs:/ldifs:Z \
    docker.io/bitnami/openldap:2.6

Test it is working.

$ ldapsearch -H ldap://localhost:1389 -D cn=admin,dc=magnuskkarlsson,dc=se -w changeit -b dc=magnuskkarlsson,dc=se -s sub

Keycloak Installation

Here we will use the supported Keycloak version - RH SSO.

$ unzip rh-sso-7.6.0-server-dist.zip
$ mv rh-sso-7.6 rh-sso-7.6.0
$ cd rh-sso-7.6.0/bin/

$ ./add-user-keycloak.sh --user admin --password admin

Configure LDAP logging in RH SSO

$ vim rh-sso-7.6.0/standalone/configuration/standalone.xml 
...
    <profile>
        <subsystem xmlns="urn:jboss:domain:logging:8.0">
...
            </logger>
            <logger category="org.keycloak.storage.ldap">
                <level name="TRACE"/>
            </logger>            
            <root-logger>
...

Start

$ export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk

$ ./standalone.sh -Djboss.socket.binding.port-offset=100

Kecloak Configuration User Federation with NO Import

We will start federate openldap user and roles and not import them and test what happens, to understand how user federation works.

Add User federation

Edit mode: READ_ONLY

Import Users: OFF

No Synchronization

Add Mappers:

  • password: user-attribute-ldap-mapper; password; userPassword
  • role-ldap-mapper: role-ldap-mapper;

We can verify that user is not imported, by clicking on User and View all users

Now test federation, by logging in to user portal: http://localhost:8180/auth/realms/demo/account/

john
bitnami1

kate
bitnami1

And observe server log, that user is fetch by RDN LDAP attribute: uid

LdapOperation: search
 baseDn: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(uid=john)(objectclass=inetOrgPerson)(objectclass=organizationalPerson))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
 resultSize: 1
took: 2 ms

LdapOperation: search
 baseDn: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(uid=john)(objectclass=inetOrgPerson)(objectclass=organizationalPerson))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
 resultSize: 1
took: 1 ms

LdapOperation: search
 baseDn: ou=Groups,dc=magnuskkarlsson,dc=se
 filter: (&(member=cn=john,ou=People,dc=magnuskkarlsson,dc=se)(objectclass=groupOfNames))
 searchScope: 1
 returningAttrs: [cn]
 resultSize: 2
took: 1 ms
bitnami1

Kecloak Configuration User Federation with Import

Now Import Users: ON

Then login (user fetced by RDN LDAP attribute: uid), close private window and login again. Now is the user fetched by UUID LDAP attribute: entryUUID

LdapOperation: search
 baseDn: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(uid=john)(objectclass=inetOrgPerson)(objectclass=organizationalPerson))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
 resultSize: 1
took: 2 ms

LdapOperation: lookupById
 baseDN: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(objectClass=*)(entryUUID=b8f499ce-e341-103d-9c19-dfc37858caec))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
took: 3 ms

LdapOperation: search
 baseDn: ou=Groups,dc=magnuskkarlsson,dc=se
 filter: (&(member=cn=john,ou=People,dc=magnuskkarlsson,dc=se)(objectclass=groupOfNames))
 searchScope: 1
 returningAttrs: [cn]
 resultSize: 2
took: 1 ms

...

LdapOperation: lookupById
 baseDN: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(objectClass=*)(entryUUID=b8f499ce-e341-103d-9c19-dfc37858caec))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
took: 4 ms

When User is imported you can search and find User in Web Console and you can see in server.log that User is updated from LDAP.

LdapOperation: lookupById
 baseDN: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(objectClass=*)(entryUUID=b8f499ce-e341-103d-9c19-dfc37858caec))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
took: 2 ms

LdapOperation: lookupById
 baseDN: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(objectClass=*)(entryUUID=b8f499ce-e341-103d-9c19-dfc37858caec))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
took: 1 ms

Side effects

This online dependency has also consequence, if LDAP server is offline than User cannot log in.

But if the LDAP server comes back online, User can login again.

15:04:09,715 WARN  [org.keycloak.services] (default task-44) KC-SERVICES0013: Failed authentication: org.keycloak.models.ModelException: LDAP Query failed
...
Caused by: org.keycloak.models.ModelException: Querying of LDAP failed org.keycloak.storage.ldap.idm.query.internal.LDAPQuery@54e2e27
...
Caused by: org.keycloak.models.ModelException: Could not query server using DN [ou=People,dc=magnuskkarlsson,dc=se] and filter [(&(objectClass=*)(entryUUID=b8f499ce-e341-103d-9c19-dfc37858caec))]
...
Caused by: javax.naming.CommunicationException: localhost:1389 [Root exception is java.net.ConnectException: Connection refused (Connection refused)]
...
Caused by: java.net.ConnectException: Connection refused (Connection refused)
...
15:04:09,755 WARN  [org.keycloak.events] (default task-44) type=LOGIN_ERROR, realmId=demo, clientId=account-console, userId=null, ipAddress=127.0.0.1, error=invalid_user_credentials, auth_method=openid-connect, auth_type=code, redirect_uri=http://localhost:8180/auth/realms/demo/account/#/, code_id=618ac086-9b7f-4bd1-b8a2-0498ab2e7390, username=john, authSessionParentId=618ac086-9b7f-4bd1-b8a2-0498ab2e7390, authSessionTabId=XLmqDMVrjkc

Or if LDAP server is slow (We have set Connection and Read timeout, this is important, since default values are infinite), the User cannot login.

15:10:14,894 WARN  [org.keycloak.services] (default task-44) KC-SERVICES0013: Failed authentication: org.keycloak.models.ModelException: LDAP Query failed
...
Caused by: org.keycloak.models.ModelException: Querying of LDAP failed org.keycloak.storage.ldap.idm.query.internal.LDAPQuery@c684144
...
Caused by: org.keycloak.models.ModelException: Could not query server using DN [ou=People,dc=magnuskkarlsson,dc=se] and filter [(&(objectClass=*)(entryUUID=b8f499ce-e341-103d-9c19-dfc37858caec))]
...
Caused by: javax.naming.CommunicationException: localhost:1389 [Root exception is java.net.SocketTimeoutException: connect timed out]
...
Caused by: java.net.SocketTimeoutException: connect timed out
...
15:10:14,907 WARN  [org.keycloak.events] (default task-44) type=LOGIN_ERROR, realmId=demo, clientId=account-console, userId=null, ipAddress=127.0.0.1, error=invalid_user_credentials, auth_method=openid-connect, auth_type=code, redirect_uri=http://localhost:8180/auth/realms/demo/account/#/, code_id=f68f708a-82de-4c5b-957f-9c7084374922, username=john, authSessionParentId=f68f708a-82de-4c5b-957f-9c7084374922, authSessionTabId=RyZud1gnRGs

Another side effect of not periodically import all User, is that User, that have never logged in, will not be visible in RH SSO.

And another side effect, is that User in RH SSO will never be pruned, i.e. if user is deleted in LDAP, that User will never be removed from RH SSO.

And this online dependency, can not be circumvented, by disabling user federation.

Kecloak Configuration User Federation and Different Edit Modes

WRITABLE means data will be synced back to LDAP on demand.

UNSYNCED means user data will be imported, but not synced back to LDAP.

READ_ONLY is a read-only LDAP store. "You cannot change the username, email, first name, last name, and other mapped attributes. Red Hat Single Sign-On shows an error anytime a user attempts to update these fields. Password updates are not supported."

September 8, 2023

OpenLDAP Image and Custom LDIF with User and Group

Reference: https://hub.docker.com/r/bitnami/openldap

LDAP_PORT_NUMBER: The port OpenLDAP is listening for requests. Priviledged port is supported (e.g. 1389). Default: 1389 (non privileged port).

LDAP_ROOT: LDAP baseDN (or suffix) of the LDAP tree. Default: dc=example,dc=org

LDAP_ADMIN_USERNAME: LDAP database admin user. Default: admin

LDAP_ADMIN_PASSWORD: LDAP database admin password. Default: adminpassword

LDAP_CONFIG_ADMIN_ENABLED: Whether to create a configuration admin user. Default: no.

LDAP_USERS: Comma separated list of LDAP users to create in the default LDAP tree. Default: user01,user02

LDAP_PASSWORDS: Comma separated list of passwords to use for LDAP users. Default: bitnami1,bitnami2

LDAP_USER_DC: DC for the users' organizational unit. Default: users

LDAP_GROUP: Group used to group created users. Default: readers

LDAP_ALLOW_ANON_BINDING: Allow anonymous bindings to the LDAP server. Default: yes.

LDAP_PASSWORD_HASH: Hash to be used in generation of user passwords. Must be one of {SSHA}, {SHA}, {SMD5}, {MD5}, {CRYPT}, and {CLEARTEXT}. Default: {SSHA}.

LDAP_CUSTOM_LDIF_DIR: Location of a directory that contains LDIF files that should be used to bootstrap the database. Only files ending in .ldif will be used. Default LDAP tree based on the LDAP_USERS, LDAP_PASSWORDS, LDAP_USER_DC and LDAP_GROUP will be skipped when LDAP_CUSTOM_LDIF_DIR is used. When using this it will override the usage of LDAP_USERS, LDAP_PASSWORDS, LDAP_USER_DC and LDAP_GROUP. You should set LDAP_ROOT to your base to make sure the olcSuffix configured on the database matches the contents imported from the LDIF files. Default: /ldifs

LDAP_PASSWORD_HASH: Hash to be used in generation of user passwords. Must be one of {SSHA}, {SHA}, {SMD5}, {MD5}, {CRYPT}, and {CLEARTEXT}. Default: {SSHA}.

Create a new directory ldif with custom LDIF for Users and Groups

dn: dc=magnuskkarlsson,dc=se
objectClass: dcObject
objectClass: organization
dc: magnuskkarlsson
o: Magnus K Karlsson

dn: ou=People,dc=magnuskkarlsson,dc=se
objectClass: organizationalUnit
ou: People

dn: ou=Groups,dc=magnuskkarlsson,dc=se
objectClass: organizationalUnit
ou: Groups

## Users

dn: cn=john,ou=People,dc=magnuskkarlsson,dc=se
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: john
userPassword:: Yml0bmFtaTE=
cn: John
sn: Doe
mail: john.doe@domain.com
uidNumber: 1000
gidNumber: 1000
homeDirectory: /home/john

dn: cn=kate,ou=People,dc=magnuskkarlsson,dc=se
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: kate
userPassword:: Yml0bmFtaTE=
cn: Kate
sn: Doe
mail: kate.doe@domain.com
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/kate

## Groups

dn: cn=USER,ou=Groups,dc=magnuskkarlsson,dc=se
cn: USER
objectClass: groupOfNames
member: cn=john,ou=People,dc=magnuskkarlsson,dc=se
member: cn=kate,ou=People,dc=magnuskkarlsson,dc=se

dn: cn=ADMIN,ou=Groups,dc=magnuskkarlsson,dc=se
cn: ADMIN
objectClass: groupOfNames
member: cn=john,ou=People,dc=magnuskkarlsson,dc=se
$ podman run -d --name openldap \
    -e LDAP_ROOT=dc=magnuskkarlsson,dc=se \
    -e LDAP_ADMIN_USERNAME=admin \
    -e LDAP_ADMIN_PASSWORD=changeit \
    -e LDAP_CONFIG_ADMIN_ENABLED=true \
    -e LDAP_ALLOW_ANON_BINDING=false \
    -e LDAP_CUSTOM_LDIF_DIR=/ldifs \
    -p 1389:1389 \
    -p 1636:1636 \
    -v ./ldifs:/ldifs:Z \
    docker.io/bitnami/openldap:2.6
$ podman logs --follow openldap
...
 13:30:55.23 INFO  ==> Loading custom LDIF files...
 13:30:55.23 WARN  ==> Ignoring LDAP_USERS, LDAP_PASSWORDS, LDAP_USER_DC and LDAP_GROUP environment variables...
 13:30:56.35 INFO  ==> ** LDAP setup finished! **

And later to stop

$ podman stop openldap; podman rm openldap

Now verify ldap and it's entries. First install ldap client

$ sudo dnf install openldap-clients
$ ldapsearch -h
...
  -H URI     LDAP Uniform Resource Identifier(s)
  -D binddn  bind DN
  -x         Simple authentication  
  -w passwd  bind password (for simple authentication)
  -b basedn  base dn for search  
  -s scope   one of base, one, sub or children (search scope)
...

$ ldapsearch -H ldap://localhost:1389 -D cn=admin,dc=magnuskkarlsson,dc=se -w changeit -b dc=magnuskkarlsson,dc=se -s sub

GUI Administration Tools. Apache Directory Studio Eclipse-based LDAP tools

https://magnus-k-karlsson.blogspot.com/2015/02/understanding-ldap-and-ldap.html

August 30, 2023

Spring Boot 3 with X509 Authentication and JDBC Storage

Reference

Maven

Using same pom, but different artifactId as previous blog: Basic Auth and JDBC Password Storage

MySQL

Using same docker mysql container wiht same data as in previous blog: Basic Auth and JDBC Password Storage

Server and User Certificate and Truststore

Using same root ca, certs and truststore as in previous blog: Using Java 17 keytool as Root CA to create Server and User Certificate

Applicaiton

src/main/resources/application.properties

server.port = 8443

# https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-logging
logging.level.org.springframework.security = TRACE

# https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#appendix.application-properties.server
#server.ssl.bundle
#server.ssl.certificate
#server.ssl.certificate-private-key
#server.ssl.ciphers
server.ssl.client-auth = want
server.ssl.enabled = true
#server.ssl.enabled-protocols
server.ssl.key-alias = localhost
server.ssl.key-password = changeit
server.ssl.key-store = localhost.p12
server.ssl.key-store-password = changeit
#server.ssl.key-store-provider
server.ssl.key-store-type = PKCS12
#server.ssl.protocol
#server.ssl.trust-certificate
#server.ssl.trust-certificate-private-key =
server.ssl.trust-store = truststore.jks
server.ssl.trust-store-password = changeit
#server.ssl.trust-store-provider 
server.ssl.trust-store-type = JKS

#server.servlet.session.cookie.domain
server.servlet.session.cookie.http-only = true
server.servlet.session.cookie.max-age = 3m
#server.servlet.session.cookie.name
#server.servlet.session.cookie.path
server.servlet.session.cookie.same-site = strict
server.servlet.session.cookie.secure = true
server.servlet.session.persistent = false
#server.servlet.session.store-dir
server.servlet.session.timeout = 3m
server.servlet.session.tracking-modes = cookie

spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=user
spring.datasource.password=changeit

Spring Java configuration

package se.mkk.springboot3x509jdbc;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class X509JdbcSecurityConfig {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http //
                .sessionManagement(
                        // https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
                        // https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#appendix.application-properties.server
                        Customizer.withDefaults())
                .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests //
                        .requestMatchers("/login", "/logout").permitAll() //
                        .anyRequest().authenticated()) //
                .x509(x509 -> x509 //
                        // https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/basic.html
                        .subjectPrincipalRegex("CN=(.*?),") //
                        .userDetailsService(this.userDetailsService())) //
                .csrf(csrf -> csrf //
                        // https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-token-repository-cookie
                        .ignoringRequestMatchers("/logout", "/api"))
                .logout(logout -> logout //
                        // https://docs.spring.io/spring-security/reference/servlet/authentication/logout.html#clear-all-site-data
                        .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.ALL))));
        return http.build();
    }

//    private UserDetailsService userDetailsService() {
//        return username -> {
//            log.info("X509 {}", username);
//            return User.withUsername(username).password("DUMMY").roles("USER", "ADMIN").build();
//        };
//    }

    // https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/jdbc.html
    @Autowired
    private DataSource dataSource;

    private UserDetailsService userDetailsService() {
        JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
        return users;
    }
}

Test REST endpoint

package se.mkk.springboot3x509jdbc;

import java.security.Principal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;

@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:4200")
public class UserController {

    @GetMapping
    public Map<String, String> getUser(HttpServletRequest request, HttpSession session, Principal principal) {
        Map<String, String> rtn = new LinkedHashMap<>();
        rtn.put("session_getMaxInactiveInterval_sec", session.getMaxInactiveInterval() + "s");
        rtn.put("session_getLastAccessedTime",
                new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").format(new Date(session.getLastAccessedTime())));
        rtn.put("request_getRemoteUser", request.getRemoteUser());
        rtn.put("request_isUserInRole_USER", Boolean.toString(request.isUserInRole("USER")));
        rtn.put("request_getUserPrincipal_getClass", request.getUserPrincipal().getClass().getName());
        rtn.put("principal_getClass_getName", principal.getClass().getName());
        rtn.put("principal_getName", principal.getName());
        if (principal instanceof Authentication authentication) {
            List<String> authorities = authentication.getAuthorities().stream()
                    .map(grantedAuthority -> grantedAuthority.getAuthority()).toList();
            rtn.put("JwtAuthenticationToken.getAuthorities()", authorities.toString());
        }
        return rtn;
    }
}

Test

We need to convert p12 to pem files for curl.

$ openssl pkcs12 -in john.p12 -out john.pem -nodes
$ curl -v -X GET   --cacert RootCA.cert.pem   --cert john.cert.pem   --key john.key.pem   https://localhost:8443/api/users
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying 127.0.0.1:8443...
* Connected to localhost (127.0.0.1) port 8443 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
*  CAfile: RootCA.cert.pem
*  CApath: none
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, CERT verify (15):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
*  subject: C=SE; O=Server; OU=Localhost_Server; CN=localhost
*  start date: Aug 30 09:35:00 2023 GMT
*  expire date: Nov 28 09:35:00 2023 GMT
*  subjectAltName: host "localhost" matched cert's "localhost"
*  issuer: C=SE; O=CertificateAuthority; OU=Root_CertificateAuthority; CN=RootCA
*  SSL certificate verify ok.
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET /api/users HTTP/1.1
> Host: localhost:8443
> User-Agent: curl/7.85.0
> Accept: */*
> 
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Set-Cookie: JSESSIONID=5358CF4B62B84A8E99C4DB183CB2BDD2; Max-Age=180; Expires=Wed, 30 Aug 2023 10:07:14 GMT; Path=/; Secure; HttpOnly; SameSite=Strict
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 0
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< Strict-Transport-Security: max-age=31536000 ; includeSubDomains
< X-Frame-Options: DENY
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Wed, 30 Aug 2023 10:04:14 GMT
< 
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection #0 to host localhost left intact
{"session_getMaxInactiveInterval_sec":"180s","session_getLastAccessedTime":"2023-08-30 12:04:14 +0200","request_getRemoteUser":"john","request_isUserInRole_USER":"true","request_getUserPrincipal_getClass":"org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken","principal_getClass_getName":"org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken","principal_getName":"john","JwtAuthenticationToken.getAuthorities()":"[ROLE_ADMIN, ROLE_USER]"}
$ curl -s -X GET   --cacert RootCA.cert.pem   --cert john.cert.pem   --key john.key.pem   https://localhost:8443/api/users | jq -r
{
  "session_getMaxInactiveInterval_sec": "180s",
  "session_getLastAccessedTime": "2023-08-30 12:04:44 +0200",
  "request_getRemoteUser": "john",
  "request_isUserInRole_USER": "true",
  "request_getUserPrincipal_getClass": "org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken",
  "principal_getClass_getName": "org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken",
  "principal_getName": "john",
  "JwtAuthenticationToken.getAuthorities()": "[ROLE_ADMIN, ROLE_USER]"
}

Using Java 17 keytool as Root CA to create Server and User Certificate

Reference

Root CA

Generate Self Signed Root CA

$ keytool -genkeypair -alias RootCA -dname "cn=RootCA, ou=Root_CertificateAuthority, o=CertificateAuthority, c=SE" -validity 3650 -keyalg RSA -keysize 4096 -ext bc:c -keystore RootCA.p12 -storetype PKCS12 -storepass changeit -keypass changeit

Export Root CA certificate and truststore with Root CA

$ keytool -exportcert -alias RootCA -keystore RootCA.p12 -storetype PKCS12 -storepass changeit -rfc -file RootCA.cert.pem

$ keytool -importcert -alias RootCA -trustcacerts -noprompt -keystore truststore.jks -storetype JKS -storepass changeit -keypass changeit -file RootCA.cert.pem 

Server certificate

Generate Server certificate keypair.

$ keytool -genkeypair -alias localhost -dname "cn=localhost, ou=Localhost_Server, o=Server, c=SE" -validity 730 -keyalg RSA -keysize 2048 -keystore localhost.p12 -storetype PKCS12 -storepass changeit -keypass changeit

Generate CSR and sign with Root CA

$ keytool -certreq -alias localhost -keystore localhost.p12 -storetype PKCS12 -storepass changeit | \
    keytool -gencert -alias RootCA -keystore RootCA.p12 -storetype PKCS12 -storepass changeit -ext ku:c=dig,keyEnc -ext "san=dns:localhost,ip:127.0.0.1" -ext eku=serverAuth -rfc > localhost.cert.pem

Create certificate chain

$ cat RootCA.cert.pem >> localhost.cert.pem

Import/replace self signed certificate with Root signed

$ keytool -importcert -alias localhost -trustcacerts -noprompt -keystore localhost.p12 -storetype PKCS12 -storepass changeit -file localhost.cert.pem

Print and verify

$ keytool -list -keystore localhost.p12 -storepass changeit -v

User Certificate

Generate User certificate keypair

$ keytool -genkeypair -alias john -dname "cn=john, ou=John_User, o=User, c=SE" -validity 730 -keyalg RSA -keysize 2048 -keystore john.p12 -storetype PKCS12 -storepass changeit -keypass changeit

Generate CSR and sign with Root CA

$ keytool -certreq -alias john -keystore john.p12 -storetype PKCS12 -storepass changeit | \
    keytool -gencert -alias RootCA -keystore RootCA.p12 -storetype PKCS12 -storepass changeit -ext ku:c=digitalSignature,nonRepudiation,keyEncipherment -ext eku=clientAuth,emailProtection -rfc > john.cert.pem

Create certificate chain

$ cat RootCA.cert.pem >> john.cert.pem

Import/replace self signed certificate with Root signed

$ keytool -importcert -alias john -trustcacerts -noprompt -keystore john.p12 -storetype PKCS12 -storepass changeit -file john.cert.pem

Print and verify

$ keytool -list -keystore john.p12 -storepass changeit -v

August 29, 2023

Spring Boot 3 with Basic Authentication and JDBC Password Storage

Create new Maven Project

Create new project with Spring Initializr (https://start.spring.io/) and add dependency: Spring Data JDBC and MySQL

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

Add Spring Data JDBC properties src/main/resources/application.properties

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#appendix.application-properties.data

    spring.datasource.url=jdbc:mysql://localhost:3306/mydb
    spring.datasource.username=user
    spring.datasource.password=changeit

Start a local mysql server with a container image.

https://catalog.redhat.com/software/containers/rhel9/mysql-80/61a60915c17162a20c1c6a34

https://hub.docker.com/_/mysql

$ podman run -d --name mysqld \
    -e MYSQL_USER=user \
    -e MYSQL_PASSWORD=changeit \
    -e MYSQL_DATABASE=mydb \
    -e MYSQL_ROOT_PASSWORD=changeit \
    -p 3306:3306 \
    docker.io/library/mysql:8.0

$ podman logs --follow mysqld

Create and populate DB. Use below test method to generated hashed password.

        @Test
        public void test() throws Exception {
            PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
            System.out.println(passwordEncoder.encode("changeit"));
        }

Now connect to mysql container interactive and execute sql commands to creae tables and populate them with data.

https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/jdbc.html

$ podman exec -it mysqld /bin/bash

bash-4.4# mysql -u root -p mydb

create table users(username varchar(50) not null primary key, password varchar(500) not null, enabled boolean not null);
create table authorities (username varchar(50) not null, authority varchar(50) not null, constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username, authority);

INSERT INTO users (username, password, enabled) VALUES ('john', '{bcrypt}$2a$10$aohc8ylx1YcZx6p/L2BRv.I4oQfDin9Ed2CNTy0ZXQ3ZpdiMalLp6', true);

INSERT INTO authorities (username, authority) VALUES ('john', 'ROLE_USER');
INSERT INTO authorities (username, authority) VALUES ('john', 'ROLE_ADMIN');

Application

package se.mkk.springboot3basicjdbc;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class BasicJdbcSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http //
                .sessionManagement(
                        // https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
                        // https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#appendix.application-properties.server
                        Customizer.withDefaults())
                .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests //
                        .requestMatchers("/login", "/logout").permitAll() //
                        .anyRequest().authenticated()) //
                .httpBasic( //
                        // https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/basic.html
                        Customizer.withDefaults()) //
                .csrf(csrf -> csrf //
                        // https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-token-repository-cookie
                        .ignoringRequestMatchers("/logout", "/api"))
                .logout(logout -> logout //
                        // https://docs.spring.io/spring-security/reference/servlet/authentication/logout.html#clear-all-site-data
                        .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.ALL))));
        return http.build();
    }

    // https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/jdbc.html
    @Bean
    public UserDetailsManager jdbcUserDetailsManager(DataSource dataSource) {
        JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
        return users;
    }
}
package se.mkk.springboot3basicjdbc;

import java.security.Principal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;

@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:4200")
public class UserController {

    @GetMapping
    public Map<String, String> getUser(HttpServletRequest request, HttpSession session, Principal principal) {
        Map<String, String> rtn = new LinkedHashMap<>();
        rtn.put("session_getMaxInactiveInterval_sec", session.getMaxInactiveInterval() + "s");
        rtn.put("session_getLastAccessedTime",
                new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").format(new Date(session.getLastAccessedTime())));
        rtn.put("request_getRemoteUser", request.getRemoteUser());
        rtn.put("request_isUserInRole_USER", Boolean.toString(request.isUserInRole("USER")));
        rtn.put("request_getUserPrincipal_getClass", request.getUserPrincipal().getClass().getName());
        rtn.put("principal_getClass_getName", principal.getClass().getName());
        rtn.put("principal_getName", principal.getName());
        if (principal instanceof Authentication authentication) {
            List<String> authorities = authentication.getAuthorities().stream()
                    .map(grantedAuthority -> grantedAuthority.getAuthority()).toList();
            rtn.put("JwtAuthenticationToken.getAuthorities()", authorities.toString());
        }
        return rtn;
    }
}

Test

$ curl -v -X GET -u "john:changeit" http://localhost:8080/api/users 
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
* Server auth using Basic with user 'john'
> GET /api/users HTTP/1.1
> Host: localhost:8080
> Authorization: Basic am9objpjaGFuZ2VpdA==
> User-Agent: curl/7.85.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Set-Cookie: JSESSIONID=56FAD7EB738EB12B558F02EBC04122BE; Max-Age=180; Expires=Tue, 29 Aug 2023 21:02:41 GMT; Path=/; Secure; HttpOnly; SameSite=Strict
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 0
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Tue, 29 Aug 2023 20:59:41 GMT
< 
* Connection #0 to host localhost left intact
{"session_getMaxInactiveInterval_sec":"180s","session_getLastAccessedTime":"2023-08-29 22:59:41 +0200","request_getRemoteUser":"john","request_isUserInRole_USER":"true","request_getUserPrincipal_getClass":"org.springframework.security.authentication.UsernamePasswordAuthenticationToken","principal_getClass_getName":"org.springframework.security.authentication.UsernamePasswordAuthenticationToken","principal_getName":"john","JwtAuthenticationToken.getAuthorities()":"[ROLE_ADMIN, ROLE_USER]"}

$ curl -s -X GET -u "john:changeit" http://localhost:8080/api/users | jq -r
{
  "session_getMaxInactiveInterval_sec": "180s",
  "session_getLastAccessedTime": "2023-08-29 22:58:32 +0200",
  "request_getRemoteUser": "john",
  "request_isUserInRole_USER": "true",
  "request_getUserPrincipal_getClass": "org.springframework.security.authentication.UsernamePasswordAuthenticationToken",
  "principal_getClass_getName": "org.springframework.security.authentication.UsernamePasswordAuthenticationToken",
  "principal_getName": "john",
  "JwtAuthenticationToken.getAuthorities()": "[ROLE_ADMIN, ROLE_USER]"
}