Skip to content

Commit c152e03

Browse files
authored
Add timeout to ApplicationTokenCredentials (#676)
* Add timeout to ApplicationTokenCredentials * Fix executor and add unit tests
1 parent dbc615c commit c152e03

File tree

2 files changed

+70
-16
lines changed

2 files changed

+70
-16
lines changed

azure-client-authentication/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@
6969
<artifactId>junit</artifactId>
7070
<scope>test</scope>
7171
</dependency>
72+
<dependency>
73+
<groupId>org.mockito</groupId>
74+
<artifactId>mockito-core</artifactId>
75+
<version>3.4.4</version>
76+
<scope>test</scope>
77+
</dependency>
7278
</dependencies>
7379
<build>
7480
<plugins>

azure-client-authentication/src/main/java/com/microsoft/azure/credentials/ApplicationTokenCredentials.java

+64-16
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
import java.security.spec.InvalidKeySpecException;
2828
import java.security.spec.PKCS8EncodedKeySpec;
2929
import java.util.Date;
30-
import java.util.HashMap;
31-
import java.util.Map;
30+
import java.util.concurrent.ConcurrentHashMap;
3231
import java.util.concurrent.ExecutorService;
3332
import java.util.concurrent.Executors;
33+
import java.util.concurrent.Future;
34+
import java.util.concurrent.TimeUnit;
35+
import java.util.concurrent.locks.ReentrantLock;
3436
import java.util.regex.Matcher;
3537
import java.util.regex.Pattern;
3638

@@ -39,15 +41,19 @@
3941
*/
4042
public class ApplicationTokenCredentials extends AzureTokenCredentials {
4143
/** A mapping from resource endpoint to its cached access token. */
42-
private Map<String, AuthenticationResult> tokens;
44+
private final ConcurrentHashMap<String, AuthenticationResult> tokens;
45+
/** A mapping from resource endpoint to its current authentication locks. */
46+
private final ConcurrentHashMap<String, ReentrantLock> authenticationLocks;
4347
/** The active directory application client id. */
44-
private String clientId;
48+
private final String clientId;
4549
/** The authentication secret for the application. */
4650
private String clientSecret;
4751
/** The PKCS12 certificate byte array. */
4852
private byte[] clientCertificate;
4953
/** The certificate password. */
5054
private String clientCertificatePassword;
55+
/** The timeout for authentication calls. */
56+
private long timeoutInSeconds = 60;
5157

5258
/**
5359
* Initializes a new instance of the ApplicationTokenCredentials.
@@ -62,7 +68,8 @@ public ApplicationTokenCredentials(String clientId, String domain, String secret
6268
super(environment, domain); // defer token acquisition
6369
this.clientId = clientId;
6470
this.clientSecret = secret;
65-
this.tokens = new HashMap<>();
71+
this.tokens = new ConcurrentHashMap<>();
72+
this.authenticationLocks = new ConcurrentHashMap<>();
6673
}
6774

6875
/**
@@ -80,7 +87,8 @@ public ApplicationTokenCredentials(String clientId, String domain, byte[] certif
8087
this.clientId = clientId;
8188
this.clientCertificate = certificate;
8289
this.clientCertificatePassword = password;
83-
this.tokens = new HashMap<>();
90+
this.tokens = new ConcurrentHashMap<>();
91+
this.authenticationLocks = new ConcurrentHashMap<>();
8492
}
8593

8694
/**
@@ -132,19 +140,61 @@ String clientCertificatePassword() {
132140
return clientCertificatePassword;
133141
}
134142

143+
/**
144+
* Gets the timeout for AAD authentication calls. Default is 60 seconds.
145+
*
146+
* @return the timeout in seconds.
147+
*/
148+
public long timeoutInSeconds() {
149+
return timeoutInSeconds;
150+
}
151+
152+
/**
153+
* Sets the timeout for AAD authentication calls. Default is 60 seconds.
154+
*
155+
* @param timeoutInSeconds the timeout in seconds.
156+
* @return the modified ApplicationTokenCredentials instance
157+
*/
158+
public ApplicationTokenCredentials withTimeoutInSeconds(long timeoutInSeconds) {
159+
this.timeoutInSeconds = timeoutInSeconds;
160+
return this;
161+
}
162+
135163
@Override
136-
public synchronized String getToken(String resource) throws IOException {
164+
public String getToken(String resource) throws IOException {
137165
AuthenticationResult authenticationResult = tokens.get(resource);
138166
if (authenticationResult == null || authenticationResult.getExpiresOnDate().before(new Date())) {
139-
authenticationResult = acquireAccessToken(resource);
167+
ReentrantLock lock;
168+
synchronized (authenticationLocks) {
169+
lock = authenticationLocks.get(resource);
170+
if (lock == null) {
171+
lock = new ReentrantLock();
172+
authenticationLocks.put(resource, lock);
173+
}
174+
}
175+
lock.lock();
176+
try {
177+
authenticationResult = tokens.get(resource);
178+
if (authenticationResult == null || authenticationResult.getExpiresOnDate().before(new Date())) {
179+
ExecutorService executor = Executors.newSingleThreadExecutor();
180+
try {
181+
authenticationResult = acquireAccessToken(resource, executor).get(timeoutInSeconds(), TimeUnit.SECONDS);
182+
tokens.put(resource, authenticationResult);
183+
} finally {
184+
executor.shutdown();
185+
}
186+
}
187+
} catch (Exception e) {
188+
throw new IOException(e.getMessage(), e);
189+
} finally {
190+
lock.unlock();
191+
}
140192
}
141-
tokens.put(resource, authenticationResult);
142193
return authenticationResult.getAccessToken();
143194
}
144195

145-
private AuthenticationResult acquireAccessToken(String resource) throws IOException {
196+
Future<AuthenticationResult> acquireAccessToken(String resource, ExecutorService executor) throws IOException {
146197
String authorityUrl = this.environment().activeDirectoryEndpoint() + this.domain();
147-
ExecutorService executor = Executors.newSingleThreadExecutor();
148198
AuthenticationContext context = new AuthenticationContext(authorityUrl, false, executor);
149199
if (proxy() != null) {
150200
context.setProxy(proxy());
@@ -157,23 +207,21 @@ private AuthenticationResult acquireAccessToken(String resource) throws IOExcept
157207
return context.acquireToken(
158208
resource,
159209
new ClientCredential(this.clientId(), clientSecret),
160-
null).get();
210+
null);
161211
} else if (clientCertificate != null && clientCertificatePassword != null) {
162212
return context.acquireToken(
163213
resource,
164214
AsymmetricKeyCredential.create(clientId, new ByteArrayInputStream(clientCertificate), clientCertificatePassword),
165-
null).get();
215+
null);
166216
} else if (clientCertificate != null) {
167217
return context.acquireToken(
168218
resource,
169219
AsymmetricKeyCredential.create(clientId(), privateKeyFromPem(new String(clientCertificate)), publicKeyFromPem(new String(clientCertificate))),
170-
null).get();
220+
null);
171221
}
172222
throw new AuthenticationException("Please provide either a non-null secret or a non-null certificate.");
173223
} catch (Exception e) {
174224
throw new IOException(e.getMessage(), e);
175-
} finally {
176-
executor.shutdown();
177225
}
178226
}
179227

0 commit comments

Comments
 (0)