Skip to content

Commit 3b57be8

Browse files
authored
Update Token Exchange Auth Flow (Azure#23946)
1 parent 36f2e56 commit 3b57be8

File tree

7 files changed

+98
-35
lines changed

7 files changed

+98
-35
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Release History
22

3-
## 1.4.0-beta.1 (2021-08-16)
3+
## 1.4.0-beta.1 (2021-09-13)
44
### Features Added
55

66
- Added support to `ManagedIdentityCredential` for Bridge to Kubernetes local development authentication.
@@ -11,6 +11,7 @@
1111
- A region can also be specified through the `AZURE_REGIONAL_AUTHORITY_NAME` environment variable.
1212
- Added `loginHint()` setter to `InteractiveBrowserCredentialBuilder` which allows a username to be pre-selected for interactive logins.
1313
- Added support to consume `TenantId` challenges from `TokenRequestContext`.
14+
- Added support for AKS Token Exchange support in `ManagedIdentityCredential`
1415

1516

1617
## 1.3.6 (2021-09-08)

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import com.azure.core.credential.AccessToken;
77
import com.azure.core.credential.TokenRequestContext;
8+
import com.azure.core.util.logging.ClientLogger;
89
import com.azure.identity.implementation.IdentityClient;
910

1011
import reactor.core.publisher.Mono;
@@ -13,6 +14,7 @@
1314
* Authenticates a service principal with AAD using a client assertion.
1415
*/
1516
class ClientAssertionCredential extends ManagedIdentityServiceCredential {
17+
private final ClientLogger logger = new ClientLogger(ClientAssertionCredential.class);
1618

1719
/**
1820
* Creates an instance of ClientAssertionCredential.
@@ -26,6 +28,11 @@ class ClientAssertionCredential extends ManagedIdentityServiceCredential {
2628

2729
@Override
2830
public Mono<AccessToken> authenticate(TokenRequestContext request) {
31+
if (this.getClientId() == null) {
32+
return Mono.error(logger.logExceptionAsError(new IllegalStateException("The client id is not configured via"
33+
+ " 'AZURE_CLIENT_ID' environment variable or through the credential builder."
34+
+ " Please ensure client id is provided to authenticate via token exchange in AKS environment.")));
35+
}
2936
return identityClient.authenticatewithExchangeToken(request);
3037
}
3138
}

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import com.azure.identity.implementation.util.LoggingUtil;
1515
import reactor.core.publisher.Mono;
1616

17+
import java.time.Duration;
18+
1719
/**
1820
* The base class for Managed Service Identity token based credentials.
1921
*/
@@ -24,7 +26,7 @@ public final class ManagedIdentityCredential implements TokenCredential {
2426

2527
static final String PROPERTY_IMDS_ENDPOINT = "IMDS_ENDPOINT";
2628
static final String PROPERTY_IDENTITY_SERVER_THUMBPRINT = "IDENTITY_SERVER_THUMBPRINT";
27-
static final String TOKEN_FILE_PATH = "TOKEN_FILE_PATH";
29+
static final String AZURE_FEDERATED_TOKEN_FILE = "AZURE_FEDERATED_TOKEN_FILE";
2830

2931

3032
/**
@@ -53,13 +55,15 @@ public final class ManagedIdentityCredential implements TokenCredential {
5355
} else {
5456
managedIdentityServiceCredential = new VirtualMachineMsiCredential(clientId, clientBuilder.build());
5557
}
56-
} else if (configuration.contains(Configuration.PROPERTY_AZURE_CLIENT_ID)
57-
&& configuration.contains(Configuration.PROPERTY_AZURE_TENANT_ID)
58-
&& configuration.get(TOKEN_FILE_PATH) != null) {
58+
} else if (configuration.contains(Configuration.PROPERTY_AZURE_TENANT_ID)
59+
&& configuration.get(AZURE_FEDERATED_TOKEN_FILE) != null) {
60+
String clientIdentifier = clientId == null
61+
? configuration.get(Configuration.PROPERTY_AZURE_CLIENT_ID) : clientId;
62+
clientBuilder.clientId(clientIdentifier);
5963
clientBuilder.tenantId(configuration.get(Configuration.PROPERTY_AZURE_TENANT_ID));
60-
clientBuilder.clientAssertionPath(configuration.get(TOKEN_FILE_PATH));
61-
managedIdentityServiceCredential = new ClientAssertionCredential(clientId, clientBuilder.build());
62-
64+
clientBuilder.clientAssertionPath(configuration.get(AZURE_FEDERATED_TOKEN_FILE));
65+
clientBuilder.clientAssertionTimeout(Duration.ofMinutes(5));
66+
managedIdentityServiceCredential = new ClientAssertionCredential(clientIdentifier, clientBuilder.build());
6367
} else {
6468
managedIdentityServiceCredential = new VirtualMachineMsiCredential(clientId, clientBuilder.build());
6569
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
import com.azure.identity.implementation.util.LoggingUtil;
1414
import reactor.core.publisher.Mono;
1515

16-
import java.time.Duration;
17-
1816
/**
1917
* An AAD credential that acquires a token with a client secret and user assertion for an AAD application
2018
* on behalf of a user principal.
@@ -43,7 +41,6 @@ public OnBehalfOfCredential(String clientId, String tenantId, String clientSecre
4341
.certificatePath(certificatePath)
4442
.certificatePassword(certificatePassword)
4543
.identityClientOptions(identityClientOptions)
46-
.confidentialClientCacheTimeout(Duration.ofMinutes(5))
4744
.build();
4845
}
4946

sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import java.io.File;
5959
import java.io.FileInputStream;
6060
import java.io.IOException;
61+
import java.io.DataOutputStream;
6162
import java.io.InputStream;
6263
import java.io.InputStreamReader;
6364
import java.net.HttpURLConnection;
@@ -130,6 +131,8 @@ public class IdentityClient {
130131
private HttpPipelineAdapter httpPipelineAdapter;
131132
private final SynchronizedAccessor<PublicClientApplication> publicClientApplicationAccessor;
132133
private final SynchronizedAccessor<ConfidentialClientApplication> confidentialClientApplicationAccessor;
134+
private final SynchronizedAccessor<String> clientAssertionAccessor;
135+
133136

134137
/**
135138
* Creates an IdentityClient with the given options.
@@ -142,12 +145,12 @@ public class IdentityClient {
142145
* @param certificatePassword the password protecting the PFX certificate.
143146
* @param isSharedTokenCacheCredential Indicate whether the credential is
144147
* {@link com.azure.identity.SharedTokenCacheCredential} or not.
145-
* @param confidentialClientCacheTimeout the cache time out to use for confidential client.
148+
* @param clientAssertionTimeout the time out to use for the client assertion.
146149
* @param options the options configuring the client.
147150
*/
148151
IdentityClient(String tenantId, String clientId, String clientSecret, String certificatePath,
149152
String clientAssertionFilePath, InputStream certificate, String certificatePassword,
150-
boolean isSharedTokenCacheCredential, Duration confidentialClientCacheTimeout,
153+
boolean isSharedTokenCacheCredential, Duration clientAssertionTimeout,
151154
IdentityClientOptions options) {
152155
if (tenantId == null) {
153156
tenantId = "organizations";
@@ -167,9 +170,12 @@ public class IdentityClient {
167170
this.publicClientApplicationAccessor = new SynchronizedAccessor<>(() ->
168171
getPublicClientApplication(isSharedTokenCacheCredential));
169172

170-
this.confidentialClientApplicationAccessor = confidentialClientCacheTimeout == null
171-
? new SynchronizedAccessor<>(() -> getConfidentialClientApplication())
172-
: new SynchronizedAccessor<>(() -> getConfidentialClientApplication(), confidentialClientCacheTimeout);
173+
this.confidentialClientApplicationAccessor = new SynchronizedAccessor<>(() ->
174+
getConfidentialClientApplication());
175+
176+
this.clientAssertionAccessor = clientAssertionTimeout == null
177+
? new SynchronizedAccessor<>(() -> parseClientAssertion(), Duration.ofMinutes(5))
178+
: new SynchronizedAccessor<>(() -> parseClientAssertion(), clientAssertionTimeout);
173179
}
174180

175181
private Mono<ConfidentialClientApplication> getConfidentialClientApplication() {
@@ -211,15 +217,6 @@ private Mono<ConfidentialClientApplication> getConfidentialClientApplication() {
211217
return Mono.error(logger.logExceptionAsError(new RuntimeException(
212218
"Failed to parse the certificate for the credential: " + e.getMessage(), e)));
213219
}
214-
} else if (clientAssertionFilePath != null) {
215-
try {
216-
credential = ClientCredentialFactory
217-
.createFromClientAssertion(parseClientAssertion(clientAssertionFilePath));
218-
} catch (IOException e) {
219-
return Mono.error(logger.logExceptionAsError(new RuntimeException(
220-
"Failed to parse the client assertion from the provided file: " + clientAssertionFilePath
221-
+ ". " + e.getMessage(), e)));
222-
}
223220
} else {
224221
return Mono.error(logger.logExceptionAsError(
225222
new IllegalArgumentException("Must provide client secret or client certificate path")));
@@ -271,9 +268,19 @@ private Mono<ConfidentialClientApplication> getConfidentialClientApplication() {
271268
});
272269
}
273270

274-
private String parseClientAssertion(String clientAssertionFilePath) throws IOException {
275-
byte[] encoded = Files.readAllBytes(Paths.get(clientAssertionFilePath));
276-
return new String(encoded, StandardCharsets.UTF_8);
271+
private Mono<String> parseClientAssertion() {
272+
return Mono.fromCallable(() -> {
273+
if (clientAssertionFilePath != null) {
274+
byte[] encoded = Files.readAllBytes(Paths.get(clientAssertionFilePath));
275+
return new String(encoded, StandardCharsets.UTF_8);
276+
} else {
277+
throw logger.logExceptionAsError(new IllegalStateException(
278+
"Client Assertion File Path is not provided."
279+
+ " It should be provided to authenticate with client assertion."
280+
));
281+
}
282+
283+
});
277284
}
278285

279286
private Mono<PublicClientApplication> getPublicClientApplication(boolean sharedTokenCacheCredential) {
@@ -1038,7 +1045,52 @@ public Mono<AccessToken> authenticateToArcManagedIdentityEndpoint(String identit
10381045
* @return a Publisher that emits an AccessToken
10391046
*/
10401047
public Mono<AccessToken> authenticatewithExchangeToken(TokenRequestContext request) {
1041-
return authenticateWithConfidentialClient(request);
1048+
1049+
return clientAssertionAccessor.getValue()
1050+
.flatMap(assertionToken -> Mono.fromCallable(() -> {
1051+
String authorityUrl = options.getAuthorityHost().replaceAll("/+$", "")
1052+
+ "/" + tenantId + "/oauth2/v2.0/token";
1053+
1054+
StringBuilder urlParametersBuilder = new StringBuilder();
1055+
urlParametersBuilder.append("client_assertion=");
1056+
urlParametersBuilder.append(assertionToken);
1057+
urlParametersBuilder.append("&client_assertion_type=urn:ietf:params:oauth:client-assertion-type"
1058+
+ ":jwt-bearer");
1059+
urlParametersBuilder.append("&client_id=");
1060+
urlParametersBuilder.append(clientId);
1061+
urlParametersBuilder.append("&grant_type=client_credentials");
1062+
urlParametersBuilder.append("&scope=");
1063+
urlParametersBuilder.append(URLEncoder.encode(request.getScopes().get(0), "UTF-8"));
1064+
1065+
String urlParams = urlParametersBuilder.toString();
1066+
1067+
byte[] postData = urlParams.getBytes(StandardCharsets.UTF_8);
1068+
int postDataLength = postData.length;
1069+
1070+
HttpURLConnection connection = null;
1071+
1072+
URL url = new URL(authorityUrl);
1073+
1074+
try {
1075+
connection = (HttpURLConnection) url.openConnection();
1076+
connection.setRequestMethod("POST");
1077+
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
1078+
connection.setRequestProperty("Content-Length", Integer.toString(postDataLength));
1079+
connection.setDoOutput(true);
1080+
try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
1081+
outputStream.write(postData);
1082+
}
1083+
connection.connect();
1084+
1085+
Scanner s = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A");
1086+
String result = s.hasNext() ? s.next() : "";
1087+
return SERIALIZER_ADAPTER.deserialize(result, MSIToken.class, SerializerEncoding.JSON);
1088+
} finally {
1089+
if (connection != null) {
1090+
connection.disconnect();
1091+
}
1092+
}
1093+
}));
10421094
}
10431095

10441096
/**
@@ -1054,7 +1106,6 @@ public Mono<AccessToken> authenticateToServiceFabricManagedIdentityEndpoint(Stri
10541106
String thumbprint,
10551107
TokenRequestContext request) {
10561108
return Mono.fromCallable(() -> {
1057-
10581109
HttpsURLConnection connection = null;
10591110
String endpoint = identityEndpoint;
10601111
String headerValue = identityHeader;

sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBuilder.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public final class IdentityClientBuilder {
2323
private InputStream certificate;
2424
private String certificatePassword;
2525
private boolean sharedTokenCacheCred;
26-
private Duration confidentialClientCacheTimeout;
26+
private Duration clientAssertionTimeout;
2727

2828
/**
2929
* Sets the tenant ID for the client.
@@ -123,11 +123,12 @@ public IdentityClientBuilder sharedTokenCacheCredential(boolean isSharedTokenCac
123123
/**
124124
* Configure the time out to use re-use confidential client for. Post time out, a new instance of client is created.
125125
*
126-
* @param confidentialClientCacheTimeout the time out to use for confidential client cache.
126+
* @param clientAssertionTimeout the time out to use for the client assertion configured via
127+
* {@link IdentityClientBuilder#clientAssertionPath(String)}.
127128
* @return the updated IdentityClientBuilder.
128129
*/
129-
public IdentityClientBuilder confidentialClientCacheTimeout(Duration confidentialClientCacheTimeout) {
130-
this.confidentialClientCacheTimeout = confidentialClientCacheTimeout;
130+
public IdentityClientBuilder clientAssertionTimeout(Duration clientAssertionTimeout) {
131+
this.clientAssertionTimeout = clientAssertionTimeout;
131132
return this;
132133
}
133134

@@ -136,6 +137,6 @@ public IdentityClientBuilder confidentialClientCacheTimeout(Duration confidentia
136137
*/
137138
public IdentityClient build() {
138139
return new IdentityClient(tenantId, clientId, clientSecret, certificatePath, clientAssertionPath, certificate,
139-
certificatePassword, sharedTokenCacheCred, confidentialClientCacheTimeout, identityClientOptions);
140+
certificatePassword, sharedTokenCacheCred, clientAssertionTimeout, identityClientOptions);
140141
}
141142
}

sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/MSIToken.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import com.azure.core.credential.AccessToken;
77
import com.azure.core.util.logging.ClientLogger;
8+
import com.fasterxml.jackson.annotation.JsonAlias;
89
import com.fasterxml.jackson.annotation.JsonCreator;
910
import com.fasterxml.jackson.annotation.JsonProperty;
1011

@@ -28,6 +29,7 @@ public final class MSIToken extends AccessToken {
2829
private String accessToken;
2930

3031
@JsonProperty(value = "expires_on")
32+
@JsonAlias("expires_in")
3133
private String expiresOn;
3234

3335
/**

0 commit comments

Comments
 (0)