Skip to content

Commit

Permalink
record JTI when creating access tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Oct 21, 2024
1 parent 3845b4a commit 146e4f6
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
import org.eclipse.edc.verifiablecredentials.jwt.JwtPresentationVerifier;
import org.eclipse.edc.verifiablecredentials.jwt.rules.HasSubjectRule;
import org.eclipse.edc.verifiablecredentials.jwt.rules.IssuerEqualsSubjectRule;
import org.eclipse.edc.verifiablecredentials.jwt.rules.JtiValidationRule;
import org.eclipse.edc.verifiablecredentials.jwt.rules.SubJwkIsNullRule;
import org.eclipse.edc.verifiablecredentials.jwt.rules.TokenNotNullRule;
import org.eclipse.edc.verifiablecredentials.linkeddata.DidMethodResolver;
Expand Down Expand Up @@ -153,7 +152,6 @@ public void initialize(ServiceExtensionContext context) {
rulesRegistry.addRule(DCP_SELF_ISSUED_TOKEN_CONTEXT, new IssuerEqualsSubjectRule());
rulesRegistry.addRule(DCP_SELF_ISSUED_TOKEN_CONTEXT, new SubJwkIsNullRule());
rulesRegistry.addRule(DCP_SELF_ISSUED_TOKEN_CONTEXT, new AudienceValidationRule(getOwnDid(context)));
rulesRegistry.addRule(DCP_SELF_ISSUED_TOKEN_CONTEXT, new JtiValidationRule(jtiValidationStore, context.getMonitor()));
rulesRegistry.addRule(DCP_SELF_ISSUED_TOKEN_CONTEXT, new ExpirationIssuedAtValidationRule(clock, 5));
rulesRegistry.addRule(DCP_SELF_ISSUED_TOKEN_CONTEXT, new TokenNotNullRule());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public Result<ClaimToken> verifyJwtToken(TokenRepresentation tokenRepresentation
var claimTokenResult = tokenValidationAction.apply(tokenRepresentation);

if (claimTokenResult.failed()) {
return claimTokenResult.mapTo();
return claimTokenResult.mapEmpty();
}

// create our own SI token, to request the VPs
Expand All @@ -151,7 +151,7 @@ public Result<ClaimToken> verifyJwtToken(TokenRepresentation tokenRepresentation
.compose(url -> credentialServiceClient.requestPresentation(url, siTokenString, context.getScopes().stream().toList()));

if (vpResponse.failed()) {
return vpResponse.mapTo();
return vpResponse.mapEmpty();
}

var presentations = vpResponse.getContent();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.iam.identitytrust.sts.embedded;

import org.eclipse.edc.spi.iam.TokenParameters;
import org.eclipse.edc.token.spi.TokenDecorator;

import java.time.Instant;
import java.util.Date;
import java.util.Map;

import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.EXPIRATION_TIME;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUED_AT;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.JWT_ID;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.NOT_BEFORE;

public class AccessTokenDecorator implements TokenDecorator {

private final String jti;
private final Instant now;
private final Instant expiration;
private final Map<String, String> claims;

public AccessTokenDecorator(String jti, Instant now, Instant expiration, Map<String, String> claims) {
this.jti = jti;
this.now = now;
this.expiration = expiration;
this.claims = claims;
}

@Override
public TokenParameters.Builder decorate(TokenParameters.Builder tokenParameters) {
this.claims.forEach(tokenParameters::claims);
return tokenParameters
.claims(ISSUED_AT, Date.from(now))
.claims(NOT_BEFORE, Date.from(now))
.claims(EXPIRATION_TIME, Date.from(expiration))
.claims(JWT_ID, jti);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package org.eclipse.edc.iam.identitytrust.sts.embedded;

import org.eclipse.edc.iam.identitytrust.spi.SecureTokenService;
import org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames;
import org.eclipse.edc.jwt.validation.jti.JtiValidationEntry;
import org.eclipse.edc.jwt.validation.jti.JtiValidationStore;
import org.eclipse.edc.spi.iam.TokenRepresentation;
Expand All @@ -28,6 +27,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
Expand Down Expand Up @@ -73,24 +73,17 @@ public Result<TokenRepresentation> createToken(Map<String, String> claims, @Null
return ofNullable(bearerAccessScope)
.map(scope -> createAndAcceptAccessToken(claims, scope, selfIssuedClaims::put))
.orElse(success())
.compose(v -> recordToken(claims))
.compose(v -> {
var keyIdDecorator = new KeyIdDecorator(publicKeyIdSupplier.get());
return tokenGenerationService.generate(privateKeyIdSupplier.get(), keyIdDecorator, new SelfIssuedTokenDecorator(selfIssuedClaims, clock, validity));
});
}

private Result<Void> recordToken(Map<String, String> claims) {
var jti = claims.get(JwtRegisteredClaimNames.JWT_ID);
if (jti != null) {
var exp = claims.get(JwtRegisteredClaimNames.EXPIRATION_TIME);
var expTime = ofNullable(exp).map(Long::parseLong).orElse(null);
var storeResult = jtiValidationStore.storeEntry(new JtiValidationEntry(jti, expTime));
return storeResult.succeeded()
? Result.success()
: failure("error storing JTI for later validation: %s".formatted(storeResult.getFailureDetail()));
}
return Result.success();
private Result<Void> recordToken(String jti, Long exp) {
var storeResult = jtiValidationStore.storeEntry(new JtiValidationEntry(jti, exp));
return storeResult.succeeded()
? Result.success()
: failure("error storing JTI for later validation: %s".formatted(storeResult.getFailureDetail()));
}

private Result<Void> createAndAcceptAccessToken(Map<String, String> claims, String scope, BiConsumer<String, String> consumer) {
Expand All @@ -102,13 +95,18 @@ private Result<Void> createAndAcceptAccessToken(Map<String, String> claims, Stri

private Result<TokenRepresentation> createAccessToken(Map<String, String> claims, String bearerAccessScope) {
var accessTokenClaims = new HashMap<>(accessTokenInheritedClaims(claims));
var now = clock.instant();
var exp = now.plusSeconds(validity);
var jti = "accesstoken-%s".formatted(UUID.randomUUID());

accessTokenClaims.put(SCOPE, bearerAccessScope);

return addClaim(claims, ISSUER, withClaim(AUDIENCE, accessTokenClaims::put))
.compose(v -> addClaim(claims, AUDIENCE, withClaim(SUBJECT, accessTokenClaims::put)))
.compose(v -> {
var keyIdDecorator = new KeyIdDecorator(publicKeyIdSupplier.get());
return tokenGenerationService.generate(privateKeyIdSupplier.get(), keyIdDecorator, new SelfIssuedTokenDecorator(accessTokenClaims, clock, validity));
});
return tokenGenerationService.generate(privateKeyIdSupplier.get(), keyIdDecorator, new AccessTokenDecorator(jti, now, exp, accessTokenClaims));
}).compose(tr -> recordToken(jti, exp.toEpochMilli()).map(v -> tr));

}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.iam.identitytrust.sts.embedded;

import org.eclipse.edc.spi.iam.TokenParameters;
import org.junit.jupiter.api.Test;

import java.time.Instant;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.EXPIRATION_TIME;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUED_AT;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.JWT_ID;
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.NOT_BEFORE;

class AccessTokenDecoratorTest {

@Test
void verifyExpectedClaims() {
var builder = TokenParameters.Builder.newInstance();
var now = Instant.now();
var decorator = new AccessTokenDecorator("test-id", now, now.plusSeconds(5), Map.of("claim1", "value1"));
decorator.decorate(builder);

var tokenParams = builder.build();
assertThat(tokenParams.getClaims())
.containsEntry("claim1", "value1")
.containsEntry(JWT_ID, "test-id")
.containsKeys(ISSUED_AT, EXPIRATION_TIME, NOT_BEFORE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ void createToken_withBearerAccessScope() {
.satisfies(decorators -> {
assertThat(decorators.get(0))
.hasSize(2)
.hasOnlyElementsOfTypes(KeyIdDecorator.class, SelfIssuedTokenDecorator.class);
.hasOnlyElementsOfTypes(KeyIdDecorator.class, AccessTokenDecorator.class, SelfIssuedTokenDecorator.class);

assertThat(decorators.get(1))
.hasSize(2)
.hasOnlyElementsOfTypes(KeyIdDecorator.class, SelfIssuedTokenDecorator.class);
.hasOnlyElementsOfTypes(KeyIdDecorator.class, AccessTokenDecorator.class, SelfIssuedTokenDecorator.class);
});

}
Expand All @@ -125,7 +125,7 @@ void createToken_error_whenAccessTokenFails() {

assertThat(captor.getValue())
.hasSize(2)
.hasOnlyElementsOfTypes(SelfIssuedTokenDecorator.class, KeyIdDecorator.class);
.hasOnlyElementsOfTypes(SelfIssuedTokenDecorator.class, AccessTokenDecorator.class, KeyIdDecorator.class);

}

Expand Down

0 comments on commit 146e4f6

Please sign in to comment.