Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,203 @@ void agentCca_AppAndUserTokens_CacheIsolation() throws Exception {
"Cache should have exactly 2 entries (app + user)");
}

// ========================================================================
// High-level AcquireTokenForAgent tests (composite API)
// ========================================================================

/**
* Tests the high-level acquireTokenForAgent API with a UPN-based AgentIdentity.
* Exercises the full 3-leg flow orchestrated internally by AcquireTokenForAgentSupplier.
*/
@Test
void acquireTokenForAgent_withUpn_fullFlow() throws Exception {
IClientCertificate clientCert = ClientCredentialFactory.createFromCertificate(privateKey, certificate);

ConfidentialClientApplication blueprintCca = ConfidentialClientApplication.builder(
TestConstants.AGENTIC_BLUEPRINT_CLIENT_ID, clientCert)
.authority(AUTHORITY)
.sendX5c(true)
.azureRegion(TestConstants.AGENTIC_AZURE_REGION)
.build();

AgentIdentity agentId = AgentIdentity.withUsername(TestConstants.AGENTIC_AGENT_APP_ID, TestConstants.AGENTIC_USER_UPN);

IAuthenticationResult result = blueprintCca.acquireTokenForAgent(
AcquireTokenForAgentParameters.builder(
Collections.singleton(TestConstants.AGENTIC_GRAPH_SCOPE), agentId).build()
).get();

assertNotNull(result, "Result should not be null");
assertNotNull(result.accessToken(), "Access token should not be null");
assertFalse(result.accessToken().isEmpty(), "Access token should not be empty");
assertNotNull(result.account(), "Account should not be null for user token");
}

/**
* Tests the high-level acquireTokenForAgent API for app-only (no user) scenarios.
* Only Legs 1-2 are performed.
*/
@Test
void acquireTokenForAgent_appOnly() throws Exception {
IClientCertificate clientCert = ClientCredentialFactory.createFromCertificate(privateKey, certificate);

ConfidentialClientApplication blueprintCca = ConfidentialClientApplication.builder(
TestConstants.AGENTIC_BLUEPRINT_CLIENT_ID, clientCert)
.authority(AUTHORITY)
.sendX5c(true)
.azureRegion(TestConstants.AGENTIC_AZURE_REGION)
.build();

AgentIdentity agentId = AgentIdentity.appOnly(TestConstants.AGENTIC_AGENT_APP_ID);

IAuthenticationResult result = blueprintCca.acquireTokenForAgent(
AcquireTokenForAgentParameters.builder(
Collections.singleton(TestConstants.AGENTIC_GRAPH_SCOPE), agentId).build()
).get();

assertNotNull(result, "Result should not be null");
assertNotNull(result.accessToken(), "Access token should not be null");
assertFalse(result.accessToken().isEmpty(), "Access token should not be empty");
}

/**
* Tests the high-level acquireTokenForAgent API with ForceRefresh.
* First call populates cache, second call (forceRefresh) bypasses it.
*/
@Test
void acquireTokenForAgent_forceRefresh() throws Exception {
IClientCertificate clientCert = ClientCredentialFactory.createFromCertificate(privateKey, certificate);

ConfidentialClientApplication blueprintCca = ConfidentialClientApplication.builder(
TestConstants.AGENTIC_BLUEPRINT_CLIENT_ID, clientCert)
.authority(AUTHORITY)
.sendX5c(true)
.azureRegion(TestConstants.AGENTIC_AZURE_REGION)
.build();

AgentIdentity agentId = AgentIdentity.withUsername(TestConstants.AGENTIC_AGENT_APP_ID, TestConstants.AGENTIC_USER_UPN);

// First call — populates cache
IAuthenticationResult result1 = blueprintCca.acquireTokenForAgent(
AcquireTokenForAgentParameters.builder(
Collections.singleton(TestConstants.AGENTIC_GRAPH_SCOPE), agentId).build()
).get();
assertNotNull(result1.accessToken());

// Second call without forceRefresh — should return cached token
IAuthenticationResult result2 = blueprintCca.acquireTokenForAgent(
AcquireTokenForAgentParameters.builder(
Collections.singleton(TestConstants.AGENTIC_GRAPH_SCOPE), agentId).build()
).get();
assertEquals(result1.accessToken(), result2.accessToken(),
"Second call should return cached token");

// Third call with forceRefresh — should get a fresh token
IAuthenticationResult result3 = blueprintCca.acquireTokenForAgent(
AcquireTokenForAgentParameters.builder(
Collections.singleton(TestConstants.AGENTIC_GRAPH_SCOPE), agentId)
.forceRefresh(true).build()
).get();
assertNotNull(result3.accessToken());
// The fresh token may be the same string (if not expired) but the flow exercised network
}

/**
* Tests cache isolation between two blueprint CCA instances.
* Each blueprint should have its own agent CCA cache.
*/
@Test
void acquireTokenForAgent_cacheIsolation_twoBlueprintCcas() throws Exception {
IClientCertificate clientCert = ClientCredentialFactory.createFromCertificate(privateKey, certificate);

ConfidentialClientApplication blueprint1 = ConfidentialClientApplication.builder(
TestConstants.AGENTIC_BLUEPRINT_CLIENT_ID, clientCert)
.authority(AUTHORITY)
.sendX5c(true)
.azureRegion(TestConstants.AGENTIC_AZURE_REGION)
.build();

ConfidentialClientApplication blueprint2 = ConfidentialClientApplication.builder(
TestConstants.AGENTIC_BLUEPRINT_CLIENT_ID, clientCert)
.authority(AUTHORITY)
.sendX5c(true)
.azureRegion(TestConstants.AGENTIC_AZURE_REGION)
.build();

AgentIdentity agentId = AgentIdentity.withUsername(TestConstants.AGENTIC_AGENT_APP_ID, TestConstants.AGENTIC_USER_UPN);

// Acquire via blueprint1
IAuthenticationResult result1 = blueprint1.acquireTokenForAgent(
AcquireTokenForAgentParameters.builder(
Collections.singleton(TestConstants.AGENTIC_GRAPH_SCOPE), agentId).build()
).get();
assertNotNull(result1.accessToken());

// Blueprint1 should have agent CCA cached, blueprint2 should not
assertEquals(1, blueprint1.agentCcaCache.size(),
"Blueprint1 should have one cached agent CCA");
assertTrue(blueprint2.agentCcaCache.isEmpty(),
"Blueprint2 should have no cached agent CCAs (no bleed)");

// Acquire via blueprint2
IAuthenticationResult result2 = blueprint2.acquireTokenForAgent(
AcquireTokenForAgentParameters.builder(
Collections.singleton(TestConstants.AGENTIC_GRAPH_SCOPE), agentId).build()
).get();
assertNotNull(result2.accessToken());

// Both should now have their own cache entries
assertEquals(1, blueprint1.agentCcaCache.size());
assertEquals(1, blueprint2.agentCcaCache.size());
}

/**
* Tests that a UPN-based token can be found by OID lookup on the same blueprint.
* Discovers the OID via the UPN flow, then verifies OID-based call returns cached token.
*/
@Test
void acquireTokenForAgent_upnThenOid_sharesCache() throws Exception {
IClientCertificate clientCert = ClientCredentialFactory.createFromCertificate(privateKey, certificate);

ConfidentialClientApplication blueprintCca = ConfidentialClientApplication.builder(
TestConstants.AGENTIC_BLUEPRINT_CLIENT_ID, clientCert)
.authority(AUTHORITY)
.sendX5c(true)
.azureRegion(TestConstants.AGENTIC_AZURE_REGION)
.build();

// Step 1: Acquire via UPN
AgentIdentity upnIdentity = AgentIdentity.withUsername(TestConstants.AGENTIC_AGENT_APP_ID, TestConstants.AGENTIC_USER_UPN);
IAuthenticationResult upnResult = blueprintCca.acquireTokenForAgent(
AcquireTokenForAgentParameters.builder(
Collections.singleton(TestConstants.AGENTIC_GRAPH_SCOPE), upnIdentity).build()
).get();
assertNotNull(upnResult.account(), "Account should not be null");

// Extract OID from account's homeAccountId (format: oid.tid)
String homeAccountId = upnResult.account().homeAccountId();
assertNotNull(homeAccountId);
String oidString = homeAccountId.contains(".")
? homeAccountId.substring(0, homeAccountId.indexOf('.'))
: homeAccountId;
java.util.UUID userOid = java.util.UUID.fromString(oidString);

// Step 2: Acquire via OID — should come from cache
AgentIdentity oidIdentity = new AgentIdentity(TestConstants.AGENTIC_AGENT_APP_ID, userOid);
IAuthenticationResult oidResult = blueprintCca.acquireTokenForAgent(
AcquireTokenForAgentParameters.builder(
Collections.singleton(TestConstants.AGENTIC_GRAPH_SCOPE), oidIdentity).build()
).get();

// Should return the same cached token
assertEquals(upnResult.accessToken(), oidResult.accessToken(),
"OID-based call should return the same cached token as UPN-based call");
}

// ========================================================================
// Helpers
// ========================================================================

/**
* Helper: acquires an FMI credential from the RMA (Resource Management Application).
* Uses TestConstants.AGENTIC_FMI_EXCHANGE_SCOPE, matching FmiIT's Flow3 pattern.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ private AuthenticationResultSupplier getAuthenticationResultSupplier(MsalRequest
supplier = new AcquireTokenByUserFederatedIdentityCredentialSupplier(
(ConfidentialClientApplication) this,
(UserFederatedIdentityCredentialRequest) msalRequest);
} else if (msalRequest instanceof AcquireTokenForAgentRequest) {
supplier = new AcquireTokenForAgentSupplier(
(ConfidentialClientApplication) this,
(AcquireTokenForAgentRequest) msalRequest);
} else if (msalRequest instanceof ManagedIdentityRequest) {
supplier = new AcquireTokenByManagedIdentitySupplier(
(ManagedIdentityApplication) this,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.microsoft.aad.msal4j;

import java.util.Map;
import java.util.Set;

import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotNull;

/**
* Object containing parameters for the composite agent token acquisition flow.
* This orchestrates the full three-leg FMI/FIC token exchange: the developer passes
* scopes and an {@link AgentIdentity}, and MSAL handles Legs 1-3 internally.
* <p>
* Can be used as parameter to
* {@link ConfidentialClientApplication#acquireTokenForAgent(AcquireTokenForAgentParameters)}
*/
public class AcquireTokenForAgentParameters implements IAcquireTokenParameters {

private Set<String> scopes;
private AgentIdentity agentIdentity;
private boolean forceRefresh;
private ClaimsRequest claims;
private Map<String, String> extraHttpHeaders;
private Map<String, String> extraQueryParameters;
private String tenant;

private AcquireTokenForAgentParameters(
Set<String> scopes,
AgentIdentity agentIdentity,
boolean forceRefresh,
ClaimsRequest claims,
Map<String, String> extraHttpHeaders,
Map<String, String> extraQueryParameters,
String tenant) {
this.scopes = scopes;
this.agentIdentity = agentIdentity;
this.forceRefresh = forceRefresh;
this.claims = claims;
this.extraHttpHeaders = extraHttpHeaders;
this.extraQueryParameters = extraQueryParameters;
this.tenant = tenant;
}

/**
* Builder for {@link AcquireTokenForAgentParameters}.
*
* @param scopes scopes application is requesting access to
* @param agentIdentity the identity of the agent and (optionally) the target user
* @return builder that can be used to construct AcquireTokenForAgentParameters
*/
public static AcquireTokenForAgentParametersBuilder builder(
Set<String> scopes, AgentIdentity agentIdentity) {
validateNotNull("scopes", scopes);
validateNotNull("agentIdentity", agentIdentity);

return new AcquireTokenForAgentParametersBuilder()
.scopes(scopes)
.agentIdentity(agentIdentity);
}

public Set<String> scopes() {
return this.scopes;
}

public AgentIdentity agentIdentity() {
return this.agentIdentity;
}

public boolean forceRefresh() {
return this.forceRefresh;
}

public ClaimsRequest claims() {
return this.claims;
}

public Map<String, String> extraHttpHeaders() {
return this.extraHttpHeaders;
}

public Map<String, String> extraQueryParameters() {
return this.extraQueryParameters;
}

public String tenant() {
return this.tenant;
}

public static class AcquireTokenForAgentParametersBuilder {
private Set<String> scopes;
private AgentIdentity agentIdentity;
private boolean forceRefresh;
private ClaimsRequest claims;
private Map<String, String> extraHttpHeaders;
private Map<String, String> extraQueryParameters;
private String tenant;

AcquireTokenForAgentParametersBuilder() {
}

AcquireTokenForAgentParametersBuilder scopes(Set<String> scopes) {
this.scopes = scopes;
return this;
}

AcquireTokenForAgentParametersBuilder agentIdentity(AgentIdentity agentIdentity) {
this.agentIdentity = agentIdentity;
return this;
}

/**
* If true, the request will ignore cached access tokens on read, but will still write
* them to the cache once obtained from the identity provider. The default is false.
*
* @param forceRefresh whether to bypass the user token cache
* @return this builder
*/
public AcquireTokenForAgentParametersBuilder forceRefresh(boolean forceRefresh) {
this.forceRefresh = forceRefresh;
return this;
}

/**
* Claims to be requested through the OIDC claims request parameter, allowing requests
* for standard and custom claims.
*
* @param claims {@link ClaimsRequest}
* @return this builder
*/
public AcquireTokenForAgentParametersBuilder claims(ClaimsRequest claims) {
this.claims = claims;
return this;
}

/**
* Adds additional headers to the token request.
*
* @param extraHttpHeaders headers to include
* @return this builder
*/
public AcquireTokenForAgentParametersBuilder extraHttpHeaders(Map<String, String> extraHttpHeaders) {
this.extraHttpHeaders = extraHttpHeaders;
return this;
}

/**
* Adds additional query parameters to the token request.
*
* @param extraQueryParameters query parameters to include
* @return this builder
*/
public AcquireTokenForAgentParametersBuilder extraQueryParameters(Map<String, String> extraQueryParameters) {
this.extraQueryParameters = extraQueryParameters;
return this;
}

/**
* Sets the tenant for the request, overriding the application's configured authority.
*
* @param tenant tenant ID or domain
* @return this builder
*/
public AcquireTokenForAgentParametersBuilder tenant(String tenant) {
this.tenant = tenant;
return this;
}

public AcquireTokenForAgentParameters build() {
return new AcquireTokenForAgentParameters(
scopes, agentIdentity, forceRefresh, claims,
extraHttpHeaders, extraQueryParameters, tenant);
}
}
}
Loading
Loading