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+ * 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 *
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 )
4145class 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