spring-projects / spring-projects/spring-security

Allow to configure a clock for Authorization Server

Open
#18,379 6 comments 6 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

in: oauth2 type: enhancement
Dominant language
Java
Stars
9.6k
Forks
6.3k
Avg merge
2d 11h
Merged PRs (30d)
52

Description

Hi @jgrandja,

First, thank you for the work that went into commit 469ed09. My team and I lost track of updates across the issues and the project's migration to Spring Security, so we didn't notice the setClock additions until last week. We just finished a pass at using these new setClock methods to help us improve the automated testing of our system's authentication mechanisms and we ran into some issues. Since you previously asked for feedback on our use case, here it is!

Our use case

We are validating time-based behavior in a multi-service system (Authorization Server and OAuth2 client services) using integration tests. To do this, we introduce a controllable notion of "now" by configuring a coordinated, fixed Clock across all services when running under test.

What we tried

The most complete configuration we could find to set the clock in an OIDC authorization server looks like this:

import com.nimbusds.jose.jwk.source.JWKSource
import com.nimbusds.jose.proc.SecurityContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.oauth2.core.OAuth2Token
import org.springframework.security.oauth2.jwt.*
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings
import org.springframework.security.oauth2.server.authorization.token.*
import java.time.Clock

@Configuration
class TokenConfiguration {

  @Bean
  fun tokenGenerator(
    jwkSource: JWKSource<SecurityContext>,
    tokenClaimCustomizer: OAuth2TokenCustomizer<JwtEncodingContext>,
    clock: Clock,
  ): OAuth2TokenGenerator<out OAuth2Token> {
    val jwtEncoder = NimbusJwtEncoder(jwkSource)
    val jwtGenerator = JwtGenerator(jwtEncoder)
    jwtGenerator.setJwtCustomizer(tokenClaimCustomizer)
    jwtGenerator.setClock(clock)

    val accessTokenGenerator = OAuth2AccessTokenGenerator()
    accessTokenGenerator.setClock(clock)

    val refreshTokenGenerator = OAuth2RefreshTokenGenerator()
    refreshTokenGenerator.setClock(clock)

    return DelegatingOAuth2TokenGenerator(
      jwtGenerator,
      accessTokenGenerator,
      refreshTokenGenerator,
    )
  }

  @Bean
  fun jwtDecoder(
    jwkSource: JWKSource<SecurityContext>,
    authorizationServerSettings: AuthorizationServerSettings,
    clock: Clock,
  ): JwtDecoder {
    val issuerValidator = JwtIssuerValidator(authorizationServerSettings.issuer)
    val timestampValidator = JwtTimestampValidator()
    timestampValidator.setClock(clock)
    val jwtValidator = JwtValidators.createDefaultWithValidators(
      issuerValidator,
      timestampValidator,
    )

    val jwtDecoder = NimbusJwtDecoder.withJwkSource(jwkSource).build()
    jwtDecoder.setJwtValidator(jwtValidator)
    return jwtDecoder
  }
}

with a corresponding SecurityFilterChain

@Configuration
class SecurityConfiguration {
  @Bean
  @Order(1)
  fun authorizationServerSecurityFilterChain(
    http: HttpSecurity,
  ): SecurityFilterChain {
    http.oauth2AuthorizationServer { authorizationServer ->
      authorizationServer.oidc {}
    }

    // ...

    return http.build()
  }
}

In our client services performing OIDC login with the authorization server, we configured a matching clock setup like this:

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenValidator
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator
import org.springframework.security.oauth2.jwt.JwtTimestampValidator
import java.time.Clock

@Configuration
class TokenConfiguration {

  @Bean
  fun oidcIdTokenDecoderFactory(clock: Clock): OidcIdTokenDecoderFactory {
    val factory = OidcIdTokenDecoderFactory()

    factory.setJwtValidatorFactory { clientRegistration ->
      val jwtTimestampValidator = JwtTimestampValidator()
      jwtTimestampValidator.setClock(clock)

      val oidcIdTokenValidator = OidcIdTokenValidator(clientRegistration)
      oidcIdTokenValidator.setClock(clock)

      DelegatingOAuth2TokenValidator(jwtTimestampValidator, oidcIdTokenValidator)
    }

    return factory
  }
}

What happened

The OAuth2 login seems to work but the subsequent call to the OIDC UserInfo endpoint is rejected. Our debugging suggests it's because OidcUserInfoAuthenticationProvider uses OAuth2Authorization to validate the token but OAuth2Authorization validates the token with hardcoded calls to Instant.now().

While investigating, we searched the sources of spring-security-oauth2-authorization-server-7.0.2.jar for remaining uses of Instant.now() (not using a Clock) and found instances in the following classes:

  • OAuth2Authorization
  • ClientSecretAuthenticationProvider
  • OAuth2AuthorizationCodeGenerator
  • OAuth2AuthorizationCodeRequestAuthenticationProvider
  • OAuth2DeviceAuthorizationRequestAuthenticationProvider
  • OAuth2PushedAuthorizationRequestUri
  • X509SelfSignedCertificateVerifier
  • JdbcRegisteredClientRepository
  • OAuth2ClientRegistrationRegisteredClientConverter
  • OidcClientRegistrationRegisteredClientConverter
  • OAuth2PushedAuthorizationRequestEndpointFilter

The most problematic for us is OAuth2Authorization, since its OAuth2Authorization.Token::isActive method is used by OidcUserInfoAuthenticationProvider when validating the token provided for the UserInfo endpoint.

What we are asking / suggesting

At this point, the improvement in 469ed09 helps with token generation, but not enough to allow us to "run the authorization server with a custom clock."

This leaves us with two questions:

  1. Could you continue the work in 469ed09 throughout the rest of Spring Authorization Server to allow the Clock to be configured?
  2. Or could you revisit the idea I suggested last year that Spring Authorization Server should consume Clock beans the way Modulith Moments or Micrometer do: https://github.com/spring-projects/spring-security/issues/18017#issuecomment-3378617754

Thanks again for the work you (and @AlessandroMinoccheri) have already done here. We realize this is a non-trivial design problem, but hopefully this feedback helps clarify where the remaining gaps are for us (or anyone trying to test Spring Authorization Server in this way).

Happy to provide more information or feedback.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with OAuth2Authorization.Token::isActive and OidcUserInfoAuthenticationProvider, then review the listed Spring Authorization Server classes for remaining Instant.now() calls. Trace how the existing setClock configuration is applied to token generation and validation. Done means the authorization server can use a coordinated custom Clock, including successful UserInfo token validation, with coverage for the affected paths.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring
Domain
authentication, authorization, backend, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.