diff --git a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml index 62cdc65ab594..f9dd1b4c24cc 100755 --- a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml +++ b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml @@ -257,6 +257,10 @@ + + + diff --git a/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml b/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml index 832838ef7c26..ae836425cc20 100755 --- a/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml +++ b/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml @@ -45,6 +45,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 559fd54d1a32..7385ba0344e9 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -84,6 +84,7 @@ com.microsoft.azure:azure-mgmt-search;1.24.1 com.microsoft.azure:azure-mgmt-storage;1.3.0 com.microsoft.azure:azure-storage;8.0.0 com.microsoft.azure:msal4j;1.3.0 +com.microsoft.azure:msal4j-persistence-extension;0.1 com.sun.activation:jakarta.activation;1.2.1 io.opentelemetry:opentelemetry-api;0.2.4 io.opentelemetry:opentelemetry-sdk;0.2.4 diff --git a/sdk/identity/azure-identity/pom.xml b/sdk/identity/azure-identity/pom.xml index a73759a857d1..18c041aeb051 100644 --- a/sdk/identity/azure-identity/pom.xml +++ b/sdk/identity/azure-identity/pom.xml @@ -93,6 +93,12 @@ 2.8.5 test + + + com.microsoft.azure + msal4j-persistence-extension + 0.1 + @@ -107,6 +113,7 @@ com.azure:* com.microsoft.azure:msal4j:[1.3.0] + com.microsoft.azure:msal4j-persistence-extension:[0.1] com.nimbusds:oauth2-oidc-sdk:[6.14] net.java.dev.jna:jna-platform:[5.4.0] org.nanohttpd:nanohttpd:[2.3.1] diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java index 65b1fcf7ec09..e3a039ce5015 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java @@ -38,9 +38,6 @@ public class AuthorizationCodeCredential implements TokenCredential { */ AuthorizationCodeCredential(String clientId, String tenantId, String authCode, URI redirectUri, IdentityClientOptions identityClientOptions) { - if (tenantId == null) { - tenantId = "common"; - } identityClient = new IdentityClientBuilder() .tenantId(tenantId) .clientId(clientId) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredentialBuilder.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredentialBuilder.java index 7978a70249f8..be577746e9e4 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredentialBuilder.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredentialBuilder.java @@ -20,9 +20,24 @@ public class DefaultAzureCredentialBuilder extends CredentialBuilderBase getCredentialsChain() { - ArrayDeque output = new ArrayDeque<>(4); + ArrayDeque output = new ArrayDeque<>(5); if (!excludeEnvironmentCredential) { output.add(new EnvironmentCredential(identityClientOptions)); } @@ -128,6 +153,10 @@ private ArrayDeque getCredentialsChain() { output.add(new AzureCliCredential(identityClientOptions)); } + if (!excludeVsCodeCredential) { + output.add(new VisualStudioCodeCredential(tenantId, identityClientOptions)); + } + if (output.size() == 0) { throw logger.logExceptionAsError(new IllegalArgumentException("At least one credential type must be" + " included in the authentication flow.")); diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java index 638a8ce9bed9..dbd820196392 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java @@ -36,9 +36,6 @@ public class DeviceCodeCredential implements TokenCredential { DeviceCodeCredential(String clientId, String tenantId, Consumer challengeConsumer, IdentityClientOptions identityClientOptions) { this.challengeConsumer = challengeConsumer; - if (tenantId == null) { - tenantId = "common"; - } identityClient = new IdentityClientBuilder() .tenantId(tenantId) .clientId(clientId) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java index a0bce6a715e7..28aa716c9de0 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java @@ -42,9 +42,6 @@ public class InteractiveBrowserCredential implements TokenCredential { InteractiveBrowserCredential(String clientId, String tenantId, int port, IdentityClientOptions identityClientOptions) { this.port = port; - if (tenantId == null) { - tenantId = "common"; - } identityClient = new IdentityClientBuilder() .tenantId(tenantId) .clientId(clientId) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java index 5c2f5d4e14cb..5e40ce338567 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/UsernamePasswordCredential.java @@ -43,9 +43,6 @@ public class UsernamePasswordCredential implements TokenCredential { Objects.requireNonNull(password, "'password' cannot be null."); this.username = username; this.password = password; - if (tenantId == null) { - tenantId = "common"; - } identityClient = new IdentityClientBuilder() .tenantId(tenantId) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java new file mode 100644 index 000000000000..4d1f741d1d77 --- /dev/null +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/VisualStudioCodeCredential.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity; + +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; +import com.azure.identity.implementation.IdentityClient; +import com.azure.identity.implementation.IdentityClientBuilder; +import com.azure.identity.implementation.IdentityClientOptions; +import com.azure.identity.implementation.MsalToken; +import com.azure.identity.implementation.VisualStudioCacheAccessor; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Enables authentication to Azure Active Directory using data from Visual Studio Code + */ +class VisualStudioCodeCredential implements TokenCredential { + private final IdentityClient identityClient; + private final AtomicReference cachedToken; + private final String cloudInstance; + + /** + * Creates a public class VisualStudioCodeCredential implements TokenCredential with the given tenant and + * identity client options. + * + * @param tenantId the tenant ID of the application + * @param identityClientOptions the options for configuring the identity client + */ + VisualStudioCodeCredential(String tenantId, IdentityClientOptions identityClientOptions) { + + IdentityClientOptions options = (identityClientOptions == null ? new IdentityClientOptions() + : identityClientOptions); + + String tenant = tenantId; + if (tenant == null) { + tenant = "common"; + } + VisualStudioCacheAccessor accessor = new VisualStudioCacheAccessor(); + Map userSettings = accessor.getUserSettingsDetails(tenant); + cloudInstance = userSettings.get("cloud"); + options.setAuthorityHost(accessor.getAzureAuthHost(cloudInstance)); + + identityClient = new IdentityClientBuilder() + .clientId("aebc6443-996d-45c2-90f0-388ff96faa56") + .identityClientOptions(options) + .build(); + + this.cachedToken = new AtomicReference<>(); + } + + @Override + public Mono getToken(TokenRequestContext request) { + return Mono.defer(() -> { + if (cachedToken.get() != null) { + return identityClient.authenticateWithUserRefreshToken(request, cachedToken.get()) + .onErrorResume(t -> Mono.empty()); + } else { + return Mono.empty(); + } + }).switchIfEmpty( + Mono.defer(() -> identityClient.authenticateWithVsCodeCredential(request, cloudInstance))) + .map(msalToken -> { + cachedToken.set(msalToken); + return msalToken; + }); + } +} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index ebd38a924eee..5cb68f9d2086 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -29,6 +29,7 @@ import com.microsoft.aad.msal4j.ConfidentialClientApplication; import com.microsoft.aad.msal4j.DeviceCodeFlowParameters; import com.microsoft.aad.msal4j.PublicClientApplication; +import com.microsoft.aad.msal4j.RefreshTokenParameters; import com.microsoft.aad.msal4j.SilentParameters; import com.microsoft.aad.msal4j.UserNamePasswordParameters; import reactor.core.publisher.Mono; @@ -101,7 +102,7 @@ public class IdentityClient { */ IdentityClient(String tenantId, String clientId, IdentityClientOptions options) { if (tenantId == null) { - tenantId = "common"; + tenantId = "organizations"; } if (options == null) { options = new IdentityClientOptions(); @@ -112,7 +113,7 @@ public class IdentityClient { if (clientId == null) { this.publicClientApplication = null; } else { - String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/organizations/" + tenantId; + String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "") + "/" + tenantId; PublicClientApplication.Builder publicClientApplicationBuilder = PublicClientApplication.builder(clientId); try { publicClientApplicationBuilder = publicClientApplicationBuilder.authority(authorityUrl); @@ -429,6 +430,26 @@ public Mono authenticateWithDeviceCode(TokenRequestContext request, }).map(ar -> new MsalToken(ar, options)); } + /** + * Asynchronously acquire a token from Active Directory with Visual Sutdio cached refresh token. + * + * @param request the details of the token request + * @return a Publisher that emits an AccessToken. + */ + public Mono authenticateWithVsCodeCredential(TokenRequestContext request, String cloud) { + + VisualStudioCacheAccessor accessor = new VisualStudioCacheAccessor(); + + String credential = accessor.getCredentials("VS Code Azure", cloud); + + RefreshTokenParameters parameters = RefreshTokenParameters + .builder(new HashSet<>(request.getScopes()), credential) + .build(); + + return Mono.defer(() -> Mono.fromFuture(publicClientApplication.acquireToken(parameters)) + .map(ar -> new MsalToken(ar, options))); + } + /** * Asynchronously acquire a token from Active Directory with an authorization code from an oauth flow. * diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/LinuxKeyRingAccessor.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/LinuxKeyRingAccessor.java new file mode 100644 index 000000000000..20ed6c4461a2 --- /dev/null +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/LinuxKeyRingAccessor.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity.implementation; + +import com.azure.core.util.logging.ClientLogger; +import com.microsoft.aad.msal4jextensions.persistence.linux.ISecurityLibrary; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +/** + * This class exposes functions from Key Ring on Linux platform + * via JNA. + */ +public class LinuxKeyRingAccessor { + private final ClientLogger logger = new ClientLogger(LinuxKeyRingAccessor.class); + private String keyringSchemaName; + private String attributeKey1; + private String attributeValue1; + private String attributeKey2; + private String attributeValue2; + private Pointer libSecretSchema; + + /** + * Creates an instance of the {@link LinuxKeyRingAccessor} with specified attributes and schema. + * + * @param keyringSchemaName the key ring schema to access. + * @param attributeKey1 the key value of the attribute to lookup + * @param attributeValue1 the value of the attribute to lookup + * @param attributeKey2 the key value of the attribute to lookup + * @param attributeValue2 the value of the attribute to lookup + */ + public LinuxKeyRingAccessor(String keyringSchemaName, String attributeKey1, + String attributeValue1, String attributeKey2, String attributeValue2) { + this.keyringSchemaName = keyringSchemaName; + this.attributeKey1 = attributeKey1; + this.attributeValue1 = attributeValue1; + this.attributeKey2 = attributeKey2; + this.attributeValue2 = attributeValue2; + } + + + private byte[] read(String attributeValue1, String attributeValue2) { + byte[] data = null; + Pointer[] error = new Pointer[1]; + String secret = ISecurityLibrary.library.secret_password_lookup_sync(this.getLibSecretSchema(), + (Pointer) null, error, this.attributeKey1, attributeValue1, this.attributeKey2, + attributeValue2, (Pointer) null); + if (error[0] != Pointer.NULL) { + GError err = new GError(error[0]); + throw logger.logExceptionAsError(new RuntimeException("An error while reading secret from keyring, domain:" + + err.domain + " code:" + err.code + " message:" + err.message)); + } else { + if (secret != null && !secret.isEmpty()) { + data = secret.getBytes(StandardCharsets.UTF_8); + } + return data; + } + } + + /** + * Read the value of the configured secret attributes. + * @return the byte array holding the secret. + */ + public byte[] read() { + return this.read(this.attributeValue1, this.attributeValue2); + } + + private Pointer getLibSecretSchema() { + if (this.libSecretSchema == Pointer.NULL) { + this.libSecretSchema = ISecurityLibrary.library.secret_schema_new(this.keyringSchemaName, + 0, this.attributeKey1, 0, this.attributeKey2, 0, (Pointer) null); + if (this.libSecretSchema == Pointer.NULL) { + throw logger.logExceptionAsError( + new RuntimeException("Failed to create libSecret schema " + this.keyringSchemaName)); + } + } + + return this.libSecretSchema; + } + + static class GError extends Structure { + int domain; + int code; + String message; + + GError(Pointer p) { + super(p); + this.read(); + } + + protected List getFieldOrder() { + return Arrays.asList("domain", "code", "message"); + } + } +} + diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/VisualStudioCacheAccessor.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/VisualStudioCacheAccessor.java new file mode 100644 index 000000000000..8f7ec711abb0 --- /dev/null +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/VisualStudioCacheAccessor.java @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity.implementation; + +import com.azure.core.util.CoreUtils; +import com.azure.core.util.logging.ClientLogger; +import com.azure.identity.CredentialUnavailableException; +import com.azure.identity.KnownAuthorityHosts; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.aad.msal4jextensions.persistence.mac.KeyChainAccessor; +import com.sun.jna.Platform; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * This class allows access to Visual Studio Code cached credential data. + */ +public class VisualStudioCacheAccessor { + private static final String PLATFORM_NOT_SUPPORTED_ERROR = "Platform could not be determined for VS Code" + + " credential authentication."; + private final ClientLogger logger = new ClientLogger(VisualStudioCacheAccessor.class); + private static final Pattern REFRESH_TOKEN_PATTERN = Pattern.compile("^[-_.a-zA-Z0-9]+$"); + + + private JsonNode getUserSettings() { + JsonNode output = null; + String homeDir = System.getProperty("user.home"); + String settingsPath = ""; + ObjectMapper mapper = new ObjectMapper(); + try { + if (Platform.isWindows()) { + settingsPath = Paths.get(System.getenv("APPDATA"), "Code", "User", "settings.json") + .toString(); + } else if (Platform.isMac()) { + settingsPath = Paths.get(homeDir, "Library", + "Application Support", "Code", "User", "settings.json").toString(); + } else if (Platform.isLinux()) { + settingsPath = Paths.get(homeDir, ".config", "Code", "User", "settings.json") + .toString(); + } else { + throw logger.logExceptionAsError( + new CredentialUnavailableException(PLATFORM_NOT_SUPPORTED_ERROR)); + } + File settingsFile = new File(settingsPath); + output = mapper.readTree(settingsFile); + } catch (Exception e) { + return null; + } + return output; + } + + /** + * Get the user configured settings of Visual Studio code. + * + * @param tenantId the user specified tenant id. + * @return a Map containing Vs Code user settings + */ + public Map getUserSettingsDetails(String tenantId) { + JsonNode userSettings = getUserSettings(); + Map details = new HashMap<>(); + + String tenant = tenantId; + + String cloud = "Azure"; + + if (userSettings != null && !userSettings.isNull()) { + if (userSettings.has("azure.tenant") && CoreUtils.isNullOrEmpty(tenant)) { + tenant = userSettings.get("azure.tenant").asText(); + } + + if (userSettings.has("azure.cloud")) { + cloud = userSettings.get("azure.cloud").asText(); + } + } + + details.put("tenant", tenant); + details.put("cloud", cloud); + return details; + } + + + + /** + * Get the credential for the specified service and account name. + * + * @param serviceName the name of the service to lookup. + * @param accountName the account of the service to lookup. + * @return the credential. + */ + public String getCredentials(String serviceName, String accountName) { + String credential; + + if (Platform.isWindows()) { + + try { + WindowsCredentialAccessor winCredAccessor = + new WindowsCredentialAccessor(serviceName, accountName); + credential = winCredAccessor.read(); + } catch (RuntimeException e) { + throw logger.logExceptionAsError(new CredentialUnavailableException( + "Failed to read Vs Code credentials from Windows Credential API.", e)); + } + + } else if (Platform.isMac()) { + + try { + KeyChainAccessor keyChainAccessor = new KeyChainAccessor(null, + serviceName, accountName); + + byte[] readCreds = keyChainAccessor.read(); + credential = new String(readCreds, StandardCharsets.UTF_8); + } catch (RuntimeException e) { + throw logger.logExceptionAsError(new CredentialUnavailableException( + "Failed to read Vs Code credentials from Mac Native Key Chain.", e)); + } + + } else if (Platform.isLinux()) { + + try { + LinuxKeyRingAccessor keyRingAccessor = new LinuxKeyRingAccessor( + "org.freedesktop.Secret.Generic", "service", + serviceName, "account", accountName); + + byte[] readCreds = keyRingAccessor.read(); + credential = new String(readCreds, StandardCharsets.UTF_8); + } catch (RuntimeException e) { + throw logger.logExceptionAsError(new CredentialUnavailableException( + "Failed to read Vs Code credentials from Linux Key Ring.", e)); + } + + } else { + throw logger.logExceptionAsError( + new CredentialUnavailableException(PLATFORM_NOT_SUPPORTED_ERROR)); + } + + if (CoreUtils.isNullOrEmpty(credential) || !isRefreshTokenString(credential)) { + throw logger.logExceptionAsError( + new CredentialUnavailableException("Please authenticate via Azure Tools plugin in VS Code IDE.")); + } + return credential; + } + + private boolean isRefreshTokenString(String str) { + return REFRESH_TOKEN_PATTERN.matcher(str).matches(); + } + + /** + * Get the auth host of the specified {@code azureEnvironment}. + * + * @return the auth host. + */ + public String getAzureAuthHost(String cloud) { + + switch (cloud) { + case "Azure": + return KnownAuthorityHosts.AZURE_CLOUD; + case "AzureChina": + return KnownAuthorityHosts.AZURE_CHINA_CLOUD; + case "AzureGermanCloud": + return KnownAuthorityHosts.AZURE_GERMAN_CLOUD; + case "AzureUSGovernment": + return KnownAuthorityHosts.AZURE_US_GOVERNMENT; + default: + return KnownAuthorityHosts.AZURE_CLOUD; + } + } + +} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/WindowsCredentialAccessor.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/WindowsCredentialAccessor.java new file mode 100644 index 000000000000..f54067a1afea --- /dev/null +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/WindowsCredentialAccessor.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity.implementation; + +import com.azure.core.util.logging.ClientLogger; +import com.sun.jna.LastErrorException; +import com.sun.jna.Native; +import com.sun.jna.platform.win32.Kernel32; +import com.sun.jna.platform.win32.Kernel32Util; +import com.sun.jna.win32.W32APIOptions; + +import java.nio.charset.StandardCharsets; + +/** + * This class allows access to windows credentials via JNA. + */ +public class WindowsCredentialAccessor { + private final ClientLogger logger = new ClientLogger(WindowsCredentialAccessor.class); + private WindowsCredentialApi accessor; + private String serviceName; + private String accountName; + + /** + * Creates an instance of the {@link WindowsCredentialAccessor} + * + *

The specified {@code serviceName} and {@code accountName} are used to form target credential name.

+ * + * @param serviceName the service name to lookup. + * @param accountName the account name to lookup. + */ + public WindowsCredentialAccessor(String serviceName, String accountName) { + accessor = Native.load("Advapi32", WindowsCredentialApi.class, W32APIOptions.UNICODE_OPTIONS); + this.serviceName = serviceName; + this.accountName = accountName; + } + + /** + * Reads the credential from windows credential store. + * @return the credential. + */ + public String read() { + WindowsCredentialApi.PCREDENTIAL pcredential = new WindowsCredentialApi.PCREDENTIAL(); + try { + boolean readOk = accessor.CredRead(String.format("%s/%s", serviceName, accountName), + WindowsCredentialApi.CRED_TYPE_GENERIC, 0, pcredential); + + if (!readOk) { + int rc = Kernel32.INSTANCE.GetLastError(); + String errMsg = Kernel32Util.formatMessage(rc); + throw logger.logExceptionAsError(new RuntimeException(errMsg)); + } + final WindowsCredentialApi.CREDENTIAL credential = + new WindowsCredentialApi.CREDENTIAL(pcredential.credential); + + byte[] secretBytes = credential.CredentialBlob.getByteArray(0, credential.CredentialBlobSize); + final String secret = new String(secretBytes, StandardCharsets.UTF_8); + return secret; + } catch (LastErrorException e) { + int errorCode = e.getErrorCode(); + String errMsg = Kernel32Util.formatMessage(errorCode); + throw logger.logExceptionAsError(new RuntimeException(errMsg)); + + } finally { + if (pcredential.credential != null) { + accessor.CredFree(pcredential.credential); + } + } + } + +} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/WindowsCredentialApi.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/WindowsCredentialApi.java new file mode 100644 index 000000000000..533286c5db37 --- /dev/null +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/WindowsCredentialApi.java @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity.implementation; + +import com.sun.jna.Pointer; +import com.sun.jna.Memory; +import com.sun.jna.LastErrorException; +import com.sun.jna.Structure; +import com.sun.jna.platform.win32.WinBase; +import com.sun.jna.win32.StdCallLibrary; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * This class exposes functions from credential manager on Windows platform + * via JNA. + */ +public interface WindowsCredentialApi extends StdCallLibrary { + + /** + * Type of Credential + */ + int CRED_TYPE_GENERIC = 1; + + /** + * Credential attributes + * + * typedef struct _CREDENTIAL_ATTRIBUTE { + * LPTSTR Keyword; + * DWORD Flags; + * DWORD ValueSize; + * LPBYTE Value; + * } CREDENTIAL_ATTRIBUTE, *PCREDENTIAL_ATTRIBUTE; + * + */ + class CREDENTIAL_ATTRIBUTE extends Structure { + + public static class ByReference extends CREDENTIAL_ATTRIBUTE implements Structure.ByReference { } + + @Override + protected List getFieldOrder() { + return Arrays.asList("Keyword", + "Flags", + "ValueSize", + "Value"); + } + + /** + * Name of the application-specific attribute. Names should be of the form "CompanyName_Name". + * This member cannot be longer than CRED_MAX_STRING_LENGTH (256) characters. + */ + public String Keyword; + + /** + * Identifies characteristics of the credential attribute. This member is reserved and should be originally + * initialized as zero and not otherwise altered to permit future enhancement. + */ + public int Flags; + + /** + * Length of Value in bytes. This member cannot be larger than CRED_MAX_VALUE_SIZE (256). + */ + public int ValueSize; + + /** + * Data associated with the attribute. By convention, if Value is a text string, then Value should not + * include the trailing zero character and should be in UNICODE. + * + * Credentials are expected to be portable. The application should take care to ensure that the data in + * value is portable. It is the responsibility of the application to define the byte-endian and alignment + * of the data in Value. + */ + public Pointer Value; + } + + + /** + * The CREDENTIAL structure contains an individual credential + * + * typedef struct _CREDENTIAL { + * DWORD Flags; + * DWORD Type; + * LPTSTR TargetName; + * LPTSTR Comment; + * FILETIME LastWritten; + * DWORD CredentialBlobSize; + * LPBYTE CredentialBlob; + * DWORD Persist; + * DWORD AttributeCount; + * PCREDENTIAL_ATTRIBUTE Attributes; + * LPTSTR TargetAlias; + * LPTSTR UserName; + * } CREDENTIAL, *PCREDENTIAL; + */ + class CREDENTIAL extends Structure { + + @Override + protected List getFieldOrder() { + return Arrays.asList("Flags", + "Type", + "TargetName", + "Comment", + "LastWritten", + "CredentialBlobSize", + "CredentialBlob", + "Persist", + "AttributeCount", + "Attributes", + "TargetAlias", + "UserName"); + } + + public CREDENTIAL() { + super(); + } + + public CREDENTIAL(final int size) { + super(new Memory(size)); + } + + public CREDENTIAL(Pointer memory) { + super(memory); + read(); + } + + /** + * A bit member that identifies characteristics of the credential. Undefined bits should be initialized + * as zero and not otherwise altered to permit future enhancement. + * + * See MSDN doc for all possible flags + */ + public int Flags; + + /** + * The type of the credential. This member cannot be changed after the credential is created. + * + * See MSDN doc for all possible types + */ + public int Type; + + /** + * The name of the credential. The TargetName and Type members uniquely identify the credential. + * This member cannot be changed after the credential is created. Instead, the credential with the old + * name should be deleted and the credential with the new name created. + * + * See MSDN doc for additional requirement + */ + public String TargetName; + + /** + * A string comment from the user that describes this credential. This member cannot be longer than + * CRED_MAX_STRING_LENGTH (256) characters. + */ + public String Comment; + + /** + * The time, in Coordinated Universal Time (Greenwich Mean Time), of the last modification of the credential. + * For write operations, the value of this member is ignored. + */ + public WinBase.FILETIME LastWritten; + + /** + * The size, in bytes, of the CredentialBlob member. This member cannot be larger than + * CRED_MAX_CREDENTIAL_BLOB_SIZE (512) bytes. + */ + public int CredentialBlobSize; + + /** + * Secret data for the credential. The CredentialBlob member can be both read and written. + * If the Type member is CRED_TYPE_DOMAIN_PASSWORD, this member contains the plaintext Unicode password + * for UserName. The CredentialBlob and CredentialBlobSize members do not include a trailing zero character. + * Also, for CRED_TYPE_DOMAIN_PASSWORD, this member can only be read by the authentication packages. + * + * If the Type member is CRED_TYPE_DOMAIN_CERTIFICATE, this member contains the clear test + * Unicode PIN for UserName. The CredentialBlob and CredentialBlobSize members do not include a trailing + * zero character. Also, this member can only be read by the authentication packages. + * + * If the Type member is CRED_TYPE_GENERIC, this member is defined by the application. + * Credentials are expected to be portable. Applications should ensure that the data in CredentialBlob is + * portable. The application defines the byte-endian and alignment of the data in CredentialBlob. + */ + public Pointer CredentialBlob; + + /** + * Defines the persistence of this credential. This member can be read and written. + * + * See MSDN doc for all possible values + */ + public int Persist; + + /** + * The number of application-defined attributes to be associated with the credential. This member can be + * read and written. Its value cannot be greater than CRED_MAX_ATTRIBUTES (64). + */ + public int AttributeCount; + + /** + * Application-defined attributes that are associated with the credential. This member can be read + * and written. + */ + public CREDENTIAL_ATTRIBUTE.ByReference Attributes; + + /** + * Alias for the TargetName member. This member can be read and written. It cannot be longer than + * CRED_MAX_STRING_LENGTH (256) characters. + * + * If the credential Type is CRED_TYPE_GENERIC, this member can be non-NULL, but the credential manager + * ignores the member. + */ + public String TargetAlias; + + /** + * The user name of the account used to connect to TargetName. + * If the credential Type is CRED_TYPE_DOMAIN_PASSWORD, this member can be either a DomainName\UserName + * or a UPN. + * + * If the credential Type is CRED_TYPE_DOMAIN_CERTIFICATE, this member must be a marshaled certificate + * reference created by calling CredMarshalCredential with a CertCredential. + * + * If the credential Type is CRED_TYPE_GENERIC, this member can be non-NULL, but the credential manager + * ignores the member. + * + * This member cannot be longer than CRED_MAX_USERNAME_LENGTH (513) characters. + */ + public String UserName; + } + + /** + * Pointer to {@see CREDENTIAL} struct + */ + class PCREDENTIAL extends Structure { + + @Override + protected List getFieldOrder() { + return Collections.singletonList("credential"); + } + + public PCREDENTIAL() { + super(); + } + + public PCREDENTIAL(byte[] data) { + super(new Memory(data.length)); + getPointer().write(0, data, 0, data.length); + read(); + } + + public PCREDENTIAL(Pointer memory) { + super(memory); + read(); + } + + public Pointer credential; + } + + /** + * The CredRead function reads a credential from the user's credential set. + * + * The credential set used is the one associated with the logon session of the current token. + * The token must not have the user's SID disabled. + * + * @param targetName + * String that contains the name of the credential to read. + * @param type + * Type of the credential to read. Type must be one of the CRED_TYPE_* defined types. + * @param flags + * Currently reserved and must be zero. + * @param pcredential + * Out - Pointer to a single allocated block buffer to return the credential. + * Any pointers contained within the buffer are pointers to locations within this single allocated block. + * The single returned buffer must be freed by calling CredFree. + * + * @return + * True if CredRead succeeded, false otherwise + * + * @throws LastErrorException + * GetLastError + */ + boolean CredRead(String targetName, int type, int flags, PCREDENTIAL pcredential) throws LastErrorException; + + /** + * The CredFree function frees a buffer returned by any of the credentials management functions. + * + * @param credential + * Pointer to CREDENTIAL to be freed + * + * @throws LastErrorException + * GetLastError + */ + void CredFree(Pointer credential) throws LastErrorException; +} diff --git a/sdk/identity/azure-identity/src/main/java/module-info.java b/sdk/identity/azure-identity/src/main/java/module-info.java index 16a8b19c892f..bb1493631ae3 100644 --- a/sdk/identity/azure-identity/src/main/java/module-info.java +++ b/sdk/identity/azure-identity/src/main/java/module-info.java @@ -5,6 +5,7 @@ requires transitive com.azure.core; requires msal4j; + requires msal4j.persistence.extension; requires com.sun.jna; requires com.sun.jna.platform; requires nanohttpd; diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java index 183964f47dd7..b63cf51061cf 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java @@ -128,14 +128,21 @@ public void testNoCredentialWorks() throws Exception { PowerMockito.whenNew(AzureCliCredential.class).withAnyArguments() .thenReturn(azureCliCredential); + VisualStudioCodeCredential vscodeCredential = PowerMockito.mock(VisualStudioCodeCredential.class); + when(vscodeCredential.getToken(request)) + .thenReturn(Mono.error(new CredentialUnavailableException("Cannot get token from VS Code credential"))); + PowerMockito.whenNew(VisualStudioCodeCredential.class).withAnyArguments() + .thenReturn(vscodeCredential); + // test DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build(); StepVerifier.create(credential.getToken(request)) .expectErrorMatches(t -> t instanceof CredentialUnavailableException && t.getMessage() .matches("Tried EnvironmentCredential, ManagedIdentityCredential, " - + "SharedTokenCacheCredential" - + "[\\$\\w]+\\$\\d*,\\s+AzureCliCredential[\\$\\w\\s\\.]+")) + + "SharedTokenCacheCredential[\\$\\w]+\\$\\d*,\\s+" + + "AzureCliCredential[\\$\\w]+\\$\\d*,\\s+" + + "VisualStudioCodeCredential[\\$\\w]+\\$\\d* but [\\$\\w\\s\\.]+")) .verify(); } @@ -146,6 +153,7 @@ public void testExcludeCredentials() throws Exception { .excludeAzureCliCredential() .excludeManagedIdentityCredential() .excludeSharedTokenCacheCredential() + .excludeVSCodeCredential() .build(); } @@ -155,6 +163,11 @@ public void testExcludeEnvironmentCredential() throws Exception { // setup TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); + VisualStudioCodeCredential vscodeCredential = PowerMockito.mock(VisualStudioCodeCredential.class); + when(vscodeCredential.getToken(request)) + .thenReturn(Mono.error(new CredentialUnavailableException("Cannot get token from VS Code credential"))); + PowerMockito.whenNew(VisualStudioCodeCredential.class).withAnyArguments() + .thenReturn(vscodeCredential); ManagedIdentityCredential managedIdentityCredential = PowerMockito.mock(ManagedIdentityCredential.class); when(managedIdentityCredential.getToken(request)) @@ -171,7 +184,8 @@ public void testExcludeEnvironmentCredential() throws Exception { .expectErrorMatches(t -> t instanceof CredentialUnavailableException && t.getMessage() .matches("Tried ManagedIdentityCredential[\\$\\w]+\\$\\d*," + " SharedTokenCacheCredential, " - + "AzureCliCredential[\\$\\w\\s\\.]+")) + + "AzureCliCredential, " + + "VisualStudioCodeCredential[\\$\\w]+\\$\\d* but [\\$\\w\\s\\.]+")) .verify(); } @@ -181,15 +195,22 @@ public void testExclueManagedIdentityCredential() throws Exception { TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); + VisualStudioCodeCredential vscodeCredential = PowerMockito.mock(VisualStudioCodeCredential.class); + when(vscodeCredential.getToken(request)) + .thenReturn(Mono.error(new CredentialUnavailableException("Cannot get token from VS Code credential"))); + PowerMockito.whenNew(VisualStudioCodeCredential.class).withAnyArguments() + .thenReturn(vscodeCredential); + // test DefaultAzureCredential credential = new DefaultAzureCredentialBuilder() .excludeManagedIdentityCredential() .build(); StepVerifier.create(credential.getToken(request)) .expectErrorMatches(t -> t instanceof CredentialUnavailableException && t.getMessage() - .startsWith("Tried EnvironmentCredential, " + .matches("Tried EnvironmentCredential, " + "SharedTokenCacheCredential, " - + "AzureCliCredential")) + + "AzureCliCredential, " + + "VisualStudioCodeCredential[\\$\\w]+\\$\\d* but [\\$\\w\\s\\.]+")) .verify(); } @@ -205,6 +226,12 @@ public void testExcludeSharedTokenCacheCredential() throws Exception { PowerMockito.whenNew(ManagedIdentityCredential.class).withAnyArguments() .thenReturn(managedIdentityCredential); + VisualStudioCodeCredential vscodeCredential = PowerMockito.mock(VisualStudioCodeCredential.class); + when(vscodeCredential.getToken(request)) + .thenReturn(Mono.error(new CredentialUnavailableException("Cannot get token from VS Code credential"))); + PowerMockito.whenNew(VisualStudioCodeCredential.class).withAnyArguments() + .thenReturn(vscodeCredential); + // test DefaultAzureCredential credential = new DefaultAzureCredentialBuilder() @@ -213,8 +240,9 @@ public void testExcludeSharedTokenCacheCredential() throws Exception { StepVerifier.create(credential.getToken(request)) .expectErrorMatches(t -> t instanceof CredentialUnavailableException && t.getMessage() .matches("Tried EnvironmentCredential, " - + "ManagedIdentityCredential[\\$\\w]+\\$\\d*, " - + "AzureCliCredential[\\$\\w\\s\\.]+")) + + "ManagedIdentityCredential[\\$\\w]+\\$\\d*, " + + "AzureCliCredential, " + + "VisualStudioCodeCredential[\\$\\w]+\\$\\d* but [\\$\\w\\s\\.]+")) .verify(); } @@ -230,6 +258,12 @@ public void testExcludeAzureCliCredential() throws Exception { PowerMockito.whenNew(ManagedIdentityCredential.class).withAnyArguments() .thenReturn(managedIdentityCredential); + VisualStudioCodeCredential vscodeCredential = PowerMockito.mock(VisualStudioCodeCredential.class); + when(vscodeCredential.getToken(request)) + .thenReturn(Mono.error(new CredentialUnavailableException("Cannot get token from VS Code credential"))); + PowerMockito.whenNew(VisualStudioCodeCredential.class).withAnyArguments() + .thenReturn(vscodeCredential); + // test DefaultAzureCredential credential = new DefaultAzureCredentialBuilder() @@ -238,8 +272,9 @@ public void testExcludeAzureCliCredential() throws Exception { StepVerifier.create(credential.getToken(request)) .expectErrorMatches(t -> t instanceof CredentialUnavailableException && t.getMessage() .matches("Tried EnvironmentCredential, " - + "ManagedIdentityCredential[\\$\\w]+\\$\\d*, " - + "SharedTokenCacheCredential but[\\$\\w\\s\\.]+")) + + "ManagedIdentityCredential[\\$\\w]+\\$\\d*, " + + "SharedTokenCacheCredential, " + + "VisualStudioCodeCredential[\\$\\w]+\\$\\d* but [\\$\\w\\s\\.]+")) .verify(); } @@ -257,15 +292,22 @@ public void testCredentialUnavailable() throws Exception { PowerMockito.whenNew(ManagedIdentityCredential.class).withAnyArguments() .thenReturn(managedIdentityCredential); + VisualStudioCodeCredential vscodeCredential = PowerMockito.mock(VisualStudioCodeCredential.class); + when(vscodeCredential.getToken(request)) + .thenReturn(Mono.error(new CredentialUnavailableException("Cannot get token from VS Code credential"))); + PowerMockito.whenNew(VisualStudioCodeCredential.class).withAnyArguments() + .thenReturn(vscodeCredential); + // test DefaultAzureCredential credential = new DefaultAzureCredentialBuilder() .build(); StepVerifier.create(credential.getToken(request)) .expectErrorMatches(t -> t instanceof CredentialUnavailableException && t.getMessage() .matches("Tried EnvironmentCredential, " - + "ManagedIdentityCredential[\\$\\w]+\\$\\d*, " - + "SharedTokenCacheCredential, " - + "AzureCliCredential but[\\$\\w\\s\\.]+")) + + "ManagedIdentityCredential[\\$\\w]+\\$\\d*, " + + "SharedTokenCacheCredential, " + + "AzureCliCredential, " + + "VisualStudioCodeCredential[\\$\\w]+\\$\\d* but [\\$\\w\\s\\.]+")) .verify(); } }