Skip to content

fix: use numeric JWT claims instead of ISO-8601 #4992

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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 @@ -19,7 +19,6 @@
import org.eclipse.edc.token.spi.TokenValidationRule;
import org.junit.jupiter.api.Test;

import java.sql.Date;
import java.time.Clock;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
Expand All @@ -39,7 +38,7 @@ class ExpirationIssuedAtValidationRuleTest {
@Test
void validationOk() {
var token = ClaimToken.Builder.newInstance()
.claim(EXPIRATION_TIME, Date.from(now.plusSeconds(600)))
.claim(EXPIRATION_TIME, now.plusSeconds(600).getEpochSecond())
.build();

var result = rule.checkRule(token, emptyMap());
Expand All @@ -50,7 +49,7 @@ void validationOk() {
@Test
void validationKoBecauseExpirationTimeNotRespected() {
var token = ClaimToken.Builder.newInstance()
.claim(EXPIRATION_TIME, Date.from(now.minusSeconds(10)))
.claim(EXPIRATION_TIME, now.minusSeconds(10).getEpochSecond())
.build();

var result = rule.checkRule(token, emptyMap());
Expand Down Expand Up @@ -85,8 +84,8 @@ void validationKoBecauseExpirationTimeNotProvided_doesNotAllowNull() {
@Test
void validationKoBecauseIssuedAtAfterExpires() {
var token = ClaimToken.Builder.newInstance()
.claim(EXPIRATION_TIME, Date.from(now.plusSeconds(60)))
.claim(ISSUED_AT, Date.from(now.plusSeconds(65)))
.claim(EXPIRATION_TIME, now.plusSeconds(60).getEpochSecond())
.claim(ISSUED_AT, now.plusSeconds(65).getEpochSecond())
.build();

var result = rule.checkRule(token, emptyMap());
Expand All @@ -98,8 +97,8 @@ void validationKoBecauseIssuedAtAfterExpires() {
@Test
void validationKoBecauseIssuedAtInFuture() {
var token = ClaimToken.Builder.newInstance()
.claim(EXPIRATION_TIME, Date.from(now.plusSeconds(60)))
.claim(ISSUED_AT, Date.from(now.plusSeconds(10)))
.claim(EXPIRATION_TIME, now.plusSeconds(60).getEpochSecond())
.claim(ISSUED_AT, now.plusSeconds(10).getEpochSecond())
.build();

var result = rule.checkRule(token, emptyMap());
Expand All @@ -113,8 +112,8 @@ void validationKoBecauseIssuedAtInFutureOutsideLeeway() {
var rule = new ExpirationIssuedAtValidationRule(clock, 5, false);

var token = ClaimToken.Builder.newInstance()
.claim(EXPIRATION_TIME, Date.from(now.plusSeconds(60)))
.claim(ISSUED_AT, Date.from(now.plusSeconds(10)))
.claim(EXPIRATION_TIME, now.plusSeconds(60).getEpochSecond())
.claim(ISSUED_AT, now.plusSeconds(10).getEpochSecond())
.build();

var result = rule.checkRule(token, emptyMap());
Expand All @@ -128,8 +127,8 @@ void validationOkBecauseIssuedAtInFutureButWithinLeeway() {
var rule = new ExpirationIssuedAtValidationRule(clock, 20, false);

var token = ClaimToken.Builder.newInstance()
.claim(EXPIRATION_TIME, Date.from(now.plusSeconds(60)))
.claim(ISSUED_AT, Date.from(now.plusSeconds(10)))
.claim(EXPIRATION_TIME, now.plusSeconds(60).getEpochSecond())
.claim(ISSUED_AT, now.plusSeconds(10).getEpochSecond())
.build();

var result = rule.checkRule(token, emptyMap());
Expand All @@ -155,8 +154,8 @@ void validationKoWithRoundedIssuedAtAndNoLeeway() {
var rule = new ExpirationIssuedAtValidationRule(clock, 0, false);

var token = ClaimToken.Builder.newInstance()
.claim(EXPIRATION_TIME, Date.from(expiresAt))
.claim(ISSUED_AT, Date.from(issuedAt))
.claim(EXPIRATION_TIME, expiresAt.getEpochSecond())
.claim(ISSUED_AT, expiresAt.getEpochSecond())
.build();

var result = rule.checkRule(token, emptyMap());
Expand All @@ -183,8 +182,8 @@ void validationOkWithRoundedIssuedAtAndMinimalLeeway() {
var rule = new ExpirationIssuedAtValidationRule(clock, 2, false);

var token = ClaimToken.Builder.newInstance()
.claim(EXPIRATION_TIME, Date.from(expiresAt))
.claim(ISSUED_AT, Date.from(issuedAt))
.claim(EXPIRATION_TIME, expiresAt.getEpochSecond())
.claim(ISSUED_AT, issuedAt.getEpochSecond())
.build();

var result = rule.checkRule(token, emptyMap());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import org.eclipse.edc.token.spi.TokenValidationRule;
import org.junit.jupiter.api.Test;

import java.sql.Date;
import java.time.Clock;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
Expand All @@ -39,7 +38,7 @@ class NotBeforeValidationRuleTest {
@Test
void validNotBefore() {
var token = ClaimToken.Builder.newInstance()
.claim(NOT_BEFORE, Date.from(now.plusSeconds(notBeforeLeeway)))
.claim(NOT_BEFORE, now.plusSeconds(notBeforeLeeway).getEpochSecond())
.build();

var result = rule.checkRule(token, emptyMap());
Expand All @@ -50,7 +49,7 @@ void validNotBefore() {
@Test
void validationKoBecauseNotBeforeTimeNotRespected() {
var token = ClaimToken.Builder.newInstance()
.claim(NOT_BEFORE, Date.from(now.plusSeconds(notBeforeLeeway + 1)))
.claim(NOT_BEFORE, now.plusSeconds(notBeforeLeeway + 1).getEpochSecond())
.build();

var result = rule.checkRule(token, emptyMap());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@

import java.time.Clock;
import java.util.Map;
import java.util.function.Consumer;

import static org.eclipse.edc.spi.result.Result.success;

Expand Down Expand Up @@ -99,10 +98,4 @@ public ClaimTokenCreatorFunction defaultClaimTokenFunction() {
};
}

private void checkProperty(String key, String value, Consumer<String> onMissing) {
if (value == null) {
onMissing.accept("No setting found for key '%s'.".formatted(key));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public Result<TokenRepresentation> obtainClientCredentials(TokenParameters param
}

// create claims for the STS
var claims = new HashMap<String, String>();
var claims = new HashMap<String, Object>();
parameters.getClaims().forEach((k, v) -> claims.replace(k, v.toString()));

claims.putAll(Map.of(
Expand All @@ -134,12 +134,12 @@ public Result<ClaimToken> verifyJwtToken(TokenRepresentation tokenRepresentation
var accessToken = claimToken.getStringClaim(PRESENTATION_TOKEN_CLAIM);
var issuer = claimToken.getStringClaim(ISSUER);

var siTokenClaims = Map.of(PRESENTATION_TOKEN_CLAIM, accessToken,
ISSUED_AT, Instant.now().toString(),
Map<String, Object> siTokenClaims = Map.of(PRESENTATION_TOKEN_CLAIM, accessToken,
ISSUED_AT, Instant.now().getEpochSecond(),
AUDIENCE, issuer,
ISSUER, myOwnDid,
SUBJECT, myOwnDid,
EXPIRATION_TIME, Instant.now().plus(5, ChronoUnit.MINUTES).toString());
EXPIRATION_TIME, Instant.now().plus(5, ChronoUnit.MINUTES).getEpochSecond());
var siToken = secureTokenService.createToken(siTokenClaims, null);
if (siToken.failed()) {
return siToken.mapFailure();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ public RemoteSecureTokenService(Oauth2Client oauth2Client, StsRemoteClientConfig
}

@Override
public Result<TokenRepresentation> createToken(Map<String, String> claims, @Nullable String bearerAccessScope) {
public Result<TokenRepresentation> createToken(Map<String, Object> claims, @Nullable String bearerAccessScope) {
return createRequest(claims, bearerAccessScope)
.compose(oauth2Client::requestToken);
}

@NotNull
private Result<Oauth2CredentialsRequest> createRequest(Map<String, String> claims, @Nullable String bearerAccessScope) {
private Result<Oauth2CredentialsRequest> createRequest(Map<String, Object> claims, @Nullable String bearerAccessScope) {

var secret = vault.resolveSecret(configuration.clientSecretAlias());
if (secret != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ private static FormBody createRequestBody(Oauth2CredentialsRequest request) {
var builder = new FormBody.Builder();
request.getParams().entrySet().stream()
.filter(entry -> entry.getValue() != null)
.forEach(entry -> builder.add(entry.getKey(), entry.getValue()));
.forEach(entry -> builder.add(entry.getKey(), entry.getValue().toString()));
return builder.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ void verifyRequestTokenSuccess() {

var formParameters = new Parameters(
request.getParams().entrySet().stream()
.map(entry -> Parameter.param(entry.getKey(), entry.getValue()))
.map(entry -> Parameter.param(entry.getKey(), entry.getValue().toString()))
.collect(Collectors.toList())
);

Expand All @@ -75,7 +75,7 @@ void verifyRequestTokenSuccess_withExpiresIn() {

var formParameters = new Parameters(
request.getParams().entrySet().stream()
.map(entry -> Parameter.param(entry.getKey(), entry.getValue()))
.map(entry -> Parameter.param(entry.getKey(), entry.getValue().toString()))
.collect(Collectors.toList())
);

Expand All @@ -97,7 +97,7 @@ void verifyRequestTokenSuccess_withExpiresIn_whenNotNumber() {

var formParameters = new Parameters(
request.getParams().entrySet().stream()
.map(entry -> Parameter.param(entry.getKey(), entry.getValue()))
.map(entry -> Parameter.param(entry.getKey(), entry.getValue().toString()))
.collect(Collectors.toList())
);

Expand All @@ -118,7 +118,7 @@ void verifyRequestTokenSuccess_withAdditionalProperties() {

var formParameters = new Parameters(
request.getParams().entrySet().stream()
.map(entry -> Parameter.param(entry.getKey(), entry.getValue()))
.map(entry -> Parameter.param(entry.getKey(), entry.getValue().toString()))
.collect(Collectors.toList())
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ void shouldCreatePrivateKeyRequest_whenPrivateKeyNameIsPresent() throws JOSEExce
})
.extracting(PrivateKeyOauth2CredentialsRequest::getClientAssertion)
.satisfies(assertion -> {
var assertionToken = SignedJWT.parse(assertion);
var assertionToken = SignedJWT.parse(assertion.toString());
var now = clock.instant().truncatedTo(ChronoUnit.SECONDS);
assertThat(assertionToken.verify(new RSASSAVerifier(keyPair.toRSAPublicKey()))).isTrue();
assertThat(assertionToken.getJWTClaimsSet().getClaims())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ private TokenParameters createTokenParams(DataFlowStartMessage message) {
.claims(JwtRegisteredClaimNames.AUDIENCE, message.getParticipantId())
.claims(JwtRegisteredClaimNames.ISSUER, ownParticipantId)
.claims(JwtRegisteredClaimNames.SUBJECT, ownParticipantId)
.claims(JwtRegisteredClaimNames.ISSUED_AT, clock.instant().toEpochMilli()) // todo: milli or second?
.claims(JwtRegisteredClaimNames.ISSUED_AT, clock.instant().getEpochSecond())
.build();
}

Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" }
kafkaClients = { module = "org.apache.kafka:kafka-clients", version.ref = "kafkaClients" }
jsonschema = { module = "com.networknt:json-schema-validator", version = "1.5.6" }

# DCP-TCK libraries
# DSP-TCK libraries
dsp-tck-runtime = { module = "org.eclipse.dataspacetck.dsp:tck-runtime", version.ref = "dsp-tck" }
dsp-tck-core = { module = "org.eclipse.dataspacetck.dsp:core", version.ref = "dsp-tck" }
dsp-tck-api = { module = "org.eclipse.dataspacetck.dsp:dsp-api", version.ref = "dsp-tck" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,29 @@ public List<?> getListClaim(String claimName) {
}

/**
* Get the date claim value by name converted to {@link Instant}
* Get the NumericDate claim value by name converted to {@link Instant}. The claim must be a long value.
*
* @param claimName the name of the claim
* @return the claim value, null if it does not exist
*/
public Instant getInstantClaim(String claimName) {
return Optional.of(claims)
.map(it -> it.get(claimName))
.map(Date.class::cast)
.map(o -> {
if (o instanceof Long epoch) {
return epoch;
}
if (o instanceof Date d) {
return d.toInstant().getEpochSecond();
}
return null;
})
.map(this::convertToUtcTime)
.orElse(null);
}

private Instant convertToUtcTime(Date date) {
return date.toInstant().atOffset(UTC).toInstant();
private Instant convertToUtcTime(Long epochSecond) {
return Instant.ofEpochSecond(epochSecond).atOffset(UTC).toInstant();
}

public static class Builder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ public interface SecureTokenService {
* if bearerAccessScope != null -> creates a {@code token} claim, which is another JWT containing the scope as claims.
* if bearerAccessScope == null -> creates a normal JWT using all the claims in the map
*/
Result<TokenRepresentation> createToken(Map<String, String> claims, @Nullable String bearerAccessScope);
Result<TokenRepresentation> createToken(Map<String, Object> claims, @Nullable String bearerAccessScope);

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import org.eclipse.edc.token.spi.TokenDecorator;

import java.time.Clock;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
Expand Down Expand Up @@ -51,8 +50,8 @@ public TokenParameters.Builder decorate(TokenParameters.Builder tokenParameters)
.claims(ISSUER, clientId)
.claims(SUBJECT, clientId)
.claims(JWT_ID, UUID.randomUUID().toString())
.claims(ISSUED_AT, Date.from(clock.instant()))
.claims(EXPIRATION_TIME, Date.from(clock.instant().plusSeconds(validity)));
.claims(ISSUED_AT, clock.instant().getEpochSecond())
.claims(EXPIRATION_TIME, clock.instant().plusSeconds(validity).getEpochSecond());
}

public static class Builder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,47 +15,45 @@
package org.eclipse.edc.iam.oauth2.spi.client;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static java.util.Optional.ofNullable;

public abstract class Oauth2CredentialsRequest {

private static final String GRANT_TYPE = "grant_type";
private static final String SCOPE = "scope";
private static final String RESOURCE = "resource";

protected String url;
protected final Map<String, String> params = new HashMap<>();
protected final Map<String, Object> params = new HashMap<>();

@NotNull
public String getUrl() {
return url;
}

@Nullable
public String getScope() {
return params.get(SCOPE);
return ofNullable(params.get(SCOPE)).map(Object::toString).orElse(null);
}

@NotNull
public String getGrantType() {
return params.get(GRANT_TYPE);
return ofNullable(params.get(GRANT_TYPE)).map(Object::toString).orElse(null);
}

/**
* The audience for which an access token will be requested.
*
* @return The value of the resource form parameter.
*/
@Nullable
public String getResource() {
return this.params.get(RESOURCE);
return ofNullable(params.get(RESOURCE)).map(Object::toString).orElse(null);
}

public Map<String, String> getParams() {
public Map<String, Object> getParams() {
return params;
}

Expand All @@ -81,12 +79,12 @@ public B scope(String scope) {
return self();
}

public B param(String key, String value) {
public B param(String key, Object value) {
request.params.put(key, value);
return self();
}

public B params(Map<String, String> params) {
public B params(Map<String, Object> params) {
request.params.putAll(params);
return self();
}
Expand All @@ -95,8 +93,8 @@ public B params(Map<String, String> params) {
* Adds the resource form parameter to the request.
*
* @param targetedAudience The audience for which an access token will be requested.
* @see <a href="https://www.rfc-editor.org/rfc/rfc8707.html">RFC-8707</a>
* @return this builder
* @see <a href="https://www.rfc-editor.org/rfc/rfc8707.html">RFC-8707</a>
*/
public B resource(String targetedAudience) {
return param(RESOURCE, targetedAudience);
Expand Down
Loading