Return to site

🔐🌱 BUILDING A SPRING AUTHORIZATION SERVER: A PRACTICAL STARTING POINT

· spring

Implementing your own authorization server is more than adding a login page.

You need to understand clients, OAuth 2.0 grants, token signing, refresh-token management and the security responsibilities that come with them.

Section image

🔸 TL;DR

▪️ Use Authorization Code with PKCE for public applications.

▪️ Use Client Credentials for service-to-service access.

▪️ Sign JWTs and validate their signature, issuer, audience and expiration.

▪️ Use short-lived access tokens and securely managed refresh tokens.

Here is a simplified starting point with Spring Authorization Server. 👇

🔸 1. CONFIGURE SPRING AUTHORIZATION SERVER

@Bean
@Order(1)
SecurityFilterChain authorizationServer(HttpSecurity http)
        throws Exception {

    var configurer =
        OAuth2AuthorizationServerConfigurer.authorizationServer();

    http
        .securityMatcher(configurer.getEndpointsMatcher())
        .with(configurer, server ->
            server.oidc(Customizer.withDefaults()));

    return http.build();
}

@Bean
@Order(2)
SecurityFilterChain applicationSecurity(HttpSecurity http)
        throws Exception {

    return http
        .authorizeHttpRequests(auth ->
            auth.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults())
        .build();
}

The first filter chain secures OAuth 2.0 and OpenID Connect endpoints. The second one provides user authentication, which is required when using the Authorization Code flow.

🔸 2. ENABLE AUTHORIZATION CODE WITH PKCE AND CLIENT CREDENTIALS

RegisteredClient publicClient =
    RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("frontend")
        .clientAuthenticationMethod(
            ClientAuthenticationMethod.NONE)
        .authorizationGrantType(
            AuthorizationGrantType.AUTHORIZATION_CODE)
        .redirectUri("https://app.example.com/callback")
        .scope("orders.read")
        .clientSettings(ClientSettings.builder()
            .requireProofKey(true)
            .requireAuthorizationConsent(true)
            .build())
        .build();

RegisteredClient serviceClient =
    RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("order-service")
        .clientSecret(passwordEncoder.encode("change-me"))
        .clientAuthenticationMethod(
            ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(
            AuthorizationGrantType.CLIENT_CREDENTIALS)
        .scope("orders.write")
        .build();

Authorization Code with PKCE is designed for clients that cannot safely keep a secret, such as browser or mobile applications. Client Credentials is for machine-to-machine communication. It represents the client itself—not an authenticated end user. 🤖

🔸 3. SIGN ACCESS TOKENS AS JSON WEB TOKENS

@Bean
JWKSource<SecurityContext> jwkSource() throws Exception {
    KeyPairGenerator generator =
        KeyPairGenerator.getInstance("RSA");

    generator.initialize(2048);
    KeyPair keyPair = generator.generateKeyPair();

    RSAKey rsaKey = new RSAKey.Builder(
            (RSAPublicKey) keyPair.getPublic())
        .privateKey((RSAPrivateKey) keyPair.getPrivate())
        .keyID(UUID.randomUUID().toString())
        .build();

    JWKSet jwkSet = new JWKSet(rsaKey);

    return (selector, context) ->
        selector.select(jwkSet);
}

@Bean
JwtDecoder jwtDecoder(
        JWKSource<SecurityContext> jwkSource) {

    return OAuth2AuthorizationServerConfiguration
        .jwtDecoder(jwkSource);
}

The private key signs the JWT, while resource servers use the corresponding public key to verify its integrity. ⚠️ Generating a new key at every startup is acceptable only for a demo. Production systems need persistent keys, secure storage and key rotation.

🔸 4. CONFIGURE THE REFRESH TOKEN FLOW

RegisteredClient webClient =
    RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("web-client")
        .clientSecret(passwordEncoder.encode("change-me"))
        .clientAuthenticationMethod(
            ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(
            AuthorizationGrantType.AUTHORIZATION_CODE)
        .authorizationGrantType(
            AuthorizationGrantType.REFRESH_TOKEN)
        .redirectUri(
            "https://app.example.com/login/oauth2/code/web-client")
        .scope("orders.read")
        .tokenSettings(TokenSettings.builder()
            .accessTokenTimeToLive(Duration.ofMinutes(10))
            .refreshTokenTimeToLive(Duration.ofDays(30))
            .reuseRefreshTokens(false)
            .build())
        .build();

The client can request a new access token without asking the user to authenticate again:

curl -u web-client:change-me \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN" \
  https://auth.example.com/oauth2/token 

Disabling refresh-token reuse enables rotation: after a successful refresh, the previous token should no longer be used.

🔸 TAKEAWAYS

▪️ An authorization server centralizes token issuance—not every authorization decision in your business domain.

▪️ PKCE protects the authorization-code exchange but does not replace HTTPS.

▪️ Client Credentials must not be used as a replacement for user authentication.

▪️ Refresh tokens require secure storage, rotation and revocation strategies.

▪️ A production deployment also needs persistent clients and authorizations, secret management, auditing, monitoring and signing-key rotation. 🛡️

Spring Authorization Server provides the building blocks.

Operating those building blocks securely remains our responsibility as developers and architects.

#Java #SpringBoot #SpringSecurity #SpringAuthorizationServer #OAuth2 #OpenIDConnect #PKCE #JWT #CyberSecurity #SoftwareSecurity #BackendDevelopment

Go further with Java certification:

Java👇

Spring👇

SpringBook👇

JavaBook👇