Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit edaa5f8

Browse files
authored
Add User Authentication API to Identity Java. (Azure#11450)
1 parent 95e2c61 commit edaa5f8

15 files changed

+517
-32
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.identity;
5+
6+
import com.fasterxml.jackson.annotation.JsonProperty;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import com.microsoft.aad.msal4j.IAuthenticationResult;
9+
import reactor.core.publisher.Mono;
10+
11+
import java.io.IOException;
12+
import java.io.InputStream;
13+
import java.io.OutputStream;
14+
15+
/**
16+
* Represents the account information relating to an authentication request
17+
*/
18+
public class AuthenticationRecord {
19+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
20+
21+
@JsonProperty("authority")
22+
private String authority;
23+
24+
@JsonProperty("homeAccountId")
25+
private String homeAccountId;
26+
27+
@JsonProperty("tenantId")
28+
private String tenantId;
29+
30+
@JsonProperty("username")
31+
private String username;
32+
33+
34+
AuthenticationRecord() { }
35+
36+
AuthenticationRecord(IAuthenticationResult authenticationResult, String tenantId) {
37+
authority = authenticationResult.account().environment();
38+
homeAccountId = authenticationResult.account().homeAccountId();
39+
username = authenticationResult.account().username();
40+
this.tenantId = tenantId;
41+
}
42+
43+
/**
44+
* Get the authority host used to authenticate the account.
45+
*
46+
* @return the authority host.
47+
*/
48+
public String getAuthority() {
49+
return authority;
50+
}
51+
52+
/**
53+
* Get the unique identifier of the account.
54+
*
55+
* @return the account id.
56+
*/
57+
public String getHomeAccountId() {
58+
return homeAccountId;
59+
}
60+
61+
/**
62+
* Get the tenant, which the account authenticated in.
63+
*
64+
* @return the tenant id.
65+
*/
66+
public String getTenantId() {
67+
return tenantId;
68+
}
69+
70+
/**
71+
* Get the user principal name of the account.
72+
*
73+
* @return the username.
74+
*/
75+
public String getUsername() {
76+
return username;
77+
}
78+
79+
/**
80+
* Serializes the {@link AuthenticationRecord} to the specified {@link OutputStream}
81+
*
82+
* @param outputStream The {@link OutputStream} to which the serialized record will be written to.
83+
* @return A {@link Mono} containing {@link Void}
84+
*/
85+
public Mono<Void> serialize(OutputStream outputStream) {
86+
return Mono.defer(() -> {
87+
try {
88+
OBJECT_MAPPER.writeValue(outputStream, this);
89+
} catch (IOException e) {
90+
return Mono.error(e);
91+
}
92+
return Mono.empty();
93+
});
94+
}
95+
96+
/**
97+
* Deserializes the {@link AuthenticationRecord} from the specified {@link InputStream}
98+
*
99+
* @param inputStream The {@link InputStream} from which the serialized record will be read.
100+
* @return A {@link Mono} containing the {@link AuthenticationRecord} object.
101+
*/
102+
public static Mono<AuthenticationRecord> deserialize(InputStream inputStream) {
103+
return Mono.defer(() -> {
104+
AuthenticationRecord authenticationRecord;
105+
try {
106+
authenticationRecord =
107+
OBJECT_MAPPER.readValue(inputStream, AuthenticationRecord.class);
108+
} catch (IOException e) {
109+
return Mono.error(e);
110+
}
111+
return Mono.just(authenticationRecord);
112+
});
113+
}
114+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.identity;
5+
6+
import com.azure.core.credential.TokenRequestContext;
7+
8+
/**
9+
* The exception thrown to indicate that interactive authentication is required.
10+
*/
11+
public class AuthenticationRequiredException extends CredentialUnavailableException {
12+
13+
private final transient TokenRequestContext request;
14+
15+
/**
16+
* Initializes a new instance of the {@link AuthenticationRequiredException} class.
17+
*
18+
* @param message The exception message.
19+
* @param request The details of the authentication request.
20+
*/
21+
public AuthenticationRequiredException(String message, TokenRequestContext request) {
22+
super(message);
23+
this.request = request;
24+
}
25+
26+
/**
27+
* Initializes a new instance of the {@link AuthenticationRequiredException} class.
28+
*
29+
* @param message The exception message.
30+
* @param request The details of the authentication request.
31+
* @param cause The {@link Throwable} which caused the creation of this exception.
32+
*/
33+
public AuthenticationRequiredException(String message, TokenRequestContext request, Throwable cause) {
34+
super(message, cause);
35+
this.request = request;
36+
}
37+
38+
/**
39+
* Get the details of the authentication request which resulted in the authentication failure.
40+
*
41+
* @return the token request context.
42+
*/
43+
public TokenRequestContext getTokenRequestContext() {
44+
return request;
45+
}
46+
}

sdk/identity/azure-identity/src/main/java/com/azure/identity/AuthorizationCodeCredential.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import com.azure.identity.implementation.IdentityClient;
1111
import com.azure.identity.implementation.IdentityClientBuilder;
1212
import com.azure.identity.implementation.IdentityClientOptions;
13-
import com.azure.identity.implementation.MsalToken;
13+
import com.azure.identity.implementation.MsalAuthenticationAccount;
1414
import reactor.core.publisher.Mono;
1515

1616
import java.net.URI;
@@ -25,7 +25,7 @@ public class AuthorizationCodeCredential implements TokenCredential {
2525
private final String authCode;
2626
private final URI redirectUri;
2727
private final IdentityClient identityClient;
28-
private final AtomicReference<MsalToken> cachedToken;
28+
private final AtomicReference<MsalAuthenticationAccount> cachedToken;
2929

3030
/**
3131
* Creates an AuthorizationCodeCredential with the given identity client options.
@@ -52,16 +52,18 @@ public class AuthorizationCodeCredential implements TokenCredential {
5252
public Mono<AccessToken> getToken(TokenRequestContext request) {
5353
return Mono.defer(() -> {
5454
if (cachedToken.get() != null) {
55-
return identityClient.authenticateWithPublicClientCache(request, cachedToken.get().getAccount())
55+
return identityClient.authenticateWithPublicClientCache(request, cachedToken.get())
5656
.onErrorResume(t -> Mono.empty());
5757
} else {
5858
return Mono.empty();
5959
}
6060
}).switchIfEmpty(
6161
Mono.defer(() -> identityClient.authenticateWithAuthorizationCode(request, authCode, redirectUri)))
62-
.map(msalToken -> {
63-
cachedToken.set(msalToken);
64-
return msalToken;
65-
});
62+
.map(msalToken -> {
63+
cachedToken.set(new MsalAuthenticationAccount(
64+
new AuthenticationRecord(msalToken.getAuthenticationResult(),
65+
identityClient.getTenantId())));
66+
return msalToken;
67+
});
6668
}
6769
}

sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredential.java

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
import com.azure.core.credential.AccessToken;
88
import com.azure.core.credential.TokenCredential;
99
import com.azure.core.credential.TokenRequestContext;
10+
import com.azure.core.util.logging.ClientLogger;
1011
import com.azure.identity.implementation.IdentityClient;
1112
import com.azure.identity.implementation.IdentityClientBuilder;
1213
import com.azure.identity.implementation.IdentityClientOptions;
13-
import com.azure.identity.implementation.MsalToken;
14+
import com.azure.identity.implementation.MsalAuthenticationAccount;
1415
import reactor.core.publisher.Mono;
1516

1617
import java.util.concurrent.atomic.AtomicReference;
@@ -23,41 +24,100 @@
2324
public class DeviceCodeCredential implements TokenCredential {
2425
private final Consumer<DeviceCodeInfo> challengeConsumer;
2526
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+
2732

2833
/**
2934
* Creates a DeviceCodeCredential with the given identity client options.
3035
*
3136
* @param clientId the client ID of the application
3237
* @param tenantId the tenant ID of the application
3338
* @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.
3440
* @param identityClientOptions the options for configuring the identity client
3541
*/
3642
DeviceCodeCredential(String clientId, String tenantId, Consumer<DeviceCodeInfo> challengeConsumer,
37-
IdentityClientOptions identityClientOptions) {
43+
boolean automaticAuthentication, IdentityClientOptions identityClientOptions) {
3844
this.challengeConsumer = challengeConsumer;
3945
identityClient = new IdentityClientBuilder()
4046
.tenantId(tenantId)
4147
.clientId(clientId)
4248
.identityClientOptions(identityClientOptions)
4349
.build();
4450
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+
}
4556
}
4657

4758
@Override
4859
public Mono<AccessToken> getToken(TokenRequestContext request) {
4960
return Mono.defer(() -> {
5061
if (cachedToken.get() != null) {
51-
return identityClient.authenticateWithPublicClientCache(request, cachedToken.get().getAccount())
62+
return identityClient.authenticateWithPublicClientCache(request, cachedToken.get())
5263
.onErrorResume(t -> Mono.empty());
5364
} else {
5465
return Mono.empty();
5566
}
5667
}).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+
}))
5876
.map(msalToken -> {
59-
cachedToken.set(msalToken);
77+
cachedToken.set(
78+
new MsalAuthenticationAccount(
79+
new AuthenticationRecord(msalToken.getAuthenticationResult(),
80+
identityClient.getTenantId())));
6081
return msalToken;
6182
});
6283
}
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+
}
63123
}

sdk/identity/azure-identity/src/main/java/com/azure/identity/DeviceCodeCredentialBuilder.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
package com.azure.identity;
55

6+
import com.azure.core.credential.TokenRequestContext;
67
import com.azure.identity.implementation.util.ValidationUtil;
78

89
import java.util.HashMap;
@@ -15,10 +16,10 @@
1516
*/
1617
public class DeviceCodeCredentialBuilder extends AadCredentialBuilderBase<DeviceCodeCredentialBuilder> {
1718
private Consumer<DeviceCodeInfo> challengeConsumer;
19+
private boolean automaticAuthentication = true;
1820

1921
/**
20-
* Sets the port for the local HTTP server, for which {@code http://localhost:{port}} must be
21-
* registered as a valid reply URL on the application.
22+
* Sets the consumer to meet the device code challenge.
2223
*
2324
* @param challengeConsumer A method allowing the user to meet the device code challenge.
2425
* @return the InteractiveBrowserCredentialBuilder itself
@@ -54,6 +55,33 @@ public DeviceCodeCredentialBuilder enablePersistentCache(boolean enabled) {
5455
return this;
5556
}
5657

58+
/**
59+
* Sets the {@link AuthenticationRecord} captured from a previous authentication.
60+
*
61+
* @param authenticationRecord the authentication record to ser.
62+
*
63+
* @return An updated instance of this builder with if the shared token cache enabled specified.
64+
*/
65+
public DeviceCodeCredentialBuilder authenticationRecord(AuthenticationRecord authenticationRecord) {
66+
this.identityClientOptions.setAuthenticationRecord(authenticationRecord);
67+
return this;
68+
}
69+
70+
/**
71+
* Disables the automatic authentication and prevents the {@link DeviceCodeCredential} from automatically
72+
* prompting the user. If automatic authentication is disabled a {@link AuthenticationRequiredException}
73+
* will be thrown from {@link DeviceCodeCredential#getToken(TokenRequestContext)} in the case that
74+
* user interaction is necessary. The application is responsible for handling this exception, and
75+
* calling {@link DeviceCodeCredential#authenticate()} or
76+
* {@link DeviceCodeCredential#authenticate(TokenRequestContext)} to authenticate the user interactively.
77+
*
78+
* @return An updated instance of this builder with automatic authentication disabled.
79+
*/
80+
public DeviceCodeCredentialBuilder disableAutomaticAuthentication() {
81+
this.automaticAuthentication = false;
82+
return this;
83+
}
84+
5785
/**
5886
* Creates a new {@link DeviceCodeCredential} with the current configurations.
5987
*
@@ -64,6 +92,7 @@ public DeviceCodeCredential build() {
6492
put("clientId", clientId);
6593
put("challengeConsumer", challengeConsumer);
6694
}});
67-
return new DeviceCodeCredential(clientId, tenantId, challengeConsumer, identityClientOptions);
95+
return new DeviceCodeCredential(clientId, tenantId, challengeConsumer, automaticAuthentication,
96+
identityClientOptions);
6897
}
6998
}

0 commit comments

Comments
 (0)