VisualStudioCacheAccessor.java
// 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.AzureAuthorityHosts;
import com.azure.identity.CredentialUnavailableException;
import com.fasterxml.jackson.core.json.JsonReadFeature;
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.io.IOException;
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 static final ObjectMapper MAPPER = new ObjectMapper()
.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true)
.configure(JsonReadFeature.ALLOW_JAVA_COMMENTS.mappedFeature(), true)
.configure(JsonReadFeature.ALLOW_TRAILING_COMMA.mappedFeature(), true);
private JsonNode getUserSettings() {
JsonNode output;
String homeDir = System.getProperty("user.home");
String settingsPath;
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));
}
output = readJsonFile(settingsPath);
} catch (Exception e) {
return null;
}
return output;
}
static JsonNode readJsonFile(String path) throws IOException {
return MAPPER.readTree(new File(path));
}
/**
* Get the user configured settings of Visual Studio code.
*
* @return a Map containing Vs Code user settings
*/
public Map<String, String> getUserSettingsDetails() {
JsonNode userSettings = getUserSettings();
Map<String, String> details = new HashMap<>();
String tenant = null;
String cloud = "AzureCloud";
if (userSettings != null && !userSettings.isNull()) {
if (userSettings.has("azure.tenant")) {
tenant = userSettings.get("azure.tenant").asText();
}
if (userSettings.has("azure.cloud")) {
cloud = userSettings.get("azure.cloud").asText();
}
}
if (!CoreUtils.isNullOrEmpty(tenant)) {
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 {
credential = new WindowsCredentialAccessor(serviceName, accountName).read();
} catch (Exception | Error 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 (Exception | Error 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 (Exception | Error 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 "AzureCloud":
return AzureAuthorityHosts.AZURE_PUBLIC_CLOUD;
case "AzureChina":
return AzureAuthorityHosts.AZURE_CHINA;
case "AzureGermanCloud":
return AzureAuthorityHosts.AZURE_GERMANY;
case "AzureUSGovernment":
return AzureAuthorityHosts.AZURE_GOVERNMENT;
default:
return AzureAuthorityHosts.AZURE_PUBLIC_CLOUD;
}
}
}