|
7 | 7 | import com.azure.core.credential.AccessToken;
|
8 | 8 | import com.azure.core.credential.TokenCredential;
|
9 | 9 | import com.azure.core.credential.TokenRequestContext;
|
| 10 | +import com.azure.core.util.logging.ClientLogger; |
10 | 11 | import com.azure.identity.implementation.IdentityClient;
|
11 | 12 | import com.azure.identity.implementation.IdentityClientBuilder;
|
12 | 13 | import com.azure.identity.implementation.IdentityClientOptions;
|
13 |
| -import com.azure.identity.implementation.MsalToken; |
| 14 | +import com.azure.identity.implementation.MsalAuthenticationAccount; |
14 | 15 | import reactor.core.publisher.Mono;
|
15 | 16 |
|
16 | 17 | import java.util.concurrent.atomic.AtomicReference;
|
|
23 | 24 | public class DeviceCodeCredential implements TokenCredential {
|
24 | 25 | private final Consumer<DeviceCodeInfo> challengeConsumer;
|
25 | 26 | private final IdentityClient identityClient;
|
26 |
| - private final AtomicReference<MsalToken> cachedToken; |
| 27 | + private final AtomicReference<MsalAuthenticationAccount> cachedToken; |
| 28 | + private final String authorityHost; |
| 29 | + private final boolean automaticAuthentication; |
| 30 | + private final ClientLogger logger = new ClientLogger(DeviceCodeCredential.class); |
| 31 | + |
27 | 32 |
|
28 | 33 | /**
|
29 | 34 | * Creates a DeviceCodeCredential with the given identity client options.
|
30 | 35 | *
|
31 | 36 | * @param clientId the client ID of the application
|
32 | 37 | * @param tenantId the tenant ID of the application
|
33 | 38 | * @param challengeConsumer a method allowing the user to meet the device code challenge
|
| 39 | + * @param automaticAuthentication indicates whether automatic authentication should be attempted or not. |
34 | 40 | * @param identityClientOptions the options for configuring the identity client
|
35 | 41 | */
|
36 | 42 | DeviceCodeCredential(String clientId, String tenantId, Consumer<DeviceCodeInfo> challengeConsumer,
|
37 |
| - IdentityClientOptions identityClientOptions) { |
| 43 | + boolean automaticAuthentication, IdentityClientOptions identityClientOptions) { |
38 | 44 | this.challengeConsumer = challengeConsumer;
|
39 | 45 | identityClient = new IdentityClientBuilder()
|
40 | 46 | .tenantId(tenantId)
|
41 | 47 | .clientId(clientId)
|
42 | 48 | .identityClientOptions(identityClientOptions)
|
43 | 49 | .build();
|
44 | 50 | this.cachedToken = new AtomicReference<>();
|
| 51 | + this.authorityHost = identityClientOptions.getAuthorityHost(); |
| 52 | + this.automaticAuthentication = automaticAuthentication; |
| 53 | + if (identityClientOptions.getAuthenticationRecord() != null) { |
| 54 | + cachedToken.set(new MsalAuthenticationAccount(identityClientOptions.getAuthenticationRecord())); |
| 55 | + } |
45 | 56 | }
|
46 | 57 |
|
47 | 58 | @Override
|
48 | 59 | public Mono<AccessToken> getToken(TokenRequestContext request) {
|
49 | 60 | return Mono.defer(() -> {
|
50 | 61 | if (cachedToken.get() != null) {
|
51 |
| - return identityClient.authenticateWithPublicClientCache(request, cachedToken.get().getAccount()) |
| 62 | + return identityClient.authenticateWithPublicClientCache(request, cachedToken.get()) |
52 | 63 | .onErrorResume(t -> Mono.empty());
|
53 | 64 | } else {
|
54 | 65 | return Mono.empty();
|
55 | 66 | }
|
56 | 67 | }).switchIfEmpty(
|
57 |
| - Mono.defer(() -> identityClient.authenticateWithDeviceCode(request, challengeConsumer))) |
| 68 | + Mono.defer(() -> { |
| 69 | + if (!automaticAuthentication) { |
| 70 | + return Mono.error(logger.logExceptionAsError(new AuthenticationRequiredException("Interactive " |
| 71 | + + "authentication is needed to acquire token. Call Authenticate to initiate the device " |
| 72 | + + "code authentication.", request))); |
| 73 | + } |
| 74 | + return identityClient.authenticateWithDeviceCode(request, challengeConsumer); |
| 75 | + })) |
58 | 76 | .map(msalToken -> {
|
59 |
| - cachedToken.set(msalToken); |
| 77 | + cachedToken.set( |
| 78 | + new MsalAuthenticationAccount( |
| 79 | + new AuthenticationRecord(msalToken.getAuthenticationResult(), |
| 80 | + identityClient.getTenantId()))); |
60 | 81 | return msalToken;
|
61 | 82 | });
|
62 | 83 | }
|
| 84 | + |
| 85 | + /** |
| 86 | + * Authenticates a user via the device code flow. |
| 87 | + * |
| 88 | + * <p> The credential acquires a verification URL and code from the Azure Active Directory. The user must |
| 89 | + * browse to the URL, enter the code, and authenticate with Azure Active Directory. If the user authenticates |
| 90 | + * successfully, the credential receives an access token. </p> |
| 91 | + * |
| 92 | + * @param request The details of the authentication request. |
| 93 | + * |
| 94 | + * @return The {@link AuthenticationRecord} which can be used to silently authenticate the account |
| 95 | + * on future execution if persistent caching was enabled via |
| 96 | + * {@link DeviceCodeCredentialBuilder#enablePersistentCache(boolean)} when credential was instantiated. |
| 97 | + */ |
| 98 | + public Mono<AuthenticationRecord> authenticate(TokenRequestContext request) { |
| 99 | + return Mono.defer(() -> identityClient.authenticateWithDeviceCode(request, challengeConsumer)) |
| 100 | + .map(msalToken -> new AuthenticationRecord(msalToken.getAuthenticationResult(), |
| 101 | + identityClient.getTenantId())); |
| 102 | + } |
| 103 | + |
| 104 | + /** |
| 105 | + * Authenticates a user via the device code flow. |
| 106 | + * |
| 107 | + * <p> The credential acquires a verification URL and code from the Azure Active Directory. The user must |
| 108 | + * browse to the URL, enter the code, and authenticate with Azure Active Directory. If the user authenticates |
| 109 | + * successfully, the credential receives an access token. </p> |
| 110 | + * |
| 111 | + * @return The {@link AuthenticationRecord} which can be used to silently authenticate the account |
| 112 | + * on future execution if persistent caching was enabled via |
| 113 | + * {@link DeviceCodeCredentialBuilder#enablePersistentCache(boolean)} when credential was instantiated. |
| 114 | + */ |
| 115 | + public Mono<AuthenticationRecord> authenticate() { |
| 116 | + String defaultScope = KnownAuthorityHosts.getDefaultScope(authorityHost); |
| 117 | + if (defaultScope == null) { |
| 118 | + return Mono.error(logger.logExceptionAsError(new CredentialUnavailableException("Authenticating in this " |
| 119 | + + "environment requires specifying a TokenRequestContext."))); |
| 120 | + } |
| 121 | + return authenticate(new TokenRequestContext().addScopes(defaultScope)); |
| 122 | + } |
63 | 123 | }
|
0 commit comments