View Javadoc
1   // Copyright (c) Microsoft Corporation. All rights reserved.
2   // Licensed under the MIT License.
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   * The identity client that contains APIs to retrieve access tokens
42   * from various configurations.
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       * Creates an IdentityClient with default options.
51       */
52      public IdentityClient() {
53          this.options = new IdentityClientOptions();
54      }
55  
56      /**
57       * Creates an IdentityClient with the given options.
58       *
59       * @param options the options configuring the client.
60       */
61      public IdentityClient(IdentityClientOptions options) {
62          this.options = options;
63      }
64  
65      /**
66       * Asynchronously acquire a token from Active Directory with a client secret.
67       *
68       * @param tenantId     the tenant ID of the application
69       * @param clientId     the client ID of the application
70       * @param clientSecret the client secret of the application
71       * @param scopes       the scopes to authenticate to
72       * @return a Publisher that emits an AccessToken
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       * Asynchronously acquire a token from Active Directory with a PKCS12 certificate.
90       *
91       * @param tenantId               the tenant ID of the application
92       * @param clientId               the client ID of the application
93       * @param pfxCertificatePath     the path to the PKCS12 certificate of the application
94       * @param pfxCertificatePassword the password protecting the PFX certificate
95       * @param scopes                 the scopes to authenticate to
96       * @return a Publisher that emits an AccessToken
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      * Asynchronously acquire a token from Active Directory with a PEM certificate.
118      *
119      * @param tenantId           the tenant ID of the application
120      * @param clientId           the client ID of the application
121      * @param pemCertificatePath the path to the PEM certificate of the application
122      * @param scopes             the scopes to authenticate to
123      * @return a Publisher that emits an AccessToken
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      * Asynchronously acquire a token from the App Service Managed Service Identity endpoint.
158      *
159      * @param msiEndpoint the endpoint to acquire token from
160      * @param msiSecret   the secret to acquire token with
161      * @param clientId    the client ID of the application service
162      * @param scopes      the scopes to authenticate to
163      * @return a Publisher that emits an AccessToken
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      * Asynchronously acquire a token from the Virtual Machine IMDS endpoint.
209      *
210      * @param clientId the client ID of the virtual machine
211      * @param scopes   the scopes to authenticate to
212      * @return a Publisher that emits an AccessToken
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                     // Error code 410 indicates IMDS upgrade is in progress, which can take up to 70s
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 }