-
Notifications
You must be signed in to change notification settings - Fork 6.2k
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:
- Could you continue the work in 469ed09 throughout the rest of Spring Authorization Server to allow the Clock to be configured?
- 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.