mapper = Mockito.mockStatic(ResourceMapper.class)) {
mapper.when(() -> ResourceMapper.writeValueAsString(matchRequest)).thenReturn(A_BODY_BYTES);
- when(requestBuilderFactory.create()).thenReturn(signedRequestBuilder);
-
- when(identityService.createSignedRequest(SDK_ID, keyPair, DIGITAL_ID_MATCH_PATH, POST, A_BODY_BYTES))
- .thenReturn(signedRequest);
+ when(testObj.createRequest(DIGITAL_ID_MATCH_PATH, POST, A_BODY_BYTES))
+ .thenReturn(yotiHttpRequest);
+ when(yotiHttpRequest.execute(MatchResult.class)).thenReturn(matchResult);
- when(signedRequest.execute(MatchResult.class)).thenReturn(matchResult);
+ MatchResult result = testObj.fetchMatch(matchRequest);
- MatchResult result = identityService.fetchMatch(SDK_ID, keyPair, matchRequest);
assertSame(matchResult, result);
}
}
diff --git a/yoti-sdk-api/src/test/java/com/yoti/api/client/spi/remote/call/qrcode/DynamicSharingServiceTest.java b/yoti-sdk-api/src/test/java/com/yoti/api/client/spi/remote/call/qrcode/DynamicSharingServiceTest.java
index 55ff43d29..6d7c79f5b 100644
--- a/yoti-sdk-api/src/test/java/com/yoti/api/client/spi/remote/call/qrcode/DynamicSharingServiceTest.java
+++ b/yoti-sdk-api/src/test/java/com/yoti/api/client/spi/remote/call/qrcode/DynamicSharingServiceTest.java
@@ -15,7 +15,7 @@
import com.yoti.api.client.shareurl.DynamicShareException;
import com.yoti.api.client.shareurl.ShareUrlResult;
import com.yoti.api.client.spi.remote.call.ResourceException;
-import com.yoti.api.client.spi.remote.call.SignedRequest;
+import com.yoti.api.client.spi.remote.call.YotiHttpRequest;
import com.yoti.api.client.spi.remote.call.factory.UnsignedPathFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -44,7 +44,7 @@ public class DynamicSharingServiceTest {
@Mock ObjectMapper objectMapperMock;
@Mock DynamicScenario simpleDynamicScenarioMock;
- @Mock SignedRequest signedRequestMock;
+ @Mock YotiHttpRequest yotiHttpRequestMock;
@Mock(answer = RETURNS_DEEP_STUBS) KeyPair keyPairMock;
@Mock ShareUrlResult shareUrlResultMock;
@@ -60,17 +60,12 @@ public void setUp() {
@Test(expected = IllegalArgumentException.class)
public void shouldFailWithNullAppId() throws Exception {
- testObj.createShareUrl(null, keyPairMock, simpleDynamicScenarioMock);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void shouldFailWithNullKeyPair() throws Exception {
- testObj.createShareUrl(APP_ID, null, simpleDynamicScenarioMock);
+ testObj.createShareUrl(null, simpleDynamicScenarioMock);
}
@Test(expected = IllegalArgumentException.class)
public void shouldFailWithNullDynamicScenario() throws Exception {
- testObj.createShareUrl(APP_ID, keyPairMock, null);
+ testObj.createShareUrl(APP_ID, null);
}
@Test
@@ -79,7 +74,7 @@ public void shouldThrowDynamicShareExceptionWhenParsingFails() throws Exception
when(objectMapperMock.writeValueAsString(simpleDynamicScenarioMock)).thenThrow(jsonProcessingException);
try {
- testObj.createShareUrl(APP_ID, keyPairMock, simpleDynamicScenarioMock);
+ testObj.createShareUrl(APP_ID, simpleDynamicScenarioMock);
fail("Expected a DynamicShareException");
} catch (DynamicShareException ex) {
assertSame(jsonProcessingException, ex.getCause());
@@ -90,11 +85,11 @@ public void shouldThrowDynamicShareExceptionWhenParsingFails() throws Exception
public void shouldThrowExceptionForIOError() throws Exception {
when(objectMapperMock.writeValueAsString(simpleDynamicScenarioMock)).thenReturn(SOME_BODY);
IOException ioException = new IOException();
- doReturn(signedRequestMock).when(testObj).createSignedRequest(keyPairMock, DYNAMIC_QRCODE_PATH, SOME_BODY_BYTES);
- when(signedRequestMock.execute(ShareUrlResult.class)).thenThrow(ioException);
+ doReturn(yotiHttpRequestMock).when(testObj).createRequest(DYNAMIC_QRCODE_PATH, SOME_BODY_BYTES);
+ when(yotiHttpRequestMock.execute(ShareUrlResult.class)).thenThrow(ioException);
try {
- testObj.createShareUrl(APP_ID, keyPairMock, simpleDynamicScenarioMock);
+ testObj.createShareUrl(APP_ID, simpleDynamicScenarioMock);
fail("Expected a DynamicShareException");
} catch (DynamicShareException ex) {
assertSame(ioException, ex.getCause());
@@ -105,11 +100,11 @@ public void shouldThrowExceptionForIOError() throws Exception {
public void shouldThrowExceptionWithResourceExceptionCause() throws Exception {
when(objectMapperMock.writeValueAsString(simpleDynamicScenarioMock)).thenReturn(SOME_BODY);
ResourceException resourceException = new ResourceException(404, "Not Found", "Test exception");
- doReturn(signedRequestMock).when(testObj).createSignedRequest(keyPairMock, DYNAMIC_QRCODE_PATH, SOME_BODY_BYTES);
- when(signedRequestMock.execute(ShareUrlResult.class)).thenThrow(resourceException);
+ doReturn(yotiHttpRequestMock).when(testObj).createRequest(DYNAMIC_QRCODE_PATH, SOME_BODY_BYTES);
+ when(yotiHttpRequestMock.execute(ShareUrlResult.class)).thenThrow(resourceException);
try {
- testObj.createShareUrl(APP_ID, keyPairMock, simpleDynamicScenarioMock);
+ testObj.createShareUrl(APP_ID, simpleDynamicScenarioMock);
fail("Expected a DynamicShareException");
} catch (DynamicShareException ex) {
assertSame(resourceException, ex.getCause());
@@ -119,10 +114,10 @@ public void shouldThrowExceptionWithResourceExceptionCause() throws Exception {
@Test
public void shouldReturnReceiptForCorrectRequest() throws Exception {
when(objectMapperMock.writeValueAsString(simpleDynamicScenarioMock)).thenReturn(SOME_BODY);
- doReturn(signedRequestMock).when(testObj).createSignedRequest(keyPairMock, DYNAMIC_QRCODE_PATH, SOME_BODY_BYTES);
- when(signedRequestMock.execute(ShareUrlResult.class)).thenReturn(shareUrlResultMock);
+ doReturn(yotiHttpRequestMock).when(testObj).createRequest(DYNAMIC_QRCODE_PATH, SOME_BODY_BYTES);
+ when(yotiHttpRequestMock.execute(ShareUrlResult.class)).thenReturn(shareUrlResultMock);
- ShareUrlResult result = testObj.createShareUrl(APP_ID, keyPairMock, simpleDynamicScenarioMock);
+ ShareUrlResult result = testObj.createShareUrl(APP_ID, simpleDynamicScenarioMock);
assertSame(shareUrlResultMock, result);
}
diff --git a/yoti-sdk-auth/pom.xml b/yoti-sdk-auth/pom.xml
new file mode 100644
index 000000000..4ce1c511d
--- /dev/null
+++ b/yoti-sdk-auth/pom.xml
@@ -0,0 +1,113 @@
+
+
+ 4.0.0
+
+
+ com.yoti
+ yoti-sdk-parent
+ 4.0.0
+ ../yoti-sdk-parent
+
+
+ yoti-sdk-auth
+
+
+ 0.13.0
+
+
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+
+
+ com.yoti
+ yoti-sdk-api
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jjwt.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ commons-logging
+ commons-logging
+ 1.1.1
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jjwt.version}
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jjwt.version}
+ runtime
+
+
+
+
+ org.hamcrest
+ hamcrest-library
+ test
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
+
+
+
+ org.owasp
+ dependency-check-maven
+
+
+ org.codehaus.mojo
+ animal-sniffer-maven-plugin
+
+
+ maven-enforcer-plugin
+
+
+ maven-compiler-plugin
+ ${maven-compiler-plugin.version}
+
+
+ maven-source-plugin
+
+
+ maven-javadoc-plugin
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+
+
+
+
+ maven-project-info-reports-plugin
+
+
+
+
+
diff --git a/yoti-sdk-auth/src/main/java/com/yoti/auth/AuthenticationTokenGenerator.java b/yoti-sdk-auth/src/main/java/com/yoti/auth/AuthenticationTokenGenerator.java
new file mode 100644
index 000000000..8a0cf0369
--- /dev/null
+++ b/yoti-sdk-auth/src/main/java/com/yoti/auth/AuthenticationTokenGenerator.java
@@ -0,0 +1,193 @@
+package com.yoti.auth;
+
+import static com.yoti.validation.Validation.notNull;
+import static com.yoti.validation.Validation.notNullOrEmpty;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.KeyPair;
+import java.time.OffsetDateTime;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Supplier;
+
+import com.yoti.api.client.InitialisationException;
+import com.yoti.api.client.KeyPairSource;
+import com.yoti.api.client.spi.remote.KeyStreamVisitor;
+import com.yoti.api.client.spi.remote.call.ResourceException;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.jsonwebtoken.Jwts;
+
+/**
+ * The {@link AuthenticationTokenGenerator} is used for generation authorization tokens
+ * that can be used for accessing Yoti services. An authorization token must have
+ * a unique identifier, and an expiry timestamp. One or more scopes can be provided
+ * to allow the authorization token access to different parts of Yoti systems.
+ *
+ * The {@link AuthenticationTokenGenerator.Builder} can be accessed via {@code AuthorizationTokenGenerator.builder()}
+ * method, and then configured via the fluent API.
+ */
+public class AuthenticationTokenGenerator {
+
+ private final String sdkId;
+ private final KeyPair keyPair;
+ private final Supplier jwtIdSupplier;
+ private final FormRequestClient formRequestClient;
+
+ private final URL authApiUrl;
+ private final ObjectMapper objectMapper;
+
+ AuthenticationTokenGenerator(
+ String sdkId,
+ KeyPair keyPair,
+ Supplier jwtIdSupplier,
+ FormRequestClient formRequestClient,
+ ObjectMapper objectMapper) {
+ this.sdkId = sdkId;
+ this.keyPair = keyPair;
+ this.jwtIdSupplier = jwtIdSupplier;
+ this.formRequestClient = formRequestClient;
+ this.objectMapper = objectMapper;
+
+ try {
+ authApiUrl = new URL(System.getProperty(Properties.PROPERTY_YOTI_AUTH_URL, Properties.DEFAULT_YOTI_AUTH_URL));
+ } catch (MalformedURLException e) {
+ throw new IllegalStateException("Invalid Yoti auth url", e);
+ }
+ }
+
+ /**
+ * Creates a new instance of {@link AuthenticationTokenGenerator.Builder}
+ *
+ * @return the builder
+ */
+ public static AuthenticationTokenGenerator.Builder builder() {
+ return new AuthenticationTokenGenerator.Builder();
+ }
+
+ /**
+ * Creates a new authentication token, using the supplied scopes and comment.
+ *
+ * @param scopes a list of scopes to be used by the authentication token
+ * @return a {@link CreateAuthenticationTokenResponse} containing information about the created token.
+ * @throws ResourceException if something was incorrect with the request to the Yoti authentication service
+ * @throws IOException
+ */
+ public CreateAuthenticationTokenResponse generate(List scopes) throws ResourceException, IOException {
+ notNullOrEmpty(scopes, "scopes");
+
+ String jwts = createSignedJwt(sdkId, keyPair, jwtIdSupplier, authApiUrl);
+
+ Map formParams = new HashMap<>();
+ formParams.put("grant_type", "client_credentials");
+ formParams.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+ formParams.put("scope", String.join(" ", scopes));
+ formParams.put("client_assertion", jwts);
+
+ byte[] responseBody = formRequestClient.performRequest(authApiUrl, "POST", formParams);
+
+ return objectMapper.readValue(responseBody, CreateAuthenticationTokenResponse.class);
+ }
+
+ private String createSignedJwt(String sdkId, KeyPair keyPair, Supplier jwtIdSupplier, URL authApiUrl) {
+ String sdkIdProperty = String.format("sdk:%s", sdkId);
+ OffsetDateTime now = OffsetDateTime.now();
+ return Jwts.builder()
+ .issuer(sdkIdProperty)
+ .subject(sdkIdProperty)
+ .id(jwtIdSupplier.get())
+ .audience()
+ .add(authApiUrl.toString())
+ .and()
+ .expiration(new Date(now.plusMinutes(5).toInstant().toEpochMilli()))
+ .issuedAt(new Date(now.toInstant().toEpochMilli()))
+ .header()
+ .add("alg", "PS384")
+ .add("typ", "JWT")
+ .and()
+ .signWith(keyPair.getPrivate(), Jwts.SIG.PS384)
+ .compact();
+ }
+
+ public static final class Builder {
+
+ private String sdkId;
+ private KeyPairSource keyPairSource;
+ private Supplier jwtIdSupplier = () -> UUID.randomUUID().toString();
+
+ private Builder() {
+ }
+
+ /**
+ * Sets the SDK ID that the authorization token will be generated against.
+ *
+ * @param sdkId the SDK ID
+ * @return the builder for method chaining.
+ */
+ public Builder withSdkId(String sdkId) {
+ this.sdkId = sdkId;
+ return this;
+ }
+
+ /**
+ * Sets the {@link KeyPairSource} that will be used to load the {@link KeyPair}
+ *
+ * @param keyPairSource the key pair source that will be used to load the {@link KeyPair}
+ * @return the builder for method chaining.
+ */
+ public Builder withKeyPairSource(KeyPairSource keyPairSource) {
+ this.keyPairSource = keyPairSource;
+ return this;
+ }
+
+ /**
+ * Sets the supplier that will be used to generate a unique ID for the
+ * authorization token. By default, this will be a UUID v4.
+ *
+ * @param jwtIdSupplier the supplier used for generating authorization token ID
+ * @return the builder for method chaining.
+ */
+ public Builder withJwtIdSupplier(Supplier jwtIdSupplier) {
+ this.jwtIdSupplier = jwtIdSupplier;
+ return this;
+ }
+
+ /**
+ * Builds an {@link AuthenticationTokenGenerator} using the values supplied to the {@link Builder}.
+ *
+ * @return the configured {@link AuthenticationTokenGenerator}
+ */
+ public AuthenticationTokenGenerator build() {
+ notNullOrEmpty(sdkId, "sdkId");
+ notNull(keyPairSource, "keyPairSource");
+ notNull(jwtIdSupplier, "jwtIdSupplier");
+
+ ObjectMapper objectMapper = new ObjectMapper()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+ return new AuthenticationTokenGenerator(
+ sdkId,
+ loadKeyPair(keyPairSource),
+ jwtIdSupplier,
+ new FormRequestClient(),
+ objectMapper
+ );
+ }
+
+ private KeyPair loadKeyPair(KeyPairSource kpSource) throws InitialisationException {
+ try {
+ return kpSource.getFromStream(new KeyStreamVisitor());
+ } catch (IOException e) {
+ throw new InitialisationException("Cannot load key pair", e);
+ }
+ }
+
+ }
+
+}
diff --git a/yoti-sdk-auth/src/main/java/com/yoti/auth/CreateAuthenticationTokenResponse.java b/yoti-sdk-auth/src/main/java/com/yoti/auth/CreateAuthenticationTokenResponse.java
new file mode 100644
index 000000000..3f4cb439c
--- /dev/null
+++ b/yoti-sdk-auth/src/main/java/com/yoti/auth/CreateAuthenticationTokenResponse.java
@@ -0,0 +1,56 @@
+package com.yoti.auth;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public final class CreateAuthenticationTokenResponse {
+
+ @JsonProperty("access_token")
+ private String accessToken;
+
+ @JsonProperty("token_type")
+ private String tokenType;
+
+ @JsonProperty("expires_in")
+ private Integer expiresIn;
+
+ @JsonProperty("scope")
+ private String scope;
+
+ /**
+ * Returns the Yoti Authentication token used to perform requests to other Yoti services.
+ *
+ * @return the newly created access token
+ */
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ /**
+ * Returns the type of the newly generated authentication token.
+ *
+ * @return the token type
+ */
+ public String getTokenType() {
+ return tokenType;
+ }
+
+ /**
+ * Returns the amount of time (in seconds) in which the newly generated Authentication Token
+ * will expire in.
+ *
+ * @return the time (in seconds) of when the token will expire
+ */
+ public Integer getExpiresIn() {
+ return expiresIn;
+ }
+
+ /**
+ * A whitespace delimited string of scopes that the Authentication token has.
+ *
+ * @return the scopes of the token as a whitespace delimited string
+ */
+ public String getScope() {
+ return scope;
+ }
+
+}
diff --git a/yoti-sdk-auth/src/main/java/com/yoti/auth/FormRequestClient.java b/yoti-sdk-auth/src/main/java/com/yoti/auth/FormRequestClient.java
new file mode 100644
index 000000000..becfc1dda
--- /dev/null
+++ b/yoti-sdk-auth/src/main/java/com/yoti/auth/FormRequestClient.java
@@ -0,0 +1,84 @@
+package com.yoti.auth;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.yoti.api.client.spi.remote.call.ResourceException;
+import com.yoti.api.client.spi.remote.util.QuietCloseable;
+
+/**
+ * Internal use only.
+ *
+ * The {@link FormRequestClient} is used for performing an application/x-www-form-urlencoded
+ * HTTP request using base Java libraries only.
+ */
+final class FormRequestClient {
+
+ byte[] performRequest(URL url, String method, Map formParams) throws IOException, ResourceException {
+ byte[] postData = getData(formParams);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setDoOutput(true);
+ connection.setRequestMethod(method);
+ connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ connection.setRequestProperty("Content-Length", Integer.toString(postData.length));
+ connection.setRequestProperty("charset", StandardCharsets.UTF_8.toString());
+ connection.setUseCaches(false);
+ connection.setInstanceFollowRedirects(false);
+
+ try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
+ wr.write(postData);
+ }
+
+ return parseResponse(connection);
+ }
+
+ private byte[] getData(Map params) {
+ return params.entrySet().stream()
+ .map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue()))
+ .collect(Collectors.joining("&"))
+ .getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static String encode(String v) {
+ try {
+ return URLEncoder.encode(v, StandardCharsets.UTF_8.toString());
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private byte[] parseResponse(HttpURLConnection httpUrlConnection) throws ResourceException, IOException {
+ int responseCode = httpUrlConnection.getResponseCode();
+ if (responseCode >= HttpURLConnection.HTTP_BAD_REQUEST) {
+ byte[] responseBody = readBody(httpUrlConnection.getErrorStream());
+ throw new ResourceException(responseCode, httpUrlConnection.getResponseMessage(), new String(responseBody));
+ }
+ return readBody(httpUrlConnection.getInputStream());
+ }
+
+ private byte[] readBody(InputStream httpInputStream) throws IOException {
+ try (QuietCloseable inputStream = new QuietCloseable<>(httpInputStream)) {
+ return readChunked(inputStream.get());
+ }
+ }
+
+ private byte[] readChunked(InputStream inputStream) throws IOException {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ byte[] byteChunk = new byte[4096];
+ int n;
+ while ((n = inputStream.read(byteChunk)) > 0) {
+ byteArrayOutputStream.write(byteChunk, 0, n);
+ }
+ return byteArrayOutputStream.toByteArray();
+ }
+
+}
diff --git a/yoti-sdk-auth/src/main/java/com/yoti/auth/Properties.java b/yoti-sdk-auth/src/main/java/com/yoti/auth/Properties.java
new file mode 100644
index 000000000..d72e30926
--- /dev/null
+++ b/yoti-sdk-auth/src/main/java/com/yoti/auth/Properties.java
@@ -0,0 +1,17 @@
+package com.yoti.auth;
+
+/**
+ * Internal use only.
+ *
+ * Contains property values used by the `yoti-sdk-auth` module for
+ * creating Yoti Authentication tokens.
+ */
+final class Properties {
+
+ private static final String YOTI_AUTH_HOST = "https://auth.api.yoti.com";
+ private static final String YOTI_AUTH_PATH_PREFIX = "/v1/oauth/token";
+
+ public static final String PROPERTY_YOTI_AUTH_URL = "yoti.auth.url";
+ public static final String DEFAULT_YOTI_AUTH_URL = YOTI_AUTH_HOST + YOTI_AUTH_PATH_PREFIX;
+
+}
diff --git a/yoti-sdk-auth/src/test/java/com/yoti/auth/AuthenticationTokenGeneratorTest.java b/yoti-sdk-auth/src/test/java/com/yoti/auth/AuthenticationTokenGeneratorTest.java
new file mode 100644
index 000000000..cec6eb0af
--- /dev/null
+++ b/yoti-sdk-auth/src/test/java/com/yoti/auth/AuthenticationTokenGeneratorTest.java
@@ -0,0 +1,124 @@
+package com.yoti.auth;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.KeyPair;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Supplier;
+
+import com.yoti.api.client.InitialisationException;
+import com.yoti.api.client.KeyPairSource;
+import com.yoti.auth.util.CryptoUtil;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AuthenticationTokenGeneratorTest {
+
+ private static final String SOME_AUTH_API_URL = "https://someAuthApiUrl";
+ private static final String SOME_SDK_ID = "someSdkId";
+ private static final Supplier SOME_JWT_ID_SUPPLIER = () -> UUID.randomUUID().toString();
+ private static final byte[] SOME_RESPONSE_BODY = new byte[] { 1, 2, 3 };
+
+ @Mock FormRequestClient formRequestClientMock;
+ @Mock ObjectMapper objectMapperMock;
+
+ @Mock KeyPairSource keyPairSourceMock;
+ @Mock CreateAuthenticationTokenResponse createAuthenticationTokenResponseMock;
+
+ @Captor ArgumentCaptor urlArgumentCaptor;
+ @Captor ArgumentCaptor