Skip to content

Commit 0b931f3

Browse files
authored
Hadoop 16890. Change in expiry calculation for MSI token provider.
Contributed by Bilahari T H
1 parent cf9cf83 commit 0b931f3

File tree

5 files changed

+177
-15
lines changed

5 files changed

+177
-15
lines changed

hadoop-tools/hadoop-azure/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,12 @@
283283
<artifactId>assertj-core</artifactId>
284284
<scope>test</scope>
285285
</dependency>
286+
<dependency>
287+
<groupId>org.hamcrest</groupId>
288+
<artifactId>hamcrest-library</artifactId>
289+
<scope>test</scope>
290+
</dependency>
291+
286292
</dependencies>
287293

288294
<profiles>

hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/AccessTokenProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public synchronized AzureADToken getToken() throws IOException {
7272
*
7373
* @return true if the token is expiring in next 5 minutes
7474
*/
75-
private boolean isTokenAboutToExpire() {
75+
protected boolean isTokenAboutToExpire() {
7676
if (token == null) {
7777
LOG.debug("AADToken: no token. Returning expiring=true");
7878
return true; // no token should have same response as expired token

hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/AzureADAuthenticator.java

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public static AzureADToken getTokenFromMsi(final String authEndpoint,
137137
headers.put("Metadata", "true");
138138

139139
LOG.debug("AADToken: starting to fetch token using MSI");
140-
return getTokenCall(authEndpoint, qp.serialize(), headers, "GET");
140+
return getTokenCall(authEndpoint, qp.serialize(), headers, "GET", true);
141141
}
142142

143143
/**
@@ -258,8 +258,13 @@ public UnexpectedResponseException(final int httpErrorCode,
258258
}
259259

260260
private static AzureADToken getTokenCall(String authEndpoint, String body,
261-
Hashtable<String, String> headers, String httpMethod)
262-
throws IOException {
261+
Hashtable<String, String> headers, String httpMethod) throws IOException {
262+
return getTokenCall(authEndpoint, body, headers, httpMethod, false);
263+
}
264+
265+
private static AzureADToken getTokenCall(String authEndpoint, String body,
266+
Hashtable<String, String> headers, String httpMethod, boolean isMsi)
267+
throws IOException {
263268
AzureADToken token = null;
264269
ExponentialRetryPolicy retryPolicy
265270
= new ExponentialRetryPolicy(3, 0, 1000, 2);
@@ -272,7 +277,7 @@ private static AzureADToken getTokenCall(String authEndpoint, String body,
272277
httperror = 0;
273278
ex = null;
274279
try {
275-
token = getTokenSingleCall(authEndpoint, body, headers, httpMethod);
280+
token = getTokenSingleCall(authEndpoint, body, headers, httpMethod, isMsi);
276281
} catch (HttpException e) {
277282
httperror = e.httpErrorCode;
278283
ex = e;
@@ -288,8 +293,9 @@ private static AzureADToken getTokenCall(String authEndpoint, String body,
288293
return token;
289294
}
290295

291-
private static AzureADToken getTokenSingleCall(
292-
String authEndpoint, String payload, Hashtable<String, String> headers, String httpMethod)
296+
private static AzureADToken getTokenSingleCall(String authEndpoint,
297+
String payload, Hashtable<String, String> headers, String httpMethod,
298+
boolean isMsi)
293299
throws IOException {
294300

295301
AzureADToken token = null;
@@ -336,7 +342,7 @@ private static AzureADToken getTokenSingleCall(
336342
if (httpResponseCode == HttpURLConnection.HTTP_OK
337343
&& responseContentType.startsWith("application/json") && responseContentLength > 0) {
338344
InputStream httpResponseStream = conn.getInputStream();
339-
token = parseTokenFromStream(httpResponseStream);
345+
token = parseTokenFromStream(httpResponseStream, isMsi);
340346
} else {
341347
InputStream stream = conn.getErrorStream();
342348
if (stream == null) {
@@ -390,10 +396,12 @@ private static AzureADToken getTokenSingleCall(
390396
return token;
391397
}
392398

393-
private static AzureADToken parseTokenFromStream(InputStream httpResponseStream) throws IOException {
399+
private static AzureADToken parseTokenFromStream(
400+
InputStream httpResponseStream, boolean isMsi) throws IOException {
394401
AzureADToken token = new AzureADToken();
395402
try {
396-
int expiryPeriod = 0;
403+
int expiryPeriodInSecs = 0;
404+
long expiresOnInSecs = -1;
397405

398406
JsonFactory jf = new JsonFactory();
399407
JsonParser jp = jf.createJsonParser(httpResponseStream);
@@ -408,17 +416,38 @@ private static AzureADToken parseTokenFromStream(InputStream httpResponseStream)
408416
if (fieldName.equals("access_token")) {
409417
token.setAccessToken(fieldValue);
410418
}
419+
411420
if (fieldName.equals("expires_in")) {
412-
expiryPeriod = Integer.parseInt(fieldValue);
421+
expiryPeriodInSecs = Integer.parseInt(fieldValue);
422+
}
423+
424+
if (fieldName.equals("expires_on")) {
425+
expiresOnInSecs = Long.parseLong(fieldValue);
413426
}
427+
414428
}
415429
jp.nextToken();
416430
}
417431
jp.close();
418-
long expiry = System.currentTimeMillis();
419-
expiry = expiry + expiryPeriod * 1000L; // convert expiryPeriod to milliseconds and add
420-
token.setExpiry(new Date(expiry));
421-
LOG.debug("AADToken: fetched token with expiry " + token.getExpiry().toString());
432+
if (expiresOnInSecs > 0) {
433+
LOG.debug("Expiry based on expires_on: {}", expiresOnInSecs);
434+
token.setExpiry(new Date(expiresOnInSecs * 1000));
435+
} else {
436+
if (isMsi) {
437+
// Currently there is a known issue that MSI does not update expires_in
438+
// for refresh and will have the value from first AAD token fetch request.
439+
// Due to this known limitation, expires_in is not supported for MSI token fetch flow.
440+
throw new UnsupportedOperationException("MSI Responded with invalid expires_on");
441+
}
442+
443+
LOG.debug("Expiry based on expires_in: {}", expiryPeriodInSecs);
444+
long expiry = System.currentTimeMillis();
445+
expiry = expiry + expiryPeriodInSecs * 1000L; // convert expiryPeriod to milliseconds and add
446+
token.setExpiry(new Date(expiry));
447+
}
448+
449+
LOG.debug("AADToken: fetched token with expiry {}, expiresOn passed: {}",
450+
token.getExpiry().toString(), expiresOnInSecs);
422451
} catch (Exception ex) {
423452
LOG.debug("AADToken: got exception when parsing json token " + ex.toString());
424453
throw ex;

hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/MsiTokenProvider.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ public class MsiTokenProvider extends AccessTokenProvider {
3636

3737
private final String clientId;
3838

39+
private long tokenFetchTime = -1;
40+
41+
private static final long ONE_HOUR = 3600 * 1000;
42+
3943
private static final Logger LOG = LoggerFactory.getLogger(AccessTokenProvider.class);
4044

4145
public MsiTokenProvider(final String authEndpoint, final String tenantGuid,
@@ -51,6 +55,36 @@ protected AzureADToken refreshToken() throws IOException {
5155
LOG.debug("AADToken: refreshing token from MSI");
5256
AzureADToken token = AzureADAuthenticator
5357
.getTokenFromMsi(authEndpoint, tenantGuid, clientId, authority, false);
58+
tokenFetchTime = System.currentTimeMillis();
5459
return token;
5560
}
61+
62+
/**
63+
* Checks if the token is about to expire as per base expiry logic.
64+
* Otherwise try to expire every 1 hour
65+
*
66+
* @return true if the token is expiring in next 1 hour or if a token has
67+
* never been fetched
68+
*/
69+
@Override
70+
protected boolean isTokenAboutToExpire() {
71+
if (tokenFetchTime == -1 || super.isTokenAboutToExpire()) {
72+
return true;
73+
}
74+
75+
boolean expiring = false;
76+
long elapsedTimeSinceLastTokenRefreshInMillis =
77+
System.currentTimeMillis() - tokenFetchTime;
78+
expiring = elapsedTimeSinceLastTokenRefreshInMillis >= ONE_HOUR
79+
|| elapsedTimeSinceLastTokenRefreshInMillis < 0;
80+
// In case of, Token is not refreshed for 1 hr or any clock skew issues,
81+
// refresh token.
82+
if (expiring) {
83+
LOG.debug("MSIToken: token renewing. Time elapsed since last token fetch:"
84+
+ " {} milli seconds", elapsedTimeSinceLastTokenRefreshInMillis);
85+
}
86+
87+
return expiring;
88+
}
89+
5690
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
* <p>
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
* <p>
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.hadoop.fs.azurebfs;
20+
21+
import java.io.IOException;
22+
import java.util.Date;
23+
24+
import org.junit.Test;
25+
26+
import org.apache.commons.lang3.StringUtils;
27+
import org.apache.hadoop.fs.azurebfs.oauth2.AccessTokenProvider;
28+
import org.apache.hadoop.fs.azurebfs.oauth2.AzureADToken;
29+
import org.apache.hadoop.fs.azurebfs.oauth2.MsiTokenProvider;
30+
31+
import static org.junit.Assume.assumeThat;
32+
import static org.hamcrest.CoreMatchers.is;
33+
import static org.hamcrest.CoreMatchers.not;
34+
import static org.hamcrest.Matchers.isEmptyOrNullString;
35+
import static org.hamcrest.Matchers.isEmptyString;
36+
37+
import static org.apache.hadoop.fs.azurebfs.constants.AuthConfigurations.DEFAULT_FS_AZURE_ACCOUNT_OAUTH_MSI_AUTHORITY;
38+
import static org.apache.hadoop.fs.azurebfs.constants.AuthConfigurations.DEFAULT_FS_AZURE_ACCOUNT_OAUTH_MSI_ENDPOINT;
39+
import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID;
40+
import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_ACCOUNT_OAUTH_MSI_AUTHORITY;
41+
import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_ACCOUNT_OAUTH_MSI_ENDPOINT;
42+
import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_ACCOUNT_OAUTH_MSI_TENANT;
43+
44+
/**
45+
* Test MsiTokenProvider.
46+
*/
47+
public final class ITestAbfsMsiTokenProvider
48+
extends AbstractAbfsIntegrationTest {
49+
50+
public ITestAbfsMsiTokenProvider() throws Exception {
51+
super();
52+
}
53+
54+
@Test
55+
public void test() throws IOException {
56+
AbfsConfiguration conf = getConfiguration();
57+
assumeThat(conf.get(FS_AZURE_ACCOUNT_OAUTH_MSI_ENDPOINT),
58+
not(isEmptyOrNullString()));
59+
assumeThat(conf.get(FS_AZURE_ACCOUNT_OAUTH_MSI_TENANT),
60+
not(isEmptyOrNullString()));
61+
assumeThat(conf.get(FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID),
62+
not(isEmptyOrNullString()));
63+
assumeThat(conf.get(FS_AZURE_ACCOUNT_OAUTH_MSI_AUTHORITY),
64+
not(isEmptyOrNullString()));
65+
66+
String tenantGuid = conf
67+
.getPasswordString(FS_AZURE_ACCOUNT_OAUTH_MSI_TENANT);
68+
String clientId = conf.getPasswordString(FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID);
69+
String authEndpoint = getTrimmedPasswordString(conf,
70+
FS_AZURE_ACCOUNT_OAUTH_MSI_ENDPOINT,
71+
DEFAULT_FS_AZURE_ACCOUNT_OAUTH_MSI_ENDPOINT);
72+
String authority = getTrimmedPasswordString(conf,
73+
FS_AZURE_ACCOUNT_OAUTH_MSI_AUTHORITY,
74+
DEFAULT_FS_AZURE_ACCOUNT_OAUTH_MSI_AUTHORITY);
75+
AccessTokenProvider tokenProvider = new MsiTokenProvider(authEndpoint,
76+
tenantGuid, clientId, authority);
77+
78+
AzureADToken token = null;
79+
token = tokenProvider.getToken();
80+
assertThat(token.getAccessToken(), not(isEmptyString()));
81+
assertThat(token.getExpiry().after(new Date()), is(true));
82+
}
83+
84+
private String getTrimmedPasswordString(AbfsConfiguration conf, String key,
85+
String defaultValue) throws IOException {
86+
String value = conf.getPasswordString(key);
87+
if (StringUtils.isBlank(value)) {
88+
value = defaultValue;
89+
}
90+
return value.trim();
91+
}
92+
93+
}

0 commit comments

Comments
 (0)