Skip to content

Commit 0d4b9a6

Browse files
committed
First draft FIC support
1 parent deba5cb commit 0d4b9a6

9 files changed

Lines changed: 767 additions & 0 deletions

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractApplicationBase.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ private AuthenticationResultSupplier getAuthenticationResultSupplier(MsalRequest
111111
supplier = new AcquireTokenByOnBehalfOfSupplier(
112112
(ConfidentialClientApplication) this,
113113
(OnBehalfOfRequest) msalRequest);
114+
} else if (msalRequest instanceof UserFederatedIdentityCredentialRequest) {
115+
supplier = new AcquireTokenByUserFederatedIdentityCredentialSupplier(
116+
(ConfidentialClientApplication) this,
117+
(UserFederatedIdentityCredentialRequest) msalRequest);
114118
} else if (msalRequest instanceof ManagedIdentityRequest) {
115119
supplier = new AcquireTokenByManagedIdentitySupplier(
116120
(ManagedIdentityApplication) this,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 AcquireTokenByUserFederatedIdentityCredentialSupplier extends AuthenticationResultSupplier {
10+
11+
private static final Logger LOG = LoggerFactory.getLogger(AcquireTokenByUserFederatedIdentityCredentialSupplier.class);
12+
private UserFederatedIdentityCredentialRequest userFicRequest;
13+
14+
AcquireTokenByUserFederatedIdentityCredentialSupplier(ConfidentialClientApplication clientApplication,
15+
UserFederatedIdentityCredentialRequest userFicRequest) {
16+
super(clientApplication, userFicRequest);
17+
this.userFicRequest = userFicRequest;
18+
}
19+
20+
@Override
21+
AuthenticationResult execute() throws Exception {
22+
if (!userFicRequest.parameters.forceRefresh()) {
23+
LOG.debug("ForceRefresh is false. Attempting cache lookup");
24+
try {
25+
SilentParameters parameters = SilentParameters
26+
.builder(this.userFicRequest.parameters.scopes())
27+
.claims(this.userFicRequest.parameters.claims())
28+
.tenant(this.userFicRequest.parameters.tenant())
29+
.build();
30+
31+
RequestContext context = new RequestContext(
32+
this.clientApplication,
33+
PublicApi.ACQUIRE_TOKEN_SILENTLY,
34+
parameters);
35+
36+
SilentRequest silentRequest = new SilentRequest(
37+
parameters,
38+
this.clientApplication,
39+
context,
40+
null);
41+
42+
AcquireTokenSilentSupplier supplier = new AcquireTokenSilentSupplier(
43+
this.clientApplication,
44+
silentRequest);
45+
46+
return supplier.execute();
47+
} catch (MsalClientException ex) {
48+
LOG.debug("Cache lookup failed: {}", ex.getMessage());
49+
return acquireTokenByUserFic();
50+
}
51+
}
52+
53+
LOG.debug("ForceRefresh is true. Skipping cache lookup");
54+
return acquireTokenByUserFic();
55+
}
56+
57+
private AuthenticationResult acquireTokenByUserFic() throws Exception {
58+
AcquireTokenByAuthorizationGrantSupplier supplier = new AcquireTokenByAuthorizationGrantSupplier(
59+
this.clientApplication,
60+
userFicRequest,
61+
null);
62+
63+
return supplier.execute();
64+
}
65+
}

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,24 @@ public CompletableFuture<IAuthenticationResult> acquireToken(OnBehalfOfParameter
6363
return this.executeRequest(oboRequest);
6464
}
6565

66+
@Override
67+
public CompletableFuture<IAuthenticationResult> acquireToken(UserFederatedIdentityCredentialParameters parameters) {
68+
validateNotNull("parameters", parameters);
69+
70+
RequestContext context = new RequestContext(
71+
this,
72+
PublicApi.ACQUIRE_TOKEN_BY_USER_FEDERATED_IDENTITY_CREDENTIAL,
73+
parameters);
74+
75+
UserFederatedIdentityCredentialRequest userFicRequest =
76+
new UserFederatedIdentityCredentialRequest(
77+
parameters,
78+
this,
79+
context);
80+
81+
return this.executeRequest(userFicRequest);
82+
}
83+
6684
private ConfidentialClientApplication(Builder builder) {
6785
super(builder);
6886
sendX5c = builder.sendX5c;

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/GrantConstants.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@ class GrantConstants {
1414
static final String USERNAME_PARAMETER = "username";
1515
static final String PASSWORD_PARAMETER = "password";
1616

17+
//Parameter names for user_fic flow
18+
static final String USER_FEDERATED_IDENTITY_CREDENTIAL = "user_federated_identity_credential";
19+
static final String USER_ID_PARAMETER = "user_id";
20+
1721
//Grant types
1822
static final String AUTHORIZATION_CODE = "authorization_code";
1923
static final String CLIENT_CREDENTIALS = "client_credentials";
24+
static final String USER_FIC = "user_fic";
2025
static final String PASSWORD = "password";
2126
static final String SAML_2_BEARER = "urn:ietf:params:oauth:grant-type:saml2-bearer";
2227
static final String SAML_1_1_BEARER = "urn:ietf:params:oauth:grant-type:saml1_1-bearer";

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,17 @@ public interface IConfidentialClientApplication extends IClientApplicationBase {
4949
* @return {@link CompletableFuture} containing an {@link IAuthenticationResult}
5050
*/
5151
CompletableFuture<IAuthenticationResult> acquireToken(OnBehalfOfParameters parameters);
52+
53+
/**
54+
* Acquires a token using the User Federated Identity Credential (user_fic) flow.
55+
* This is Leg 3 of the agent identity protocol, where a federated identity credential
56+
* (obtained from Leg 2) is exchanged for a user-scoped token.
57+
* <p>
58+
* The user can be identified by either UPN (username) or Object ID, as specified
59+
* in the {@link UserFederatedIdentityCredentialParameters}.
60+
*
61+
* @param parameters instance of {@link UserFederatedIdentityCredentialParameters}
62+
* @return {@link CompletableFuture} containing an {@link IAuthenticationResult}
63+
*/
64+
CompletableFuture<IAuthenticationResult> acquireToken(UserFederatedIdentityCredentialParameters parameters);
5265
}

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicApi.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ enum PublicApi {
1313
ACQUIRE_TOKEN_BY_DEVICE_CODE_FLOW(620),
1414
ACQUIRE_TOKEN_FOR_CLIENT(729),
1515
ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE(831),
16+
ACQUIRE_TOKEN_BY_USER_FEDERATED_IDENTITY_CREDENTIAL(900),
1617
ACQUIRE_TOKEN_SILENTLY(800),
1718
GET_ACCOUNTS(801),
1819
REMOVE_ACCOUNTS(802),
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
import java.util.Map;
7+
import java.util.Set;
8+
import java.util.UUID;
9+
10+
import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotBlank;
11+
import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotNull;
12+
13+
/**
14+
* Object containing parameters for the User Federated Identity Credential (user_fic) flow.
15+
* This is used for Leg 3 of the agent identity protocol, where a federated identity credential
16+
* (obtained from Leg 2) is exchanged for a user-scoped token.
17+
* <p>
18+
* Can be used as parameter to
19+
* {@link ConfidentialClientApplication#acquireToken(UserFederatedIdentityCredentialParameters)}
20+
*/
21+
public class UserFederatedIdentityCredentialParameters implements IAcquireTokenParameters {
22+
23+
private Set<String> scopes;
24+
private String username;
25+
private UUID userObjectId;
26+
private String assertion;
27+
private boolean forceRefresh;
28+
private ClaimsRequest claims;
29+
private Map<String, String> extraHttpHeaders;
30+
private Map<String, String> extraQueryParameters;
31+
private String tenant;
32+
33+
private UserFederatedIdentityCredentialParameters(
34+
Set<String> scopes,
35+
String username,
36+
UUID userObjectId,
37+
String assertion,
38+
boolean forceRefresh,
39+
ClaimsRequest claims,
40+
Map<String, String> extraHttpHeaders,
41+
Map<String, String> extraQueryParameters,
42+
String tenant) {
43+
this.scopes = scopes;
44+
this.username = username;
45+
this.userObjectId = userObjectId;
46+
this.assertion = assertion;
47+
this.forceRefresh = forceRefresh;
48+
this.claims = claims;
49+
this.extraHttpHeaders = extraHttpHeaders;
50+
this.extraQueryParameters = extraQueryParameters;
51+
this.tenant = tenant;
52+
}
53+
54+
/**
55+
* Builder for {@link UserFederatedIdentityCredentialParameters} using a UPN (User Principal Name).
56+
*
57+
* @param scopes scopes application is requesting access to
58+
* @param username the UPN of the target user (e.g., "user@contoso.com")
59+
* @param assertion the federated identity credential assertion (JWT) obtained from Leg 2
60+
* @return builder that can be used to construct UserFederatedIdentityCredentialParameters
61+
*/
62+
public static UserFederatedIdentityCredentialParametersBuilder builder(
63+
Set<String> scopes, String username, String assertion) {
64+
validateNotNull("scopes", scopes);
65+
validateNotBlank("username", username);
66+
validateNotBlank("assertion", assertion);
67+
68+
return new UserFederatedIdentityCredentialParametersBuilder()
69+
.scopes(scopes)
70+
.username(username)
71+
.assertion(assertion);
72+
}
73+
74+
/**
75+
* Builder for {@link UserFederatedIdentityCredentialParameters} using a user Object ID.
76+
*
77+
* @param scopes scopes application is requesting access to
78+
* @param userObjectId the Object ID (OID) of the target user
79+
* @param assertion the federated identity credential assertion (JWT) obtained from Leg 2
80+
* @return builder that can be used to construct UserFederatedIdentityCredentialParameters
81+
*/
82+
public static UserFederatedIdentityCredentialParametersBuilder builder(
83+
Set<String> scopes, UUID userObjectId, String assertion) {
84+
validateNotNull("scopes", scopes);
85+
validateNotNull("userObjectId", userObjectId);
86+
validateNotBlank("assertion", assertion);
87+
88+
return new UserFederatedIdentityCredentialParametersBuilder()
89+
.scopes(scopes)
90+
.userObjectId(userObjectId)
91+
.assertion(assertion);
92+
}
93+
94+
public Set<String> scopes() {
95+
return this.scopes;
96+
}
97+
98+
/**
99+
* @return the UPN of the target user, or null if user was identified by Object ID
100+
*/
101+
public String username() {
102+
return this.username;
103+
}
104+
105+
/**
106+
* @return the Object ID of the target user, or null if user was identified by UPN
107+
*/
108+
public UUID userObjectId() {
109+
return this.userObjectId;
110+
}
111+
112+
/**
113+
* @return the federated identity credential assertion (JWT)
114+
*/
115+
public String assertion() {
116+
return this.assertion;
117+
}
118+
119+
/**
120+
* @return whether to bypass the token cache and force a fresh token request
121+
*/
122+
public boolean forceRefresh() {
123+
return this.forceRefresh;
124+
}
125+
126+
public ClaimsRequest claims() {
127+
return this.claims;
128+
}
129+
130+
public Map<String, String> extraHttpHeaders() {
131+
return this.extraHttpHeaders;
132+
}
133+
134+
public Map<String, String> extraQueryParameters() {
135+
return this.extraQueryParameters;
136+
}
137+
138+
public String tenant() {
139+
return this.tenant;
140+
}
141+
142+
public static class UserFederatedIdentityCredentialParametersBuilder {
143+
private Set<String> scopes;
144+
private String username;
145+
private UUID userObjectId;
146+
private String assertion;
147+
private boolean forceRefresh;
148+
private ClaimsRequest claims;
149+
private Map<String, String> extraHttpHeaders;
150+
private Map<String, String> extraQueryParameters;
151+
private String tenant;
152+
153+
UserFederatedIdentityCredentialParametersBuilder() {
154+
}
155+
156+
UserFederatedIdentityCredentialParametersBuilder scopes(Set<String> scopes) {
157+
this.scopes = scopes;
158+
return this;
159+
}
160+
161+
UserFederatedIdentityCredentialParametersBuilder username(String username) {
162+
this.username = username;
163+
return this;
164+
}
165+
166+
UserFederatedIdentityCredentialParametersBuilder userObjectId(UUID userObjectId) {
167+
this.userObjectId = userObjectId;
168+
return this;
169+
}
170+
171+
UserFederatedIdentityCredentialParametersBuilder assertion(String assertion) {
172+
this.assertion = assertion;
173+
return this;
174+
}
175+
176+
/**
177+
* Forces MSAL to refresh the token from the identity provider even if a cached token is available.
178+
*
179+
* @param forceRefresh true to bypass the cache; otherwise false. Default is false.
180+
* @return the builder
181+
*/
182+
public UserFederatedIdentityCredentialParametersBuilder forceRefresh(boolean forceRefresh) {
183+
this.forceRefresh = forceRefresh;
184+
return this;
185+
}
186+
187+
/**
188+
* Claims to be requested through the OIDC claims request parameter.
189+
*/
190+
public UserFederatedIdentityCredentialParametersBuilder claims(ClaimsRequest claims) {
191+
this.claims = claims;
192+
return this;
193+
}
194+
195+
/**
196+
* Adds additional headers to the token request.
197+
*/
198+
public UserFederatedIdentityCredentialParametersBuilder extraHttpHeaders(Map<String, String> extraHttpHeaders) {
199+
this.extraHttpHeaders = extraHttpHeaders;
200+
return this;
201+
}
202+
203+
/**
204+
* Adds additional parameters to the token request.
205+
*/
206+
public UserFederatedIdentityCredentialParametersBuilder extraQueryParameters(Map<String, String> extraQueryParameters) {
207+
this.extraQueryParameters = extraQueryParameters;
208+
return this;
209+
}
210+
211+
/**
212+
* Overrides the tenant value in the authority URL for this request.
213+
*/
214+
public UserFederatedIdentityCredentialParametersBuilder tenant(String tenant) {
215+
this.tenant = tenant;
216+
return this;
217+
}
218+
219+
public UserFederatedIdentityCredentialParameters build() {
220+
return new UserFederatedIdentityCredentialParameters(
221+
this.scopes, this.username, this.userObjectId, this.assertion,
222+
this.forceRefresh, this.claims, this.extraHttpHeaders,
223+
this.extraQueryParameters, this.tenant);
224+
}
225+
}
226+
}

0 commit comments

Comments
 (0)