com.azure.identity.implementation.IdentityClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of azure-identity Show documentation
Show all versions of azure-identity Show documentation
This module contains client library for Microsoft Azure Identity.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.identity.implementation;
import com.azure.core.credentials.AccessToken;
import com.azure.core.credentials.TokenRequest;
import com.azure.core.http.ProxyOptions;
import com.azure.core.implementation.serializer.SerializerAdapter;
import com.azure.core.implementation.serializer.SerializerEncoding;
import com.azure.core.implementation.serializer.jackson.JacksonAdapter;
import com.azure.core.implementation.util.ScopeUtil;
import com.azure.core.util.logging.ClientLogger;
import com.azure.identity.DeviceCodeChallenge;
import com.azure.identity.implementation.util.CertificateUtil;
import com.microsoft.aad.msal4j.AuthorizationCodeParameters;
import com.microsoft.aad.msal4j.ClientCredentialFactory;
import com.microsoft.aad.msal4j.ClientCredentialParameters;
import com.microsoft.aad.msal4j.ConfidentialClientApplication;
import com.microsoft.aad.msal4j.DeviceCodeFlowParameters;
import com.microsoft.aad.msal4j.PublicClientApplication;
import com.microsoft.aad.msal4j.SilentParameters;
import com.microsoft.aad.msal4j.UserNamePasswordParameters;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.awt.Desktop;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet;
import java.util.Random;
import java.util.Scanner;
import java.util.UUID;
import java.util.function.Consumer;
/**
* The identity client that contains APIs to retrieve access tokens
* from various configurations.
*/
public class IdentityClient {
private static final SerializerAdapter SERIALIZER_ADAPTER = JacksonAdapter.createDefaultSerializerAdapter();
private static final Random RANDOM = new Random();
private final ClientLogger logger = new ClientLogger(IdentityClient.class);
private final IdentityClientOptions options;
private final PublicClientApplication publicClientApplication;
private final String tenantId;
private final String clientId;
/**
* Creates an IdentityClient with the given options.
*
* @param tenantId the tenant ID of the application.
* @param clientId the client ID of the application.
* @param options the options configuring the client.
*/
IdentityClient(String tenantId, String clientId, IdentityClientOptions options) {
if (tenantId == null) {
tenantId = "common";
}
if (options == null) {
options = new IdentityClientOptions();
}
this.tenantId = tenantId;
this.clientId = clientId;
this.options = options;
if (clientId == null) {
this.publicClientApplication = null;
} else {
String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/organizations/" + tenantId;
PublicClientApplication.Builder publicClientApplicationBuilder = PublicClientApplication.builder(clientId);
try {
publicClientApplicationBuilder = publicClientApplicationBuilder.authority(authorityUrl);
} catch (MalformedURLException e) {
throw logger.logExceptionAsWarning(new IllegalStateException(e));
}
if (options.getProxyOptions() != null) {
publicClientApplicationBuilder.proxy(proxyOptionsToJavaNetProxy(options.getProxyOptions()));
}
this.publicClientApplication = publicClientApplicationBuilder.build();
}
}
/**
* Asynchronously acquire a token from Active Directory with a client secret.
*
* @param clientSecret the client secret of the application
* @param request the details of the token request
* @return a Publisher that emits an AccessToken
*/
public Mono authenticateWithClientSecret(String clientSecret, TokenRequest request) {
String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/" + tenantId;
try {
ConfidentialClientApplication.Builder applicationBuilder =
ConfidentialClientApplication.builder(clientId, ClientCredentialFactory.create(clientSecret))
.authority(authorityUrl);
if (options.getProxyOptions() != null) {
applicationBuilder.proxy(proxyOptionsToJavaNetProxy(options.getProxyOptions()));
}
ConfidentialClientApplication application = applicationBuilder.build();
return Mono.fromFuture(application.acquireToken(
ClientCredentialParameters.builder(new HashSet<>(request.getScopes()))
.build()))
.map(ar -> new AccessToken(ar.accessToken(), OffsetDateTime.ofInstant(ar.expiresOnDate().toInstant(),
ZoneOffset.UTC)));
} catch (MalformedURLException e) {
return Mono.error(e);
}
}
/**
* Asynchronously acquire a token from Active Directory with a PKCS12 certificate.
*
* @param pfxCertificatePath the path to the PKCS12 certificate of the application
* @param pfxCertificatePassword the password protecting the PFX certificate
* @param request the details of the token request
* @return a Publisher that emits an AccessToken
*/
public Mono authenticateWithPfxCertificate(String pfxCertificatePath, String pfxCertificatePassword,
TokenRequest request) {
String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/" + tenantId;
try {
ConfidentialClientApplication.Builder applicationBuilder =
ConfidentialClientApplication.builder(clientId,
ClientCredentialFactory.create(new FileInputStream(pfxCertificatePath), pfxCertificatePassword))
.authority(authorityUrl);
if (options.getProxyOptions() != null) {
applicationBuilder.proxy(proxyOptionsToJavaNetProxy(options.getProxyOptions()));
}
ConfidentialClientApplication application = applicationBuilder.build();
return Mono.fromFuture(application.acquireToken(
ClientCredentialParameters.builder(new HashSet<>(request.getScopes()))
.build()))
.map(ar -> new AccessToken(ar.accessToken(), OffsetDateTime.ofInstant(ar.expiresOnDate().toInstant(),
ZoneOffset.UTC)));
} catch (CertificateException
| UnrecoverableKeyException
| NoSuchAlgorithmException
| KeyStoreException
| NoSuchProviderException
| IOException e) {
return Mono.error(e);
}
}
/**
* Asynchronously acquire a token from Active Directory with a PEM certificate.
*
* @param pemCertificatePath the path to the PEM certificate of the application
* @param request the details of the token request
* @return a Publisher that emits an AccessToken
*/
public Mono authenticateWithPemCertificate(String pemCertificatePath, TokenRequest request) {
String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/" + tenantId;
try {
byte[] pemCertificateBytes = Files.readAllBytes(Paths.get(pemCertificatePath));
ConfidentialClientApplication.Builder applicationBuilder =
ConfidentialClientApplication.builder(clientId,
ClientCredentialFactory.create(CertificateUtil.privateKeyFromPem(pemCertificateBytes),
CertificateUtil.publicKeyFromPem(pemCertificateBytes))).authority(authorityUrl);
if (options.getProxyOptions() != null) {
applicationBuilder.proxy(proxyOptionsToJavaNetProxy(options.getProxyOptions()));
}
ConfidentialClientApplication application = applicationBuilder.build();
return Mono.fromFuture(application.acquireToken(
ClientCredentialParameters.builder(new HashSet<>(request.getScopes()))
.build()))
.map(ar -> new AccessToken(ar.accessToken(), OffsetDateTime.ofInstant(ar.expiresOnDate().toInstant(),
ZoneOffset.UTC)));
} catch (IOException e) {
return Mono.error(e);
}
}
/**
* Asynchronously acquire a token from Active Directory with a username and a password.
*
* @param request the details of the token request
* @param username the username of the user
* @param password the password of the user
* @return a Publisher that emits an AccessToken
*/
public Mono authenticateWithUsernamePassword(TokenRequest request, String username, String password) {
return Mono.fromFuture(publicClientApplication.acquireToken(
UserNamePasswordParameters.builder(new HashSet<>(request.getScopes()), username, password.toCharArray())
.build()))
.map(MsalToken::new);
}
/**
* Asynchronously acquire a token from the currently logged in client.
*
* @param request the details of the token request
* @return a Publisher that emits an AccessToken
*/
public Mono authenticateWithUserRefreshToken(TokenRequest request, MsalToken msalToken) {
SilentParameters parameters;
if (msalToken.getAccount() != null) {
parameters = SilentParameters.builder(new HashSet<>(request.getScopes()), msalToken.getAccount()).build();
} else {
parameters = SilentParameters.builder(new HashSet<>(request.getScopes())).build();
}
return Mono.defer(() -> {
try {
return Mono.fromFuture(publicClientApplication.acquireTokenSilently(parameters)).map(MsalToken::new);
} catch (MalformedURLException e) {
return Mono.error(e);
}
});
}
/**
* Asynchronously acquire a token from Active Directory with a device code challenge. Active Directory will provide
* a device code for login and the user must meet the challenge by authenticating in a browser on the current or a
* different device.
*
* @param request the details of the token request
* @param deviceCodeConsumer the user provided closure that will consume the device code challenge
* @return a Publisher that emits an AccessToken when the device challenge is met, or an exception if the device
* code expires
*/
public Mono authenticateWithDeviceCode(TokenRequest request,
Consumer deviceCodeConsumer) {
return Mono.fromFuture(() -> {
DeviceCodeFlowParameters parameters = DeviceCodeFlowParameters.builder(new HashSet<>(request.getScopes()),
dc -> deviceCodeConsumer.accept(new DeviceCodeChallenge(dc.userCode(), dc.deviceCode(),
dc.verificationUri(), dc.expiresIn(), dc.interval(), dc.message()))).build();
return publicClientApplication.acquireToken(parameters);
}).map(MsalToken::new);
}
/**
* Asynchronously acquire a token from Active Directory with an authorization code from an oauth flow.
*
* @param request the details of the token request
* @param authorizationCode the oauth2 authorization code
* @param redirectUri the redirectUri where the authorization code is sent to
* @return a Publisher that emits an AccessToken
*/
public Mono authenticateWithAuthorizationCode(TokenRequest request, String authorizationCode,
URI redirectUri) {
return Mono.fromFuture(() -> publicClientApplication.acquireToken(
AuthorizationCodeParameters.builder(authorizationCode, redirectUri)
.scopes(new HashSet<>(request.getScopes()))
.build()))
.map(MsalToken::new);
}
/**
* Asynchronously acquire a token from Active Directory by opening a browser and wait for the user to login. The
* credential will run a minimal local HttpServer at the given port, so {@code http://localhost:{port}} must be
* listed as a valid reply URL for the application.
*
* @param request the details of the token request
* @param port the port on which the HTTP server is listening
* @return a Publisher that emits an AccessToken
*/
public Mono authenticateWithBrowserInteraction(TokenRequest request, int port) {
String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/" + tenantId;
return AuthorizationCodeListener.create(port)
.flatMap(server -> {
URI redirectUri;
URI browserUri;
try {
redirectUri = new URI(String.format("http://localhost:%s", port));
browserUri =
new URI(String.format("%s/oauth2/v2.0/authorize?response_type=code&response_mode=query&prompt"
+ "=select_account&client_id=%s&redirect_uri=%s&state=%s&scope=%s",
authorityUrl, clientId, redirectUri.toString(), UUID.randomUUID(), String.join(" ",
request.getScopes())));
} catch (URISyntaxException e) {
return server.dispose().then(Mono.error(e));
}
return server.listen()
.mergeWith(Mono.fromRunnable(() -> {
try {
Desktop.getDesktop().browse(browserUri);
} catch (IOException e) {
throw logger.logExceptionAsError(new IllegalStateException(e));
}
}).subscribeOn(Schedulers.newSingle("browser")))
.next()
.flatMap(code -> authenticateWithAuthorizationCode(request, code, redirectUri))
.onErrorResume(t -> server.dispose().then(Mono.error(t)))
.flatMap(msalToken -> server.dispose().then(Mono.just(msalToken)));
});
}
/**
* Asynchronously acquire a token from the App Service Managed Service Identity endpoint.
*
* @param msiEndpoint the endpoint to acquire token from
* @param msiSecret the secret to acquire token with
* @param request the details of the token request
* @return a Publisher that emits an AccessToken
*/
public Mono authenticateToManagedIdentityEndpoint(String msiEndpoint, String msiSecret,
TokenRequest request) {
String resource = ScopeUtil.scopesToResource(request.getScopes());
HttpURLConnection connection = null;
StringBuilder payload = new StringBuilder();
try {
payload.append("resource=");
payload.append(URLEncoder.encode(resource, "UTF-8"));
payload.append("&api-version=");
payload.append(URLEncoder.encode("2017-09-01", "UTF-8"));
if (clientId != null) {
payload.append("&client_id=");
payload.append(URLEncoder.encode(clientId, "UTF-8"));
}
} catch (IOException exception) {
return Mono.error(exception);
}
try {
URL url = new URL(String.format("%s?%s", msiEndpoint, payload));
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
if (msiSecret != null) {
connection.setRequestProperty("Secret", msiSecret);
}
connection.setRequestProperty("Metadata", "true");
connection.connect();
Scanner s = new Scanner(connection.getInputStream(), StandardCharsets.UTF_8.name()).useDelimiter("\\A");
String result = s.hasNext() ? s.next() : "";
return Mono.just(SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON));
} catch (IOException e) {
return Mono.error(e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
/**
* Asynchronously acquire a token from the Virtual Machine IMDS endpoint.
*
* @param request the details of the token request
* @return a Publisher that emits an AccessToken
*/
public Mono authenticateToIMDSEndpoint(TokenRequest request) {
String resource = ScopeUtil.scopesToResource(request.getScopes());
StringBuilder payload = new StringBuilder();
final int imdsUpgradeTimeInMs = 70 * 1000;
try {
payload.append("api-version=");
payload.append(URLEncoder.encode("2018-02-01", "UTF-8"));
payload.append("&resource=");
payload.append(URLEncoder.encode(resource, "UTF-8"));
if (clientId != null) {
payload.append("&client_id=");
payload.append(URLEncoder.encode(clientId, "UTF-8"));
}
} catch (IOException exception) {
return Mono.error(exception);
}
int retry = 1;
while (retry <= options.getMaxRetry()) {
URL url = null;
HttpURLConnection connection = null;
try {
url =
new URL(String.format("http://169.254.169.254/metadata/identity/oauth2/token?%s",
payload.toString()));
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Metadata", "true");
connection.connect();
Scanner s = new Scanner(connection.getInputStream(), StandardCharsets.UTF_8.name()).useDelimiter("\\A");
String result = s.hasNext() ? s.next() : "";
return Mono.just(SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON));
} catch (IOException exception) {
if (connection == null) {
return Mono.error(new RuntimeException(String.format("Could not connect to the url: %s.", url),
exception));
}
int responseCode = 0;
try {
responseCode = connection.getResponseCode();
} catch (IOException e) {
return Mono.error(e);
}
if (responseCode == 410
|| responseCode == 429
|| responseCode == 404
|| (responseCode >= 500 && responseCode <= 599)) {
int retryTimeoutInMs = options.getRetryTimeout().apply(RANDOM.nextInt(retry));
// Error code 410 indicates IMDS upgrade is in progress, which can take up to 70s
//
retryTimeoutInMs =
(responseCode == 410 && retryTimeoutInMs < imdsUpgradeTimeInMs) ? imdsUpgradeTimeInMs
: retryTimeoutInMs;
retry++;
if (retry > options.getMaxRetry()) {
break;
} else {
sleep(retryTimeoutInMs);
}
} else {
return Mono.error(new RuntimeException(
"Couldn't acquire access token from IMDS, verify your objectId, clientId or msiResourceId",
exception));
}
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
return Mono.error(new RuntimeException(String.format("MSI: Failed to acquire tokens after retrying %s times",
options.getMaxRetry())));
}
private static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ex) {
throw new IllegalStateException(ex);
}
}
private static Proxy proxyOptionsToJavaNetProxy(ProxyOptions options) {
switch (options.getType()) {
case SOCKS4:
case SOCKS5:
return new Proxy(Type.SOCKS, options.getAddress());
case HTTP:
default:
return new Proxy(Type.HTTP, options.getAddress());
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy