1
2
3
4 package com.azure.identity;
5
6 import com.azure.core.credentials.AccessToken;
7 import com.azure.core.http.ProxyOptions;
8 import com.azure.core.http.ProxyOptions.Type;
9 import com.azure.core.implementation.serializer.SerializerAdapter;
10 import com.azure.core.implementation.serializer.SerializerEncoding;
11 import com.azure.core.implementation.serializer.jackson.JacksonAdapter;
12 import com.azure.core.implementation.util.ScopeUtil;
13 import com.azure.identity.implementation.MSIToken;
14 import com.azure.identity.implementation.util.Adal4jUtil;
15 import com.microsoft.aad.adal4j.AsymmetricKeyCredential;
16 import com.microsoft.aad.adal4j.AuthenticationContext;
17 import com.microsoft.aad.adal4j.AuthenticationResult;
18 import com.microsoft.aad.adal4j.ClientCredential;
19 import reactor.core.Exceptions;
20 import reactor.core.publisher.Mono;
21 import reactor.core.publisher.MonoSink;
22
23 import java.io.IOException;
24 import java.net.HttpURLConnection;
25 import java.net.MalformedURLException;
26 import java.net.Proxy;
27 import java.net.URL;
28 import java.net.URLEncoder;
29 import java.nio.charset.StandardCharsets;
30 import java.nio.file.Files;
31 import java.nio.file.Paths;
32 import java.time.OffsetDateTime;
33 import java.time.ZoneOffset;
34 import java.util.Random;
35 import java.util.Scanner;
36 import java.util.concurrent.ExecutorService;
37 import java.util.concurrent.Executors;
38 import java.util.function.Consumer;
39
40
41
42
43
44 public final class IdentityClient {
45 private final IdentityClientOptions options;
46 private final SerializerAdapter adapter = JacksonAdapter.createDefaultSerializerAdapter();
47 private static final Random RANDOM = new Random();
48
49
50
51
52 public IdentityClient() {
53 this.options = new IdentityClientOptions();
54 }
55
56
57
58
59
60
61 public IdentityClient(IdentityClientOptions options) {
62 this.options = options;
63 }
64
65
66
67
68
69
70
71
72
73
74 public Mono<AccessToken> authenticateWithClientSecret(String tenantId, String clientId, String clientSecret, String[] scopes) {
75 String resource = ScopeUtil.scopesToResource(scopes);
76 String authorityUrl = options.authorityHost().replaceAll("/+$", "") + "/" + tenantId;
77 ExecutorService executor = Executors.newSingleThreadExecutor();
78 AuthenticationContext context = createAuthenticationContext(executor, authorityUrl, options.proxyOptions());
79 return Mono.create((Consumer<MonoSink<AuthenticationResult>>) callback -> {
80 context.acquireToken(
81 resource,
82 new ClientCredential(clientId, clientSecret),
83 Adal4jUtil.authenticationDelegate(callback));
84 }).map(ar -> new AccessToken(ar.getAccessToken(), OffsetDateTime.ofInstant(ar.getExpiresOnDate().toInstant(), ZoneOffset.UTC)))
85 .doFinally(s -> executor.shutdown());
86 }
87
88
89
90
91
92
93
94
95
96
97
98 public Mono<AccessToken> authenticateWithPfxCertificate(String tenantId, String clientId, String pfxCertificatePath, String pfxCertificatePassword, String[] scopes) {
99 String resource = ScopeUtil.scopesToResource(scopes);
100 String authorityUrl = options.authorityHost().replaceAll("/+$", "") + "/" + tenantId;
101 ExecutorService executor = Executors.newSingleThreadExecutor();
102 AuthenticationContext context = createAuthenticationContext(executor, authorityUrl, options.proxyOptions());
103 return Mono.create((Consumer<MonoSink<AuthenticationResult>>) callback -> {
104 try {
105 context.acquireToken(
106 resource,
107 Adal4jUtil.createAsymmetricKeyCredential(clientId, Files.readAllBytes(Paths.get(pfxCertificatePath)), pfxCertificatePassword),
108 Adal4jUtil.authenticationDelegate(callback));
109 } catch (IOException e) {
110 callback.error(e);
111 }
112 }).map(ar -> new AccessToken(ar.getAccessToken(), OffsetDateTime.ofInstant(ar.getExpiresOnDate().toInstant(), ZoneOffset.UTC)))
113 .doFinally(s -> executor.shutdown());
114 }
115
116
117
118
119
120
121
122
123
124
125 public Mono<AccessToken> authenticateWithPemCertificate(String tenantId, String clientId, String pemCertificatePath, String[] scopes) {
126 String resource = ScopeUtil.scopesToResource(scopes);
127 String authorityUrl = options.authorityHost().replaceAll("/+$", "") + "/" + tenantId;
128 ExecutorService executor = Executors.newSingleThreadExecutor();
129 AuthenticationContext context = createAuthenticationContext(executor, authorityUrl, options.proxyOptions());
130 return Mono.create((Consumer<MonoSink<AuthenticationResult>>) callback -> {
131 try {
132 context.acquireToken(
133 resource,
134 AsymmetricKeyCredential.create(clientId, Adal4jUtil.privateKeyFromPem(Files.readAllBytes(Paths.get(pemCertificatePath))), Adal4jUtil.publicKeyFromPem(Files.readAllBytes(Paths.get(pemCertificatePath)))),
135 Adal4jUtil.authenticationDelegate(callback));
136 } catch (IOException e) {
137 callback.error(e);
138 }
139 }).map(ar -> new AccessToken(ar.getAccessToken(), OffsetDateTime.ofInstant(ar.getExpiresOnDate().toInstant(), ZoneOffset.UTC)))
140 .doFinally(s -> executor.shutdown());
141 }
142
143 private static AuthenticationContext createAuthenticationContext(ExecutorService executor, String authorityUrl, ProxyOptions proxyOptions) {
144 AuthenticationContext context;
145 try {
146 context = new AuthenticationContext(authorityUrl, false, executor);
147 } catch (MalformedURLException mue) {
148 throw Exceptions.propagate(mue);
149 }
150 if (proxyOptions != null) {
151 context.setProxy(new Proxy(proxyOptions.type() == Type.HTTP ? Proxy.Type.HTTP : Proxy.Type.SOCKS, proxyOptions.address()));
152 }
153 return context;
154 }
155
156
157
158
159
160
161
162
163
164
165 public Mono<AccessToken> authenticateToManagedIdentityEnpoint(String msiEndpoint, String msiSecret, String clientId, String[] scopes) {
166 String resource = ScopeUtil.scopesToResource(scopes);
167 HttpURLConnection connection = null;
168 StringBuilder payload = new StringBuilder();
169
170 try {
171 payload.append("resource=");
172 payload.append(URLEncoder.encode(resource, "UTF-8"));
173 payload.append("&api-version=");
174 payload.append(URLEncoder.encode("2017-09-01", "UTF-8"));
175 if (clientId != null) {
176 payload.append("&client_id=");
177 payload.append(URLEncoder.encode(clientId, "UTF-8"));
178 }
179 } catch (IOException exception) {
180 return Mono.error(exception);
181 }
182 try {
183 URL url = new URL(String.format("%s?%s", msiEndpoint, payload));
184 connection = (HttpURLConnection) url.openConnection();
185
186 connection.setRequestMethod("GET");
187 if (msiSecret != null) {
188 connection.setRequestProperty("Secret", msiSecret);
189 }
190 connection.setRequestProperty("Metadata", "true");
191
192 connection.connect();
193
194 Scanner s = new Scanner(connection.getInputStream(), StandardCharsets.UTF_8.name()).useDelimiter("\\A");
195 String result = s.hasNext() ? s.next() : "";
196
197 return Mono.just(adapter.deserialize(result, MSIToken.class, SerializerEncoding.JSON));
198 } catch (IOException e) {
199 return Mono.error(e);
200 } finally {
201 if (connection != null) {
202 connection.disconnect();
203 }
204 }
205 }
206
207
208
209
210
211
212
213
214 public Mono<AccessToken> authenticateToIMDSEndpoint(String clientId, String[] scopes) {
215 String resource = ScopeUtil.scopesToResource(scopes);
216 StringBuilder payload = new StringBuilder();
217 final int imdsUpgradeTimeInMs = 70 * 1000;
218
219 try {
220 payload.append("api-version=");
221 payload.append(URLEncoder.encode("2018-02-01", "UTF-8"));
222 payload.append("&resource=");
223 payload.append(URLEncoder.encode(resource, "UTF-8"));
224 if (clientId != null) {
225 payload.append("&client_id=");
226 payload.append(URLEncoder.encode(clientId, "UTF-8"));
227 }
228 } catch (IOException exception) {
229 return Mono.error(exception);
230 }
231
232 int retry = 1;
233 while (retry <= options.maxRetry()) {
234 URL url = null;
235 HttpURLConnection connection = null;
236 try {
237 url = new URL(String.format("http://169.254.169.254/metadata/identity/oauth2/token?%s", payload.toString()));
238
239 connection = (HttpURLConnection) url.openConnection();
240 connection.setRequestMethod("GET");
241 connection.setRequestProperty("Metadata", "true");
242 connection.connect();
243
244 Scanner s = new Scanner(connection.getInputStream(), StandardCharsets.UTF_8.name()).useDelimiter("\\A");
245 String result = s.hasNext() ? s.next() : "";
246
247 return Mono.just(adapter.deserialize(result, MSIToken.class, SerializerEncoding.JSON));
248 } catch (IOException exception) {
249 if (connection == null) {
250 return Mono.error(new RuntimeException(String.format("Could not connect to the url: %s.", url), exception));
251 }
252 int responseCode = 0;
253 try {
254 responseCode = connection.getResponseCode();
255 } catch (IOException e) {
256 return Mono.error(e);
257 }
258 if (responseCode == 410 || responseCode == 429 || responseCode == 404 || (responseCode >= 500 && responseCode <= 599)) {
259 int retryTimeoutInMs = options.retryTimeout().apply(RANDOM.nextInt(retry));
260
261
262 retryTimeoutInMs = (responseCode == 410 && retryTimeoutInMs < imdsUpgradeTimeInMs) ? imdsUpgradeTimeInMs : retryTimeoutInMs;
263 retry++;
264 if (retry > options.maxRetry()) {
265 break;
266 } else {
267 sleep(retryTimeoutInMs);
268 }
269 } else {
270 return Mono.error(new RuntimeException("Couldn't acquire access token from IMDS, verify your objectId, clientId or msiResourceId", exception));
271 }
272 } finally {
273 if (connection != null) {
274 connection.disconnect();
275 }
276 }
277 }
278 return Mono.error(new RuntimeException(String.format("MSI: Failed to acquire tokens after retrying %s times", options.maxRetry())));
279 }
280
281 private static void sleep(int millis) {
282 try {
283 Thread.sleep(millis);
284 } catch (InterruptedException ex) {
285 throw new RuntimeException(ex);
286 }
287 }
288 }