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 ListCredFree
.
+ *
+ * @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();
}
}