Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.io.Serial;
import java.io.Serializable;
import java.time.Clock;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -238,12 +239,16 @@ public static Builder withRegisteredClient(RegisteredClient registeredClient) {
public static Builder from(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
Assert.notNull(authorization.tokens, "tokens cannot be null");
return new Builder(authorization.getRegisteredClientId()).id(authorization.getId())
Builder builder = new Builder(authorization.getRegisteredClientId()).id(authorization.getId())
.principalName(authorization.getPrincipalName())
.authorizationGrantType(authorization.getAuthorizationGrantType())
.authorizedScopes(authorization.getAuthorizedScopes())
.tokens(authorization.tokens)
.attributes((attrs) -> attrs.putAll(authorization.getAttributes()));
if (!authorization.tokens.isEmpty()) {
builder.clock(authorization.tokens.values().iterator().next().getClock());
}
return builder;
}

/**
Expand Down Expand Up @@ -273,13 +278,16 @@ public static class Token<T extends OAuth2Token> implements Serializable {

private final Map<String, Object> metadata;

private transient @Nullable Clock clock;

protected Token(T token) {
this(token, defaultMetadata());
}

protected Token(T token, Map<String, Object> metadata) {
this.token = token;
this.metadata = Collections.unmodifiableMap(metadata);

}

/**
Expand All @@ -304,7 +312,7 @@ public boolean isInvalidated() {
* @return {@code true} if the token has expired, {@code false} otherwise
*/
public boolean isExpired() {
return getToken().getExpiresAt() != null && Instant.now().isAfter(getToken().getExpiresAt());
return getToken().getExpiresAt() != null && this.getClock().instant().isAfter(getToken().getExpiresAt());
}

/**
Expand All @@ -317,7 +325,7 @@ public boolean isBeforeUse() {
if (!CollectionUtils.isEmpty(getClaims())) {
notBefore = (Instant) getClaims().get("nbf");
}
return notBefore != null && Instant.now().isBefore(notBefore);
return notBefore != null && this.getClock().instant().isBefore(notBefore);
}

/**
Expand Down Expand Up @@ -362,6 +370,15 @@ protected static Map<String, Object> defaultMetadata() {
return metadata;
}

/**
* Returns the {@link Clock} used for computing time-based checks.
* @return the {@link Clock} to use, or {@link Clock#systemDefaultZone()} if not
* set
*/
protected Clock getClock() {
return (this.clock != null) ? this.clock : Clock.systemDefaultZone();
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
Expand Down Expand Up @@ -396,6 +413,8 @@ public static class Builder {

private @Nullable Set<String> authorizedScopes;

private transient @Nullable Clock clock;

private Map<Class<? extends OAuth2Token>, Token<?>> tokens = new HashMap<>();

private final Map<String, Object> attributes = new HashMap<>();
Expand Down Expand Up @@ -491,7 +510,9 @@ public <T extends OAuth2Token> Builder token(T token, Consumer<Map<String, Objec
}
metadataConsumer.accept(metadata);
Class<? extends OAuth2Token> tokenClass = token.getClass();
this.tokens.put(tokenClass, new Token<>(token, metadata));
Token<T> tokenWithClock = new Token<>(token, metadata);
tokenWithClock.clock = this.clock;
this.tokens.put(tokenClass, tokenWithClock);
return this;
}

Expand Down Expand Up @@ -570,11 +591,32 @@ public OAuth2Authorization build() {
authorization.authorizationGrantType = this.authorizationGrantType;
authorization.authorizedScopes = Collections.unmodifiableSet(!CollectionUtils.isEmpty(this.authorizedScopes)
? new HashSet<>(this.authorizedScopes) : new HashSet<>());

if (this.clock != null && this.tokens != null) {
for (Token<?> token : this.tokens.values()) {
token.clock = this.clock;
}
}

authorization.tokens = Collections.unmodifiableMap(this.tokens);
authorization.attributes = Collections.unmodifiableMap(this.attributes);
return authorization;
}

/**
* Sets the {@link Clock} used for determining if a {@link Token} is
* {@link Token#isExpired() expired} or {@link Token#isBeforeUse() before use}. If
* not set, defaults to {@link Clock#systemDefaultZone()}.
* @param clock the {@link Clock} to use
* @return the {@link Builder} for further configuration
* @since 7.1.1
*/
public Builder clock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
return this;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationTokenMixin;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;

/**
Expand Down Expand Up @@ -84,6 +86,7 @@ public void setupModule(SetupContext context) {
context.setMixIn(SignatureAlgorithm.class, JwsAlgorithmMixin.class);
context.setMixIn(MacAlgorithm.class, JwsAlgorithmMixin.class);
context.setMixIn(OAuth2TokenFormat.class, OAuth2TokenFormatMixin.class);
context.setMixIn(OAuth2Authorization.Token.class, OAuth2AuthorizationTokenMixin.class);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
Expand Down Expand Up @@ -97,6 +98,7 @@ public void setupModule(SetupContext context) {
context.setMixInAnnotations(MacAlgorithm.class, JwsAlgorithmMixin.class);
context.setMixInAnnotations(OAuth2TokenFormat.class, OAuth2TokenFormatMixin.class);
context.setMixInAnnotations(String[].class, StringArrayMixin.class);
context.setMixInAnnotations(OAuth2Authorization.Token.class, OAuth2AuthorizationTokenMixin.class);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.oauth2.server.authorization.jackson2;

import java.time.Clock;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.jspecify.annotations.Nullable;

import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.jackson.OAuth2AuthorizationServerJacksonModule;

/**
* This mixin class is used to serialize/deserialize {@link OAuth2Authorization.Token}.
*
* @author Dawid Szczepaniak
* @since 7.1.1
* @see OAuth2Authorization.Token
* @see OAuth2AuthorizationServerJackson2Module
* @see OAuth2AuthorizationServerJacksonModule
*/
@SuppressWarnings("removal")
public abstract class OAuth2AuthorizationTokenMixin {

@JsonIgnore
@Nullable private Clock clock;

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@

package org.springframework.security.oauth2.server.authorization;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -138,4 +143,124 @@ public void buildWhenAllAttributesAreProvidedThenAllAttributesAreSet() {
assertThat(authorization.getRefreshToken().getToken()).isEqualTo(REFRESH_TOKEN);
}

@Test
public void clockWhenNullThenThrowIllegalArgumentException() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT).clock(null))
.withMessage("clock cannot be null");
}

@Test
public void isExpiredWhenClockBeforeExpiresAtThenActive() {
Instant fixedNow = Instant.parse("2024-01-01T12:00:00Z");
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token",
fixedNow.minusSeconds(60), fixedNow.plusSeconds(3600));
Clock clock = Clock.fixed(fixedNow, ZoneOffset.UTC);

OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.principalName(PRINCIPAL_NAME)
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
.clock(clock)
.accessToken(accessToken)
.build();

OAuth2Authorization.Token<OAuth2AccessToken> token = authorization.getAccessToken();
assertThat(token.isExpired()).isFalse();
assertThat(token.isActive()).isTrue();
}

@Test
public void isExpiredWhenClockAfterExpiresAtThenInactive() {
Instant fixedNow = Instant.parse("2024-01-01T12:00:00Z");
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token",
fixedNow.minusSeconds(7200), fixedNow.minusSeconds(3600));
Clock clock = Clock.fixed(fixedNow, ZoneOffset.UTC);

OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.principalName(PRINCIPAL_NAME)
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
.clock(clock)
.accessToken(accessToken)
.build();

OAuth2Authorization.Token<OAuth2AccessToken> token = authorization.getAccessToken();
assertThat(token.isExpired()).isTrue();
assertThat(token.isActive()).isFalse();
}

@Test
public void isBeforeUseWhenClockBeforeNbfThenInactive() {
Instant fixedNow = Instant.parse("2024-01-01T12:00:00Z");
Instant notBefore = fixedNow.plusSeconds(600);
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token",
fixedNow, fixedNow.plusSeconds(3600));
Map<String, Object> claims = new HashMap<>();
claims.put("nbf", notBefore);
Clock clock = Clock.fixed(fixedNow, ZoneOffset.UTC);

OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.principalName(PRINCIPAL_NAME)
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
.clock(clock)
.token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, claims))
.build();

OAuth2Authorization.Token<OAuth2AccessToken> token = authorization.getAccessToken();
assertThat(token.isBeforeUse()).isTrue();
assertThat(token.isActive()).isFalse();
}

@Test
public void isBeforeUseWhenClockAfterNbfThenActive() {
Instant fixedNow = Instant.parse("2024-01-01T12:00:00Z");
Instant notBefore = fixedNow.minusSeconds(600);
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token",
fixedNow.minusSeconds(1200), fixedNow.plusSeconds(3600));
Map<String, Object> claims = new HashMap<>();
claims.put("nbf", notBefore);
Clock clock = Clock.fixed(fixedNow, ZoneOffset.UTC);

OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.principalName(PRINCIPAL_NAME)
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
.clock(clock)
.token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, claims))
.build();

OAuth2Authorization.Token<OAuth2AccessToken> token = authorization.getAccessToken();
assertThat(token.isBeforeUse()).isFalse();
assertThat(token.isActive()).isTrue();
}

@Test
public void whenClockNotConfiguredThenSystemDefaultZoneUsed() {
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token",
Instant.now().minusSeconds(120), Instant.now().minusSeconds(60));

OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.principalName(PRINCIPAL_NAME)
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
.accessToken(accessToken)
.build();

assertThat(Objects.requireNonNull(authorization.getAccessToken()).isExpired()).isTrue();
}

@Test
public void whenClockConfiguredAfterTokenAddedThenStillAppliedOnBuild() {
Instant fixedNow = Instant.parse("2024-01-01T12:00:00Z");
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token",
fixedNow.minusSeconds(7200), fixedNow.minusSeconds(3600));
Clock clock = Clock.fixed(fixedNow, ZoneOffset.UTC);

OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.principalName(PRINCIPAL_NAME)
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
.accessToken(accessToken)
.clock(clock)
.build();

assertThat(authorization.getAccessToken().isExpired()).isTrue();
}

}