com.azure.identity.ChainedTokenCredential Maven / Gradle / Ivy
Show all versions of azure-identity Show documentation
// 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.exception.ClientAuthenticationException;
import com.azure.core.util.logging.ClientLogger;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* ChainedTokenCredential allows you to chain together a set of TokenCredential instances. Each credential in the chain is attempted
* sequentially. The token from the first credential that successfully authenticates is returned. For more information, see
* ChainedTokenCredential overview.
*
* Sample: Construct a ChainedTokenCredential.
*
* The following code sample demonstrates the creation of a {@link ChainedTokenCredential},
* using the {@link ChainedTokenCredentialBuilder} to configure it. The sample below
* tries silent username+password login tried first, then interactive browser login as needed
* (e.g. when 2FA is turned on in the directory). Once this credential is created, it may be passed into the builder
* of many of the Azure SDK for Java client builders as the 'credential' parameter.
*
*
*
* TokenCredential usernamePasswordCredential = new UsernamePasswordCredentialBuilder().clientId(clientId)
* .username(fakeUsernamePlaceholder)
* .password(fakePasswordPlaceholder)
* .build();
* TokenCredential interactiveBrowserCredential = new InteractiveBrowserCredentialBuilder().clientId(clientId)
* .port(8765)
* .build();
* TokenCredential credential = new ChainedTokenCredentialBuilder().addLast(usernamePasswordCredential)
* .addLast(interactiveBrowserCredential)
* .build();
*
*
*
* @see com.azure.identity
* @see ChainedTokenCredentialBuilder
*/
@Immutable
public class ChainedTokenCredential implements TokenCredential {
private static final ClientLogger LOGGER = new ClientLogger(ChainedTokenCredential.class);
private final List credentials;
private final String unavailableError = this.getClass().getSimpleName() + " authentication failed. ---> ";
private final AtomicReference selectedCredential;
private boolean useCachedWorkingCredential = false;
/**
* Create an instance of chained token credential that aggregates a list of token
* credentials.
*/
ChainedTokenCredential(List credentials) {
this.credentials = Collections.unmodifiableList(credentials);
selectedCredential = new AtomicReference<>();
}
/**
* Sequentially calls {@link TokenCredential#getToken(TokenRequestContext)} on all the specified credentials,
* returning the first successfully obtained {@link AccessToken}.
*
* This method is called automatically by Azure SDK client libraries.
* You may call this method directly, but you must also handle token
* caching and token refreshing.
*
* @param request the details of the token request
* @return a Publisher that emits a single access token
*/
@Override
public Mono getToken(TokenRequestContext request) {
List exceptions = new ArrayList<>(4);
Mono accessTokenMono;
if (selectedCredential.get() != null && useCachedWorkingCredential) {
accessTokenMono = Mono.defer(() -> selectedCredential.get()
.getToken(request)
.doOnNext(t -> logTokenMessage("Azure Identity => Returning token from cached credential {}",
selectedCredential.get()))
.onErrorResume(Exception.class, handleExceptionAsync(exceptions, selectedCredential.get(),
"Azure Identity => Cached credential {} is unavailable.")));
} else {
accessTokenMono = Flux.fromIterable(credentials).flatMap(p -> p.getToken(request).doOnNext(t -> {
logTokenMessage("Azure Identity => Attempted credential {} returns a token", p);
selectedCredential.set(p);
})
.onErrorResume(Exception.class,
handleExceptionAsync(exceptions, p, "Azure Identity => Attempted credential {} is unavailable.")),
1).next();
}
return accessTokenMono.switchIfEmpty(Mono.defer(() -> {
// Chain Exceptions.
CredentialUnavailableException last = exceptions.get(exceptions.size() - 1);
for (int z = exceptions.size() - 2; z >= 0; z--) {
CredentialUnavailableException current = exceptions.get(z);
last = new CredentialUnavailableException(current.getMessage() + "\r\n" + last.getMessage()
+ (z == 0
? "To mitigate this issue, please refer to the troubleshooting guidelines here at "
+ "https://aka.ms/azure-identity-java-default-azure-credential-troubleshoot"
: ""));
}
return Mono.error(last);
}));
}
private Function>
handleExceptionAsync(List exceptions, TokenCredential p, String logMessage) {
return t -> {
if (!t.getClass().getSimpleName().equals("CredentialUnavailableException")) {
return Mono.error(new ClientAuthenticationException(getCredUnavailableMessage(p, t), null, t));
}
exceptions.add((CredentialUnavailableException) t);
logTokenMessage(logMessage, p);
return Mono.empty();
};
}
@Override
public AccessToken getTokenSync(TokenRequestContext request) {
List exceptions = new ArrayList<>(4);
if (selectedCredential.get() != null && useCachedWorkingCredential) {
try {
AccessToken accessToken = selectedCredential.get().getTokenSync(request);
logTokenMessage("Azure Identity => Returning token from cached credential {}",
selectedCredential.get());
return accessToken;
} catch (Exception e) {
handleExceptionSync(e, selectedCredential.get(), exceptions,
"Azure Identity => Cached credential {} is unavailable.", selectedCredential.get());
}
} else {
for (TokenCredential credential : credentials) {
try {
AccessToken accessToken = credential.getTokenSync(request);
logTokenMessage("Azure Identity => Attempted credential {} returns a token", credential);
selectedCredential.set(credential);
return accessToken;
} catch (Exception e) {
handleExceptionSync(e, credential, exceptions,
"Azure Identity => Attempted credential {} is unavailable.", credential);
}
}
}
CredentialUnavailableException last = exceptions.get(exceptions.size() - 1);
for (int z = exceptions.size() - 2; z >= 0; z--) {
CredentialUnavailableException current = exceptions.get(z);
last = new CredentialUnavailableException(current.getMessage() + "\r\n" + last.getMessage()
+ (z == 0
? "To mitigate this issue, please refer to the troubleshooting guidelines here at "
+ "https://aka.ms/azure-identity-java-default-azure-credential-troubleshoot"
: ""));
}
throw last;
}
private void logTokenMessage(String format, TokenCredential selectedCredential) {
LOGGER.info(format, selectedCredential.getClass().getSimpleName());
}
private String getCredUnavailableMessage(TokenCredential p, Exception t) {
return unavailableError + p.getClass().getSimpleName() + " authentication failed. Error Details: "
+ t.getMessage();
}
private void handleExceptionSync(Exception e, TokenCredential selectedCredential,
List exceptions, String logMessage, TokenCredential selectedCredential1) {
if (e.getClass() != CredentialUnavailableException.class) {
throw new ClientAuthenticationException(getCredUnavailableMessage(selectedCredential, e), null, e);
} else {
if (e instanceof CredentialUnavailableException) {
exceptions.add((CredentialUnavailableException) e);
}
}
logTokenMessage(logMessage, selectedCredential1);
}
WorkloadIdentityCredential getWorkloadIdentityCredentialIfPresent() {
List tokenCredentials = this.credentials.stream()
.filter(tokenCredential -> tokenCredential instanceof WorkloadIdentityCredential)
.collect(Collectors.toList());
if (tokenCredentials.size() == 1) {
return (WorkloadIdentityCredential) tokenCredentials.get(0);
} else {
return null;
}
}
void enableUseCachedWorkingCredential() {
this.useCachedWorkingCredential = true;
}
}