1515import java .security .cert .CertificateException ;
1616import java .security .cert .X509Certificate ;
1717import java .util .Collections ;
18+ import java .util .Set ;
1819import java .util .concurrent .atomic .AtomicReference ;
1920import 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+ * Corresponds to .NET's Agentic.cs — tests the MSAL-level APIs for the agent identity flow
25+ * (specifically the FMI portions that are available on this branch, plus FIC user_fic flows).
2426 *
2527 * <p>These tests use MSAL token acquisition APIs (unlike AgenticRawHttpIT which uses raw HTTP).
2628 *
2729 * <p>Test configuration:
2830 * <ul>
29- * <li>RMA app: {@link #RMA_CLIENT_ID }</li>
30- * <li>Agent app: {@link #AGENT_APP_ID }</li>
31- * <li>Tenant: {@link #TENANT_ID }</li>
31+ * <li>RMA app: see {@link TestConstants#AGENTIC_RMA_CLIENT_ID }</li>
32+ * <li>Agent app: see {@link TestConstants#AGENTIC_AGENT_APP_ID }</li>
33+ * <li>Tenant: see {@link TestConstants#AGENTIC_TENANT_ID }</li>
3234 * </ul>
3335 *
3436 * <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 )
4145class AgenticIT {
4246
43- // Lab test configuration
44- private static final String RMA_CLIENT_ID = "3bf56293-fbb5-42bd-a407-248ba7431a8c" ;
45- private static final String TENANT_ID = "10c419d4-4a50-45b2-aa4e-919fb84df24f" ;
46- private static final String AGENT_APP_ID = "ab18ca07-d139-4840-8b3b-4be9610c6ed5" ;
47- private static final String FMI_EXCHANGE_SCOPE = "api://AzureFMITokenExchange/.default" ;
48- private static final String AZURE_REGION = "westus3" ;
49-
50- private static final String AUTHORITY = "https://login.microsoftonline.com/" + TENANT_ID + "/" ;
47+ private static final String AUTHORITY = "https://login.microsoftonline.com/" + TestConstants .AGENTIC_TENANT_ID + "/" ;
5148
5249 private PrivateKey privateKey ;
5350 private X509Certificate certificate ;
@@ -66,6 +63,42 @@ void init() throws KeyStoreException, NoSuchProviderException,
6663 assertNotNull (certificate , "Lab certificate not found. Ensure the lab cert is installed." );
6764 }
6865
66+ /**
67+ * Agent gets an app-only token for Graph using an FMI-sourced client assertion.
68+ * This tests Leg 2 of the agent identity flow:
69+ * 1. Blueprint CCA acquires FMI credential (fmi_path = agentAppId)
70+ * 2. Agent CCA uses that credential as client_assertion to get Graph token
71+ *
72+ * Corresponds to .NET's AgentGetsAppTokenForGraphTest.
73+ */
74+ @ Test
75+ void agentGetsAppToken_UsingFmiAssertion () throws Exception {
76+ // The assertion callback simulates what an SDK or middleware would do:
77+ // it calls the blueprint app to get an FMI credential for the agent
78+ Function <AssertionRequestOptions , String > assertionProvider = options -> {
79+ try {
80+ return acquireFmiCredentialForAgent (TestConstants .AGENTIC_AGENT_APP_ID );
81+ } catch (Exception e ) {
82+ throw new RuntimeException ("Failed to acquire FMI credential" , e );
83+ }
84+ };
85+
86+ IClientCredential credential = ClientCredentialFactory .createFromCallback (assertionProvider );
87+
88+ ConfidentialClientApplication agentCca = ConfidentialClientApplication .builder (TestConstants .AGENTIC_AGENT_APP_ID , credential )
89+ .authority (AUTHORITY )
90+ .build ();
91+
92+ IAuthenticationResult result = agentCca .acquireToken (ClientCredentialParameters
93+ .builder (Collections .singleton (TestConstants .AGENTIC_GRAPH_SCOPE ))
94+ .build ())
95+ .get ();
96+
97+ assertNotNull (result , "Auth result should not be null" );
98+ assertNotNull (result .accessToken (), "Access token should not be null" );
99+ assertFalse (result .accessToken ().isEmpty (), "Access token should not be empty" );
100+ }
101+
69102 /**
70103 * Verifies that the context-aware assertion callback receives the correct fmiPath
71104 * when the ClientCredentialParameters include an fmiPath.
@@ -91,20 +124,20 @@ void assertionCallback_ReceivesFmiPathContext() throws Exception {
91124 ConfidentialClientApplication cca = ConfidentialClientApplication .builder (
92125 "urn:microsoft:identity:fmi" , credential )
93126 .authority (AUTHORITY )
94- .azureRegion (AZURE_REGION )
127+ .azureRegion (TestConstants . AGENTIC_AZURE_REGION )
95128 .build ();
96129
97130 ClientCredentialParameters params = ClientCredentialParameters
98- .builder (Collections .singleton (FMI_EXCHANGE_SCOPE ))
99- .fmiPath (AGENT_APP_ID )
131+ .builder (Collections .singleton (TestConstants . AGENTIC_FMI_EXCHANGE_SCOPE ))
132+ .fmiPath (TestConstants . AGENTIC_AGENT_APP_ID )
100133 .skipCache (true )
101134 .build ();
102135
103136 IAuthenticationResult result = cca .acquireToken (params ).get ();
104137
105138 // Verify assertion callback received the correct context
106139 assertNotNull (capturedOptions .get (), "AssertionRequestOptions should have been passed to callback" );
107- assertEquals (AGENT_APP_ID , capturedOptions .get ().clientAssertionFmiPath (),
140+ assertEquals (TestConstants . AGENTIC_AGENT_APP_ID , capturedOptions .get ().clientAssertionFmiPath (),
108141 "clientAssertionFmiPath in callback should match the one set in parameters" );
109142 assertEquals ("urn:microsoft:identity:fmi" , capturedOptions .get ().clientId (),
110143 "clientId in callback should match the CCA client ID" );
@@ -134,19 +167,19 @@ void agentFmiToken_CacheIsolation_DifferentFmiPaths() throws Exception {
134167 ConfidentialClientApplication cca = ConfidentialClientApplication .builder (
135168 "urn:microsoft:identity:fmi" , credential )
136169 .authority (AUTHORITY )
137- .azureRegion (AZURE_REGION )
170+ .azureRegion (TestConstants . AGENTIC_AZURE_REGION )
138171 .build ();
139172
140173 // Acquire with first fmi_path
141174 ClientCredentialParameters params1 = ClientCredentialParameters
142- .builder (Collections .singleton (FMI_EXCHANGE_SCOPE ))
143- .fmiPath (AGENT_APP_ID )
175+ .builder (Collections .singleton (TestConstants . AGENTIC_FMI_EXCHANGE_SCOPE ))
176+ .fmiPath (TestConstants . AGENTIC_AGENT_APP_ID )
144177 .build ();
145178 IAuthenticationResult result1 = cca .acquireToken (params1 ).get ();
146179
147180 // Acquire with different fmi_path
148181 ClientCredentialParameters params2 = ClientCredentialParameters
149- .builder (Collections .singleton (FMI_EXCHANGE_SCOPE ))
182+ .builder (Collections .singleton (TestConstants . AGENTIC_FMI_EXCHANGE_SCOPE ))
150183 .fmiPath ("SomeFmiPath/DifferentAgent" )
151184 .build ();
152185 IAuthenticationResult result2 = cca .acquireToken (params2 ).get ();
@@ -159,25 +192,169 @@ void agentFmiToken_CacheIsolation_DifferentFmiPaths() throws Exception {
159192 }
160193
161194 /**
162- * Helper: acquires an FMI credential from the RMA using a certificate.
163- * Uses the FMI-specific exchange scope (api://AzureFMITokenExchange).
195+ * Full 3-leg agent identity flow: FMI → assertion → user_fic → user-scoped Graph token.
196+ * Uses the assertion callback pattern where the blueprint CCA acquires the FMI credential
197+ * and the agent CCA exchanges it for a user token.
198+ */
199+ @ Test
200+ void agentUserIdentity_GetsTokenForGraph () throws Exception {
201+ // Build agent CCA with assertion callback that acquires FMI credential
202+ Function <AssertionRequestOptions , String > assertionProvider = options -> {
203+ try {
204+ return acquireFmiCredentialForAgent (TestConstants .AGENTIC_AGENT_APP_ID );
205+ } catch (Exception e ) {
206+ throw new RuntimeException ("Failed to acquire FMI credential" , e );
207+ }
208+ };
209+
210+ IClientCredential credential = ClientCredentialFactory .createFromCallback (assertionProvider );
211+
212+ ConfidentialClientApplication agentCca = ConfidentialClientApplication .builder (TestConstants .AGENTIC_AGENT_APP_ID , credential )
213+ .authority (AUTHORITY )
214+ .build ();
215+
216+ // Get instance token (T2) for user_fic exchange
217+ String t2 = acquireInstanceTokenForAgent ();
218+
219+ // Exchange T2 for user-scoped token via user_fic grant
220+ UserFederatedIdentityCredentialParameters params = UserFederatedIdentityCredentialParameters
221+ .builder (Collections .singleton (TestConstants .AGENTIC_GRAPH_SCOPE ), TestConstants .AGENTIC_USER_UPN , t2 )
222+ .build ();
223+
224+ IAuthenticationResult result = agentCca .acquireToken (params ).get ();
225+
226+ assertNotNull (result , "Auth result should not be null" );
227+ assertNotNull (result .accessToken (), "Access token should not be null" );
228+ assertFalse (result .accessToken ().isEmpty (), "Access token should not be empty" );
229+ assertNotNull (result .account (), "Account should not be null (user token)" );
230+
231+ // Verify token is cached and silent retrieval works
232+ Set <IAccount > accounts = agentCca .getAccounts ().get ();
233+ assertFalse (accounts .isEmpty (), "Accounts should be in cache" );
234+
235+ IAccount account = accounts .iterator ().next ();
236+ IAuthenticationResult silentResult = agentCca .acquireTokenSilently (
237+ SilentParameters .builder (Collections .singleton (TestConstants .AGENTIC_GRAPH_SCOPE ), account ).build ()).get ();
238+
239+ assertEquals (result .accessToken (), silentResult .accessToken (),
240+ "Silent call should return cached token" );
241+ }
242+
243+ /**
244+ * Verifies that user_fic tokens and app-only tokens are isolated in cache
245+ * on the same agent CCA instance. App token acquisition should not interfere
246+ * with user token acquisition.
247+ */
248+ @ Test
249+ void agentCca_AppAndUserTokens_CacheIsolation () throws Exception {
250+ Function <AssertionRequestOptions , String > assertionProvider = options -> {
251+ try {
252+ return acquireFmiCredentialForAgent (TestConstants .AGENTIC_AGENT_APP_ID );
253+ } catch (Exception e ) {
254+ throw new RuntimeException ("Failed to acquire FMI credential" , e );
255+ }
256+ };
257+
258+ IClientCredential credential = ClientCredentialFactory .createFromCallback (assertionProvider );
259+
260+ ConfidentialClientApplication agentCca = ConfidentialClientApplication .builder (TestConstants .AGENTIC_AGENT_APP_ID , credential )
261+ .authority (AUTHORITY )
262+ .build ();
263+
264+ // Acquire app-only token
265+ IAuthenticationResult appResult = agentCca .acquireToken (ClientCredentialParameters
266+ .builder (Collections .singleton (TestConstants .AGENTIC_GRAPH_SCOPE ))
267+ .build ())
268+ .get ();
269+ assertNotNull (appResult .accessToken ());
270+
271+ // Acquire user token via user_fic (needs T2 = instance token)
272+ String t2 = acquireInstanceTokenForAgent ();
273+ UserFederatedIdentityCredentialParameters userParams = UserFederatedIdentityCredentialParameters
274+ .builder (Collections .singleton (TestConstants .AGENTIC_GRAPH_SCOPE ), TestConstants .AGENTIC_USER_UPN , t2 )
275+ .build ();
276+
277+ IAuthenticationResult userResult = agentCca .acquireToken (userParams ).get ();
278+ assertNotNull (userResult .accessToken ());
279+ assertNotNull (userResult .account (), "User token should have an account" );
280+
281+ // Tokens should be different (app vs user scoped)
282+ assertNotEquals (appResult .accessToken (), userResult .accessToken (),
283+ "App token and user token should be different" );
284+
285+ // App cache should have 1 entry, user cache should have user account
286+ assertEquals (2 , agentCca .tokenCache .accessTokens .size (),
287+ "Cache should have exactly 2 entries (app + user)" );
288+ }
289+
290+ /**
291+ * Helper: acquires an FMI credential from the RMA (Resource Management Application).
292+ * Uses TestConstants.AGENTIC_FMI_EXCHANGE_SCOPE, matching FmiIT's Flow3 pattern.
293+ * Suitable for use as client_assertion when client_id = "urn:microsoft:identity:fmi".
164294 */
165295 private String acquireFmiCredentialFromRma () throws Exception {
166296 IClientCertificate clientCert = ClientCredentialFactory .createFromCertificate (privateKey , certificate );
167297
168298 ConfidentialClientApplication rmaCca = ConfidentialClientApplication .builder (
169- RMA_CLIENT_ID , clientCert )
299+ TestConstants . AGENTIC_RMA_CLIENT_ID , clientCert )
170300 .authority (AUTHORITY )
171301 .sendX5c (true )
172- .azureRegion (AZURE_REGION )
302+ .azureRegion (TestConstants . AGENTIC_AZURE_REGION )
173303 .build ();
174304
175305 ClientCredentialParameters params = ClientCredentialParameters
176- .builder (Collections .singleton (FMI_EXCHANGE_SCOPE ))
306+ .builder (Collections .singleton (TestConstants . AGENTIC_FMI_EXCHANGE_SCOPE ))
177307 .fmiPath ("SomeFmiPath/FmiCredentialPath" )
178308 .build ();
179309
180310 IAuthenticationResult result = rmaCca .acquireToken (params ).get ();
181311 return result .accessToken ();
182312 }
313+
314+ /**
315+ * Helper: acquires an FMI credential from the blueprint app for the given agent app ID.
316+ * This is Leg 1 of the agent identity flow — returns T1.
317+ */
318+ private String acquireFmiCredentialForAgent (String agentAppId ) throws Exception {
319+ IClientCertificate clientCert = ClientCredentialFactory .createFromCertificate (privateKey , certificate );
320+
321+ ConfidentialClientApplication blueprintCca = ConfidentialClientApplication .builder (
322+ TestConstants .AGENTIC_BLUEPRINT_CLIENT_ID , clientCert )
323+ .authority (AUTHORITY )
324+ .sendX5c (true )
325+ .azureRegion (TestConstants .AGENTIC_AZURE_REGION )
326+ .build ();
327+
328+ ClientCredentialParameters params = ClientCredentialParameters
329+ .builder (Collections .singleton (TestConstants .AGENTIC_TOKEN_EXCHANGE_SCOPE ))
330+ .fmiPath (agentAppId )
331+ .build ();
332+
333+ IAuthenticationResult result = blueprintCca .acquireToken (params ).get ();
334+ return result .accessToken ();
335+ }
336+
337+ /**
338+ * Helper: acquires an instance token (T2) for the agent app via the full 2-leg flow.
339+ * Leg 1: Blueprint → T1 (FMI credential)
340+ * Leg 2: Agent uses T1 as client_assertion → T2 (instance token)
341+ * T2 is used as the user_federated_identity_credential in Leg 3 (user_fic exchange).
342+ */
343+ private String acquireInstanceTokenForAgent () throws Exception {
344+ String t1 = acquireFmiCredentialForAgent (TestConstants .AGENTIC_AGENT_APP_ID );
345+
346+ IClientCredential agentCredential = ClientCredentialFactory .createFromClientAssertion (t1 );
347+
348+ ConfidentialClientApplication agentCca = ConfidentialClientApplication .builder (TestConstants .AGENTIC_AGENT_APP_ID , agentCredential )
349+ .authority (AUTHORITY )
350+ .build ();
351+
352+ ClientCredentialParameters instanceParams = ClientCredentialParameters
353+ .builder (Collections .singleton (TestConstants .AGENTIC_TOKEN_EXCHANGE_SCOPE ))
354+ .skipCache (true )
355+ .build ();
356+
357+ IAuthenticationResult instanceResult = agentCca .acquireToken (instanceParams ).get ();
358+ return instanceResult .accessToken ();
359+ }
183360}
0 commit comments