Generic credential auth endpoint for call links

This commit is contained in:
Katherine Yen 2023-04-04 10:28:35 -07:00 committed by GitHub
parent 48ebafa4e0
commit e4da59c236
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 292 additions and 49 deletions

View File

@ -433,3 +433,6 @@ registrationService:
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAAAAAAAAAAAAAA
-----END CERTIFICATE-----
callLink:
userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with calling frontend to generate auth tokens for Signal users

View File

@ -20,6 +20,7 @@ import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration;
import org.whispersystems.textsecuregcm.configuration.CallLinkConfiguration;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
@ -219,6 +220,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private ArtServiceConfiguration artService;
@Valid
@NotNull
@JsonProperty
private CallLinkConfiguration callLink;
@Valid
@NotNull
@JsonProperty
@ -371,6 +377,10 @@ public class WhisperServerConfiguration extends Configuration {
return datadog;
}
public CallLinkConfiguration getCallLinkConfiguration() {
return callLink;
}
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
return unidentifiedDelivery;
}

View File

@ -89,6 +89,7 @@ import org.whispersystems.textsecuregcm.controllers.AccountControllerV2;
import org.whispersystems.textsecuregcm.controllers.ArtController;
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV3;
import org.whispersystems.textsecuregcm.controllers.CallLinkController;
import org.whispersystems.textsecuregcm.controllers.CertificateController;
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
import org.whispersystems.textsecuregcm.controllers.DeviceController;
@ -490,6 +491,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getArtServiceConfiguration());
ExternalServiceCredentialsGenerator svr2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator(
config.getSvr2Configuration());
ExternalServiceCredentialsGenerator callLinkCredentialsGenerator = CallLinkController.credentialsGenerator(
config.getCallLinkConfiguration()
);
dynamicConfigurationManager.start();
@ -774,6 +778,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new ArtController(rateLimiters, artCredentialsGenerator),
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getRegion(), config.getAwsAttachmentsConfiguration().getBucket()),
new AttachmentControllerV3(rateLimiters, config.getGcpAttachmentsConfiguration().getDomain(), config.getGcpAttachmentsConfiguration().getEmail(), config.getGcpAttachmentsConfiguration().getMaxSizeInBytes(), config.getGcpAttachmentsConfiguration().getPathPrefix(), config.getGcpAttachmentsConfiguration().getRsaSigningKey()),
new CallLinkController(callLinkCredentialsGenerator),
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()), zkAuthOperations, clock),
new ChallengeController(rateLimitChallengeManager),
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keys, rateLimiters, config.getMaxDevices()),

View File

@ -11,8 +11,10 @@ import static org.whispersystems.textsecuregcm.util.HmacUtils.hmac256TruncatedTo
import static org.whispersystems.textsecuregcm.util.HmacUtils.hmacHexStringsEqual;
import java.time.Clock;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
@ -28,6 +30,10 @@ public class ExternalServiceCredentialsGenerator {
private final boolean truncateSignature;
private final String usernameTimestampPrefix;
private final Function<Instant, Instant> usernameTimestampTruncator;
private final Clock clock;
private final int truncateLength;
@ -41,14 +47,22 @@ public class ExternalServiceCredentialsGenerator {
final byte[] userDerivationKey,
final boolean prependUsername,
final boolean truncateSignature,
final Clock clock,
final int truncateLength) {
final int truncateLength,
final String usernameTimestampPrefix,
final Function<Instant, Instant> usernameTimestampTruncator,
final Clock clock) {
this.key = requireNonNull(key);
this.userDerivationKey = requireNonNull(userDerivationKey);
this.prependUsername = prependUsername;
this.truncateSignature = truncateSignature;
this.usernameTimestampPrefix = usernameTimestampPrefix;
this.usernameTimestampTruncator = usernameTimestampTruncator;
this.clock = requireNonNull(clock);
this.truncateLength = truncateLength;
if (hasUsernameTimestampPrefix() ^ hasUsernameTimestampTruncator()) {
throw new RuntimeException("Configured to have only one of (usernameTimestampPrefix, usernameTimestampTruncator)");
}
}
/**
@ -66,13 +80,34 @@ public class ExternalServiceCredentialsGenerator {
* @return an instance of {@link ExternalServiceCredentials}
*/
public ExternalServiceCredentials generateFor(final String identity) {
if (usernameIsTimestamp()) {
throw new RuntimeException("Configured to use timestamp as username");
}
return generate(identity);
}
/**
* Generates `ExternalServiceCredentials` using a prefix concatenated with a truncated timestamp as the username, following this generator's configuration.
* @return an instance of {@link ExternalServiceCredentials}
*/
public ExternalServiceCredentials generateWithTimestampAsUsername() {
if (!usernameIsTimestamp()) {
throw new RuntimeException("Not configured to use timestamp as username");
}
final String truncatedTimestampSeconds = String.valueOf(usernameTimestampTruncator.apply(clock.instant()).getEpochSecond());
return generate(usernameTimestampPrefix + DELIMITER + truncatedTimestampSeconds);
}
private ExternalServiceCredentials generate(final String identity) {
final String username = shouldDeriveUsername()
? hmac256TruncatedToHexString(userDerivationKey, identity, truncateLength)
: identity;
final long currentTimeSeconds = currentTimeSeconds();
final String dataToSign = username + DELIMITER + currentTimeSeconds;
final String dataToSign = usernameIsTimestamp() ? username : username + DELIMITER + currentTimeSeconds;
final String signature = truncateSignature
? hmac256TruncatedToHexString(key, dataToSign, truncateLength)
@ -84,7 +119,7 @@ public class ExternalServiceCredentialsGenerator {
}
/**
* In certain cases, identity (as it was passed to `generateFor` method)
* In certain cases, identity (as it was passed to `generate` method)
* is a part of the signature (`password`, in terms of `ExternalServiceCredentials`) string itself.
* For such cases, this method returns the value of the identity string.
* @param password `password` part of `ExternalServiceCredentials`
@ -96,9 +131,15 @@ public class ExternalServiceCredentialsGenerator {
return Optional.empty();
}
// checking for the case of unexpected format
return StringUtils.countMatches(password, DELIMITER) == 2
? Optional.of(password.substring(0, password.indexOf(DELIMITER)))
: Optional.empty();
if (StringUtils.countMatches(password, DELIMITER) == 2) {
if (usernameIsTimestamp()) {
final int indexOfSecondDelimiter = password.indexOf(DELIMITER, password.indexOf(DELIMITER) + 1);
return Optional.of(password.substring(0, indexOfSecondDelimiter));
} else {
return Optional.of(password.substring(0, password.indexOf(DELIMITER)));
}
}
return Optional.empty();
}
/**
@ -115,7 +156,7 @@ public class ExternalServiceCredentialsGenerator {
// making sure password format matches our expectations based on the generator configuration
if (parts.length == 3 && prependUsername) {
final String username = parts[0];
final String username = usernameIsTimestamp() ? parts[0] + DELIMITER + parts[1] : parts[0];
// username has to match the one from `credentials`
if (!credentials.username().equals(username)) {
return Optional.empty();
@ -130,7 +171,7 @@ public class ExternalServiceCredentialsGenerator {
return Optional.empty();
}
final String signedData = credentials.username() + DELIMITER + timestampSeconds;
final String signedData = usernameIsTimestamp() ? credentials.username() : credentials.username() + DELIMITER + timestampSeconds;
final String expectedSignature = truncateSignature
? hmac256TruncatedToHexString(key, signedData, truncateLength)
: hmac256ToHexString(key, signedData);
@ -158,6 +199,18 @@ public class ExternalServiceCredentialsGenerator {
return userDerivationKey.length > 0;
}
private boolean hasUsernameTimestampPrefix() {
return usernameTimestampPrefix != null;
}
private boolean hasUsernameTimestampTruncator() {
return usernameTimestampTruncator != null;
}
private boolean usernameIsTimestamp() {
return hasUsernameTimestampPrefix() && hasUsernameTimestampTruncator();
}
private long currentTimeSeconds() {
return clock.instant().getEpochSecond();
}
@ -174,6 +227,10 @@ public class ExternalServiceCredentialsGenerator {
private int truncateLength = 10;
private String usernameTimestampPrefix = null;
private Function<Instant, Instant> usernameTimestampTruncator = null;
private Clock clock = Clock.systemUTC();
@ -208,9 +265,15 @@ public class ExternalServiceCredentialsGenerator {
return this;
}
public Builder withUsernameTimestampTruncatorAndPrefix(final Function<Instant, Instant> truncator, final String prefix) {
this.usernameTimestampTruncator = truncator;
this.usernameTimestampPrefix = prefix;
return this;
}
public ExternalServiceCredentialsGenerator build() {
return new ExternalServiceCredentialsGenerator(
key, userDerivationKey, prependUsername, truncateSignature, clock, truncateLength);
key, userDerivationKey, prependUsername, truncateSignature, truncateLength, usernameTimestampPrefix, usernameTimestampTruncator, clock);
}
}
}

View File

@ -0,0 +1,9 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.util.ExactlySize;
import javax.validation.constraints.NotEmpty;
import java.util.HexFormat;
public record CallLinkConfiguration (@ExactlySize({32}) byte[] userAuthenticationTokenSharedSecret) {
}

View File

@ -0,0 +1,56 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.configuration.CallLinkConfiguration;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.time.Clock;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
@Path("/v1/call-link")
@io.swagger.v3.oas.annotations.tags.Tag(name = "CallLink")
public class CallLinkController {
@VisibleForTesting
public static final String ANONYMOUS_CREDENTIAL_PREFIX = "anon";
private final ExternalServiceCredentialsGenerator callingFrontendServiceCredentialGenerator;
public CallLinkController(
ExternalServiceCredentialsGenerator callingFrontendServiceCredentialGenerator
) {
this.callingFrontendServiceCredentialGenerator = callingFrontendServiceCredentialGenerator;
}
public static ExternalServiceCredentialsGenerator credentialsGenerator(final CallLinkConfiguration cfg) {
return ExternalServiceCredentialsGenerator
.builder(cfg.userAuthenticationTokenSharedSecret())
.withUsernameTimestampTruncatorAndPrefix(timestamp -> timestamp.truncatedTo(ChronoUnit.DAYS), ANONYMOUS_CREDENTIAL_PREFIX)
.build();
}
@Timed
@GET
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Generate credentials for calling frontend",
description = """
These credentials enable clients to prove to calling frontend that they were a Signal user within the last day.
For client privacy, timestamps are truncated to 1 day granularity and the token does not include or derive from an ACI.
"""
)
@ApiResponse(responseCode = "200", description = "`JSON` with generated credentials.", useReturnTypeSchema = true)
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
public ExternalServiceCredentials getAuth(@Auth AuthenticatedAccount auth) {
return callingFrontendServiceCredentialGenerator.generateWithTimestampAsUsername();
}
}

View File

@ -8,9 +8,14 @@ package org.whispersystems.textsecuregcm.tests.auth;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.whispersystems.textsecuregcm.util.HmacUtils.hmac256TruncatedToHexString;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
@ -18,6 +23,7 @@ import org.whispersystems.textsecuregcm.util.MockUtils;
import org.whispersystems.textsecuregcm.util.MutableClock;
class ExternalServiceCredentialsGeneratorTest {
private static final String PREFIX = "prefix";
private static final String E164 = "+14152222222";
@ -27,6 +33,42 @@ class ExternalServiceCredentialsGeneratorTest {
private static final String TIME_SECONDS_STRING = Long.toString(TIME_SECONDS);
private static final String USERNAME_TIMESTAMP = PREFIX + ":" + Instant.ofEpochSecond(TIME_SECONDS).truncatedTo(ChronoUnit.DAYS).getEpochSecond();
private static final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
private static final ExternalServiceCredentialsGenerator standardGenerator = ExternalServiceCredentialsGenerator
.builder(new byte[32])
.withClock(clock)
.build();
private static final ExternalServiceCredentials standardCredentials = standardGenerator.generateFor(E164);
private static final ExternalServiceCredentialsGenerator usernameIsTimestampGenerator = ExternalServiceCredentialsGenerator
.builder(new byte[32])
.withUsernameTimestampTruncatorAndPrefix(timestamp -> timestamp.truncatedTo(ChronoUnit.DAYS), PREFIX)
.withClock(clock)
.build();
private static final ExternalServiceCredentials usernameIsTimestampCredentials = usernameIsTimestampGenerator.generateWithTimestampAsUsername();
@BeforeEach
public void before() throws Exception {
clock.setTimeMillis(TIME_MILLIS);
}
@Test
void testInvalidConstructor() {
assertThrows(RuntimeException.class, () -> ExternalServiceCredentialsGenerator
.builder(new byte[32])
.withUsernameTimestampTruncatorAndPrefix(null, PREFIX)
.build());
assertThrows(RuntimeException.class, () -> ExternalServiceCredentialsGenerator
.builder(new byte[32])
.withUsernameTimestampTruncatorAndPrefix(timestamp -> timestamp.truncatedTo(ChronoUnit.DAYS), null)
.build());
}
@Test
void testGenerateDerivedUsername() {
@ -42,18 +84,13 @@ class ExternalServiceCredentialsGeneratorTest {
@Test
void testGenerateNoDerivedUsername() {
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
.builder(new byte[32])
.build();
final ExternalServiceCredentials credentials = generator.generateFor(E164);
assertEquals(credentials.username(), E164);
assertTrue(credentials.password().startsWith(E164));
assertEquals(credentials.password().split(":").length, 3);
assertEquals(standardCredentials.username(), E164);
assertTrue(standardCredentials.password().startsWith(E164));
assertEquals(standardCredentials.password().split(":").length, 3);
}
@Test
public void testNotPrependUsername() throws Exception {
final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
.builder(new byte[32])
.prependUsername(false)
@ -65,52 +102,68 @@ class ExternalServiceCredentialsGeneratorTest {
assertEquals(credentials.password().split(":").length, 2);
}
@Test
public void testWithUsernameIsTimestamp() {
assertEquals(USERNAME_TIMESTAMP, usernameIsTimestampCredentials.username());
final String[] passwordComponents = usernameIsTimestampCredentials.password().split(":");
assertEquals(USERNAME_TIMESTAMP, passwordComponents[0] + ":" + passwordComponents[1]);
assertEquals(hmac256TruncatedToHexString(new byte[32], USERNAME_TIMESTAMP, 10), passwordComponents[2]);
}
@Test
public void testValidateValid() throws Exception {
final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
.builder(new byte[32])
.withClock(clock)
.build();
final ExternalServiceCredentials credentials = generator.generateFor(E164);
assertEquals(generator.validateAndGetTimestamp(credentials).orElseThrow(), TIME_SECONDS);
assertEquals(standardGenerator.validateAndGetTimestamp(standardCredentials).orElseThrow(), TIME_SECONDS);
}
@Test
public void testValidateValidWithUsernameIsTimestamp() {
final long expectedTimestamp = Instant.ofEpochSecond(TIME_SECONDS).truncatedTo(ChronoUnit.DAYS).getEpochSecond();
assertEquals(expectedTimestamp, usernameIsTimestampGenerator.validateAndGetTimestamp(usernameIsTimestampCredentials).orElseThrow());
}
@Test
public void testValidateInvalid() throws Exception {
final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
.builder(new byte[32])
.withClock(clock)
.build();
final ExternalServiceCredentials credentials = generator.generateFor(E164);
final ExternalServiceCredentials corruptedStandardUsername = new ExternalServiceCredentials(
standardCredentials.username(), standardCredentials.password().replace(E164, E164 + "0"));
final ExternalServiceCredentials corruptedStandardTimestamp = new ExternalServiceCredentials(
standardCredentials.username(), standardCredentials.password().replace(TIME_SECONDS_STRING, TIME_SECONDS_STRING + "0"));
final ExternalServiceCredentials corruptedStandardPassword = new ExternalServiceCredentials(
standardCredentials.username(), standardCredentials.password() + "0");
final ExternalServiceCredentials corruptedUsername = new ExternalServiceCredentials(
credentials.username(), credentials.password().replace(E164, E164 + "0"));
final ExternalServiceCredentials corruptedTimestamp = new ExternalServiceCredentials(
credentials.username(), credentials.password().replace(TIME_SECONDS_STRING, TIME_SECONDS_STRING + "0"));
final ExternalServiceCredentials corruptedPassword = new ExternalServiceCredentials(
credentials.username(), credentials.password() + "0");
final ExternalServiceCredentials corruptedUsernameTimestamp = new ExternalServiceCredentials(
usernameIsTimestampCredentials.username(), usernameIsTimestampCredentials.password().replace(USERNAME_TIMESTAMP, USERNAME_TIMESTAMP
+ "0"));
final ExternalServiceCredentials corruptedUsernameTimestampPassword = new ExternalServiceCredentials(
usernameIsTimestampCredentials.username(), usernameIsTimestampCredentials.password() + "0");
assertTrue(generator.validateAndGetTimestamp(corruptedUsername).isEmpty());
assertTrue(generator.validateAndGetTimestamp(corruptedTimestamp).isEmpty());
assertTrue(generator.validateAndGetTimestamp(corruptedPassword).isEmpty());
assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardUsername).isEmpty());
assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardTimestamp).isEmpty());
assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardPassword).isEmpty());
assertTrue(usernameIsTimestampGenerator.validateAndGetTimestamp(corruptedUsernameTimestamp).isEmpty());
assertTrue(usernameIsTimestampGenerator.validateAndGetTimestamp(corruptedUsernameTimestampPassword).isEmpty());
}
@Test
public void testValidateWithExpiration() throws Exception {
final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
.builder(new byte[32])
.withClock(clock)
.build();
final ExternalServiceCredentials credentials = generator.generateFor(E164);
final long elapsedSeconds = 10000;
clock.incrementSeconds(elapsedSeconds);
assertEquals(generator.validateAndGetTimestamp(credentials, elapsedSeconds + 1).orElseThrow(), TIME_SECONDS);
assertTrue(generator.validateAndGetTimestamp(credentials, elapsedSeconds - 1).isEmpty());
assertEquals(standardGenerator.validateAndGetTimestamp(standardCredentials, elapsedSeconds + 1).orElseThrow(), TIME_SECONDS);
assertTrue(standardGenerator.validateAndGetTimestamp(standardCredentials, elapsedSeconds - 1).isEmpty());
}
@Test
public void testGetIdentityFromSignature() {
final String identity = standardGenerator.identityFromSignature(standardCredentials.password()).orElseThrow();
assertEquals(E164, identity);
}
@Test
public void testGetIdentityFromSignatureIsTimestamp() {
final String identity = usernameIsTimestampGenerator.identityFromSignature(usernameIsTimestampCredentials.password()).orElseThrow();
assertEquals(USERNAME_TIMESTAMP, identity);
}
@Test

View File

@ -0,0 +1,44 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.configuration.CallLinkConfiguration;
import org.whispersystems.textsecuregcm.controllers.CallLinkController;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.MockUtils;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@ExtendWith(DropwizardExtensionsSupport.class)
public class CallLinkControllerTest {
private static final CallLinkConfiguration callLinkConfiguration = MockUtils.buildMock(
CallLinkConfiguration.class,
cfg -> Mockito.when(cfg.userAuthenticationTokenSharedSecret()).thenReturn(new byte[32])
);
private static final ExternalServiceCredentialsGenerator callLinkCredentialsGenerator = CallLinkController.credentialsGenerator(callLinkConfiguration);
private static final ResourceExtension resources = ResourceExtension.builder()
.setMapper(SystemMapper.jsonMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new CallLinkController(callLinkCredentialsGenerator))
.build();
@Test
void testGetAuth() {
ExternalServiceCredentials credentials = resources.getJerseyTest()
.target("/v1/call-link/auth")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(ExternalServiceCredentials.class);
assertThat(credentials.username()).isNotEmpty();
assertThat(credentials.password()).isNotEmpty();
}
}