Service Foundry
Young Gyu Kim <credemol@gmail.com>

Spring Cloud Gateway with Spring Authorization Server using Database

Introduction

This article is part of a series on Spring Cloud Gateway. The other articles in the series are:

  1. Part 1: Spring Cloud Gateway with Virtual Threads

  2. Part 2: Spring Cloud Gateway with Spring Authorization Server

  3. Part 3: Spring Cloud Gateway with Spring Authorization Server using Database

This is the third article in the series, which focuses on using Spring Authorization Server with a database as the backend.

In this article, we will set up a Spring Authorization Server using a database as the backend. We will use PostgreSQL as the database and set up the necessary tables for the Spring Authorization Server.

spring auth server database

There are 3 microservices in this example:

  • Spring Cloud Gateway (OAuth2 client, Port 8080)

  • Spring Authorization Server (OAuth2 server, Port 9000)

  • Resource Server example (Port 8082)

Please note that the port number of OAuth2 Server is 9000 which used to be 8081 in the previous articles.

Prerequisites

Before you start, you need to have the following:

  1. Java 21

  2. Gradle 8.10.1

  3. PostgreSQL 16.4

  4. Spring Boot 3.3.4

  5. Spring CLI 3.2.4

Database Setup

This article assumes that you have PostgreSQL 16.4 installed. Here is some information about the database used in this article:

  • Database Name: nsa2

  • User: nsa2

  • Password: changeme

Create database tables for Spring Authorization Server

Spring Authorization Server is built on top of Spring Security. It uses a set of tables to store user information, authority information, and OAuth2 information.

Some database schemas such as users and authorities are provided by Spring Security. You can find the schema for the user and authority tables in the Spring Security documentation while the schema for OAuth2 tables is provided by Spring Authorization Server.

Schema files for UserDetailsService

You can find the schema for the user and authority tables in the Spring Security documentation.

Here are tables for User and Authority:

  • users

  • authorities

  • groups

  • group_authorities

  • group_members

  • persistent_logins(remember_me)

Among these tables, we will use the users and authorities tables for the Spring Authorization Server.

Here is the schema that Spring Security provides for the users and authorities tables.

create table users(
	username varchar_ignorecase(50) not null primary key,
	password varchar_ignorecase(50) not null,
	enabled boolean not null
);

create table authorities (
	username varchar_ignorecase(50) not null,
	authority varchar_ignorecase(50) not null,
	constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);

However, the password keeps encrypted passwords in the users table. Its colume size is too small to store encrypted passwords. So, we need to update the schema for the users table to store encrypted passwords.

Here is an example of encrypted passwords for the users table of which size is 69.

{bcrypt}$2a$10$s2sHJSyGIzMpcKyVbbkYWuk9tDWmRe8rSolvCeJ14Y6JgWkGJWZ0S

Here is the updated schema for the users and authorities table.

create table users(
  username varchar(50) not null primary key,
  password varchar(256) not null,
  enabled boolean not null
);

create table authorities(
    username varchar(50) not null,
    authority varchar(256) not null,
    constraint fk_authorities_users foreign key(username) references users(username)
);

create unique index ix_auth_username on authorities (username,authority);

For your information, we can use Spring CLI to encode passwords.

# to encode password
$ spring encodepassword mypassword

{bcrypt}$2a$10$PLsAyCSJjqUZkKlLazQvreH4mzhLLlncAA3LSSSTRxmV5Xh.aR1o6

# to count characters of encoded password
$ spring encodepassword mypassword | wc -c

69

Schema files for OAuth2

Here are tables for OAuth2 needed for the Spring Authorization Server:

  • oauth2_registered_client

  • oauth2_authorization_consents

  • oauth2_authorization

You can find the schema files for OAuth2 tables in the Spring Authorization Server source code.

These schema files are for H2 database. As they added comments for PostgreSQL, we need to update ALL columns defined with 'blob' to 'text' as PostgreSQL does not support 'blob' type.

The source below can be found in a Demo application of Spring Authorization Server.

	@Bean
	public EmbeddedDatabase embeddedDatabase() {
		// @formatter:off
		return new EmbeddedDatabaseBuilder()
				.generateUniqueName(true)
				.setType(EmbeddedDatabaseType.H2)
				.setScriptEncoding("UTF-8")
				.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
				.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql")
				.addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
				.build();
		// @formatter:on
	}

This code snippet will be useful when you want to see how oauth clients are registered in the database. If you want to see about the sample application, please refer to the link below.

Schema files for OAuth2 for PostgreSQL

Here are the schema files for OAuth2 tables for PostgreSQL that I modified and used in this article.

pg-oauth2-registered-client-schema.sql
CREATE TABLE oauth2_registered_client (
  id varchar(100) NOT NULL,
  client_id varchar(100) NOT NULL,
  client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
  client_secret varchar(200) DEFAULT NULL,
  client_secret_expires_at timestamp DEFAULT NULL,
  client_name varchar(200) NOT NULL,
  client_authentication_methods varchar(1000) NOT NULL,
  authorization_grant_types varchar(1000) NOT NULL,
  redirect_uris varchar(1000) DEFAULT NULL,
  post_logout_redirect_uris varchar(1000) DEFAULT NULL,
  scopes varchar(1000) NOT NULL,
  client_settings varchar(2000) NOT NULL,
  token_settings varchar(2000) NOT NULL,
  PRIMARY KEY (id)
);
pg-oauth2-authorization-consent-schema.sql
CREATE TABLE oauth2_authorization_consent (
  registered_client_id varchar(100) NOT NULL,
  principal_name varchar(200) NOT NULL,
  authorities varchar(1000) NOT NULL,
  PRIMARY KEY (registered_client_id, principal_name)
);
pg-oauth2-authorization-schema.sql
CREATE TABLE oauth2_authorization (
  id varchar(100) NOT NULL,
  registered_client_id varchar(100) NOT NULL,
  principal_name varchar(200) NOT NULL,
  authorization_grant_type varchar(100) NOT NULL,
  authorized_scopes varchar(1000) DEFAULT NULL,
  attributes text DEFAULT NULL,
  state varchar(500) DEFAULT NULL,
  authorization_code_value text DEFAULT NULL,
  authorization_code_issued_at timestamp DEFAULT NULL,
  authorization_code_expires_at timestamp DEFAULT NULL,
  authorization_code_metadata text DEFAULT NULL,
  access_token_value text DEFAULT NULL,
  access_token_issued_at timestamp DEFAULT NULL,
  access_token_expires_at timestamp DEFAULT NULL,
  access_token_metadata text DEFAULT NULL,
  access_token_type varchar(100) DEFAULT NULL,
  access_token_scopes varchar(1000) DEFAULT NULL,
  oidc_id_token_value text DEFAULT NULL,
  oidc_id_token_issued_at timestamp DEFAULT NULL,
  oidc_id_token_expires_at timestamp DEFAULT NULL,
  oidc_id_token_metadata text DEFAULT NULL,
  refresh_token_value text DEFAULT NULL,
  refresh_token_issued_at timestamp DEFAULT NULL,
  refresh_token_expires_at timestamp DEFAULT NULL,
  refresh_token_metadata text DEFAULT NULL,
  user_code_value text DEFAULT NULL,
  user_code_issued_at timestamp DEFAULT NULL,
  user_code_expires_at timestamp DEFAULT NULL,
  user_code_metadata text DEFAULT NULL,
  device_code_value text DEFAULT NULL,
  device_code_issued_at timestamp DEFAULT NULL,
  device_code_expires_at timestamp DEFAULT NULL,
  device_code_metadata text DEFAULT NULL,
  PRIMARY KEY (id)
);

Now that we have the schema files for the users, authorities, and OAuth2 tables in PostgreSQL, we can apply them to Spring Authorization Server.

Create uuid-ossp extension

Because the primary key of oauth2_registered_client is a text type, we need to create the uuid-ossp extension to generate a UUID for the primary key.

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

Insert data into the tables

We need to insert data into the users and authorities tables to authenticate users.

Users and Authorities

insert into users(username, password, enabled)
values
    ('nsa2admin', '{noop}password', true),
    ('nsa2user', '{noop}password', true);

insert into authorities(username, authority) values
     ('nsa2admin', 'ROLE_NSA2_ADMIN'),
     ('nsa2admin', 'ROLE_NSA2_USER'),
     ('nsa2user', 'ROLE_NSA2_USER');

If you want to keep the password encrypted, you can use the bcrypt encoder to encode the password and then insert it into the users table as shown below.

insert into users(username, password, enabled)
values
    ('nsa2admin', '{bcrypt}$2a$10$Q5LFmz17lVVmPmtyeb.iVOOMmO13x08lDxH3n.lBIcF490JcYn69e', true),
    ('nsa2user', '{bcrypt}$2a$10$D2Yr.nuSq5S5IHmR7I4n.OcMiPxtg1OMy9TxxAw5FPcMPM1kxy14m', true);

Registered Client

Here is an example of inserting data into the oauth2_registered_client table.

INSERT INTO public.oauth2_registered_client
(id, client_id, client_id_issued_at, client_secret, client_secret_expires_at,
 client_name, client_authentication_methods, authorization_grant_types, redirect_uris,
 post_logout_redirect_uris, scopes, client_settings, token_settings)
VALUES ('a6bb6993-c5ed-4ee7-8af7-fd3699574ccc',
        'nsa2',
        now(),
        '{bcrypt}$2a$10$WksnCewKO8wpUCEyN7B1BuN0EqqLTp3KmWz0EMC1jYawLNCl4wBBS',
        null,
        'NSA2 OAuth 2.0 Client',
        'client_secret_basic',
        'refresh_token,client_credentials,authorization_code',
        'http://nsa2-gateway:8080/login/oauth2/code/nsa2',
        'http://nsa2-gateway:8080/logged-out',
        'openid,profile,nsa2.user.all,nsa2.user.read,nsa2.user.write,nsa2.admin',
        '{"@class":"java.util.Collections$UnmodifiableMap","settings.client.require-proof-key":false,"settings.client.require-authorization-consent":false}',
        '{"@class":"java.util.Collections$UnmodifiableMap","settings.token.reuse-refresh-tokens":true,"settings.token.x509-certificate-bound-access-tokens":false,"settings.token.id-token-signature-algorithm":["org.springframework.security.oauth2.jose.jws.SignatureAlgorithm","RS256"],"settings.token.access-token-time-to-live":["java.time.Duration",300.000000000],"settings.token.access-token-format":{"@class":"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat","value":"self-contained"},"settings.token.refresh-token-time-to-live":["java.time.Duration",3600.000000000],"settings.token.authorization-code-time-to-live":["java.time.Duration",300.000000000],"settings.token.device-code-time-to-live":["java.time.Duration",300.000000000]}')

For client_settings and token_settings, you can use the following JSON format of Map.

Here is an example of inserting data into the oauth2_authorization_consent table.

INSERT INTO public.oauth2_authorization_consent
(registered_client_id, principal_name, authorities)
VALUES
    ('a6bb6993-c5ed-4ee7-8af7-fd3699574ccc', 'nsa2user',
     'SCOPE_openid,SCOPE_profile,SCOPE_nsa2.user.all'),
    ('a6bb6993-c5ed-4ee7-8af7-fd3699574ccc', 'nsa2admin',
     'SCOPE_openid,SCOPE_profile,SCOPE_nsa2.user.all, SCOPE_nsa2.admin');

Authorization

No need to insert data into the oauth2_authorization table as it will be created when the user logs in.

Update Spring Authorization Server

We need to update the Spring Authorization Server to use the database as the backend. The source files below are the ones that I modified and used in this article.

  • application.yml

  • SecurityConfig.java

  • AuthorizationServerConfig.java

application.yml

I leave the OAuth2 configuration commented out in the application.yml file so that you can tell the difference between the configuration with the database and the configuration without the database.

spring.application.name: nsa2-auth-server

server.port: 9000

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: ${NSA2_AUTH_DB_URL:jdbc:postgresql://localhost:5432/nsa2}
    username: ${NSA2_AUTH_DB_USERNAME:nsa2}
    password: ${NSA2_AUTH_DB_PASSWORD:changeme}
  main:
    banner-mode: off

#spring.security.oauth2.authorizationserver:
#  client:
#    nsa2-client:
#      registration:
#        client-id: "nsa2"
#        client-secret: "{noop}secret"
#        client-authentication-methods:
#          - "client_secret_basic"
#        authorization-grant-types:
#          - "authorization_code"
#          - "refresh_token"
#          - "client_credentials"
#        redirect-uris:
#          - "http://127.0.0.1:8080/login/oauth2/code/nsa2"
#        post-logout-redirect-uris:
#          - "http://127.0.0.1:8080/logged-out"
#        scopes:
#          - "openid"
#          - "profile"
#          - "nsa2.user.all"
#          - "nsa2.user.read"
#          - "nsa2.user.write"
#          - "nsa2.admin"
#
#      require-authorization-consent: true
logging:
  level:
    org.springframework: INFO

management:
  endpoint.health.probes.enabled: true
  health:
    livenessstate.enabled: true
    readinessstate.enabled: true

  endpoints.web.exposure.include: #info,health,metrics,prometheus
    - info
    - health
  1. The server port is set to 9000.

  2. The datasource is set to PostgreSQL.

  3. The OAuth2 configuration is commented out.

  4. The logging level is set to INFO. Set it to DEBUG if you want to see more detailed logs.

  5. The management endpoint is enabled to be able to run on Kubernetes.

SecurityConfig.java

I deliberately keep the in-memory user details manager commented out so that you can see the difference between the in-memory user details manager and the JDBC user details manager.

SecurityConfig.java
@Configuration
@Slf4j
public class SecurityConfig {

//    (1)
//    @Bean
//    UserDetailsService inMemoryUserDetailsManager() {
//        return new InMemoryUserDetailsManager(
//                User.withUsername("nsa2user")
//                        .password("{noop}password")
//                        .roles("NSA2_USER")
//                        .build(),
//                User.withUsername("nsa2admin")
//                        .password("{noop}password")
//                        .roles("NSA2_ADMIN", "NSA2_USER")
//                        .build()
//
//        );
//    }

    (2)
    @Bean
    JdbcUserDetailsManager jdbcUserDetailsManager(DataSource dataSource) {
        return new JdbcUserDetailsManager(dataSource);
    }

    (3)
    @Bean
    UserDetailsPasswordService userDetailsPasswordService(UserDetailsManager udm) {
        return (user, newPassword) -> {
            var updated = User.withUserDetails(user)
                    .password(newPassword)
                    .build();
            udm.updateUser(updated);
            return updated;
        };
    }


    @Bean
    // @formatter:off
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        final var allowedUris = new String[] {
                "/error",
                "/actuator/health",
                "/actuator/health/liveness",
                "/actuator/health/readiness"
        };

        return http
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests.requestMatchers(allowedUris).permitAll()
                                .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults())
                .build();
    }
    // @formatter:on


}
  1. The in-memory user details manager is commented out.

  2. The JDBC user details manager is enabled.

  3. The UserDetailsPasswordService is added to update the user’s password.

  4. The security filter chain is set to allow some URIs without authentication to be able to access the health check endpoints.

UserDetailsPasswordService

When using UserDetailsPasswordService, you can update the user’s password seamlessly. For example, if the value of password is {noop}mypassword, it will be updated to the value below when users sign in.

{bcrypt}$2a$10$jnuK/QseZ0/Ns5AclK5SnuP2yVQMPDvknXM7KTVDLCHsCYuQSZGLq

AuthorizationServerConfig.java

AuthorizationServerConfig.java
@Configuration(proxyBeanMethods = false)
@Slf4j
@RequiredArgsConstructor
public class AuthorizationServerConfig {

    (1)
    // @formatter:off
    @Order(Ordered.HIGHEST_PRECEDENCE)
    @Bean
    SecurityFilterChain authorizationSecurityFilterChain(HttpSecurity http) throws Exception {

        applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());

        return http
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
                .exceptionHandling(c ->
                        c.defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))

                .formLogin(Customizer.withDefaults())
                .build();
    }
    // @formatter:on

    (2)
    @Bean
    public JdbcRegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }

    (3)
    @Bean
    public JdbcOAuth2AuthorizationService authorizationService(
            JdbcTemplate jdbcTemplate,
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    (4)
    @Bean
    public JdbcOAuth2AuthorizationConsentService authorizationConsentService(
            JdbcTemplate jdbcTemplate,
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }
}
  1. This is a typical SecurityFilterChain configuration for the Authorization Server. It is set to use the default security configuration and OIDC.

  2. The JdbcRegisteredClientRepository is added to use the JDBC backend for the registered client repository.

  3. The JdbcOAuth2AuthorizationService is added to use the JDBC backend for the OAuth2 authorization service.

  4. The JdbcOAuth2AuthorizationConsentService is added to use the JDBC backend for the OAuth2 authorization consent service.

Deploy applications on Kubernetes

Now that we have set up the database and updated the Spring Authorization Server, we can deploy the applications on Kubernetes.

To test the whole process of the Spring Authorization Server with the database, we need to deploy the following applications:

  • msa2-auth-server

  • msa2-gateway

  • msa2-resource-server-example

As always. I created a Helm chart for each application and configured Observability features to monitor the applications.

Here is an example of the deployment manifest file for the msa2-auth-server.

templates/deployment.yaml
          env:
            - name: JAVA_TOOL_OPTIONS
              value: "-javaagent:/usr/app/javaagent/opentelemetry-javaagent.jar"
            - name: OTEL_EXPORTER_OTLP_ENDPOINT
              value: "http://otel-collector:4318"
            - name: OTEL_LOGS_EXPORTER
              value: "otlp"
            - name: OTEL_TRACES_EXPORTER
              value: "otlp"
            - name: OTEL_METRICS_EXPORTER
              value: "none"
            - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK
              value: "INFO"

            - name: NSA2_AUTH_DB_URL
              valueFrom:
                secretKeyRef:
                  name: postgresql-credentials
                  key: url
            - name: NSA2_AUTH_DB_USERNAME
              valueFrom:
                secretKeyRef:
                  name: postgresql-credentials
                  key: username
            - name: NSA2_AUTH_DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgresql-credentials
                  key: password

Port forwarding

To access the applications, you need to port forward the services to your local machine.

port-forward for nsa2-auth-server
$ kubectl -n nsa2 port-forward svc/nsa2-auth-server 9000:9000
port-forward for nsa2-gateway
$ kubectl -n nsa2 port-forward svc/nsa2-gateway 8080:8080

For the msa2-resource-server-example, no port forwarding is needed as it can be accessed from the Spring Cloud Gateway.

Update /etc/hosts

To access the applications with the domain name, you need to update the /etc/hosts file.

/etc/hosts
127.0.0.1	nsa2-gateway
127.0.0.1	nsa2-auth-server

Test the applications

To ensure that the browser cache is cleared, you can use the incognito mode of the browser.

Now that we have deployed the applications, we can test the applications.

Let’s go to the browser and access the following URLs:

webbrowser 1

We can see the login page of the Spring Authorization Server with the domain name nsa2-auth-server and port 9000.

Input the username and password and click the "Log in" button.

webbrowser 2

After logging in, we can see the access token in the response.

The access token is saved in OAuth2 client which is nsa2-gateway as SecurityContext in the session. So we need Session ID to access the resource server through the gateway.

webbrowser 3

The session ID is saved in the cookie. So we need to copy the session ID from the cookie and paste it into the request header.

Access the resource server using cURL

$ curl http://nsa2-gateway:8080/resource-server/hello -I --cookie NSA2SESSION=F855460B64AC5503F5A15E950BD08CBE

HTTP/1.1 200
cache-control: no-cache, no-store, max-age=0, must-revalidate
date: Mon, 30 Sep 2024 18:53:16 GMT
expires: 0
pragma: no-cache
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 0
Content-Type: application/json
Transfer-Encoding: chunked

Access the resource server using Postman

Need to set the cookie in the Postman Cookie Manager.

postman cookie manager

After setting the cookie, we can access the resource server using Postman.

postman request 1

Now we can call the resource server using session ID cookie in the request header.

Distributed Tracing

We can monitor the applications using distributed tracing. I used Jaeger as the distributed tracing tool.

The screenshot below depicts that nsa2-gateway requests the access token from nsa2-auth-server. And then calls the resource server using the access token.

jaeger 1
Figure 1. When USER SESSION has not been created.

We can see that nsa2-auth-server made some requests to the database to authenticate the user and authorize the client.

In contrast, the nsa2-gateway directly communicates with the resource server using the access token, without needing to contact the authorization server.

jaeger 2
Figure 2. When USER SESSION has been created.

The image shows that the nsa2-gateway manages access tokens and refresh tokens in the session. So it does not need to request the access token from the authorization server.

Conclusion

In this article, we set up a Spring Authorization Server using a database as the backend. We used PostgreSQL as the database and set up the necessary tables for the Spring Authorization Server.