5858import java .io .File ;
5959import java .io .FileInputStream ;
6060import java .io .IOException ;
61+ import java .io .DataOutputStream ;
6162import java .io .InputStream ;
6263import java .io .InputStreamReader ;
6364import 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 ;
0 commit comments