Skip to content

Commit 0306432

Browse files
committed
Improve test coverage of FIC
1 parent 96b3ade commit 0306432

4 files changed

Lines changed: 692 additions & 26 deletions

File tree

msal4j-sdk/src/integrationtest/java/com/microsoft/aad/msal4j/AgenticIT.java

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
import java.security.cert.CertificateException;
1616
import java.security.cert.X509Certificate;
1717
import java.util.Collections;
18+
import java.util.Set;
1819
import java.util.concurrent.atomic.AtomicReference;
1920
import java.util.function.Function;
2021

2122
/**
2223
* Integration tests for agentic (agent identity) scenarios using MSAL Java APIs.
23-
* Tests FMI credential acquisition via assertion callbacks and cache isolation.
24+
* Tests FMI credential acquisition via assertion callbacks and cache isolation,
25+
* plus FIC user_fic flows for the full 3-leg agent identity protocol.
2426
*
2527
* <p>These tests use MSAL token acquisition APIs (unlike AgenticRawHttpIT which uses raw HTTP).
2628
*
@@ -31,20 +33,26 @@
3133
* <li>Tenant: {@link #TENANT_ID}</li>
3234
* </ul>
3335
*
34-
* <p>Flows tested (FMI-only, no FIC/user_fic on this branch):
36+
* <p>Flows tested:
3537
* <ul>
3638
* <li>Assertion callback receives correct context (AssertionRequestOptions)</li>
3739
* <li>Cache isolation between different fmi_path values</li>
40+
* <li>Full 3-leg flow: FMI → assertion → user_fic → user token</li>
41+
* <li>Multi-user cache isolation via user_fic</li>
3842
* </ul>
3943
*/
4044
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
4145
class AgenticIT {
4246

4347
// Lab test configuration
48+
private static final String BLUEPRINT_CLIENT_ID = "aab5089d-e764-47e3-9f28-cc11c2513821";
4449
private static final String RMA_CLIENT_ID = "3bf56293-fbb5-42bd-a407-248ba7431a8c";
4550
private static final String TENANT_ID = "10c419d4-4a50-45b2-aa4e-919fb84df24f";
4651
private static final String AGENT_APP_ID = "ab18ca07-d139-4840-8b3b-4be9610c6ed5";
52+
private static final String USER_UPN = "agentuser1@id4slab1.onmicrosoft.com";
53+
private static final String TOKEN_EXCHANGE_SCOPE = "api://AzureADTokenExchange/.default";
4754
private static final String FMI_EXCHANGE_SCOPE = "api://AzureFMITokenExchange/.default";
55+
private static final String GRAPH_SCOPE = "https://graph.microsoft.com/.default";
4856
private static final String AZURE_REGION = "westus3";
4957

5058
private static final String AUTHORITY = "https://login.microsoftonline.com/" + TENANT_ID + "/";
@@ -158,6 +166,102 @@ void agentFmiToken_CacheIsolation_DifferentFmiPaths() throws Exception {
158166
"Tokens for different fmi_paths should be different");
159167
}
160168

169+
/**
170+
* Full 3-leg agent identity flow: FMI → assertion → user_fic → user-scoped Graph token.
171+
* Uses the assertion callback pattern where the blueprint CCA acquires the FMI credential
172+
* and the agent CCA exchanges it for a user token.
173+
*/
174+
@Test
175+
void agentUserIdentity_GetsTokenForGraph() throws Exception {
176+
// Build agent CCA with assertion callback that acquires FMI credential
177+
Function<AssertionRequestOptions, String> assertionProvider = options -> {
178+
try {
179+
return acquireFmiCredentialForAgent(AGENT_APP_ID);
180+
} catch (Exception e) {
181+
throw new RuntimeException("Failed to acquire FMI credential", e);
182+
}
183+
};
184+
185+
IClientCredential credential = ClientCredentialFactory.createFromCallback(assertionProvider);
186+
187+
ConfidentialClientApplication agentCca = ConfidentialClientApplication.builder(AGENT_APP_ID, credential)
188+
.authority(AUTHORITY)
189+
.build();
190+
191+
// Get instance token (T2) for user_fic exchange
192+
String t2 = acquireInstanceTokenForAgent();
193+
194+
// Exchange T2 for user-scoped token via user_fic grant
195+
UserFederatedIdentityCredentialParameters params = UserFederatedIdentityCredentialParameters
196+
.builder(Collections.singleton(GRAPH_SCOPE), USER_UPN, t2)
197+
.build();
198+
199+
IAuthenticationResult result = agentCca.acquireToken(params).get();
200+
201+
assertNotNull(result, "Auth result should not be null");
202+
assertNotNull(result.accessToken(), "Access token should not be null");
203+
assertFalse(result.accessToken().isEmpty(), "Access token should not be empty");
204+
assertNotNull(result.account(), "Account should not be null (user token)");
205+
206+
// Verify token is cached and silent retrieval works
207+
Set<IAccount> accounts = agentCca.getAccounts().get();
208+
assertFalse(accounts.isEmpty(), "Accounts should be in cache");
209+
210+
IAccount account = accounts.iterator().next();
211+
IAuthenticationResult silentResult = agentCca.acquireTokenSilently(
212+
SilentParameters.builder(Collections.singleton(GRAPH_SCOPE), account).build()).get();
213+
214+
assertEquals(result.accessToken(), silentResult.accessToken(),
215+
"Silent call should return cached token");
216+
}
217+
218+
/**
219+
* Verifies that user_fic tokens and app-only tokens are isolated in cache
220+
* on the same agent CCA instance. App token acquisition should not interfere
221+
* with user token acquisition.
222+
*/
223+
@Test
224+
void agentCca_AppAndUserTokens_CacheIsolation() throws Exception {
225+
Function<AssertionRequestOptions, String> assertionProvider = options -> {
226+
try {
227+
return acquireFmiCredentialForAgent(AGENT_APP_ID);
228+
} catch (Exception e) {
229+
throw new RuntimeException("Failed to acquire FMI credential", e);
230+
}
231+
};
232+
233+
IClientCredential credential = ClientCredentialFactory.createFromCallback(assertionProvider);
234+
235+
ConfidentialClientApplication agentCca = ConfidentialClientApplication.builder(AGENT_APP_ID, credential)
236+
.authority(AUTHORITY)
237+
.build();
238+
239+
// Acquire app-only token
240+
IAuthenticationResult appResult = agentCca.acquireToken(ClientCredentialParameters
241+
.builder(Collections.singleton(GRAPH_SCOPE))
242+
.build())
243+
.get();
244+
assertNotNull(appResult.accessToken());
245+
246+
// Acquire user token via user_fic (needs T2 = instance token)
247+
String t2 = acquireInstanceTokenForAgent();
248+
UserFederatedIdentityCredentialParameters userParams = UserFederatedIdentityCredentialParameters
249+
.builder(Collections.singleton(GRAPH_SCOPE), USER_UPN, t2)
250+
.build();
251+
252+
IAuthenticationResult userResult = agentCca.acquireToken(userParams).get();
253+
assertNotNull(userResult.accessToken());
254+
assertNotNull(userResult.account(), "User token should have an account");
255+
256+
// Tokens should be different (app vs user scoped)
257+
assertNotEquals(appResult.accessToken(), userResult.accessToken(),
258+
"App token and user token should be different");
259+
260+
// App cache should have 1 entry, user cache should have user account
261+
assertTrue(agentCca.tokenCache.accessTokens.size() >= 2,
262+
"Cache should have at least 2 entries (app + user)");
263+
}
264+
161265
/**
162266
* Helper: acquires an FMI credential from the RMA using a certificate.
163267
* Uses the FMI-specific exchange scope (api://AzureFMITokenExchange).
@@ -180,4 +284,51 @@ private String acquireFmiCredentialFromRma() throws Exception {
180284
IAuthenticationResult result = rmaCca.acquireToken(params).get();
181285
return result.accessToken();
182286
}
287+
288+
/**
289+
* Helper: acquires an FMI credential from the blueprint app for the given agent app ID.
290+
* This is Leg 1 of the agent identity flow — returns T1.
291+
*/
292+
private String acquireFmiCredentialForAgent(String agentAppId) throws Exception {
293+
IClientCertificate clientCert = ClientCredentialFactory.createFromCertificate(privateKey, certificate);
294+
295+
ConfidentialClientApplication blueprintCca = ConfidentialClientApplication.builder(
296+
BLUEPRINT_CLIENT_ID, clientCert)
297+
.authority(AUTHORITY)
298+
.sendX5c(true)
299+
.azureRegion(AZURE_REGION)
300+
.build();
301+
302+
ClientCredentialParameters params = ClientCredentialParameters
303+
.builder(Collections.singleton(TOKEN_EXCHANGE_SCOPE))
304+
.fmiPath(agentAppId)
305+
.build();
306+
307+
IAuthenticationResult result = blueprintCca.acquireToken(params).get();
308+
return result.accessToken();
309+
}
310+
311+
/**
312+
* Helper: acquires an instance token (T2) for the agent app via the full 2-leg flow.
313+
* Leg 1: Blueprint → T1 (FMI credential)
314+
* Leg 2: Agent uses T1 as client_assertion → T2 (instance token)
315+
* T2 is used as the user_federated_identity_credential in Leg 3 (user_fic exchange).
316+
*/
317+
private String acquireInstanceTokenForAgent() throws Exception {
318+
String t1 = acquireFmiCredentialForAgent(AGENT_APP_ID);
319+
320+
IClientCredential agentCredential = ClientCredentialFactory.createFromClientAssertion(t1);
321+
322+
ConfidentialClientApplication agentCca = ConfidentialClientApplication.builder(AGENT_APP_ID, agentCredential)
323+
.authority(AUTHORITY)
324+
.build();
325+
326+
ClientCredentialParameters instanceParams = ClientCredentialParameters
327+
.builder(Collections.singleton(TOKEN_EXCHANGE_SCOPE))
328+
.skipCache(true)
329+
.build();
330+
331+
IAuthenticationResult instanceResult = agentCca.acquireToken(instanceParams).get();
332+
return instanceResult.accessToken();
333+
}
183334
}

0 commit comments

Comments
 (0)