Skip to content

Commit f6a93ac

Browse files
fix: validate tenantUrl, derive cache TTL from JWT exp, dedupe tests
- Validate tenantUrl is a bare hostname (reject scheme/port/path/whitespace) so users get a clear error instead of an opaque gRPC connect failure - Drive cached-token TTL from the JWT exp claim instead of a fixed 3600s default, with safe fallback when the JWT can't be parsed - Drop the brittle reflection-based test that forced an unreachable rebuild failure; replace with focused secondsUntilJwtExpiry unit tests - Collapse the duplicate TokenProcessorSupplier delegation test that re-exercised JWT parsing already covered in DirectCdpTokenProcessorTest - Generate JWTs dynamically in tests so exp stays in the future
1 parent ea58df1 commit f6a93ac

4 files changed

Lines changed: 190 additions & 122 deletions

File tree

jdbc-http/src/main/java/com/salesforce/datacloud/jdbc/auth/DirectCdpTokenProcessor.java

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
import static com.salesforce.datacloud.jdbc.util.PropertyParsingUtils.takeOptional;
88
import static com.salesforce.datacloud.jdbc.util.PropertyParsingUtils.takeRequired;
99

10+
import com.fasterxml.jackson.databind.JsonNode;
11+
import com.fasterxml.jackson.databind.ObjectMapper;
1012
import com.salesforce.datacloud.jdbc.auth.model.DataCloudTokenResponse;
1113
import java.sql.SQLException;
14+
import java.time.Instant;
15+
import java.util.Base64;
1216
import java.util.Optional;
1317
import java.util.Properties;
1418
import lombok.extern.slf4j.Slf4j;
@@ -18,7 +22,8 @@ public class DirectCdpTokenProcessor {
1822
static final String CDP_TOKEN_KEY = "cdpToken";
1923
static final String TENANT_URL_KEY = "tenantUrl";
2024
static final String DATASPACE_KEY = "dataspace";
21-
static final int DEFAULT_EXPIRES_IN = 3600;
25+
static final int FALLBACK_EXPIRES_IN_SECONDS = 3600;
26+
private static final ObjectMapper JSON = new ObjectMapper();
2227

2328
private final String cdpToken;
2429
private final String tenantUrl;
@@ -38,7 +43,7 @@ public static boolean hasCdpToken(Properties properties) {
3843
public static DirectCdpTokenProcessor ofDestructive(Properties properties) throws SQLException {
3944
try {
4045
String cdpToken = takeRequired(properties, CDP_TOKEN_KEY);
41-
String tenantUrl = takeRequired(properties, TENANT_URL_KEY);
46+
String tenantUrl = validateTenantHost(takeRequired(properties, TENANT_URL_KEY));
4247
String dataspace = takeOptional(properties, DATASPACE_KEY).orElse(null);
4348
DirectCdpTokenProcessor processor = new DirectCdpTokenProcessor(cdpToken, tenantUrl, dataspace);
4449
processor.validateToken();
@@ -48,6 +53,24 @@ public static DirectCdpTokenProcessor ofDestructive(Properties properties) throw
4853
}
4954
}
5055

56+
/**
57+
* tenantUrl must be a bare hostname — gRPC's {@code ManagedChannelBuilder.forAddress} requires it.
58+
* Reject schemes, ports, paths, and whitespace so users get a clear error rather than a confusing
59+
* connection failure later.
60+
*/
61+
static String validateTenantHost(String tenantUrl) {
62+
if (tenantUrl.contains("://") || tenantUrl.contains("/") || tenantUrl.contains(":")) {
63+
throw new IllegalArgumentException(
64+
"tenantUrl must be a bare hostname (e.g. 'tenant.c360a.salesforce.com'), got: '" + tenantUrl + "'");
65+
}
66+
String trimmed = tenantUrl.trim();
67+
if (trimmed.isEmpty() || trimmed.length() != tenantUrl.length()) {
68+
throw new IllegalArgumentException(
69+
"tenantUrl must be a non-empty hostname with no whitespace, got: '" + tenantUrl + "'");
70+
}
71+
return trimmed;
72+
}
73+
5174
private void validateToken() throws SQLException {
5275
try {
5376
DataCloudToken token = buildDataCloudToken();
@@ -60,18 +83,10 @@ private void validateToken() throws SQLException {
6083
}
6184

6285
public DataCloudToken getDataCloudToken() throws SQLException {
63-
if (cachedDataCloudToken != null && cachedDataCloudToken.isAlive()) {
64-
return cachedDataCloudToken;
65-
}
66-
67-
try {
68-
DataCloudToken token = buildDataCloudToken();
69-
cachedDataCloudToken = token;
70-
return token;
71-
} catch (Exception ex) {
72-
cachedDataCloudToken = null;
73-
throw new SQLException(ex.getMessage(), "28000", ex);
86+
if (cachedDataCloudToken == null || !cachedDataCloudToken.isAlive()) {
87+
cachedDataCloudToken = buildDataCloudToken();
7488
}
89+
return cachedDataCloudToken;
7590
}
7691

7792
public String getLakehouse() throws SQLException {
@@ -87,7 +102,32 @@ private DataCloudToken buildDataCloudToken() throws SQLException {
87102
response.setToken(cdpToken);
88103
response.setInstanceUrl(tenantUrl);
89104
response.setTokenType("Bearer");
90-
response.setExpiresIn(DEFAULT_EXPIRES_IN);
105+
response.setExpiresIn(secondsUntilJwtExpiry(cdpToken));
91106
return DataCloudToken.of(response);
92107
}
108+
109+
/**
110+
* Returns seconds remaining until the JWT's `exp` claim, clamped to a minimum of 0.
111+
* If the JWT cannot be parsed or has no `exp` claim, falls back to {@link #FALLBACK_EXPIRES_IN_SECONDS}
112+
* so callers behave identically to the previous fixed-TTL behavior.
113+
*/
114+
static int secondsUntilJwtExpiry(String jwt) {
115+
try {
116+
String[] chunks = jwt.split("\\.", -1);
117+
if (chunks.length < 2) {
118+
return FALLBACK_EXPIRES_IN_SECONDS;
119+
}
120+
byte[] decoded = Base64.getUrlDecoder().decode(chunks[1]);
121+
JsonNode payload = JSON.readTree(decoded);
122+
JsonNode exp = payload.get("exp");
123+
if (exp == null || !exp.canConvertToLong()) {
124+
return FALLBACK_EXPIRES_IN_SECONDS;
125+
}
126+
long remaining = exp.asLong() - Instant.now().getEpochSecond();
127+
return remaining > 0 ? (int) Math.min(remaining, Integer.MAX_VALUE) : 0;
128+
} catch (Exception ex) {
129+
log.debug("Could not derive exp from CDP token, using default TTL", ex);
130+
return FALLBACK_EXPIRES_IN_SECONDS;
131+
}
132+
}
93133
}

jdbc-http/src/test/java/com/salesforce/datacloud/jdbc/auth/DirectCdpTokenProcessorTest.java

Lines changed: 102 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,48 @@
77
import static org.assertj.core.api.Assertions.assertThat;
88
import static org.junit.jupiter.api.Assertions.assertThrows;
99

10-
import com.salesforce.datacloud.jdbc.auth.model.DataCloudTokenResponse;
11-
import java.lang.reflect.Field;
10+
import com.fasterxml.jackson.databind.ObjectMapper;
11+
import com.fasterxml.jackson.databind.node.ObjectNode;
12+
import java.nio.charset.StandardCharsets;
1213
import java.sql.SQLException;
14+
import java.time.Instant;
15+
import java.util.Base64;
1316
import java.util.Properties;
1417
import java.util.UUID;
1518
import org.junit.jupiter.api.Test;
1619

1720
class DirectCdpTokenProcessorTest {
1821

19-
private static final String TENANT_URL = "https://test.c360a.salesforce.com";
20-
static final String FAKE_TOKEN =
21-
"eyJraWQiOiJDT1JFLjAwRE9LMDAwMDAwOVp6ci4xNzE4MDUyMTU0NDIyIiwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJodHRwczovL2xvZ2luLnRlc3QxLnBjLXJuZC5zYWxlc2ZvcmNlLmNvbS9pZC8wMERPSzAwMDAwMDlaenIyQUUvMDA1T0swMDAwMDBVeTkxWUFDIiwic2NwIjoiY2RwX3Byb2ZpbGVfYXBpIGNkcF9pbmdlc3RfYXBpIGNkcF9pZGVudGl0eXJlc29sdXRpb25fYXBpIGNkcF9zZWdtZW50X2FwaSBjZHBfcXVlcnlfYXBpIGNkcF9hcGkiLCJpc3MiOiJodHRwczovL2xvZ2luLnRlc3QxLnBjLXJuZC5zYWxlc2ZvcmNlLmNvbS8iLCJvcmdJZCI6IjAwRE9LMDAwMDAwOVp6ciIsImlzc3VlclRlbmFudElkIjoiY29yZS9mYWxjb250ZXN0MS1jb3JlNG9yYTE1LzAwRE9LMDAwMDAwOVp6cjJBRSIsInNmYXBwaWQiOiIzTVZHOVhOVDlUbEI3VmtZY0tIVm5sUUZzWEd6cUJuMGszUC5zNHJBU0I5V09oRU1OdkgyNzNpM1NFRzF2bWl3WF9YY2NXOUFZbHA3VnJnQ3BGb0ZXIiwiYXVkaWVuY2VUZW5hbnRJZCI6ImEzNjAvZmFsY29uZGV2L2E2ZDcyNmE3M2Y1MzQzMjdhNmE4ZTJlMGYzY2MzODQwIiwiY3VzdG9tX2F0dHJpYnV0ZXMiOnsiZGF0YXNwYWNlIjoiZGVmYXVsdCJ9LCJhdWQiOiJhcGkuYTM2MC5zYWxlc2ZvcmNlLmNvbSIsIm5iZiI6MTcyMDczMTAyMSwic2ZvaWQiOiIwMERPSzAwMDAwMDlaenIiLCJzZnVpZCI6IjAwNU9LMDAwMDAwVXk5MSIsImV4cCI6MTcyMDczODI4MCwiaWF0IjoxNzIwNzMxMDgxLCJqdGkiOiIwYjYwMzc4OS1jMGI2LTQwZTMtYmIzNi03NDQ3MzA2MzAxMzEifQ.lXgeAhJIiGoxgNpBi0W5oBWyn2_auB2bFxxajGuK6DMHlkqDhHJAlFN_uf6QPSjGSJCh5j42Ow5SrEptUDJwmQ";
22-
static final String FAKE_TENANT_ID = "a360/falcondev/a6d726a73f534327a6a8e2e0f3cc3840";
22+
private static final String TENANT_HOST = "test.c360a.salesforce.com";
23+
private static final String TENANT_ID = "a360/falcondev/a6d726a73f534327a6a8e2e0f3cc3840";
24+
25+
/**
26+
* Builds an unsigned JWT with the given audienceTenantId and exp claim. The {@link DataCloudToken}
27+
* decoder only base64-parses the payload — it does not verify the signature — so a fixed bogus
28+
* signature is enough to exercise the production path.
29+
*/
30+
private static String jwtWithExp(long expEpochSeconds) {
31+
try {
32+
ObjectNode header = new ObjectMapper().createObjectNode();
33+
header.put("alg", "ES256");
34+
header.put("typ", "JWT");
35+
36+
ObjectNode payload = new ObjectMapper().createObjectNode();
37+
payload.put("audienceTenantId", TENANT_ID);
38+
payload.put("exp", expEpochSeconds);
39+
40+
Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding();
41+
String h = enc.encodeToString(header.toString().getBytes(StandardCharsets.UTF_8));
42+
String p = enc.encodeToString(payload.toString().getBytes(StandardCharsets.UTF_8));
43+
return h + "." + p + ".sig";
44+
} catch (Exception e) {
45+
throw new RuntimeException(e);
46+
}
47+
}
48+
49+
private static String validJwt() {
50+
return jwtWithExp(Instant.now().getEpochSecond() + 3600);
51+
}
2352

2453
private static Properties propertiesForCdpToken(String cdpToken, String tenantUrl) {
2554
Properties properties = new Properties();
@@ -30,51 +59,69 @@ private static Properties propertiesForCdpToken(String cdpToken, String tenantUr
3059

3160
@Test
3261
void getDataCloudTokenReturnsValidToken() throws SQLException {
62+
String token = validJwt();
3363
DirectCdpTokenProcessor processor =
34-
DirectCdpTokenProcessor.ofDestructive(propertiesForCdpToken(FAKE_TOKEN, TENANT_URL));
35-
DataCloudToken token = processor.getDataCloudToken();
64+
DirectCdpTokenProcessor.ofDestructive(propertiesForCdpToken(token, TENANT_HOST));
65+
DataCloudToken dcToken = processor.getDataCloudToken();
66+
67+
assertThat(dcToken.getAccessToken()).isEqualTo("Bearer " + token);
68+
assertThat(dcToken.getTenantUrl()).isEqualTo(TENANT_HOST);
69+
assertThat(dcToken.getTenantId()).isEqualTo(TENANT_ID);
70+
assertThat(dcToken.isAlive()).isTrue();
71+
}
3672

37-
assertThat(token.getAccessToken()).isEqualTo("Bearer " + FAKE_TOKEN);
38-
assertThat(token.getTenantUrl()).isEqualTo(TENANT_URL);
39-
assertThat(token.getTenantId()).isEqualTo(FAKE_TENANT_ID);
40-
assertThat(token.isAlive()).isTrue();
73+
@Test
74+
void ofDestructiveRejectsTenantUrlWithScheme() {
75+
for (String invalid : new String[] {
76+
"https://" + TENANT_HOST, "http://" + TENANT_HOST, TENANT_HOST + ":443", TENANT_HOST + "/",
77+
}) {
78+
Properties props = propertiesForCdpToken(validJwt(), invalid);
79+
SQLException ex = assertThrows(
80+
SQLException.class,
81+
() -> DirectCdpTokenProcessor.ofDestructive(props),
82+
"Expected rejection of: " + invalid);
83+
assertThat(ex.getMessage()).contains("bare hostname");
84+
}
85+
}
86+
87+
@Test
88+
void ofDestructiveRejectsTenantUrlWithWhitespace() {
89+
Properties props = propertiesForCdpToken(validJwt(), " " + TENANT_HOST + " ");
90+
SQLException ex = assertThrows(SQLException.class, () -> DirectCdpTokenProcessor.ofDestructive(props));
91+
assertThat(ex.getMessage()).contains("whitespace");
4192
}
4293

4394
@Test
4495
void getDataCloudTokenReturnsCachedToken() throws SQLException {
4596
DirectCdpTokenProcessor processor =
46-
DirectCdpTokenProcessor.ofDestructive(propertiesForCdpToken(FAKE_TOKEN, TENANT_URL));
97+
DirectCdpTokenProcessor.ofDestructive(propertiesForCdpToken(validJwt(), TENANT_HOST));
4798
DataCloudToken first = processor.getDataCloudToken();
4899
DataCloudToken second = processor.getDataCloudToken();
49100

50-
assertThat(first.getAccessToken()).isEqualTo(second.getAccessToken());
101+
assertThat(first).isSameAs(second);
51102
}
52103

53104
@Test
54105
void getLakehouseWithoutDataspace() throws SQLException {
55106
DirectCdpTokenProcessor processor =
56-
DirectCdpTokenProcessor.ofDestructive(propertiesForCdpToken(FAKE_TOKEN, TENANT_URL));
57-
String lakehouse = processor.getLakehouse();
58-
59-
assertThat(lakehouse).isEqualTo("lakehouse:" + FAKE_TENANT_ID + ";");
107+
DirectCdpTokenProcessor.ofDestructive(propertiesForCdpToken(validJwt(), TENANT_HOST));
108+
assertThat(processor.getLakehouse()).isEqualTo("lakehouse:" + TENANT_ID + ";");
60109
}
61110

62111
@Test
63112
void getLakehouseWithDataspace() throws SQLException {
64113
String dataspace = UUID.randomUUID().toString();
65-
Properties props = propertiesForCdpToken(FAKE_TOKEN, TENANT_URL);
114+
Properties props = propertiesForCdpToken(validJwt(), TENANT_HOST);
66115
props.setProperty("dataspace", dataspace);
67116

68117
DirectCdpTokenProcessor processor = DirectCdpTokenProcessor.ofDestructive(props);
69-
String lakehouse = processor.getLakehouse();
70-
71-
assertThat(lakehouse).isEqualTo("lakehouse:" + FAKE_TENANT_ID + ";" + dataspace);
118+
assertThat(processor.getLakehouse()).isEqualTo("lakehouse:" + TENANT_ID + ";" + dataspace);
72119
}
73120

74121
@Test
75122
void ofDestructiveThrowsWhenCdpTokenMissing() {
76123
Properties props = new Properties();
77-
props.setProperty("tenantUrl", TENANT_URL);
124+
props.setProperty("tenantUrl", TENANT_HOST);
78125

79126
SQLException ex = assertThrows(SQLException.class, () -> DirectCdpTokenProcessor.ofDestructive(props));
80127
assertThat(ex.getMessage()).contains("cdpToken");
@@ -83,23 +130,23 @@ void ofDestructiveThrowsWhenCdpTokenMissing() {
83130
@Test
84131
void ofDestructiveThrowsWhenTenantUrlMissing() {
85132
Properties props = new Properties();
86-
props.setProperty("cdpToken", FAKE_TOKEN);
133+
props.setProperty("cdpToken", validJwt());
87134

88135
SQLException ex = assertThrows(SQLException.class, () -> DirectCdpTokenProcessor.ofDestructive(props));
89136
assertThat(ex.getMessage()).contains("tenantUrl");
90137
}
91138

92139
@Test
93140
void ofDestructiveThrowsWhenCdpTokenIsInvalidJwt() {
94-
Properties props = propertiesForCdpToken("not-a-valid-jwt", TENANT_URL);
141+
Properties props = propertiesForCdpToken("not-a-valid-jwt", TENANT_HOST);
95142

96143
SQLException ex = assertThrows(SQLException.class, () -> DirectCdpTokenProcessor.ofDestructive(props));
97144
assertThat(ex.getMessage()).contains("Invalid CDP token");
98145
}
99146

100147
@Test
101148
void ofDestructiveRemovesPropertiesFromInput() throws SQLException {
102-
Properties props = propertiesForCdpToken(FAKE_TOKEN, TENANT_URL);
149+
Properties props = propertiesForCdpToken(validJwt(), TENANT_HOST);
103150
props.setProperty("dataspace", "myspace");
104151

105152
DirectCdpTokenProcessor.ofDestructive(props);
@@ -111,75 +158,50 @@ void ofDestructiveRemovesPropertiesFromInput() throws SQLException {
111158

112159
@Test
113160
void hasCdpTokenReturnsTrueWhenBothPresent() {
114-
Properties props = propertiesForCdpToken(FAKE_TOKEN, TENANT_URL);
161+
Properties props = propertiesForCdpToken(validJwt(), TENANT_HOST);
115162
assertThat(DirectCdpTokenProcessor.hasCdpToken(props)).isTrue();
116163
}
117164

118165
@Test
119-
void getDataCloudTokenRebuildsWhenCachedTokenExpired() throws Exception {
120-
DirectCdpTokenProcessor processor =
121-
DirectCdpTokenProcessor.ofDestructive(propertiesForCdpToken(FAKE_TOKEN, TENANT_URL));
122-
123-
DataCloudTokenResponse expiredResponse = new DataCloudTokenResponse();
124-
expiredResponse.setToken(FAKE_TOKEN);
125-
expiredResponse.setInstanceUrl(TENANT_URL);
126-
expiredResponse.setTokenType("Bearer");
127-
expiredResponse.setExpiresIn(-1);
128-
DataCloudToken expired = DataCloudToken.of(expiredResponse);
129-
assertThat(expired.isAlive()).isFalse();
166+
void hasCdpTokenReturnsFalseWhenMissing() {
167+
assertThat(DirectCdpTokenProcessor.hasCdpToken(new Properties())).isFalse();
168+
assertThat(DirectCdpTokenProcessor.hasCdpToken(null)).isFalse();
130169

131-
Field cache = DirectCdpTokenProcessor.class.getDeclaredField("cachedDataCloudToken");
132-
cache.setAccessible(true);
133-
cache.set(processor, expired);
170+
Properties onlyCdpToken = new Properties();
171+
onlyCdpToken.setProperty("cdpToken", validJwt());
172+
assertThat(DirectCdpTokenProcessor.hasCdpToken(onlyCdpToken)).isFalse();
134173

135-
DataCloudToken rebuilt = processor.getDataCloudToken();
136-
assertThat(rebuilt).isNotSameAs(expired);
137-
assertThat(rebuilt.isAlive()).isTrue();
174+
Properties onlyTenantUrl = new Properties();
175+
onlyTenantUrl.setProperty("tenantUrl", TENANT_HOST);
176+
assertThat(DirectCdpTokenProcessor.hasCdpToken(onlyTenantUrl)).isFalse();
138177
}
139178

140179
@Test
141-
void getDataCloudTokenRebuildsAfterCacheCleared() throws Exception {
142-
DirectCdpTokenProcessor processor =
143-
DirectCdpTokenProcessor.ofDestructive(propertiesForCdpToken(FAKE_TOKEN, TENANT_URL));
144-
145-
Field cache = DirectCdpTokenProcessor.class.getDeclaredField("cachedDataCloudToken");
146-
cache.setAccessible(true);
147-
cache.set(processor, null);
148-
149-
DataCloudToken rebuilt = processor.getDataCloudToken();
150-
assertThat(rebuilt.getAccessToken()).isEqualTo("Bearer " + FAKE_TOKEN);
151-
assertThat(rebuilt.getTenantId()).isEqualTo(FAKE_TENANT_ID);
180+
void secondsUntilJwtExpiryReturnsRemainingForValidJwt() {
181+
long futureExp = Instant.now().getEpochSecond() + 1234;
182+
int remaining = DirectCdpTokenProcessor.secondsUntilJwtExpiry(jwtWithExp(futureExp));
183+
assertThat(remaining).isBetween(1230, 1234);
152184
}
153185

154186
@Test
155-
void getDataCloudTokenWrapsRebuildFailure() throws Exception {
156-
DirectCdpTokenProcessor processor =
157-
DirectCdpTokenProcessor.ofDestructive(propertiesForCdpToken(FAKE_TOKEN, TENANT_URL));
158-
159-
Field cache = DirectCdpTokenProcessor.class.getDeclaredField("cachedDataCloudToken");
160-
cache.setAccessible(true);
161-
cache.set(processor, null);
162-
163-
Field tenantUrl = DirectCdpTokenProcessor.class.getDeclaredField("tenantUrl");
164-
tenantUrl.setAccessible(true);
165-
tenantUrl.set(processor, null);
166-
167-
SQLException ex = assertThrows(SQLException.class, processor::getDataCloudToken);
168-
assertThat(ex.getSQLState()).isEqualTo("28000");
169-
assertThat(cache.get(processor)).isNull();
187+
void secondsUntilJwtExpiryFallsBackWhenJwtSingleSegment() {
188+
assertThat(DirectCdpTokenProcessor.secondsUntilJwtExpiry("only-one-segment"))
189+
.isEqualTo(DirectCdpTokenProcessor.FALLBACK_EXPIRES_IN_SECONDS);
170190
}
171191

172192
@Test
173-
void hasCdpTokenReturnsFalseWhenMissing() {
174-
assertThat(DirectCdpTokenProcessor.hasCdpToken(new Properties())).isFalse();
175-
assertThat(DirectCdpTokenProcessor.hasCdpToken(null)).isFalse();
176-
177-
Properties onlyCdpToken = new Properties();
178-
onlyCdpToken.setProperty("cdpToken", FAKE_TOKEN);
179-
assertThat(DirectCdpTokenProcessor.hasCdpToken(onlyCdpToken)).isFalse();
193+
void secondsUntilJwtExpiryFallsBackWhenPayloadNotBase64() {
194+
// Two segments but second one is not valid base64url
195+
assertThat(DirectCdpTokenProcessor.secondsUntilJwtExpiry("header.@@not-base64@@.sig"))
196+
.isEqualTo(DirectCdpTokenProcessor.FALLBACK_EXPIRES_IN_SECONDS);
197+
}
180198

181-
Properties onlyTenantUrl = new Properties();
182-
onlyTenantUrl.setProperty("tenantUrl", TENANT_URL);
183-
assertThat(DirectCdpTokenProcessor.hasCdpToken(onlyTenantUrl)).isFalse();
199+
@Test
200+
void secondsUntilJwtExpiryFallsBackWhenExpClaimMissing() {
201+
Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding();
202+
String header = enc.encodeToString("{\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8));
203+
String payload = enc.encodeToString("{\"sub\":\"x\"}".getBytes(StandardCharsets.UTF_8));
204+
assertThat(DirectCdpTokenProcessor.secondsUntilJwtExpiry(header + "." + payload + ".sig"))
205+
.isEqualTo(DirectCdpTokenProcessor.FALLBACK_EXPIRES_IN_SECONDS);
184206
}
185207
}

0 commit comments

Comments
 (0)