EnvironmentCredential.java

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.identity;

import com.azure.core.annotation.Immutable;
import com.azure.core.credential.AccessToken;
import com.azure.core.credential.TokenCredential;
import com.azure.core.credential.TokenRequestContext;
import com.azure.core.util.Configuration;
import com.azure.core.util.logging.ClientLogger;
import com.azure.identity.implementation.IdentityClientOptions;
import com.azure.identity.implementation.util.LoggingUtil;
import com.azure.identity.implementation.util.ValidationUtil;
import reactor.core.publisher.Mono;

/**
 * A credential provider that provides token credentials based on environment variables.  The environment variables
 * expected are:
 * <ul>
 *     <li>{@link Configuration#PROPERTY_AZURE_CLIENT_ID AZURE_CLIENT_ID}</li>
 *     <li>{@link Configuration#PROPERTY_AZURE_CLIENT_SECRET AZURE_CLIENT_SECRET}</li>
 *     <li>{@link Configuration#PROPERTY_AZURE_TENANT_ID AZURE_TENANT_ID}</li>
 * </ul>
 * or:
 * <ul>
 *     <li>{@link Configuration#PROPERTY_AZURE_CLIENT_ID AZURE_CLIENT_ID}</li>
 *     <li>{@link Configuration#PROPERTY_AZURE_CLIENT_CERTIFICATE_PATH AZURE_CLIENT_CERTIFICATE_PATH}</li>
 *     <li>{@link Configuration#PROPERTY_AZURE_TENANT_ID AZURE_TENANT_ID}</li>
 * </ul>
 * or:
 * <ul>
 *     <li>{@link Configuration#PROPERTY_AZURE_CLIENT_ID AZURE_CLIENT_ID}</li>
 *     <li>{@link Configuration#PROPERTY_AZURE_USERNAME AZURE_USERNAME}</li>
 *     <li>{@link Configuration#PROPERTY_AZURE_PASSWORD AZURE_PASSWORD}</li>
 * </ul>
 */
@Immutable
public class EnvironmentCredential implements TokenCredential {
    private final Configuration configuration;
    private final ClientLogger logger = new ClientLogger(EnvironmentCredential.class);
    private final TokenCredential tokenCredential;

    /**
     * Creates an instance of the default environment credential provider.
     *
     * @param identityClientOptions the options for configuring the identity client
     */
    EnvironmentCredential(IdentityClientOptions identityClientOptions) {
        this.configuration = identityClientOptions.getConfiguration() == null
            ? Configuration.getGlobalConfiguration().clone() : identityClientOptions.getConfiguration();
        TokenCredential targetCredential = null;

        String clientId = configuration.get(Configuration.PROPERTY_AZURE_CLIENT_ID);
        String tenantId = configuration.get(Configuration.PROPERTY_AZURE_TENANT_ID);
        String clientSecret = configuration.get(Configuration.PROPERTY_AZURE_CLIENT_SECRET);
        String certPath = configuration.get(Configuration.PROPERTY_AZURE_CLIENT_CERTIFICATE_PATH);
        String username = configuration.get(Configuration.PROPERTY_AZURE_USERNAME);
        String password = configuration.get(Configuration.PROPERTY_AZURE_PASSWORD);
        ValidationUtil.validateTenantIdCharacterRange(getClass().getSimpleName(), tenantId);
        LoggingUtil.logAvailableEnvironmentVariables(logger, configuration);
        if (verifyNotNull(clientId)) {
            // 1 - Attempt ClientSecretCredential or ClientCertificateCredential
            if (verifyNotNull(tenantId)) {
                if (verifyNotNull(clientSecret)) {
                    // 1.1 Attempt ClientSecretCredential
                    logger.info("Azure Identity => EnvironmentCredential invoking ClientSecretCredential");
                    targetCredential = new ClientSecretCredential(tenantId, clientId, clientSecret,
                        identityClientOptions);
                } else if (verifyNotNull(certPath)) {
                    // 1.2 Attempt ClientCertificateCredential
                    logger.info("Azure Identity => EnvironmentCredential invoking ClientCertificateCredential");
                    targetCredential = new ClientCertificateCredential(tenantId, clientId, certPath, null, null,
                            identityClientOptions);
                } else {
                    // 1.3 Log error if neither is found
                    logger.error("Azure Identity => ERROR in EnvironmentCredential: Failed to create a "
                        + "ClientSecretCredential or ClientCertificateCredential. Missing required environment "
                        + "variable either {} or {}", Configuration.PROPERTY_AZURE_CLIENT_SECRET,
                        Configuration.PROPERTY_AZURE_CLIENT_CERTIFICATE_PATH);
                }
            } else if (verifyNotNull(clientSecret) || verifyNotNull(certPath)) {
                // 1.4 Log error if secret / cert is found but tenant is missing
                logger.error("Azure Identity => ERROR in EnvironmentCredential: Failed to create a "
                        + "ClientSecretCredential or ClientCertificateCredential. Missing required environment "
                        + "variable {}", Configuration.PROPERTY_AZURE_TENANT_ID);
            }

            // 2 - Attempt UsernamePasswordCredential (tenant not required)
            if (targetCredential == null && verifyNotNull(username, password)) {
                // 2.1 - both username and password found
                logger.info("Azure Identity => EnvironmentCredential invoking UsernamePasswordCredential");
                targetCredential = new UsernamePasswordCredential(clientId, tenantId, username, password,
                    identityClientOptions);
            } else if (verifyNotNull(username) ^ verifyNotNull(password)) {
                // 2.2 - only one is found, likely missing the other
                logger.error("Azure Identity => ERROR in EnvironmentCredential: Failed to create a "
                    + "UsernamePasswordCredential. Missing required environment variable {}",
                    username == null ? Configuration.PROPERTY_AZURE_USERNAME : Configuration.PROPERTY_AZURE_PASSWORD);
            }

            // 3 - cannot determine scenario based on clientId alone
            if (targetCredential == null) {
                String msg = String.format("Azure Identity => ERROR in EnvironmentCredential: Failed to determine an "
                    + "authentication scheme based on the available environment variables. Please specify %1$s and "
                    + "%2$s to authenticate through a ClientSecretCredential; %1$s and %3$s to authenticate through a "
                    + "ClientCertificateCredential; or %4$s and %5$s to authenticate through a "
                    + "UserPasswordCredential.", Configuration.PROPERTY_AZURE_TENANT_ID,
                    Configuration.PROPERTY_AZURE_CLIENT_SECRET, Configuration.PROPERTY_AZURE_CLIENT_CERTIFICATE_PATH,
                    Configuration.PROPERTY_AZURE_USERNAME, Configuration.PROPERTY_AZURE_PASSWORD);
                logger.error(msg);
            }
        } else {
            // 4 - not even clientId is available
            logger.error("Azure Identity => ERROR in EnvironmentCredential: Missing required environment variable {}",
                Configuration.PROPERTY_AZURE_CLIENT_ID);
        }
        tokenCredential = targetCredential;
    }

    @Override
    public Mono<AccessToken> getToken(TokenRequestContext request) {
        if (tokenCredential == null) {
            return Mono.error(logger.logExceptionAsError(new CredentialUnavailableException(
                    "EnvironmentCredential authentication unavailable."
                        + " Environment variables are not fully configured."
                        + "To mitigate this issue, please refer to the troubleshooting guidelines here at"
                        + " https://aka.ms/azsdk/net/identity/environmentcredential/troubleshoot")));
        } else {
            return tokenCredential.getToken(request);
        }
    }

    private boolean verifyNotNull(String... configs) {
        for (String config: configs) {
            if (config == null) {
                return false;
            }
        }
        return true;
    }
}