Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
import com.azure.core.util.Configuration;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;
import com.azure.identity.implementation.IdentityClient;
import com.azure.identity.implementation.IdentityClientBuilder;
import com.azure.identity.implementation.IdentityClientOptions;
import com.azure.identity.implementation.IdentitySyncClient;
import com.azure.identity.implementation.util.LoggingUtil;
import com.azure.identity.implementation.util.ValidationUtil;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

import static com.azure.identity.ManagedIdentityCredential.AZURE_FEDERATED_TOKEN_FILE;

/**
Expand Down Expand Up @@ -54,9 +56,9 @@
*/
public class WorkloadIdentityCredential implements TokenCredential {
private static final ClientLogger LOGGER = new ClientLogger(WorkloadIdentityCredential.class);
private final IdentityClient identityClient;
private final IdentitySyncClient identitySyncClient;
private final ClientAssertionCredential clientAssertionCredential;
private final IdentityClientOptions identityClientOptions;
private final String clientId;

/**
* WorkloadIdentityCredential supports Azure workload identity on Kubernetes.
Expand All @@ -70,6 +72,10 @@ public class WorkloadIdentityCredential implements TokenCredential {
IdentityClientOptions identityClientOptions) {
ValidationUtil.validateTenantIdCharacterRange(tenantId, LOGGER);

if (identityClientOptions == null) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("identityClientOptions cannot be null"));
}

Configuration configuration = identityClientOptions.getConfiguration() == null
? Configuration.getGlobalConfiguration().clone()
: identityClientOptions.getConfiguration();
Expand All @@ -88,44 +94,79 @@ public class WorkloadIdentityCredential implements TokenCredential {
|| CoreUtils.isNullOrEmpty(federatedTokenFilePathInput)
|| CoreUtils.isNullOrEmpty(clientIdInput)
|| CoreUtils.isNullOrEmpty(identityClientOptions.getAuthorityHost()))) {
IdentityClientBuilder builder = new IdentityClientBuilder().clientAssertionPath(federatedTokenFilePathInput)

ClientAssertionCredentialBuilder builder = new ClientAssertionCredentialBuilder().tenantId(tenantIdInput)
.clientId(clientIdInput)
.tenantId(tenantIdInput)
.identityClientOptions(identityClientOptions);
identityClient = builder.build();
identitySyncClient = builder.buildSyncClient();
.clientAssertion(() -> readFederatedTokenFromFile(federatedTokenFilePathInput));

if (identityClientOptions.getAuthorityHost() != null) {
builder.authorityHost(identityClientOptions.getAuthorityHost());
}
builder.maxRetry(identityClientOptions.getMaxRetry());

if (identityClientOptions.getHttpClient() != null) {
builder.httpClient(identityClientOptions.getHttpClient());
}
if (identityClientOptions.getRetryTimeout() != null) {
builder.retryTimeout(identityClientOptions.getRetryTimeout());
}

if (identityClientOptions.getAdditionallyAllowedTenants() != null
&& !identityClientOptions.getAdditionallyAllowedTenants().isEmpty()) {
builder.additionallyAllowedTenants(
identityClientOptions.getAdditionallyAllowedTenants().toArray(new String[0]));
}

clientAssertionCredential = builder.build();
this.clientId = clientIdInput;
} else {
identityClient = null;
identitySyncClient = null;
clientAssertionCredential = null;
this.clientId = null;
}
this.identityClientOptions = identityClientOptions;
}

@Override
public Mono<AccessToken> getToken(TokenRequestContext request) {
if (identityClient == null) {
if (clientAssertionCredential == null) {
return Mono.error(LoggingUtil.logCredentialUnavailableException(LOGGER, identityClientOptions,
new CredentialUnavailableException("WorkloadIdentityCredential"
+ " authentication unavailable. The workload options are not fully configured. See the troubleshooting"
+ " guide for more information."
+ " https://aka.ms/azsdk/java/identity/workloadidentitycredential/troubleshoot")));
}
return identityClient.authenticateWithWorkloadIdentityConfidentialClient(request);
return clientAssertionCredential.getToken(request);
}

@Override
public AccessToken getTokenSync(TokenRequestContext request) {
if (identitySyncClient == null) {
if (clientAssertionCredential == null) {
throw LoggingUtil.logCredentialUnavailableException(LOGGER, identityClientOptions,
new CredentialUnavailableException("WorkloadIdentityCredential"
+ " authentication unavailable. The workload options are not fully configured. See the troubleshooting"
+ " guide for more information."
+ " https://aka.ms/azsdk/java/identity/workloadidentitycredential/troubleshoot"));
}
return identitySyncClient.authenticateWithWorkloadIdentityConfidentialClient(request);
return clientAssertionCredential.getTokenSync(request);
}

String getClientId() {
return this.identityClient.getClientId();
return this.clientId;
}

/**
* Reads the federated token from the specified file path.
* This token will be used as a client assertion for authentication.
*/
private String readFederatedTokenFromFile(String filePath) {
if (filePath == null) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Federated token file path cannot be null"));
}
try {
byte[] bytes = Files.readAllBytes(Paths.get(filePath));
return new String(bytes, StandardCharsets.UTF_8).trim();
} catch (IOException e) {
throw LOGGER.logExceptionAsError(new RuntimeException("Failed to read federated token from file. ", e));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,25 @@
import com.azure.core.credential.TokenRequestContext;
import com.azure.core.test.utils.TestConfigurationSource;
import com.azure.core.util.Configuration;
import com.azure.identity.implementation.IdentityClient;
import com.azure.identity.implementation.IdentitySyncClient;
import com.azure.identity.util.TestUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.MockedConstruction;

import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.when;

Expand All @@ -29,65 +34,71 @@ public class WorkloadIdentityCredentialTest {
private static final String CLIENT_ID = UUID.randomUUID().toString();

@Test
public void testWorkloadIdentityFlow() {
public void testWorkloadIdentityFlow(@TempDir Path tempDir) throws IOException {
// setup
String endpoint = "https://localhost";
String token1 = "token1";
TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com");
TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com/.default");
OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1);
Configuration configuration = TestUtils.createTestConfiguration(
new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint));

// Create a temporary token file
Path tokenFile = tempDir.resolve("token.txt");
Files.write(tokenFile, "dummy-token".getBytes());

// mock
try (MockedConstruction<IdentityClient> identityClientMock
= mockConstruction(IdentityClient.class, (identityClient, context) -> {
when(identityClient.authenticateWithWorkloadIdentityConfidentialClient(request1))
try (MockedConstruction<ClientAssertionCredential> clientAssertionMock
= mockConstruction(ClientAssertionCredential.class, (clientAssertion, context) -> {
when(clientAssertion.getToken(any(TokenRequestContext.class)))
.thenReturn(TestUtils.getMockAccessToken(token1, expiresAt));
})) {
// test
WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId("dummy-tenantid")
.clientId("dummy-clientid")
.tokenFilePath("dummy-path")
.configuration(configuration)
.clientId(CLIENT_ID)
.tokenFilePath(tokenFile.toString())
.configuration(configuration)
.build();
StepVerifier.create(credential.getToken(request1))
.expectNextMatches(token -> token1.equals(token.getToken())
&& expiresAt.getSecond() == token.getExpiresAt().getSecond())
.verifyComplete();
assertNotNull(identityClientMock);
assertNotNull(clientAssertionMock);
}
}

@Test
public void testWorkloadIdentityFlowSync() {
public void testWorkloadIdentityFlowSync(@TempDir Path tempDir) throws IOException {
// setup
String endpoint = "https://localhost";
String token1 = "token1";
TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com");
TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com/.default");
OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1);
Configuration configuration = TestUtils.createTestConfiguration(
new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint));

// Create a temporary token file
Path tokenFile = tempDir.resolve("token.txt");
Files.write(tokenFile, "dummy-token".getBytes());

// mock
try (MockedConstruction<IdentitySyncClient> identityClientMock
= mockConstruction(IdentitySyncClient.class, (identityClient, context) -> {
when(identityClient.authenticateWithWorkloadIdentityConfidentialClient(request1))
try (MockedConstruction<ClientAssertionCredential> clientAssertionMock
= mockConstruction(ClientAssertionCredential.class, (clientAssertion, context) -> {
when(clientAssertion.getTokenSync(any(TokenRequestContext.class)))
.thenReturn(TestUtils.getMockAccessTokenSync(token1, expiresAt));
})) {
// test
WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId("dummy-tenantid")
.clientId("dummy-clientid")
.tokenFilePath("dummy-path")
.configuration(configuration)
.clientId(CLIENT_ID)
.tokenFilePath(tokenFile.toString())
.configuration(configuration)
.build();

AccessToken token = credential.getTokenSync(request1);

assertTrue(token1.equals(token.getToken()));
assertTrue(expiresAt.getSecond() == token.getExpiresAt().getSecond());
assertNotNull(identityClientMock);
assertNotNull(clientAssertionMock);
}
}

Expand Down Expand Up @@ -135,4 +146,44 @@ public void testWorkloadIdentityFlowFailureNoTokenPath() {
.clientId("client-id")
.build());
}

@Test
public void testGetClientId(@TempDir Path tempDir) throws IOException {
// setup
String endpoint = "https://localhost";
Configuration configuration = TestUtils.createTestConfiguration(
new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint));

// test
WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId("dummy-tenantid")
.clientId(CLIENT_ID)
.tokenFilePath("dummy-path")
.configuration(configuration)
.build();

Assertions.assertEquals(CLIENT_ID, credential.getClientId());
}

@Test
public void testFileReadingError(@TempDir Path tempDir) {
// setup
String endpoint = "https://localhost";
Configuration configuration = TestUtils.createTestConfiguration(
new TestConfigurationSource().put(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, endpoint));
TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default");

String nonExistentFile = tempDir.resolve("non-existent-file.txt").toString();

WorkloadIdentityCredential credential = new WorkloadIdentityCredentialBuilder().tenantId("dummy-tenantid")
.clientId(CLIENT_ID)
.tokenFilePath(nonExistentFile)
.configuration(configuration)
.build();

StepVerifier.create(credential.getToken(request)).expectErrorSatisfies(error -> {
assertTrue(error instanceof RuntimeException);
assertTrue(error.getMessage().contains("Failed to read federated token from file"));
assertTrue(error.getCause() instanceof IOException); // Original IOException from Files.readAllBytes
}).verify();
}
}
Loading