BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News Spring Authorization Server 1.0 Provides Oauth 2.1 and OpenID Connect 1.0 Implementations

Spring Authorization Server 1.0 Provides Oauth 2.1 and OpenID Connect 1.0 Implementations

More than two-and-a-half years after being introduced to the Java community, VMWare has released Spring Authorization Server 1.0. Built on top of Spring Security, the Spring Authorization Server project supports the creation of OpenID Connect 1.0 Identity Providers and OAuth 2.1 Authorization Servers. The project supersedes the Spring Security OAuth project which is no longer maintained.

Spring Authorization Server is also based on Spring Framework 6.0 and requires Java 17 as a minimal version. The project supports Authorization Grants, Token Format, Client Authentication and Protocol Endpoints as described on the Feature List.

An example application is used to explain the basic configuration for a Spring Boot application created with Spring Initializr. The example application is REST based and requires the spring-boot-starter-web dependency in the pom.xml:

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

In order to demonstrate the login functionality, consider this example in which a REST endpoint is created:

@RestController
public class TimeController {

    @GetMapping("/time")
    public String retrieveTime() {
        DateTimeFormatter dateTimeFormatter =    
            DateTimeFormatter.ofPattern("HH:mm:ss");
        LocalTime localTime = LocalTime.now();
        return dateTimeFormatter.format(localTime);
    }
}

A basic Spring Boot application class is used to start the application with the previously created REST endpoint:

@SpringBootApplication
public class TimeApplication {

    public static void main(String[] args) {
   	 SpringApplication.run(TimeApplication.class, args);
    }
}

After starting the application and opening the url http://localhost:8080/time, the time is shown:

21:00:34

Now the Spring Authorization Server dependency is added:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>1.0.0</version>
</dependency>

When starting the application again, the password is logged, for example:

Using generated security password: d73d5904-25a1-44ed-91e1-a32c4c5aedb8

Now when browsing to http://localhost:8080/time the request is redirected to http://localhost:8080/login and shows the following page:

The default username user and the logged password may be used to login after which the request is redirected to http://localhost:8080/time?continue and the time is displayed again.

The Developing Your First Application documentation details several @Bean components, required for Spring Authorization Server, which should be defined in a class annotated with @Configuration. The first bean is used to define the OAuth2 Protocol Endpoint:

@Bean
@Order(1)
public SecurityFilterChain protocolFilterChain(HttpSecurity http)
    throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    http
        .exceptionHandling((exceptions) -> exceptions
        .authenticationEntryPoint(
            new LoginUrlAuthenticationEntryPoint("/login"))
        )
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
        .getConfigurer(OAuth2AuthorizationServerConfigurer.class)
        .oidc(Customizer.withDefaults());

        return http.build();
}

The second bean is used to define the Spring Security Authentication:

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

    return http.build();
}

A proper solution to store users should be used for real products, however this simplified example stores the user james with the password gosling in memory:

@Bean
public UserDetailsService userDetailsService() {
    UserDetails userDetails = User.withDefaultPasswordEncoder()
        .username("james")
        .password("gosling")
        .roles("FOUNDER")
        .build();

    return new InMemoryUserDetailsManager(userDetails);
}

New clients are registered in memory with the RegisteredClientRepository:

@Bean
public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient registeredClient =            
        RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("id")
        .clientSecret("secret")
        .clientAuthenticationMethod(
            ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
        .redirectUri(
          "http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
        .redirectUri("http://127.0.0.1:8080/authorized")
        .scope(OidcScopes.OPENID)
        .scope(OidcScopes.PROFILE)
        .scope("message.read")
        .scope("message.write")
        .clientSettings(
            ClientSettings.builder()
            .requireAuthorizationConsent(true).build())
        .build();

	return new InMemoryRegisteredClientRepository(registeredClient);
}

Access tokens are signed with the help of the following bean, by using com.nimbusds.jose.jwk.RSAKey and not java.security.interfaces.RSAKey:

@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;
}

The JwtDecoder is used to decode signed access tokens, by using com.nimbusds.jose.proc.SecurityContext and not org.springframework.security.core.context.SecurityContext:

@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
    return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}

Lastly, the AuthorizationServerSettings is used to configure the OAuth2 authorization server:

@Bean
public AuthorizationServerSettings authorizationServerSettings() {
    return AuthorizationServerSettings.builder().build();
}

Now when browsing to http://localhost:8080/time, the user james with the password gosling can be used to view the current time. After following these steps, the application may be extended to use various OAuth2 and OpenID Connect 1.0 features such as tokens.

Spring Authorization Server is explained in detail by several videos, for example by Joe Grandja, core committer on the Spring Security team, who presented Getting Started with Spring Authorization Server at the San Francisco JUG and Laurentiu Spilca, author of Spring Security in Action who presented Implementing an OAuth 2 authorization server with Spring Security at Spring I/O.

The project is released based on the VMware Tanzu Open Source Software Support policy which means major releases are supported for up to three years. Alternatively VMware offers commercial 24/7 support.

More information can be found in the Getting Started guide, the Reference documentation and the examples on GitHub.

About the Author

Rate this Article

Adoption
Style

BT