Skip to content
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

Ian UI d2 1394 update attest failure reaction #125

Merged
merged 19 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from 14 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
28 changes: 16 additions & 12 deletions src/main/java/com/uid2/shared/attest/AttestationTokenRetriever.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.vertx.core.json.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.utils.Pair;

import java.io.IOException;
import java.net.*;
Expand Down Expand Up @@ -37,7 +38,7 @@ public class AttestationTokenRetriever {
private final AtomicReference<String> attestationToken;
private final AtomicReference<String> optOutJwt;
private final AtomicReference<String> coreJwt;
private final Handler<Integer> responseWatcher;
private final Handler<Pair<Integer, String>> responseWatcher;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this is only used by operator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of the core, optout, admin, and operator, the attestation token retriever is used by operator and optout, but optout uses a null responseWatcher. Will look into optout closer to make sure it is not broken by these changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested optout/core with my shared changes locally and it is able to attest and refresh with no extra changes needed

private final String attestationEndpoint;
private final IClock clock;
private final Vertx vertx;
Expand All @@ -49,26 +50,28 @@ public class AttestationTokenRetriever {
private final Lock lock;
private final AttestationTokenDecryptor attestationTokenDecryptor;
private final String appVersionHeader;
private final int attestCheckMilliseconds;

public AttestationTokenRetriever(Vertx vertx,
String attestationEndpoint,
String clientApiToken,
ApplicationVersion appVersion,
IAttestationProvider attestationProvider,
Handler<Integer> responseWatcher,
Handler<Pair<Integer, String>> responseWatcher,
Proxy proxy) {
this(vertx, attestationEndpoint, clientApiToken, appVersion, attestationProvider, responseWatcher, proxy, new InstantClock(), null, null);
this(vertx, attestationEndpoint, clientApiToken, appVersion, attestationProvider, responseWatcher, proxy, new InstantClock(), null, null, 60000);
}
public AttestationTokenRetriever(Vertx vertx,
String attestationEndpoint,
String clientApiToken,
ApplicationVersion appVersion,
IAttestationProvider attestationProvider,
Handler<Integer> responseWatcher,
Handler<Pair<Integer, String>> responseWatcher,
Proxy proxy,
IClock clock,
URLConnectionHttpClient httpClient,
AttestationTokenDecryptor attestationTokenDecryptor) {
AttestationTokenDecryptor attestationTokenDecryptor,
int attestCheckMilliseconds) {
this.vertx = vertx;
this.attestationEndpoint = attestationEndpoint;
this.clientApiToken = clientApiToken;
Expand All @@ -80,8 +83,8 @@ public AttestationTokenRetriever(Vertx vertx,
this.responseWatcher = responseWatcher;
this.clock = clock;
this.lock = new ReentrantLock();
this.isExpiryCheckScheduled = false;
this.isAttesting = new AtomicBoolean(false);
this.attestCheckMilliseconds = attestCheckMilliseconds;
if (httpClient == null) {
this.httpClient = new URLConnectionHttpClient(proxy);
} else {
Expand Down Expand Up @@ -112,6 +115,7 @@ private void attestationExpirationCheck(long timerId) {
}

try {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra line

Instant currentTime = clock.now();
Instant tenMinutesBeforeExpire = attestationTokenExpiresAt.minusSeconds(600);
if (!currentTime.isAfter(tenMinutesBeforeExpire)) {
Expand All @@ -127,7 +131,7 @@ private void attestationExpirationCheck(long timerId) {

attest();
} catch (AttestationTokenRetrieverException | IOException e) {
thomasm-ttd marked this conversation as resolved.
Show resolved Hide resolved
notifyResponseStatusWatcher(401);
notifyResponseWatcher(401, e.getMessage());
LOGGER.info("Re-attest failed: ", e);
} finally {
this.isAttesting.set(false);
Expand All @@ -137,7 +141,7 @@ private void attestationExpirationCheck(long timerId) {
private void scheduleAttestationExpirationCheck() {
if (!this.isExpiryCheckScheduled) {
// Schedule the task to run every minute
this.vertx.setPeriodic(0, 60000, this::attestationExpirationCheck);
this.vertx.setPeriodic(0, attestCheckMilliseconds, this::attestationExpirationCheck);
this.isExpiryCheckScheduled = true;
}
}
Expand Down Expand Up @@ -170,14 +174,14 @@ public void attest() throws IOException, AttestationTokenRetrieverException {
HttpResponse<String> response = httpClient.post(attestationEndpoint, requestJson.toString(), headers);

int statusCode = response.statusCode();
notifyResponseStatusWatcher(statusCode);
String responseBody = response.body();
notifyResponseWatcher(statusCode, responseBody);

if (statusCode < 200 || statusCode >= 300) {
LOGGER.warn("attestation failed with UID2 Core returning statusCode=" + statusCode);
throw new AttestationTokenRetrieverException(statusCode, "unexpected status code from uid core service");
}

String responseBody = response.body();
JsonObject responseJson = (JsonObject) Json.decodeValue(responseBody);
if (isFailed(responseJson)) {
throw new AttestationTokenRetrieverException(statusCode, "response did not return a successful status");
Expand Down Expand Up @@ -274,11 +278,11 @@ private static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
return gen.generateKeyPair();
}

private void notifyResponseStatusWatcher(int statusCode) {
private void notifyResponseWatcher(int statusCode, String responseBody) {
this.lock.lock();
try {
if (this.responseWatcher != null)
this.responseWatcher.handle(statusCode);
this.responseWatcher.handle(Pair.of(statusCode, responseBody));
} finally {
lock.unlock();
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/uid2/shared/attest/UidCoreClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public ICloudStorage getContentStorage() {
@Override
public InputStream download(String path) throws CloudStorageException {
String coreJWT = this.getJWT();
return this.internalDownload(path, coreJWT);
return this.internalDownload(path, coreJWT);
}

@Deprecated
Expand All @@ -78,7 +78,7 @@ public InputStream downloadFromOptOut(String path) throws CloudStorageException
return this.internalDownload(path, optOutJWT);
}

protected String getJWT(){
protected String getJWT() {
return this.getAttestationTokenRetriever().getCoreJWT();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import software.amazon.awssdk.utils.Pair;

import java.io.IOException;
import java.net.Proxy;
Expand All @@ -40,7 +41,7 @@ public class AttestationTokenRetrieverTest {
}});

private final IAttestationProvider attestationProvider = mock(IAttestationProvider.class);
private final Handler<Integer> responseWatcher = mock(Handler.class);
private final Handler<Pair<Integer, String>> responseWatcher = mock(Handler.class);
private final IClock clock = mock(IClock.class);
private final URLConnectionHttpClient mockHttpClient = mock(URLConnectionHttpClient.class);
private Proxy proxy = CloudUtils.defaultProxy;
Expand Down Expand Up @@ -73,40 +74,35 @@ public void attest_succeed_attestationTokenSet(Vertx vertx, VertxTestContext tes
attestationTokenRetriever.attest();
Assertions.assertEquals("test_attestation_token", attestationTokenRetriever.getAttestationToken());
Assertions.assertEquals("appName=appVersion;Component1=Value1;Component2=Value2", attestationTokenRetriever.getAppVersionHeader());
verify(this.responseWatcher, times(1)).handle(200);
verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody));
testContext.completeNow();
}

@Test
public void attest_currentTimeAfterTenMinsBeforeAttestationTokenExpiry_expiryCheckCallsAttest(Vertx vertx, VertxTestContext testContext) throws Exception {
public void attest_currentTimeAfterTenMinsBeforeAttestationTokenExpiry_expiryCheckCallsAttestFixedIntervalUntilSuccess(Vertx vertx, VertxTestContext testContext) throws Exception {
attestationTokenRetriever = getAttestationTokenRetriever(vertx);
thomasm-ttd marked this conversation as resolved.
Show resolved Hide resolved

when(attestationProvider.isReady()).thenReturn(true);
when(attestationProvider.getAttestationRequest(any())).thenReturn(new byte[1]);

HttpResponse<String> mockHttpResponse = mock(HttpResponse.class);
String expectedResponseBody = "{\"body\": {\"attestation_token\": \"test\",\"expiresAt\": \"2023-08-01T00:00:00.111Z\",\"attestation_jwt_optout\": \"\", \"attestation_jwt_core\": \"\"},\"status\": \"success\"}";
when(mockHttpResponse.body()).thenReturn(expectedResponseBody);
when(mockHttpResponse.statusCode()).thenReturn(200);

HttpResponse<String> mockHttpResponseSecondAttest = mock(HttpResponse.class);
String expectedResponseBodySecondAttest = "{\"body\": {\"attestation_token\": \"test\",\"expiresAt\": \"2023-08-01T00:00:00.111Z\",\"attestation_jwt_optout\": \"\",\"attestation_jwt_core\": \"\"},\"status\": \"success\"}";
when(mockHttpResponseSecondAttest.body()).thenReturn(expectedResponseBodySecondAttest);
when(mockHttpResponseSecondAttest.statusCode()).thenReturn(300);
String expectedResponseBody = "{\"body\": {\"attestation_token\": \"test\",\"expiresAt\": \"2023-08-01T00:00:00.111Z\",\"attestation_jwt_optout\": \"\",\"attestation_jwt_core\": \"\"},\"status\": \"success\"}";
when(mockHttpResponse.statusCode()).thenReturn(200, 401, 401, 200);
thomasm-ttd marked this conversation as resolved.
Show resolved Hide resolved
when(mockHttpResponse.body()).thenReturn(expectedResponseBody, "bad", "bad", expectedResponseBody);

when(mockHttpClient.post(eq(ATTESTATION_ENDPOINT), any(String.class), any(HashMap.class))).thenReturn(mockHttpResponse, mockHttpResponseSecondAttest);
when(mockHttpClient.post(eq(ATTESTATION_ENDPOINT), any(String.class), any(HashMap.class))).thenReturn(mockHttpResponse);
when(mockAttestationTokenDecryptor.decrypt(any(), any())).thenReturn("test_attestation_token".getBytes(StandardCharsets.UTF_8));

Instant tenSecondsAfterTenMinutesBeforeExpiry = Instant.parse("2023-08-01T00:00:00.111Z").minusSeconds(600).plusSeconds(10);
Instant tenSecondsBeforeTenMinutesBeforeExpiry = Instant.parse("2023-08-01T00:00:00.111Z").minusSeconds(600).minusSeconds(10);
when(clock.now()).thenReturn(tenSecondsAfterTenMinutesBeforeExpiry, tenSecondsBeforeTenMinutesBeforeExpiry);
when(clock.now()).thenReturn(tenSecondsBeforeTenMinutesBeforeExpiry, tenSecondsAfterTenMinutesBeforeExpiry, tenSecondsAfterTenMinutesBeforeExpiry, tenSecondsAfterTenMinutesBeforeExpiry, tenSecondsBeforeTenMinutesBeforeExpiry);

attestationTokenRetriever.attest();
testContext.awaitCompletion(1, TimeUnit.SECONDS);
testContext.awaitCompletion(1100, TimeUnit.MILLISECONDS);
// Verify on httpClient because we can't mock attestationTokenRetriever
verify(mockHttpClient, times(2)).post(eq(ATTESTATION_ENDPOINT), any(String.class), any(HashMap.class));
verify(this.responseWatcher, times(1)).handle(200);
verify(this.responseWatcher, times(1)).handle(300);
verify(mockHttpClient, times(4)).post(eq(ATTESTATION_ENDPOINT), any(String.class), any(HashMap.class));
verify(this.responseWatcher, times(2)).handle(Pair.of(200, expectedResponseBody));
verify(this.responseWatcher, times(2)).handle(Pair.of(401, "bad"));
testContext.completeNow();
}

Expand All @@ -131,7 +127,7 @@ public void attest_currentTimeAfterTenMinsBeforeAttestationTokenExpiry_expiryChe
testContext.awaitCompletion(1, TimeUnit.SECONDS);
// Verify on httpClient because we can't mock attestationTokenRetriever
verify(mockHttpClient, times(1)).post(eq(ATTESTATION_ENDPOINT), any(String.class), any(HashMap.class));
verify(this.responseWatcher, times(1)).handle(200);
verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody));
testContext.completeNow();
}

Expand Down Expand Up @@ -159,8 +155,8 @@ public void attest_currentTimeAfterTenMinsBeforeAttestationTokenExpiry_providerN
testContext.awaitCompletion(1, TimeUnit.SECONDS);
// Verify on httpClient because we can't mock attestationTokenRetriever
verify(mockHttpClient, times(1)).post(eq(ATTESTATION_ENDPOINT), any(String.class), any(HashMap.class));
verify(this.responseWatcher, only()).handle(200);
verify(this.responseWatcher, times(1)).handle(200);
verify(this.responseWatcher, only()).handle(Pair.of(200, expectedResponseBody));
verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody));
testContext.completeNow();
}

Expand Down Expand Up @@ -190,7 +186,7 @@ public void attest_responseBodyHasNoAttestationToken_exceptionThrown(Vertx vertx
});
String expectedExceptionMessage = "com.uid2.shared.attest.AttestationTokenRetrieverException: http status: 200, response json does not contain body.attestation_token";
Assertions.assertEquals(expectedExceptionMessage, result.getMessage());
verify(this.responseWatcher, times(1)).handle(200);
verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody));
testContext.completeNow();
}

Expand Down Expand Up @@ -221,7 +217,7 @@ public void attest_responseBodyHasNoExpiredAt_exceptionThrown(Vertx vertx, Vertx
String expectedExceptionMessage = "com.uid2.shared.attest.AttestationTokenRetrieverException: http status: 200, response json does not contain body.expiresAt";

Assertions.assertEquals(expectedExceptionMessage, result.getMessage());
verify(this.responseWatcher, times(1)).handle(200);
verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody));
testContext.completeNow();
}

Expand Down Expand Up @@ -257,7 +253,7 @@ public void attest_succeed_optOutJwtSet(Vertx vertx, VertxTestContext testContex

attestationTokenRetriever.attest();
Assertions.assertEquals("test_jwt", attestationTokenRetriever.getOptOutJWT());
verify(this.responseWatcher, times(1)).handle(200);
verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody));
testContext.completeNow();
}
@Test
Expand All @@ -277,7 +273,7 @@ public void attest_succeed_coreJwtSet(Vertx vertx, VertxTestContext testContext)

attestationTokenRetriever.attest();
Assertions.assertEquals("test_jwt_core", attestationTokenRetriever.getCoreJWT());
verify(this.responseWatcher, times(1)).handle(200);
verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody));
testContext.completeNow();
}
@Test
Expand All @@ -298,11 +294,11 @@ public void attest_succeed_jwtsNull(Vertx vertx, VertxTestContext testContext) t
attestationTokenRetriever.attest();
Assertions.assertNull(attestationTokenRetriever.getOptOutJWT());
Assertions.assertNull(attestationTokenRetriever.getCoreJWT());
verify(this.responseWatcher, times(1)).handle(200);
verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody));
testContext.completeNow();
}

private AttestationTokenRetriever getAttestationTokenRetriever(Vertx vertx) {
return new AttestationTokenRetriever(vertx, ATTESTATION_ENDPOINT, "testApiKey", APP_VERSION, attestationProvider, responseWatcher, proxy, clock, mockHttpClient, mockAttestationTokenDecryptor);
return new AttestationTokenRetriever(vertx, ATTESTATION_ENDPOINT, "testApiKey", APP_VERSION, attestationProvider, responseWatcher, proxy, clock, mockHttpClient, mockAttestationTokenDecryptor, 250);
}
}
}