Back to Blog
Bojan Coric

2 minutes read

Exploring the Spring Authorization Server

ProductDock

Bojan Ćorić, our software engineer, delivered a talk at the year-end conference titled “Exploring the Spring Authorization Server.” He explored the functionalities of a relatively new authorization server from the Spring ecosystem, offering a potential solution tailored to your requirements.

Spring Authorization Server allows us to build a fully customizable OAuth2 and OpenID Authorization server. In this talk, we will customize the authorization server using common extension points while following best practices.

Spring Authorization Server in practice

Spring Authorization Server is a powerful and flexible identity and access management solution built on top of the Spring ecosystem, specifically designed to simplify the process of securing your applications and APIs. It provides robust authentication and authorization mechanisms, allowing you to control access to your resources effectively. It implements the OAuth 2.1 and OpenId Connect 1.0, simplifying integration with systems compliant with either of these specifications.

Minimal configuration

It works out of the box with minimal configuration. If you go to start.spring.io to generate a new project, the only dependency that you need is the OAuth2 Authorization Server (link to generate project).

The following configuration is required when starting the application as an authorization server:

#  https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.security.oauth2.authorization-server
spring:
 security:
   user:
     name: user
     password: password
   oauth2:
     authorizationserver:
       client:
         productdock:
           registration:
             client-id: "productdock"
             client-secret: "{bcrypt}$2a$10$aXhyiUX2incAVeTnN85tueTYgUkqH/hh86i4Ex9FaeoRAyTU71n4i" # bcrypt of productdock
             client-authentication-methods:
               - "client_secret_basic"
             authorization-grant-types:
               - "authorization_code"
               - "refresh_token"
               - "client_credentials"
             redirect-uris:
               - "http://127.0.0.1/login/oauth2/code/productdock"
             scopes:
               - "openid"
               - "profile"
               - "email"
               - "phone"
               - "address"
           require-authorization-consent: true
server:
 port: 9090

Explicit configuration

In case you don’t want to configure this via yml file, you can do that explicitly from your code by defining the following beans:

@Bean
public UserDetailsService userDetailsService() {
   return new InMemoryUserDetailsManager(
           User.builder().username("user").password("{bcrypt}$2a$10$K4nPRbjfGTPLmrRu07PMT.h6TWPJvSGuZRUzATYyDHJ1JZBsRrNoi").build()
   );
}

@Bean
public DaoAuthenticationProvider authenticationProvider(UserDetailsService userDetailsService) {
   var authProvider = new DaoAuthenticationProvider();
   authProvider.setUserDetailsService(userDetailsService);
   return authProvider;
}

// https://docs.spring.io/spring-authorization-server/reference/getting-started.html#defining-required-components
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
   OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
   http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
           .oidc(Customizer.withDefaults());
   http.exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(
                   new LoginUrlAuthenticationEntryPoint("/login")))
           .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));


   return http.build();
}


@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
   http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
           .formLogin(Customizer.withDefaults());


   return http.build();
}


@Bean
public RegisteredClientRepository registeredClientRepository() {
   RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
           .clientId("productdock")
           .clientSecret("{bcrypt}$2a$10$0lAOvcI202G9go0h7MD75OczUgjNb4cp2KlPsH13NFqQ0NtipN7dq")
           .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
           .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
           .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
           .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
           .redirectUri("http://127.0.0.1/login/oauth2/code/productdock")
           .scope(OidcScopes.OPENID)
           .scope(OidcScopes.PROFILE)
           .scope(OidcScopes.EMAIL)
           .scope(OidcScopes.PHONE)
           .scope(OidcScopes.ADDRESS)
           .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
           .build();


   return new InMemoryRegisteredClientRepository(client);
}

If you try now to access your resource server that is protected by this authorization server you will be redirected to the authorization server where you have to input your credentials. 

Steps towards production

While this is nice for demonstration purposes, it is not so useful in real-life scenarios. 

The first step to making this production-ready is to add some persistent storage so that our state is not lost when the application restarts. 

Lets update pom.xml with additional dependencies:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
   <groupId>org.postgresql</groupId>
   <artifactId>postgresql</artifactId>
   <scope>runtime</scope>
</dependency>

Update the application.yml with database credentials:

spring:
 datasource:
   username: authservice-rw
   password: authservice-rw
   url: jdbc:postgresql://localhost:5432/authservice
server:
 port: 9090

To use persistent storage instead of in-memory, update the following beans:

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

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

@Bean
public JdbcOAuth2AuthorizationConsentService jdbcOAuth2AuthorizationConsentService(
       JdbcOperations jdbcOperations, RegisteredClientRepository registeredClientRepository) {
   return new JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository);
}


@Bean
public JdbcOAuth2AuthorizationService jdbcOAuth2AuthorizationService(
       JdbcOperations jdbcOperations,
       RegisteredClientRepository registeredClientRepository) {
   return new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository);
}

Database scripts for this work

If you wonder how where to find related database scripts for this to work, it is on your classpath: 

  • classpath:org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql
  • classpath:org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql
  • classpath:org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql

Managing keys

Another important part of the authorization server is managing keys that are used to sign tokens. Here is an example of how to generate in-memory keys:

@Bean
public JWKSource<SecurityContext> jwkSource() {
   KeyPair keyPair = generateRsaKey();
   RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
   RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
   RSAKey rsaKey = new RSAKey.Builder(publicKey)
           .privateKey(privateKey)
           .keyID(UUID.randomUUID().toString())
           .build();
   JWKSet jwkSet = new JWKSet(rsaKey);
   return new ImmutableJWKSet<>(jwkSet);
}


private static KeyPair generateRsaKey() {
   KeyPair keyPair;
   try {
       KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
       keyPairGenerator.initialize(2048);
       keyPair = keyPairGenerator.generateKeyPair();
   }
   catch (Exception ex) {
       throw new IllegalStateException(ex);
   }
   return keyPair;
}

Note that this is not a secure way to store your keys. Instead, use secure storage mechanisms such as environment variables, encrypted files, or key vault services such as HashiCorp Vault.

Next steps

With these key concepts, we built a usable authorization server that can be used in real-life scenarios. You can build an admin console to manage clients, users, sessions, etc. You can extend it with your chosen social login provider, such as Google, GitHub, Okta, etc. You can add two-factor authentication.

New Spring Authorization server solution

Bojan explored the functionalities of a relatively new authorization server from the Spring ecosystem. He concluded that the Spring Authorization Server allows us to build a fully customizable OAuth2 and OpenID Authorization server and customize it using common extension points while following best practices. To check how to protect your resources using the Spring Authorization Server check our GitHub repository


Explore our blog section for exciting topics we’ll share with you soon.

ProductDock

ProductDock


Related posts.