Skip to content

Clock configuration in Spring Authorization Server incomplete for OIDC #18379

@lnhrdt

Description

@lnhrdt

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: Support injecting clock into token generation code #18017 (comment)

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    in: oauth2An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose)type: enhancementA general enhancement

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions