Skip to content

Commit a5e070a

Browse files
authored
Sagonzal/update client credentials (#377)
* Update obo flow to attempt cache lookup by default
1 parent 287365c commit a5e070a

14 files changed

+345
-77
lines changed

src/integrationtest/java/com.microsoft.aad.msal4j/OnBehalfOfIT.java

Lines changed: 103 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,36 @@
1111

1212
@Test
1313
public class OnBehalfOfIT {
14-
private String accessToken;
1514

1615
private Config cfg;
1716

18-
private void setUp() throws Exception{
19-
LabUserProvider labUserProvider = LabUserProvider.getInstance();
20-
User user = labUserProvider.getDefaultUser(cfg.azureEnvironment);
17+
@Test(dataProvider = "environments", dataProviderClass = EnvironmentsProvider.class)
18+
public void acquireTokenWithOBO_Managed(String environment) throws Exception {
19+
cfg = new Config(environment);
20+
String accessToken = this.getAccessToken();
2121

22-
String clientId = cfg.appProvider.getAppId();
23-
String apiReadScope = cfg.appProvider.getOboAppIdURI() + "/user_impersonation";
22+
final String clientId = cfg.appProvider.getOboAppId();
23+
final String password = cfg.appProvider.getOboAppPassword();
2424

25-
PublicClientApplication pca = PublicClientApplication.builder(
26-
clientId).
27-
authority(cfg.tenantSpecificAuthority()).
28-
build();
25+
ConfidentialClientApplication cca =
26+
ConfidentialClientApplication.builder(clientId, ClientCredentialFactory.createFromSecret(password)).
27+
authority(cfg.tenantSpecificAuthority()).
28+
build();
2929

30-
IAuthenticationResult result = pca.acquireToken(
31-
UserNamePasswordParameters.builder(Collections.singleton(apiReadScope),
32-
user.getUpn(),
33-
user.getPassword().toCharArray()).build()).get();
30+
IAuthenticationResult result =
31+
cca.acquireToken(OnBehalfOfParameters.builder(
32+
Collections.singleton(cfg.graphDefaultScope()),
33+
new UserAssertion(accessToken)).build()).
34+
get();
3435

35-
accessToken = result.accessToken();
36+
Assert.assertNotNull(result);
37+
Assert.assertNotNull(result.accessToken());
3638
}
3739

3840
@Test(dataProvider = "environments", dataProviderClass = EnvironmentsProvider.class)
39-
public void acquireTokenWithOBO_Managed(String environment) throws Exception {
41+
public void acquireTokenWithOBO_testCache(String environment) throws Exception {
4042
cfg = new Config(environment);
41-
42-
setUp();
43+
String accessToken = this.getAccessToken();
4344

4445
final String clientId = cfg.appProvider.getOboAppId();
4546
final String password = cfg.appProvider.getOboAppPassword();
@@ -49,14 +50,94 @@ public void acquireTokenWithOBO_Managed(String environment) throws Exception {
4950
authority(cfg.tenantSpecificAuthority()).
5051
build();
5152

52-
IAuthenticationResult result =
53+
IAuthenticationResult result1 =
54+
cca.acquireToken(OnBehalfOfParameters.builder(
55+
Collections.singleton(TestConstants.USER_READ_SCOPE),
56+
new UserAssertion(accessToken)).build()).
57+
get();
58+
59+
Assert.assertNotNull(result1);
60+
Assert.assertNotNull(result1.accessToken());
61+
62+
// Same scope and userAssertion, should return cached tokens
63+
IAuthenticationResult result2 =
64+
cca.acquireToken(OnBehalfOfParameters.builder(
65+
Collections.singleton(TestConstants.USER_READ_SCOPE),
66+
new UserAssertion(accessToken)).build()).
67+
get();
68+
69+
Assert.assertEquals(result1.accessToken(), result2.accessToken());
70+
71+
// Scope 2, should return new token
72+
IAuthenticationResult result3 =
5373
cca.acquireToken(OnBehalfOfParameters.builder(
5474
Collections.singleton(cfg.graphDefaultScope()),
5575
new UserAssertion(accessToken)).build()).
5676
get();
5777

58-
Assert.assertNotNull(result);
59-
Assert.assertNotNull(result.accessToken());
60-
Assert.assertNotNull(result.idToken());
78+
Assert.assertNotNull(result3);
79+
Assert.assertNotNull(result3.accessToken());
80+
Assert.assertNotEquals(result2.accessToken(), result3.accessToken());
81+
82+
// Scope 2, should return cached token
83+
IAuthenticationResult result4 =
84+
cca.acquireToken(OnBehalfOfParameters.builder(
85+
Collections.singleton(cfg.graphDefaultScope()),
86+
new UserAssertion(accessToken)).build()).
87+
get();
88+
89+
Assert.assertEquals(result3.accessToken(), result4.accessToken());
90+
91+
// skipCache=true, should return new token
92+
IAuthenticationResult result5 =
93+
cca.acquireToken(
94+
OnBehalfOfParameters.builder(
95+
Collections.singleton(cfg.graphDefaultScope()),
96+
new UserAssertion(accessToken))
97+
.skipCache(true)
98+
.build()).
99+
get();
100+
101+
Assert.assertNotNull(result5);
102+
Assert.assertNotNull(result5.accessToken());
103+
Assert.assertNotEquals(result5.accessToken(), result4.accessToken());
104+
Assert.assertNotEquals(result5.accessToken(), result2.accessToken());
105+
106+
107+
String newAccessToken = this.getAccessToken();
108+
// New new UserAssertion, should return new token
109+
IAuthenticationResult result6 =
110+
cca.acquireToken(
111+
OnBehalfOfParameters.builder(
112+
Collections.singleton(cfg.graphDefaultScope()),
113+
new UserAssertion(newAccessToken))
114+
.build()).
115+
get();
116+
117+
Assert.assertNotNull(result6);
118+
Assert.assertNotNull(result6.accessToken());
119+
Assert.assertNotEquals(result6.accessToken(), result5.accessToken());
120+
Assert.assertNotEquals(result6.accessToken(), result4.accessToken());
121+
Assert.assertNotEquals(result6.accessToken(), result2.accessToken());
122+
}
123+
124+
private String getAccessToken() throws Exception {
125+
126+
LabUserProvider labUserProvider = LabUserProvider.getInstance();
127+
User user = labUserProvider.getDefaultUser(cfg.azureEnvironment);
128+
129+
String clientId = cfg.appProvider.getAppId();
130+
String apiReadScope = cfg.appProvider.getOboAppIdURI() + "/user_impersonation";
131+
PublicClientApplication pca = PublicClientApplication.builder(
132+
clientId).
133+
authority(cfg.tenantSpecificAuthority()).
134+
build();
135+
136+
IAuthenticationResult result = pca.acquireToken(
137+
UserNamePasswordParameters.builder(Collections.singleton(apiReadScope),
138+
user.getUpn(),
139+
user.getPassword().toCharArray()).build()).get();
140+
141+
return result.accessToken();
61142
}
62143
}

src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ public CompletableFuture<IAuthenticationResult> acquireTokenSilently(SilentParam
148148
SilentRequest silentRequest = new SilentRequest(
149149
parameters,
150150
this,
151-
createRequestContext(PublicApi.ACQUIRE_TOKEN_SILENTLY, parameters));
151+
createRequestContext(PublicApi.ACQUIRE_TOKEN_SILENTLY, parameters),
152+
null);
152153

153154
return executeRequest(silentRequest);
154155
}
@@ -250,10 +251,14 @@ private AuthenticationResultSupplier getAuthenticationResultSupplier(MsalRequest
250251
supplier = new AcquireTokenByInteractiveFlowSupplier(
251252
(PublicClientApplication) this,
252253
(InteractiveRequest) msalRequest);
253-
} else if(msalRequest instanceof ClientCredentialRequest){
254+
} else if(msalRequest instanceof ClientCredentialRequest) {
254255
supplier = new AcquireTokenByClientCredentialSupplier(
255256
(ConfidentialClientApplication) this,
256257
(ClientCredentialRequest) msalRequest);
258+
} else if(msalRequest instanceof OnBehalfOfRequest){
259+
supplier = new AcquireTokenByOnBehalfOfSupplier(
260+
(ConfidentialClientApplication) this,
261+
(OnBehalfOfRequest) msalRequest);
257262
} else {
258263
supplier = new AcquireTokenByAuthorizationGrantSupplier(
259264
this,

src/main/java/com/microsoft/aad/msal4j/AccountCacheEntity.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ class AccountCacheEntity implements Serializable {
4242
@JsonProperty("client_info")
4343
protected String clientInfoStr;
4444

45+
@JsonProperty("user_assertion_hash")
46+
protected String userAssertionHash;
47+
4548
ClientInfo clientInfo() {
4649
return ClientInfo.createFromJson(clientInfoStr);
4750
}

src/main/java/com/microsoft/aad/msal4j/AcquireTokenByClientCredentialSupplier.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import org.slf4j.Logger;
77
import org.slf4j.LoggerFactory;
88

9-
public class AcquireTokenByClientCredentialSupplier extends AuthenticationResultSupplier {
9+
class AcquireTokenByClientCredentialSupplier extends AuthenticationResultSupplier {
1010

1111
private final static Logger LOG = LoggerFactory.getLogger(AcquireTokenByClientCredentialSupplier.class);
1212
private ClientCredentialRequest clientCredentialRequest;
@@ -31,7 +31,8 @@ AuthenticationResult execute() throws Exception {
3131
SilentRequest silentRequest = new SilentRequest(
3232
parameters,
3333
this.clientApplication,
34-
this.clientApplication.createRequestContext(PublicApi.ACQUIRE_TOKEN_SILENTLY, parameters));
34+
this.clientApplication.createRequestContext(PublicApi.ACQUIRE_TOKEN_SILENTLY, parameters),
35+
null);
3536

3637
AcquireTokenSilentSupplier supplier = new AcquireTokenSilentSupplier(
3738
this.clientApplication,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
class AcquireTokenByOnBehalfOfSupplier extends AuthenticationResultSupplier {
10+
11+
private final static Logger LOG = LoggerFactory.getLogger(AcquireTokenByOnBehalfOfSupplier.class);
12+
private OnBehalfOfRequest onBehalfOfRequest;
13+
14+
AcquireTokenByOnBehalfOfSupplier(ConfidentialClientApplication clientApplication,
15+
OnBehalfOfRequest onBehalfOfRequest) {
16+
super(clientApplication, onBehalfOfRequest);
17+
this.onBehalfOfRequest = onBehalfOfRequest;
18+
}
19+
20+
@Override
21+
AuthenticationResult execute() throws Exception {
22+
if (onBehalfOfRequest.parameters.skipCache() != null &&
23+
!onBehalfOfRequest.parameters.skipCache()) {
24+
LOG.info("SkipCache set to false. Attempting cache lookup");
25+
try {
26+
SilentParameters parameters = SilentParameters
27+
.builder(this.onBehalfOfRequest.parameters.scopes())
28+
.claims(this.onBehalfOfRequest.parameters.claims())
29+
.build();
30+
31+
SilentRequest silentRequest = new SilentRequest(
32+
parameters,
33+
this.clientApplication,
34+
this.clientApplication.createRequestContext(PublicApi.ACQUIRE_TOKEN_SILENTLY, parameters),
35+
onBehalfOfRequest.parameters.userAssertion());
36+
37+
AcquireTokenSilentSupplier supplier = new AcquireTokenSilentSupplier(
38+
this.clientApplication,
39+
silentRequest);
40+
41+
return supplier.execute();
42+
} catch (MsalClientException ex) {
43+
LOG.debug(String.format("Cache lookup failed: %s", ex.getMessage()));
44+
return acquireTokenOnBehalfOf();
45+
}
46+
}
47+
48+
LOG.info("SkipCache set to true. Skipping cache lookup and attempting on-behalf-of request");
49+
return acquireTokenOnBehalfOf();
50+
}
51+
52+
private AuthenticationResult acquireTokenOnBehalfOf() throws Exception {
53+
AcquireTokenByAuthorizationGrantSupplier supplier = new AcquireTokenByAuthorizationGrantSupplier(
54+
this.clientApplication,
55+
onBehalfOfRequest,
56+
null);
57+
58+
return supplier.execute();
59+
}
60+
}

src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ AuthenticationResult execute() throws Exception {
2929
res = clientApplication.tokenCache.getCachedAuthenticationResult(
3030
requestAuthority,
3131
silentRequest.parameters().scopes(),
32-
clientApplication.clientId());
32+
clientApplication.clientId(),
33+
silentRequest.assertion());
3334
} else {
3435
res = clientApplication.tokenCache.getCachedAuthenticationResult(
3536
silentRequest.parameters().account(),

src/main/java/com/microsoft/aad/msal4j/Credential.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ class Credential {
2424

2525
@JsonProperty("secret")
2626
protected String secret;
27+
28+
@JsonProperty("user_assertion_hash")
29+
protected String userAssertionHash;
2730
}

src/main/java/com/microsoft/aad/msal4j/IConfidentialClientApplication.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* For details see https://aka.ms/msal4jclientapplications
1313
*/
1414
public interface IConfidentialClientApplication extends IClientApplicationBase {
15+
1516
/**
1617
* @return a boolean value which determines whether x5c claim (public key of the certificate)
1718
* will be sent to the STS.
@@ -30,8 +31,18 @@ public interface IConfidentialClientApplication extends IClientApplicationBase {
3031
/**
3132
* Acquires an access token for this application (usually a Web API) from the authority configured
3233
* in the application, in order to access another downstream protected Web API on behalf of a user
33-
* using the On-Behalf-Of flow. This confidential client application was itself called with a token
34-
* which will be provided in the {@link UserAssertion} to the {@link OnBehalfOfParameters}
34+
* using the On-Behalf-Of flow. It will by default attempt to get tokens from the token cache.
35+
* This confidential client application was itself called with an acces token which is provided in
36+
* the {@link UserAssertion} field of {@link OnBehalfOfParameters}.
37+
*
38+
* When serializing/deserializing the in-memory token cache to permanent storage, there should be
39+
* a token cache per incoming access token, where the hash of the incoming access token can be used
40+
* as the token cache key. Access tokens are usually only valid for a 1 hour period of time,
41+
* and a new access token in the {@link UserAssertion} means there will be a new token cache and
42+
* new token cache key. To avoid your permanent storage from being filled with expired
43+
* token caches, an eviction policy should be set. For example, a token cache that
44+
* is more than a couple of hours old can be deemed expired and therefore evicted from the
45+
* serialized token cache.
3546
* @param parameters instance of {@link OnBehalfOfParameters}
3647
* @return {@link CompletableFuture} containing an {@link IAuthenticationResult}
3748
*/

src/main/java/com/microsoft/aad/msal4j/IUserAssertion.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,9 @@ public interface IUserAssertion {
1414
* @return string value
1515
*/
1616
String getAssertion();
17+
18+
/**
19+
* @return Base64 encoded SHA256 hash of the assertion
20+
*/
21+
String getAssertionHash();
1722
}

src/main/java/com/microsoft/aad/msal4j/OnBehalfOfParameters.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ public class OnBehalfOfParameters implements IApiParameters {
3030
*/
3131
private ClaimsRequest claims;
3232

33+
/**
34+
* Indicates whether the request should skip looking into the token cache. Be default it is
35+
* set to false.
36+
*/
37+
@Builder.Default
38+
private Boolean skipCache = false;
39+
3340
@NonNull
3441
private IUserAssertion userAssertion;
3542

src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818
@Getter
1919
class OnBehalfOfRequest extends MsalRequest {
2020

21+
OnBehalfOfParameters parameters;
22+
2123
OnBehalfOfRequest(OnBehalfOfParameters parameters,
2224
ConfidentialClientApplication application,
2325
RequestContext requestContext) {
2426
super(application, createAuthenticationGrant(parameters), requestContext);
27+
this.parameters = parameters;
2528
}
2629

2730
private static OAuthAuthorizationGrant createAuthenticationGrant(OnBehalfOfParameters parameters) {

src/main/java/com/microsoft/aad/msal4j/SilentRequest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@
1414
class SilentRequest extends MsalRequest {
1515

1616
private SilentParameters parameters;
17-
17+
private IUserAssertion assertion;
1818
private Authority requestAuthority;
1919

2020
SilentRequest(SilentParameters parameters,
2121
AbstractClientApplicationBase application,
22-
RequestContext requestContext) throws MalformedURLException {
22+
RequestContext requestContext,
23+
IUserAssertion assertion) throws MalformedURLException {
2324

2425
super(application, null, requestContext);
2526

2627
this.parameters = parameters;
28+
this.assertion = assertion;
2729
this.requestAuthority = StringHelper.isBlank(parameters.authorityUrl()) ?
2830
application.authenticationAuthority :
2931
Authority.createAuthority(new URL(parameters.authorityUrl()));

0 commit comments

Comments
 (0)