From 29add9ffe42cd453309f1becf7058545c182f78c Mon Sep 17 00:00:00 2001 From: avdunn Date: Wed, 26 Mar 2025 17:09:06 -0700 Subject: [PATCH 01/31] Remove usage of com.nimbusds.oauth2 from grant-related classes --- ...uireTokenByAuthorizationGrantSupplier.java | 51 +++++++-------- .../aad/msal4j/AuthorizationCodeRequest.java | 28 ++++---- .../aad/msal4j/ClientCredentialRequest.java | 20 +++--- .../microsoft/aad/msal4j/GrantConstants.java | 25 ++++++++ .../aad/msal4j/OAuthAuthorizationGrant.java | 64 ++++++------------- .../aad/msal4j/OnBehalfOfRequest.java | 24 +++---- .../aad/msal4j/RefreshTokenRequest.java | 17 +++-- .../aad/msal4j/UserNamePasswordRequest.java | 18 +++--- .../MsalOauthAuthorizatonGrantTest.java | 28 ++++---- 9 files changed, 139 insertions(+), 136 deletions(-) create mode 100644 msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/GrantConstants.java diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java index 4931c2ce..b3fe536d 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java @@ -4,14 +4,14 @@ package com.microsoft.aad.msal4j; import com.nimbusds.jose.util.Base64URL; -import com.nimbusds.oauth2.sdk.AuthorizationGrant; -import com.nimbusds.oauth2.sdk.ResourceOwnerPasswordCredentialsGrant; -import com.nimbusds.oauth2.sdk.SAML2BearerGrant; -import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; class AcquireTokenByAuthorizationGrantSupplier extends AuthenticationResultSupplier { @@ -77,7 +77,7 @@ private boolean IsUiRequiredCacheSupported() { private OAuthAuthorizationGrant processPasswordGrant( OAuthAuthorizationGrant authGrant) throws Exception { - if (!(authGrant.getAuthorizationGrant() instanceof ResourceOwnerPasswordCredentialsGrant)) { + if (!(authGrant.getParameters().get(GrantConstants.GRANT_TYPE_PARAMETER).get(0).equals(GrantConstants.PASSWORD))) { return authGrant; } @@ -85,11 +85,8 @@ private OAuthAuthorizationGrant processPasswordGrant( return authGrant; } - ResourceOwnerPasswordCredentialsGrant grant = - (ResourceOwnerPasswordCredentialsGrant) authGrant.getAuthorizationGrant(); - UserDiscoveryResponse userDiscoveryResponse = UserDiscoveryRequest.execute( - this.clientApplication.authenticationAuthority.getUserRealmEndpoint(grant.getUsername()), + this.clientApplication.authenticationAuthority.getUserRealmEndpoint(authGrant.getParameters().get("username").get(0)), msalRequest.headers().getReadonlyHeaderMap(), msalRequest.requestContext(), this.clientApplication.serviceBundle()); @@ -97,35 +94,39 @@ private OAuthAuthorizationGrant processPasswordGrant( if (userDiscoveryResponse.isAccountFederated()) { WSTrustResponse response = WSTrustRequest.execute( userDiscoveryResponse.federationMetadataUrl(), - grant.getUsername(), - grant.getPassword().getValue(), + authGrant.getParameters().get(GrantConstants.USERNAME_PARAMETER).get(0), + authGrant.getParameters().get(GrantConstants.PASSWORD_PARAMETER).get(0), userDiscoveryResponse.cloudAudienceUrn(), msalRequest.requestContext(), this.clientApplication.serviceBundle(), this.clientApplication.logPii()); - AuthorizationGrant updatedGrant = getSAMLAuthorizationGrant(response); + Map> params = getSAMLAuthGrantParameters(response); + params.putAll(authGrant.getParameters()); - authGrant = new OAuthAuthorizationGrant(updatedGrant, authGrant.getParameters()); + authGrant = new OAuthAuthorizationGrant(params); } return authGrant; } - private AuthorizationGrant getSAMLAuthorizationGrant(WSTrustResponse response) throws UnsupportedEncodingException { - AuthorizationGrant updatedGrant; + private Map> getSAMLAuthGrantParameters(WSTrustResponse response) { + Map> params = new LinkedHashMap<>(); + if (response.isTokenSaml2()) { - updatedGrant = new SAML2BearerGrant(new Base64URL( - Base64.getEncoder().encodeToString(response.getToken().getBytes(StandardCharsets.UTF_8)))); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.SAML_2_BEARER)); } else { - updatedGrant = new SAML11BearerGrant(new Base64URL( - Base64.getEncoder().encodeToString(response.getToken() - .getBytes(StandardCharsets.UTF_8)))); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.SAML_1_1_BEARER)); } - return updatedGrant; + + params.put(GrantConstants.ASSERTION_PARAMETER, Collections.singletonList(new Base64URL( + Base64.getEncoder().encodeToString(response.getToken() + .getBytes(StandardCharsets.UTF_8))).toString())); + + return params; } - private AuthorizationGrant getAuthorizationGrantIntegrated(String userName) throws Exception { - AuthorizationGrant updatedGrant; + private Map> getAuthorizationGrantIntegrated(String userName) throws Exception { + Map> params; String userRealmEndpoint = this.clientApplication.authenticationAuthority. getUserRealmEndpoint(URLEncoder.encode(userName, StandardCharsets.UTF_8.name())); @@ -152,7 +153,7 @@ private AuthorizationGrant getAuthorizationGrantIntegrated(String userName) thro this.clientApplication.serviceBundle(), this.clientApplication.logPii()); - updatedGrant = getSAMLAuthorizationGrant(wsTrustResponse); + params = getSAMLAuthGrantParameters(wsTrustResponse); } else if (userRealmResponse.isAccountManaged()) { throw new MsalClientException( "Password is required for managed user", @@ -163,6 +164,6 @@ private AuthorizationGrant getAuthorizationGrantIntegrated(String userName) thro AuthenticationErrorCode.USER_REALM_DISCOVERY_FAILED); } - return updatedGrant; + return params; } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java index e199b20a..57ff658a 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java @@ -3,10 +3,10 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.AuthorizationCode; -import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; -import com.nimbusds.oauth2.sdk.AuthorizationGrant; -import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; class AuthorizationCodeRequest extends MsalRequest { @@ -17,19 +17,19 @@ class AuthorizationCodeRequest extends MsalRequest { } private static AbstractMsalAuthorizationGrant createMsalGrant(AuthorizationCodeParameters parameters) { + Map> params = new LinkedHashMap<>(); + + params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.AUTHORIZATION_CODE)); + params.put("code", Collections.singletonList(parameters.authorizationCode())); + + if (parameters.redirectUri() != null) { + params.put("redirect_uri", Collections.singletonList(parameters.redirectUri().toString())); + } - AuthorizationGrant authorizationGrant; if (parameters.codeVerifier() != null) { - authorizationGrant = new AuthorizationCodeGrant( - new AuthorizationCode(parameters.authorizationCode()), - parameters.redirectUri(), - new CodeVerifier(parameters.codeVerifier())); - - } else { - authorizationGrant = new AuthorizationCodeGrant( - new AuthorizationCode(parameters.authorizationCode()), parameters.redirectUri()); + params.put("code_verifier", Collections.singletonList(parameters.codeVerifier())); } - return new OAuthAuthorizationGrant(authorizationGrant, parameters.scopes(), parameters.claims()); + return new OAuthAuthorizationGrant(params, parameters.scopes(), parameters.claims()); } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialRequest.java index 56de5f6c..a03ee8ea 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialRequest.java @@ -3,8 +3,10 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.ClientCredentialsGrant; - +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Function; @@ -16,14 +18,6 @@ class ClientCredentialRequest extends MsalRequest { useful to applications in general because the token provider must implement all authentication logic. */ Function> appTokenProvider; - ClientCredentialRequest(ClientCredentialParameters parameters, - ConfidentialClientApplication application, - RequestContext requestContext) { - super(application, createMsalGrant(parameters), requestContext); - this.parameters = parameters; - appTokenProvider = null; - } - ClientCredentialRequest(ClientCredentialParameters parameters, ConfidentialClientApplication application, RequestContext requestContext, @@ -34,6 +28,10 @@ class ClientCredentialRequest extends MsalRequest { } private static OAuthAuthorizationGrant createMsalGrant(ClientCredentialParameters parameters) { - return new OAuthAuthorizationGrant(new ClientCredentialsGrant(), parameters.scopes(), parameters.claims()); + Map> params = new LinkedHashMap<>(); + + params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.CLIENT_CREDENTIALS)); + + return new OAuthAuthorizationGrant(params, parameters.scopes(), parameters.claims()); } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/GrantConstants.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/GrantConstants.java new file mode 100644 index 00000000..cbc3d47d --- /dev/null +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/GrantConstants.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +/** + * Constants used by {@link OAuthAuthorizationGrant} and related classes + */ +class GrantConstants { + + //Parameter names + static final String GRANT_TYPE_PARAMETER = "grant_type"; + static final String ASSERTION_PARAMETER = "assertion"; + static final String USERNAME_PARAMETER = "username"; + static final String PASSWORD_PARAMETER = "password"; + + //Grant types + static final String AUTHORIZATION_CODE = "authorization_code"; + static final String CLIENT_CREDENTIALS = "client_credentials"; + static final String PASSWORD = "password"; + static final String SAML_2_BEARER = "urn:ietf:params:oauth:grant-type:saml2-bearer"; + static final String SAML_1_1_BEARER = "urn:ietf:params:oauth:grant-type:saml1_1-bearer"; + static final String JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + static final String REFRESH_TOKEN = "refresh_token"; +} diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java index 9c9a9323..51ed8e94 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java @@ -3,8 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.AuthorizationGrant; - import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -15,51 +13,25 @@ class OAuthAuthorizationGrant extends AbstractMsalAuthorizationGrant { - private AuthorizationGrant grant; private final Map> params = new LinkedHashMap<>(); - OAuthAuthorizationGrant(final AuthorizationGrant grant, Set scopesSet, ClaimsRequest claims) { - this(grant, scopesSet != null ? String.join(" ", scopesSet) : null, claims); - } - - String addCommonScopes(String scopes) { - Set allScopes = new HashSet<>( - Arrays.asList(COMMON_SCOPES_PARAM.split(SCOPES_DELIMITER))); - - if (!StringHelper.isBlank(scopes)) { - allScopes.addAll(Arrays.asList(scopes.split(SCOPES_DELIMITER))); - } - return String.join(SCOPES_DELIMITER, allScopes); + OAuthAuthorizationGrant(Map> params, Set scopesSet, ClaimsRequest claims) { + this(params, scopesSet != null ? String.join(" ", scopesSet) : null, claims); } - OAuthAuthorizationGrant(final AuthorizationGrant grant, String scopes, ClaimsRequest claims) { - this.grant = grant; + OAuthAuthorizationGrant(Map> params, String scopes, ClaimsRequest claims) { + this(params); - String allScopes = addCommonScopes(scopes); - this.scopes = allScopes; - params.put(SCOPE_PARAM_NAME, Collections.singletonList(allScopes)); + this.scopes = addCommonScopes(scopes); + this.params.put(SCOPE_PARAM_NAME, Collections.singletonList(this.scopes)); if (claims != null) { this.claims = claims; - params.put("claims", Collections.singletonList(claims.formatAsJSONString())); - } - } - - OAuthAuthorizationGrant(AuthorizationGrant grant, String scopes, Map> extraParams) { - this.grant = grant; - - String allScopes = addCommonScopes(scopes); - this.scopes = allScopes; - this.params.put(SCOPE_PARAM_NAME, Collections.singletonList(allScopes)); - - if (extraParams != null) { - this.params.putAll(extraParams); + this.params.put("claims", Collections.singletonList(claims.formatAsJSONString())); } } - OAuthAuthorizationGrant(AuthorizationGrant grant, Map> params) { - this.grant = grant; - + OAuthAuthorizationGrant(Map> params) { if (params != null) { this.params.putAll(params); } @@ -67,10 +39,10 @@ String addCommonScopes(String scopes) { @Override public Map> toParameters() { - final Map> outParams = new LinkedHashMap<>(); - outParams.putAll(params); + final Map> outParams = new LinkedHashMap<>(params); + outParams.put("client_info", Collections.singletonList("1")); - outParams.putAll(grant.toParameters()); + if (claims != null) { outParams.put("claims", Collections.singletonList(claims.formatAsJSONString())); } @@ -78,11 +50,17 @@ public Map> toParameters() { return Collections.unmodifiableMap(outParams); } - AuthorizationGrant getAuthorizationGrant() { - return this.grant; - } - Map> getParameters() { return params; } + + String addCommonScopes(String scopes) { + Set allScopes = new HashSet<>( + Arrays.asList(COMMON_SCOPES_PARAM.split(SCOPES_DELIMITER))); + + if (!StringHelper.isBlank(scopes)) { + allScopes.addAll(Arrays.asList(scopes.split(SCOPES_DELIMITER))); + } + return String.join(SCOPES_DELIMITER, allScopes); + } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java index 00748858..46718cfa 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java @@ -4,13 +4,12 @@ package com.microsoft.aad.msal4j; import com.nimbusds.jwt.SignedJWT; -import com.nimbusds.oauth2.sdk.AuthorizationGrant; -import com.nimbusds.oauth2.sdk.JWTBearerGrant; import lombok.Getter; import lombok.experimental.Accessors; +import java.text.ParseException; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -30,20 +29,21 @@ class OnBehalfOfRequest extends MsalRequest { } private static OAuthAuthorizationGrant createAuthenticationGrant(OnBehalfOfParameters parameters) { + Map> params = new LinkedHashMap<>(); - AuthorizationGrant jWTBearerGrant; - try { - jWTBearerGrant = new JWTBearerGrant(SignedJWT.parse(parameters.userAssertion().getAssertion())); - } catch (Exception e) { - throw new MsalClientException(e); - } - - Map> params = new HashMap<>(); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.JWT_BEARER)); params.put("requested_token_use", Collections.singletonList("on_behalf_of")); + if (parameters.claims() != null) { params.put("claims", Collections.singletonList(parameters.claims().formatAsJSONString())); } - return new OAuthAuthorizationGrant(jWTBearerGrant, String.join(SCOPES_DELIMITER, parameters.scopes()), params); + try { + params.put(GrantConstants.ASSERTION_PARAMETER, Collections.singletonList(SignedJWT.parse(parameters.userAssertion().getAssertion()).getParsedString())); + } catch (ParseException e) { + throw new RuntimeException(e); + } + + return new OAuthAuthorizationGrant(params, String.join(SCOPES_DELIMITER, parameters.scopes()), null); } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RefreshTokenRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RefreshTokenRequest.java index f9983c60..7f7a79a2 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RefreshTokenRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RefreshTokenRequest.java @@ -3,9 +3,10 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.RefreshTokenGrant; -import com.nimbusds.oauth2.sdk.token.RefreshToken; - +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -31,11 +32,13 @@ class RefreshTokenRequest extends MsalRequest { this.parentSilentRequest = silentRequest; } - private static AbstractMsalAuthorizationGrant createAuthenticationGrant( - RefreshTokenParameters parameters) { + private static AbstractMsalAuthorizationGrant createAuthenticationGrant(RefreshTokenParameters parameters) { + Map> params = new LinkedHashMap<>(); + + params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.REFRESH_TOKEN)); + params.put("refresh_token", Collections.singletonList(parameters.refreshToken())); - RefreshTokenGrant refreshTokenGrant = new RefreshTokenGrant(new RefreshToken(parameters.refreshToken())); - return new OAuthAuthorizationGrant(refreshTokenGrant, parameters.scopes(), parameters.claims()); + return new OAuthAuthorizationGrant(params, parameters.scopes(), parameters.claims()); } String getFullThumbprint() { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordRequest.java index a3870dd9..6b4d0133 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordRequest.java @@ -3,8 +3,10 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.ResourceOwnerPasswordCredentialsGrant; -import com.nimbusds.oauth2.sdk.auth.Secret; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; class UserNamePasswordRequest extends MsalRequest { @@ -14,13 +16,13 @@ class UserNamePasswordRequest extends MsalRequest { super(application, createAuthenticationGrant(parameters), requestContext); } - private static OAuthAuthorizationGrant createAuthenticationGrant( - UserNamePasswordParameters parameters) { + private static OAuthAuthorizationGrant createAuthenticationGrant(UserNamePasswordParameters parameters) { + Map> params = new LinkedHashMap<>(); - ResourceOwnerPasswordCredentialsGrant resourceOwnerPasswordCredentialsGrant = - new ResourceOwnerPasswordCredentialsGrant(parameters.username(), - new Secret(new String(parameters.password()))); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.PASSWORD)); + params.put(GrantConstants.USERNAME_PARAMETER, Collections.singletonList(parameters.username())); + params.put(GrantConstants.PASSWORD_PARAMETER, Collections.singletonList(new String(parameters.password()))); - return new OAuthAuthorizationGrant(resourceOwnerPasswordCredentialsGrant, parameters.scopes(), parameters.claims()); + return new OAuthAuthorizationGrant(params, parameters.scopes(), parameters.claims()); } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java index 3fcc6cd0..ebc96755 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java @@ -3,33 +3,29 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.AuthorizationCode; -import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; + +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; @TestInstance(TestInstance.Lifecycle.PER_CLASS) class MsalOauthAuthorizatonGrantTest { @Test - void testConstructor() { - final OAuthAuthorizationGrant grant = new OAuthAuthorizationGrant(null, - new HashMap<>()); - assertNotNull(grant); - } + void testToParameters() { + Map> params = new LinkedHashMap<>(); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList("SomeGrantType")); + + final OAuthAuthorizationGrant grant = new OAuthAuthorizationGrant(params); - @Test - void testToParameters() throws URISyntaxException { - final OAuthAuthorizationGrant grant = new OAuthAuthorizationGrant( - new AuthorizationCodeGrant(new AuthorizationCode("grant"), - new URI("http://microsoft.com")), - null); assertNotNull(grant); assertNotNull(grant.toParameters()); + assertEquals("SomeGrantType", grant.getParameters().get(GrantConstants.GRANT_TYPE_PARAMETER).get(0)); } } From a660a03f8cda5c1c4cc0c1e417cd9135e554959e Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 27 Mar 2025 17:26:16 -0700 Subject: [PATCH 02/31] Refactor and address PR feedback --- .../AbstractMsalAuthorizationGrant.java | 13 +++-- ...uireTokenByAuthorizationGrantSupplier.java | 12 ++-- .../AuthorizationRequestUrlParameters.java | 4 +- .../msal4j/DeviceCodeAuthorizationGrant.java | 55 ------------------- .../aad/msal4j/DeviceCodeFlowRequest.java | 14 ++++- .../microsoft/aad/msal4j/GrantConstants.java | 1 + .../IntegratedWindowsAuthorizationGrant.java | 2 +- .../aad/msal4j/OAuthAuthorizationGrant.java | 52 +++++++----------- .../aad/msal4j/OnBehalfOfRequest.java | 13 +---- .../com/microsoft/aad/msal4j/TokenCache.java | 2 +- .../MsalOauthAuthorizatonGrantTest.java | 4 +- 11 files changed, 54 insertions(+), 118 deletions(-) delete mode 100644 msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeAuthorizationGrant.java diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractMsalAuthorizationGrant.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractMsalAuthorizationGrant.java index caf116b7..a99b6b99 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractMsalAuthorizationGrant.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractMsalAuthorizationGrant.java @@ -3,8 +3,12 @@ package com.microsoft.aad.msal4j; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Abstract class for an MSAL grant. @@ -25,13 +29,12 @@ abstract class AbstractMsalAuthorizationGrant { static final String SCOPE_PROFILE = "profile"; static final String SCOPE_OFFLINE_ACCESS = "offline_access"; - static final String COMMON_SCOPES_PARAM = SCOPE_OPEN_ID + SCOPES_DELIMITER + - SCOPE_PROFILE + SCOPES_DELIMITER + - SCOPE_OFFLINE_ACCESS; + static final Set COMMON_SCOPES = Stream.of(SCOPE_OPEN_ID, SCOPE_PROFILE, SCOPE_OFFLINE_ACCESS) + .collect(Collectors.toCollection(HashSet::new)); - String scopes; + Set scopes; - String getScopes() { + Set getScopes() { return scopes; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java index b3fe536d..85917b97 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java @@ -77,7 +77,7 @@ private boolean IsUiRequiredCacheSupported() { private OAuthAuthorizationGrant processPasswordGrant( OAuthAuthorizationGrant authGrant) throws Exception { - if (!(authGrant.getParameters().get(GrantConstants.GRANT_TYPE_PARAMETER).get(0).equals(GrantConstants.PASSWORD))) { + if (!(authGrant.toParameters().get(GrantConstants.GRANT_TYPE_PARAMETER).get(0).equals(GrantConstants.PASSWORD))) { return authGrant; } @@ -86,7 +86,7 @@ private OAuthAuthorizationGrant processPasswordGrant( } UserDiscoveryResponse userDiscoveryResponse = UserDiscoveryRequest.execute( - this.clientApplication.authenticationAuthority.getUserRealmEndpoint(authGrant.getParameters().get("username").get(0)), + this.clientApplication.authenticationAuthority.getUserRealmEndpoint(authGrant.toParameters().get("username").get(0)), msalRequest.headers().getReadonlyHeaderMap(), msalRequest.requestContext(), this.clientApplication.serviceBundle()); @@ -94,17 +94,17 @@ private OAuthAuthorizationGrant processPasswordGrant( if (userDiscoveryResponse.isAccountFederated()) { WSTrustResponse response = WSTrustRequest.execute( userDiscoveryResponse.federationMetadataUrl(), - authGrant.getParameters().get(GrantConstants.USERNAME_PARAMETER).get(0), - authGrant.getParameters().get(GrantConstants.PASSWORD_PARAMETER).get(0), + authGrant.toParameters().get(GrantConstants.USERNAME_PARAMETER).get(0), + authGrant.toParameters().get(GrantConstants.PASSWORD_PARAMETER).get(0), userDiscoveryResponse.cloudAudienceUrn(), msalRequest.requestContext(), this.clientApplication.serviceBundle(), this.clientApplication.logPii()); Map> params = getSAMLAuthGrantParameters(response); - params.putAll(authGrant.getParameters()); + params.putAll(authGrant.toParameters()); - authGrant = new OAuthAuthorizationGrant(params); + authGrant = new OAuthAuthorizationGrant(params, null, null); } return authGrant; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java index da1feccc..a9ef32e8 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java @@ -66,9 +66,7 @@ private AuthorizationRequestUrlParameters(Builder builder) { requestParameters.put("redirect_uri", Collections.singletonList(this.redirectUri)); this.scopes = builder.scopes; - String[] commonScopes = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM.split(" "); - - Set scopesParam = new LinkedHashSet<>(Arrays.asList(commonScopes)); + Set scopesParam = new LinkedHashSet<>(AbstractMsalAuthorizationGrant.COMMON_SCOPES); scopesParam.addAll(builder.scopes); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeAuthorizationGrant.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeAuthorizationGrant.java deleted file mode 100644 index 545e3472..00000000 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeAuthorizationGrant.java +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.aad.msal4j; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Class for device code grant. - */ -class DeviceCodeAuthorizationGrant extends AbstractMsalAuthorizationGrant { - private final static String GRANT_TYPE = "device_code"; - - private final DeviceCode deviceCode; - private final String scopes; - private String correlationId; - - /** - * Create a new device code grant object from a device code and a resource. - * - * @param scopes The resource for which the device code was acquired. - */ - DeviceCodeAuthorizationGrant(DeviceCode deviceCode, final String scopes, ClaimsRequest claims) { - this.deviceCode = deviceCode; - this.correlationId = deviceCode.correlationId(); - this.scopes = scopes; - this.claims = claims; - } - - /** - * Converts the device code grant to a map of HTTP paramters. - * - * @return The map with HTTP parameters. - */ - @Override - public Map> toParameters() { - final Map> outParams = new LinkedHashMap<>(); - outParams.put(SCOPE_PARAM_NAME, Collections.singletonList(COMMON_SCOPES_PARAM + SCOPES_DELIMITER + scopes)); - outParams.put("grant_type", Collections.singletonList(GRANT_TYPE)); - outParams.put("device_code", Collections.singletonList(deviceCode.deviceCode())); - outParams.put("client_info", Collections.singletonList("1")); - if (claims != null) { - outParams.put("claims", Collections.singletonList(claims.formatAsJSONString())); - } - - return outParams; - } - - public String getCorrelationId() { - return correlationId; - } -} diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java index 6f5eb556..93775f70 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java @@ -10,6 +10,7 @@ import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -59,14 +60,23 @@ DeviceCode acquireDeviceCode(String url, } void createAuthenticationGrant(DeviceCode deviceCode) { - msalAuthorizationGrant = new DeviceCodeAuthorizationGrant(deviceCode, deviceCode.scopes(), parameters.claims()); + final Map> params = new LinkedHashMap<>(); + + params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.DEVICE_CODE)); + params.put("device_code", Collections.singletonList(deviceCode.deviceCode())); + + if (parameters.claims() != null) { + params.put("claims", Collections.singletonList(parameters.claims().formatAsJSONString())); + } + + msalAuthorizationGrant = new OAuthAuthorizationGrant(params, Collections.singleton(deviceCode.scopes()), parameters.claims()); } private String createQueryParams(String clientId) { Map> queryParameters = new HashMap<>(); queryParameters.put("client_id", Collections.singletonList(clientId)); - String scopesParam = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM + + String scopesParam = String.join(AbstractMsalAuthorizationGrant.SCOPES_DELIMITER, AbstractMsalAuthorizationGrant.COMMON_SCOPES) + AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + scopesStr; queryParameters.put("scope", Collections.singletonList(scopesParam)); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/GrantConstants.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/GrantConstants.java index cbc3d47d..1e632245 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/GrantConstants.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/GrantConstants.java @@ -22,4 +22,5 @@ class GrantConstants { static final String SAML_1_1_BEARER = "urn:ietf:params:oauth:grant-type:saml1_1-bearer"; static final String JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer"; static final String REFRESH_TOKEN = "refresh_token"; + static final String DEVICE_CODE = "device_code"; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthorizationGrant.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthorizationGrant.java index 672af4f8..ee963ed1 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthorizationGrant.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthorizationGrant.java @@ -13,7 +13,7 @@ class IntegratedWindowsAuthorizationGrant extends AbstractMsalAuthorizationGrant IntegratedWindowsAuthorizationGrant(Set scopes, String userName, ClaimsRequest claims) { this.userName = userName; - this.scopes = String.join(" ", scopes); + this.scopes = scopes; this.claims = claims; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java index 51ed8e94..7bb5b819 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java @@ -3,7 +3,6 @@ package com.microsoft.aad.msal4j; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; @@ -15,15 +14,25 @@ class OAuthAuthorizationGrant extends AbstractMsalAuthorizationGrant { private final Map> params = new LinkedHashMap<>(); - OAuthAuthorizationGrant(Map> params, Set scopesSet, ClaimsRequest claims) { - this(params, scopesSet != null ? String.join(" ", scopesSet) : null, claims); - } + /** + * Constructor to create an OAuthAuthorizationGrant + * + * @param params parameters relevant for the specific authorization grant type + * @param scopes additional scopes which will be added to a default set of common scopes + * @param claims optional claims + */ + OAuthAuthorizationGrant(Map> params, Set scopes, ClaimsRequest claims) { + this.scopes = new HashSet<>(AbstractMsalAuthorizationGrant.COMMON_SCOPES); + + if (scopes != null) { + this.scopes.addAll(scopes); + } - OAuthAuthorizationGrant(Map> params, String scopes, ClaimsRequest claims) { - this(params); + this.params.put(SCOPE_PARAM_NAME, Collections.singletonList(String.join(" ", this.scopes))); - this.scopes = addCommonScopes(scopes); - this.params.put(SCOPE_PARAM_NAME, Collections.singletonList(this.scopes)); + if (params != null) { + this.params.putAll(params); + } if (claims != null) { this.claims = claims; @@ -31,36 +40,15 @@ class OAuthAuthorizationGrant extends AbstractMsalAuthorizationGrant { } } - OAuthAuthorizationGrant(Map> params) { - if (params != null) { - this.params.putAll(params); - } - } - + /** + * Returns an unmodifiable version of the parameters map, and adds the client_info parameter + */ @Override public Map> toParameters() { final Map> outParams = new LinkedHashMap<>(params); outParams.put("client_info", Collections.singletonList("1")); - if (claims != null) { - outParams.put("claims", Collections.singletonList(claims.formatAsJSONString())); - } - return Collections.unmodifiableMap(outParams); } - - Map> getParameters() { - return params; - } - - String addCommonScopes(String scopes) { - Set allScopes = new HashSet<>( - Arrays.asList(COMMON_SCOPES_PARAM.split(SCOPES_DELIMITER))); - - if (!StringHelper.isBlank(scopes)) { - allScopes.addAll(Arrays.asList(scopes.split(SCOPES_DELIMITER))); - } - return String.join(SCOPES_DELIMITER, allScopes); - } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java index 46718cfa..d84877ae 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java @@ -3,18 +3,14 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.jwt.SignedJWT; import lombok.Getter; import lombok.experimental.Accessors; -import java.text.ParseException; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import static com.microsoft.aad.msal4j.AbstractMsalAuthorizationGrant.SCOPES_DELIMITER; - @Accessors(fluent = true) @Getter class OnBehalfOfRequest extends MsalRequest { @@ -32,18 +28,13 @@ private static OAuthAuthorizationGrant createAuthenticationGrant(OnBehalfOfParam Map> params = new LinkedHashMap<>(); params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.JWT_BEARER)); + params.put(GrantConstants.ASSERTION_PARAMETER, Collections.singletonList(parameters.userAssertion().getAssertion())); params.put("requested_token_use", Collections.singletonList("on_behalf_of")); if (parameters.claims() != null) { params.put("claims", Collections.singletonList(parameters.claims().formatAsJSONString())); } - try { - params.put(GrantConstants.ASSERTION_PARAMETER, Collections.singletonList(SignedJWT.parse(parameters.userAssertion().getAssertion()).getParsedString())); - } catch (ParseException e) { - throw new RuntimeException(e); - } - - return new OAuthAuthorizationGrant(params, String.join(SCOPES_DELIMITER, parameters.scopes()), null); + return new OAuthAuthorizationGrant(params, parameters.scopes(), null); } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java index 2c39d67e..28b6c6e7 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java @@ -255,7 +255,7 @@ private static AccessTokenCacheEntity createAccessTokenCacheEntity(TokenRequestE at.realm(tokenRequestExecutor.tenant); String scopes = !StringHelper.isBlank(authenticationResult.scopes()) ? authenticationResult.scopes() : - tokenRequestExecutor.getMsalRequest().msalAuthorizationGrant().getScopes(); + String.join(" ", tokenRequestExecutor.getMsalRequest().msalAuthorizationGrant().getScopes()); at.target(scopes); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java index ebc96755..a4b54fd2 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java @@ -22,10 +22,10 @@ void testToParameters() { Map> params = new LinkedHashMap<>(); params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList("SomeGrantType")); - final OAuthAuthorizationGrant grant = new OAuthAuthorizationGrant(params); + final OAuthAuthorizationGrant grant = new OAuthAuthorizationGrant(params, null, null); assertNotNull(grant); assertNotNull(grant.toParameters()); - assertEquals("SomeGrantType", grant.getParameters().get(GrantConstants.GRANT_TYPE_PARAMETER).get(0)); + assertEquals("SomeGrantType", grant.toParameters().get(GrantConstants.GRANT_TYPE_PARAMETER).get(0)); } } From 8e3b9d983a7b913ff8c09951300a141c7cedb534 Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 1 Apr 2025 09:52:01 -0700 Subject: [PATCH 03/31] Remove com.nimbusds's HTTPRequest, ClientAuthentication, and related classes --- .../msal4j/AbstractClientApplicationBase.java | 3 - .../aad/msal4j/ClientAuthenticationPost.java | 60 --------------- .../msal4j/ConfidentialClientApplication.java | 75 +++++-------------- .../aad/msal4j/OAuthHttpRequest.java | 24 +++--- .../aad/msal4j/PublicClientApplication.java | 11 --- .../aad/msal4j/TokenRequestExecutor.java | 49 +++++++----- .../aad/msal4j/ClientCertificateTest.java | 5 +- 7 files changed, 63 insertions(+), 164 deletions(-) delete mode 100644 msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAuthenticationPost.java diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java index 5a8b2c3b..cf4a2407 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java @@ -3,8 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; - import javax.net.ssl.SSLSocketFactory; import java.net.MalformedURLException; import java.net.Proxy; @@ -29,7 +27,6 @@ public abstract class AbstractClientApplicationBase extends AbstractApplicationB private String applicationName; private String applicationVersion; private AadInstanceDiscoveryResponse aadAadInstanceDiscoveryResponse; - protected abstract ClientAuthentication clientAuthentication(); private String clientCapabilities; private boolean autoDetectRegion; protected String azureRegion; diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAuthenticationPost.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAuthenticationPost.java deleted file mode 100644 index 1ef2bd3f..00000000 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAuthenticationPost.java +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.aad.msal4j; - -import java.util.*; - -import com.nimbusds.oauth2.sdk.SerializeException; -import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; -import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; -import com.nimbusds.oauth2.sdk.http.HTTPRequest; -import com.nimbusds.oauth2.sdk.id.ClientID; -import com.nimbusds.oauth2.sdk.util.URLUtils; - -class ClientAuthenticationPost extends ClientAuthentication { - - protected ClientAuthenticationPost(ClientAuthenticationMethod method, - ClientID clientID) { - super(method, clientID); - } - - @Override - public Set getFormParameterNames() { - return Collections.unmodifiableSet(new HashSet(Arrays.asList("client_assertion", "client_assertion_type", "client_id"))); - } - - Map> toParameters() { - - Map> params = new HashMap<>(); - - params.put("client_id", Collections.singletonList(getClientID().getValue())); - - return params; - } - - @Override - public void applyTo(HTTPRequest httpRequest) throws SerializeException { - - if (httpRequest.getMethod() != HTTPRequest.Method.POST) - throw new SerializeException("The HTTP request method must be POST"); - - String ct = String.valueOf(httpRequest.getEntityContentType()); - - if (ct == null) - throw new SerializeException("Missing HTTP Content-Type header"); - - if (!ct.equals(HTTPContentType.ApplicationURLEncoded.contentType)) - throw new SerializeException( - "The HTTP Content-Type header must be " - + HTTPContentType.ApplicationURLEncoded.contentType); - - Map> params = httpRequest.getQueryParameters(); - - params.putAll(toParameters()); - - String queryString = URLUtils.serializeParameters(params); - - httpRequest.setQuery(queryString); - } -} diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java index 211050b3..abe8fb09 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java @@ -3,12 +3,8 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.auth.*; -import com.nimbusds.oauth2.sdk.id.ClientID; import org.slf4j.LoggerFactory; -import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Function; @@ -23,10 +19,9 @@ */ public class ConfidentialClientApplication extends AbstractClientApplicationBase implements IConfidentialClientApplication { - private ClientAuthentication clientAuthentication; - - private boolean clientCertAuthentication = false; private ClientCertificate clientCertificate; + String assertion; + String secret; /** AppTokenProvider creates a Credential from a function that provides access tokens. The function must be concurrency safe. This is intended only to allow the Azure SDK to cache MSI tokens. It isn't @@ -87,65 +82,31 @@ private void initClientAuthentication(IClientCredential clientCredential) { validateNotNull("clientCredential", clientCredential); if (clientCredential instanceof ClientSecret) { - clientAuthentication = new ClientSecretPost( - new ClientID(clientId()), - new Secret(((ClientSecret) clientCredential).clientSecret())); + this.secret = ((ClientSecret) clientCredential).clientSecret(); } else if (clientCredential instanceof ClientCertificate) { - this.clientCertAuthentication = true; this.clientCertificate = (ClientCertificate) clientCredential; - clientAuthentication = buildValidClientCertificateAuthority(); + this.assertion = getAssertionString(clientCredential); } else if (clientCredential instanceof ClientAssertion) { - clientAuthentication = createClientAuthFromClientAssertion((ClientAssertion) clientCredential); + this.assertion = getAssertionString(clientCredential); } else { throw new IllegalArgumentException("Unsupported client credential"); } } - @Override - protected ClientAuthentication clientAuthentication() { - if (clientCertAuthentication) { - final Date currentDateTime = new Date(System.currentTimeMillis()); - final Date expirationTime = ((PrivateKeyJWT) clientAuthentication).getJWTAuthenticationClaimsSet().getExpirationTime(); - if (expirationTime.before(currentDateTime)) { - clientAuthentication = buildValidClientCertificateAuthority(); - } - } - return clientAuthentication; - } + String getAssertionString(IClientCredential clientCredential) { + if (clientCredential instanceof ClientCertificate) { + boolean useSha1 = Authority.detectAuthorityType(this.authenticationAuthority.canonicalAuthorityUrl()) == AuthorityType.ADFS; - private ClientAuthentication buildValidClientCertificateAuthority() { - //The library originally used SHA-1 for thumbprints as other algorithms were not supported server-side. - //When this was written support for SHA-256 had been added, however ADFS scenarios still only allowed SHA-1. - boolean useSha1 = Authority.detectAuthorityType(this.authenticationAuthority.canonicalAuthorityUrl()) == AuthorityType.ADFS; - - ClientAssertion clientAssertion = JwtHelper.buildJwt( - clientId(), - clientCertificate, - this.authenticationAuthority.selfSignedJwtAudience(), - sendX5c, - useSha1); - return createClientAuthFromClientAssertion(clientAssertion); - } - - protected ClientAuthentication createClientAuthFromClientAssertion( - final ClientAssertion clientAssertion) { - final Map> map = new HashMap<>(); - try { - - map.put("client_assertion_type", Collections.singletonList(ClientAssertion.assertionType)); - map.put("client_assertion", Collections.singletonList(clientAssertion.assertion())); - return PrivateKeyJWT.parse(map); - } catch (final ParseException e) { - //This library is not supposed to validate Issuer and subject values. - //The next lines of code ensures that exception is not thrown. - if (e.getMessage().contains("Issuer and subject in client JWT assertion must designate the same client identifier")) { - return new CustomJWTAuthentication( - ClientAuthenticationMethod.PRIVATE_KEY_JWT, - clientAssertion, - new ClientID(clientId()) - ); - } - throw new MsalClientException(e); + return JwtHelper.buildJwt( + clientId(), + clientCertificate, + this.authenticationAuthority.selfSignedJwtAudience(), + sendX5c, + useSha1).assertion(); + } else if (clientCredential instanceof ClientAssertion) { + return ((ClientAssertion) clientCredential).assertion(); + } else { + throw new IllegalArgumentException("Unsupported client credential"); } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java index a6183e60..9d2cfe99 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java @@ -4,7 +4,6 @@ package com.microsoft.aad.msal4j; import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.http.HTTPRequest; import com.nimbusds.oauth2.sdk.http.HTTPResponse; import java.io.IOException; @@ -15,32 +14,35 @@ import java.util.List; import java.util.Map; -class OAuthHttpRequest extends HTTPRequest { +class OAuthHttpRequest { + final HttpMethod method; + final URL url; + String query; private final Map extraHeaderParams; private final ServiceBundle serviceBundle; private final RequestContext requestContext; - OAuthHttpRequest(final Method method, + OAuthHttpRequest(final HttpMethod method, final URL url, final Map extraHeaderParams, RequestContext requestContext, final ServiceBundle serviceBundle) { - super(method, url); + this.method = method; + this.url = url; this.extraHeaderParams = extraHeaderParams; this.requestContext = requestContext; this.serviceBundle = serviceBundle; } - @Override public HTTPResponse send() throws IOException { Map httpHeaders = configureHttpHeaders(); HttpRequest httpRequest = new HttpRequest( HttpMethod.POST, - this.getURL().toString(), + this.url.toString(), httpHeaders, - this.getQuery()); + this.query); IHttpResponse httpResponse = serviceBundle.getHttpHelper().executeHttpRequest( httpRequest, @@ -55,10 +57,6 @@ private Map configureHttpHeaders() { Map httpHeaders = new HashMap<>(extraHeaderParams); httpHeaders.put("Content-Type", HTTPContentType.ApplicationURLEncoded.contentType); - if (this.getAuthorization() != null) { - httpHeaders.put("Authorization", this.getAuthorization()); - } - Map telemetryHeaders = serviceBundle.getServerSideTelemetry().getServerTelemetryHeaderMap(); httpHeaders.putAll(telemetryHeaders); @@ -109,6 +107,10 @@ private HTTPResponse createOauthHttpResponseFromHttpResponse(IHttpResponse httpR return response; } + void setQuery(String query) { + this.query = query; + } + Map getExtraHeaderParams() { return this.extraHeaderParams; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java index b0abc93b..e6faadea 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java @@ -3,9 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; -import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; -import com.nimbusds.oauth2.sdk.id.ClientID; import org.slf4j.LoggerFactory; import java.net.MalformedURLException; @@ -23,7 +20,6 @@ */ public class PublicClientApplication extends AbstractClientApplicationBase implements IPublicClientApplication { - private final ClientAuthenticationPost clientAuthentication; private IBroker broker; private boolean brokerEnabled; @@ -161,18 +157,11 @@ private PublicClientApplication(Builder builder) { super(builder); validateNotBlank("clientId", clientId()); log = LoggerFactory.getLogger(PublicClientApplication.class); - this.clientAuthentication = new ClientAuthenticationPost(ClientAuthenticationMethod.NONE, - new ClientID(clientId())); this.broker = builder.broker; this.brokerEnabled = builder.brokerEnabled; this.tenant = this.authenticationAuthority.tenant; } - @Override - protected ClientAuthentication clientAuthentication() { - return clientAuthentication; - } - /** * @param clientId Client ID (Application ID) of the application as registered * in the application registration portal (portal.azure.com) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java index a93478d3..522752d9 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java @@ -5,7 +5,6 @@ import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.SerializeException; -import com.nimbusds.oauth2.sdk.http.HTTPRequest; import com.nimbusds.oauth2.sdk.http.HTTPResponse; import com.nimbusds.oauth2.sdk.util.URLUtils; import com.nimbusds.openid.connect.sdk.token.OIDCTokens; @@ -42,19 +41,18 @@ AuthenticationResult executeTokenRequest() throws ParseException, IOException { return createAuthenticationResultFromOauthHttpResponse(oauthHttpResponse); } - OAuthHttpRequest createOauthHttpRequest() throws SerializeException, MalformedURLException, ParseException { + OAuthHttpRequest createOauthHttpRequest() throws SerializeException, MalformedURLException { if (requestAuthority.tokenEndpointUrl() == null) { throw new SerializeException("The endpoint URI is not specified"); } final OAuthHttpRequest oauthHttpRequest = new OAuthHttpRequest( - HTTPRequest.Method.POST, + HttpMethod.POST, requestAuthority.tokenEndpointUrl(), msalRequest.headers().getReadonlyHeaderMap(), msalRequest.requestContext(), this.serviceBundle); - oauthHttpRequest.setContentType(HTTPContentType.ApplicationURLEncoded.contentType); final Map> params = new HashMap<>(msalRequest.msalAuthorizationGrant().toParameters()); if (msalRequest.application() instanceof AbstractClientApplicationBase @@ -80,26 +78,41 @@ OAuthHttpRequest createOauthHttpRequest() throws SerializeException, MalformedUR } oauthHttpRequest.setQuery(URLUtils.serializeParameters(params)); - - if (msalRequest.application() instanceof AbstractClientApplicationBase - && ((AbstractClientApplicationBase) msalRequest.application()).clientAuthentication() != null) { - Map> queryParameters = oauthHttpRequest.getQueryParameters(); - String clientID = msalRequest.application().clientId(); - queryParameters.put("client_id", Arrays.asList(clientID)); - oauthHttpRequest.setQuery(URLUtils.serializeParameters(queryParameters)); + //Certain query parameters are required by Public and Confidential client applications, but not Managed Identity + if (msalRequest.application() instanceof AbstractClientApplicationBase) { + addQueryParameters(oauthHttpRequest); + } + return oauthHttpRequest; + } + + private void addQueryParameters(OAuthHttpRequest oauthHttpRequest) { + Map> queryParameters = URLUtils.parseParameters(oauthHttpRequest.query); + String clientID = msalRequest.application().clientId(); + queryParameters.put("client_id", Arrays.asList(clientID)); - // If the client application has a client assertion to apply to the request, check if a new client assertion - // was supplied as a request parameter. If so, use the request's assertion instead of the application's + // If the client application has a client assertion to apply to the request, check if a new client assertion + // was supplied as a request parameter. If so, use the request's assertion instead of the application's + if (msalRequest.application() instanceof ConfidentialClientApplication) { if (msalRequest instanceof ClientCredentialRequest && ((ClientCredentialRequest) msalRequest).parameters.clientCredential() != null) { - ((ConfidentialClientApplication) msalRequest.application()) - .createClientAuthFromClientAssertion((ClientAssertion) ((ClientCredentialRequest) msalRequest).parameters.clientCredential()) - .applyTo(oauthHttpRequest); + IClientCredential credential = ((ClientCredentialRequest) msalRequest).parameters.clientCredential(); + addJWTBearerAssertionParams(queryParameters, ((ConfidentialClientApplication) msalRequest.application()).getAssertionString(credential)); } else { - ((AbstractClientApplicationBase) msalRequest.application()).clientAuthentication().applyTo(oauthHttpRequest); + if (((ConfidentialClientApplication) msalRequest.application()).assertion != null) { + addJWTBearerAssertionParams(queryParameters, ((ConfidentialClientApplication) msalRequest.application()).assertion); + } else if (((ConfidentialClientApplication) msalRequest.application()).secret != null) { + // Client secrets have a different parameter than bearer assertions + queryParameters.put("client_secret", Collections.singletonList(((ConfidentialClientApplication) msalRequest.application()).secret)); + } } } - return oauthHttpRequest; + + oauthHttpRequest.setQuery(URLUtils.serializeParameters(queryParameters)); + } + + private void addJWTBearerAssertionParams(Map> queryParameters, String assertion) { + queryParameters.put("client_assertion", Collections.singletonList(assertion)); + queryParameters.put("client_assertion_type", Collections.singletonList("urn:ietf:params:oauth:client-assertion-type:jwt-bearer")); } private AuthenticationResult createAuthenticationResultFromOauthHttpResponse( diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java index a6e15ba8..d7979767 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java @@ -72,10 +72,7 @@ void testIClientCertificateInterface_CredentialFactoryUsesSha256() throws Except when(httpClientMock.send(any(HttpRequest.class))).thenAnswer( parameters -> { HttpRequest request = parameters.getArgument(0); - Set headerParams = ((PrivateKeyJWT) cca.clientAuthentication()).getClientAssertion().getHeader().getIncludedParams(); - if (request.body().contains(((PrivateKeyJWT) cca.clientAuthentication()).getClientAssertion().serialize()) - && headerParams.contains("x5t#S256")) { - + if (request.body().contains(cca.assertion)) { return TestHelper.expectedResponse(200, TestHelper.getSuccessfulTokenResponse(tokenResponseValues)); } return null; From e5f8b533b144e8461211cf16561b7e3980387545 Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 1 Apr 2025 10:54:28 -0700 Subject: [PATCH 04/31] Remove com.nimbusds's HTTPRequest, ClientAuthentication, and related classes --- .../microsoft/aad/msal4j/HttpResponse.java | 18 +++++++++- .../msal4j/MsalServiceExceptionFactory.java | 33 ------------------- .../aad/msal4j/OAuthHttpRequest.java | 31 +++++++---------- .../aad/msal4j/TokenRequestExecutor.java | 9 +++-- .../microsoft/aad/msal4j/TokenResponse.java | 9 ++--- 5 files changed, 38 insertions(+), 62 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpResponse.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpResponse.java index 9862f221..2b7c5ca2 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpResponse.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpResponse.java @@ -3,6 +3,10 @@ package com.microsoft.aad.msal4j; +import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; +import net.minidev.json.JSONObject; +import com.nimbusds.oauth2.sdk.ParseException; + import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -46,7 +50,7 @@ public void addHeaders(Map> responseHeaders) { } } - private void addHeader(final String name, final String... values) { + void addHeader(final String name, final String... values) { if (values != null && values.length > 0) { headers.put(name, Arrays.asList(values)); } else { @@ -54,6 +58,18 @@ private void addHeader(final String name, final String... values) { } } + List getHeader(String key) { + return headers.get(key); + } + + JSONObject getBodyAsJson() { + try { + return JSONObjectUtils.parse(this.body()); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + public int statusCode() { return this.statusCode; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java index 03d5e847..ada6489a 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java @@ -3,8 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; - import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -14,37 +12,6 @@ class MsalServiceExceptionFactory { private MsalServiceExceptionFactory() { } - static MsalServiceException fromHttpResponse(HTTPResponse httpResponse) { - - String responseContent = httpResponse.getContent(); - if (responseContent == null || StringHelper.isBlank(responseContent)) { - return new MsalServiceException( - String.format( - "Unknown Service Exception. Service returned status code %s", - httpResponse.getStatusCode()), - AuthenticationErrorCode.UNKNOWN); - } - - ErrorResponse errorResponse = JsonHelper.convertJsonToObject( - responseContent, - ErrorResponse.class); - - errorResponse.statusCode(httpResponse.getStatusCode()); - errorResponse.statusMessage(httpResponse.getStatusMessage()); - - if (errorResponse.error() != null && - errorResponse.error().equalsIgnoreCase(AuthenticationErrorCode.INVALID_GRANT)) { - - if (isInteractionRequired(errorResponse.subError)) { - return new MsalInteractionRequiredException(errorResponse, httpResponse.getHeaderMap()); - } - } - - return new MsalServiceException( - errorResponse, - httpResponse.getHeaderMap()); - } - static MsalServiceException fromHttpResponse(IHttpResponse response) { String responseBody = response.body(); if (StringHelper.isBlank(responseBody)) { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java index 9d2cfe99..49ecc2fc 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java @@ -3,9 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; - import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -35,7 +32,7 @@ class OAuthHttpRequest { this.serviceBundle = serviceBundle; } - public HTTPResponse send() throws IOException { + public HttpResponse send() throws IOException { Map httpHeaders = configureHttpHeaders(); HttpRequest httpRequest = new HttpRequest( @@ -64,28 +61,24 @@ private Map configureHttpHeaders() { return httpHeaders; } - private HTTPResponse createOauthHttpResponseFromHttpResponse(IHttpResponse httpResponse) + private HttpResponse createOauthHttpResponseFromHttpResponse(IHttpResponse httpResponse) throws IOException { - final HTTPResponse response = new HTTPResponse(httpResponse.statusCode()); + final HttpResponse response = new HttpResponse(); + response.statusCode(httpResponse.statusCode()); final String location = HttpUtils.headerValue(httpResponse.headers(), "Location"); if (!StringHelper.isBlank(location)) { try { - response.setLocation(new URI(location)); + response.addHeader("Location", new URI(location).toString()); } catch (URISyntaxException e) { throw new IOException("Invalid location URI " + location, e); } } - try { - String contentType = HttpUtils.headerValue(httpResponse.headers(), "Content-Type"); - if (!StringHelper.isBlank(contentType)) { - response.setContentType(contentType); - } - } catch (final ParseException e) { - throw new IOException("Couldn't parse Content-Type header: " - + e.getMessage(), e); + String contentType = HttpUtils.headerValue(httpResponse.headers(), "Content-Type"); + if (!StringHelper.isBlank(contentType)) { + response.addHeader("Content-Type", contentType); } Map> headers = httpResponse.headers(); @@ -95,14 +88,14 @@ private HTTPResponse createOauthHttpResponseFromHttpResponse(IHttpResponse httpR continue; } - String headerValue = response.getHeaderValue(header.getKey()); - if (headerValue == null || StringHelper.isBlank(headerValue)) { - response.setHeader(header.getKey(), header.getValue().toArray(new String[0])); + List headerValue = response.getHeader((header.getKey())); + if (headerValue == null) { + response.addHeader(header.getKey(), header.getValue().toArray(new String[0])); } } if (!StringHelper.isBlank(httpResponse.body())) { - response.setContent(httpResponse.body()); + response.body(httpResponse.body()); } return response; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java index 522752d9..aa5d002e 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java @@ -5,7 +5,6 @@ import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.SerializeException; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; import com.nimbusds.oauth2.sdk.util.URLUtils; import com.nimbusds.openid.connect.sdk.token.OIDCTokens; import org.slf4j.Logger; @@ -37,7 +36,7 @@ AuthenticationResult executeTokenRequest() throws ParseException, IOException { log.debug("Sending token request to: {}", requestAuthority.canonicalAuthorityUrl()); OAuthHttpRequest oAuthHttpRequest = createOauthHttpRequest(); - HTTPResponse oauthHttpResponse = oAuthHttpRequest.send(); + HttpResponse oauthHttpResponse = oAuthHttpRequest.send(); return createAuthenticationResultFromOauthHttpResponse(oauthHttpResponse); } @@ -116,10 +115,10 @@ private void addJWTBearerAssertionParams(Map> queryParamete } private AuthenticationResult createAuthenticationResultFromOauthHttpResponse( - HTTPResponse oauthHttpResponse) throws ParseException { + HttpResponse oauthHttpResponse) throws ParseException { AuthenticationResult result; - if (oauthHttpResponse.getStatusCode() == HTTPResponse.SC_OK) { + if (oauthHttpResponse.statusCode() == HttpHelper.HTTP_STATUS_200) { final TokenResponse response = TokenResponse.parseHttpResponse(oauthHttpResponse); OIDCTokens tokens = response.getOIDCTokens(); @@ -180,7 +179,7 @@ private AuthenticationResult createAuthenticationResultFromOauthHttpResponse( } else { // http codes indicating that STS did not log request - if (oauthHttpResponse.getStatusCode() == HttpHelper.HTTP_STATUS_429 || oauthHttpResponse.getStatusCode() >= HttpHelper.HTTP_STATUS_500) { + if (oauthHttpResponse.statusCode() == HttpHelper.HTTP_STATUS_429 || oauthHttpResponse.statusCode() >= HttpHelper.HTTP_STATUS_500) { serviceBundle.getServerSideTelemetry().previousRequests.putAll( serviceBundle.getServerSideTelemetry().previousRequestInProgress); } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java index a14838fe..0fa4ff66 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java @@ -4,7 +4,6 @@ package com.microsoft.aad.msal4j; import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; import com.nimbusds.oauth2.sdk.token.AccessToken; import com.nimbusds.oauth2.sdk.token.RefreshToken; import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; @@ -33,11 +32,13 @@ class TokenResponse extends OIDCTokenResponse { this.foci = foci; } - static TokenResponse parseHttpResponse(final HTTPResponse httpResponse) throws ParseException { + static TokenResponse parseHttpResponse(final HttpResponse httpResponse) throws ParseException { - httpResponse.ensureStatusCode(HTTPResponse.SC_OK); + if (httpResponse.statusCode() != HttpHelper.HTTP_STATUS_200) { + throw MsalServiceExceptionFactory.fromHttpResponse(httpResponse); + } - final JSONObject jsonObject = httpResponse.getContentAsJSONObject(); + final JSONObject jsonObject = httpResponse.getBodyAsJson(); return parseJsonObject(jsonObject); } From 579e4d159f28851e9084c74623ed0a340c0b9d38 Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 1 Apr 2025 11:10:26 -0700 Subject: [PATCH 05/31] Resolve merge conflicts --- .../java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java index fe8c9fbd..8f973ce5 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java @@ -3,17 +3,10 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.jwt.SignedJWT; -import com.nimbusds.oauth2.sdk.AuthorizationGrant; -import com.nimbusds.oauth2.sdk.JWTBearerGrant; - import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - -import static com.microsoft.aad.msal4j.AbstractMsalAuthorizationGrant.SCOPES_DELIMITER; - class OnBehalfOfRequest extends MsalRequest { OnBehalfOfParameters parameters; From c9d42372c17568940422745c1c2c6af879f8c237 Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 1 Apr 2025 12:37:33 -0700 Subject: [PATCH 06/31] Fix unit tests --- .../msal4j/MsalServiceExceptionFactory.java | 6 +++ .../aad/msal4j/CacheFormatTests.java | 7 ++-- .../aad/msal4j/TokenRequestExecutorTest.java | 42 +++++++++---------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java index ada6489a..dc1398e5 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java @@ -26,6 +26,12 @@ static MsalServiceException fromHttpResponse(IHttpResponse response) { responseBody, ErrorResponse.class); + if (errorResponse.error() != null && + errorResponse.error().equalsIgnoreCase(AuthenticationErrorCode.INVALID_GRANT) && isInteractionRequired(errorResponse.subError)) { + return new MsalInteractionRequiredException(errorResponse, response.headers()); + } + + if (!StringHelper.isBlank(errorResponse.error()) && !StringHelper.isBlank(errorResponse.errorDescription)) { errorResponse.statusCode(response.statusCode()); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java index 7f9cf2b7..619ebcb4 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java @@ -4,7 +4,6 @@ package com.microsoft.aad.msal4j; import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; import net.minidev.json.JSONObject; import org.json.JSONException; @@ -154,12 +153,12 @@ public void tokenCacheEntitiesFormatTest(String folder) throws URISyntaxExceptio TokenRequestExecutor request = spy(new TokenRequestExecutor( new AADAuthority(new URL(AUTHORIZE_REQUEST_URL)), msalRequest, serviceBundle)); OAuthHttpRequest msalOAuthHttpRequest = mock(OAuthHttpRequest.class); - HTTPResponse httpResponse = mock(HTTPResponse.class); + HttpResponse httpResponse = mock(HttpResponse.class); doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest(); doReturn(httpResponse).when(msalOAuthHttpRequest).send(); - doReturn(200).when(httpResponse).getStatusCode(); - doReturn(JSONObjectUtils.parse(tokenResponse)).when(httpResponse).getContentAsJSONObject(); + doReturn(200).when(httpResponse).statusCode(); + doReturn(JSONObjectUtils.parse(tokenResponse)).when(httpResponse).getBodyAsJson(); final AuthenticationResult result = request.executeTokenRequest(); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java index c66e5f60..5c4aa40c 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java @@ -5,7 +5,6 @@ import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.SerializeException; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -43,7 +42,8 @@ void executeOAuthRequest_SCBadRequestErrorInvalidGrant_InteractionRequiredExcept OAuthHttpRequest msalOAuthHttpRequest = mock(OAuthHttpRequest.class); - HTTPResponse httpResponse = new HTTPResponse(HTTPResponse.SC_BAD_REQUEST); + HttpResponse httpResponse = new HttpResponse(); + httpResponse.statusCode(HttpHelper.HTTP_STATUS_400); String claims = "{\\\"access_token\\\":{\\\"polids\\\":{\\\"essential\\\":true,\\\"values\\\":[\\\"5ce770ea-8690-4747-aa73-c5b3cd509cd4\\\"]}}}"; @@ -55,8 +55,8 @@ void executeOAuthRequest_SCBadRequestErrorInvalidGrant_InteractionRequiredExcept "\"correlation_id\":\"3a...95a\"," + "\"suberror\":\"basic_action\"," + "\"claims\":\"" + claims + "\"}"; - httpResponse.setContent(content); - httpResponse.setContentType(HTTPContentType.ApplicationJSON.contentType); + httpResponse.body(content); + httpResponse.addHeader("Content-Type", HTTPContentType.ApplicationJSON.contentType); doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest(); doReturn(httpResponse).when(msalOAuthHttpRequest).send(); @@ -79,7 +79,8 @@ void executeOAuthRequest_SCBadRequestErrorInvalidGrant_SubErrorFilteredServiceEx OAuthHttpRequest msalOAuthHttpRequest = mock(OAuthHttpRequest.class); - HTTPResponse httpResponse = new HTTPResponse(HTTPResponse.SC_BAD_REQUEST); + HttpResponse httpResponse = new HttpResponse(); + httpResponse.statusCode(HttpHelper.HTTP_STATUS_400); String claims = "{\\\"access_token\\\":{\\\"polids\\\":{\\\"essential\\\":true,\\\"values\\\":[\\\"5ce770ea-8690-4747-aa73-c5b3cd509cd4\\\"]}}}"; @@ -91,8 +92,8 @@ void executeOAuthRequest_SCBadRequestErrorInvalidGrant_SubErrorFilteredServiceEx "\"correlation_id\":\"3a...95a\"," + "\"suberror\":\"client_mismatch\"," + "\"claims\":\"" + claims + "\"}"; - httpResponse.setContent(content); - httpResponse.setContentType(HTTPContentType.ApplicationJSON.contentType); + httpResponse.body(content); + httpResponse.addHeader("Content-Type", HTTPContentType.ApplicationJSON.contentType); doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest(); doReturn(httpResponse).when(msalOAuthHttpRequest).send(); @@ -181,7 +182,7 @@ void testToOAuthRequestNonEmptyCorrelationId() @Test void testToOAuthRequestNullCorrelationId_NullClientAuth() throws MalformedURLException, SerializeException, - URISyntaxException, ParseException { + URISyntaxException { PublicClientApplication app = PublicClientApplication.builder("id").correlationId("corr-id").build(); @@ -230,15 +231,13 @@ void testExecuteOAuth_Success() throws SerializeException, ParseException, MsalE final OAuthHttpRequest msalOAuthHttpRequest = mock(OAuthHttpRequest.class); - final HTTPResponse httpResponse = mock(HTTPResponse.class); + final HttpResponse httpResponse = mock(HttpResponse.class); doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest(); doReturn(httpResponse).when(msalOAuthHttpRequest).send(); - doReturn(JSONObjectUtils.parse(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE)).when(httpResponse).getContentAsJSONObject(); - - httpResponse.ensureStatusCode(200); + doReturn(JSONObjectUtils.parse(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE)).when(httpResponse).getBodyAsJson(); - doReturn(200).when(httpResponse).getStatusCode(); + doReturn(200).when(httpResponse).statusCode(); final AuthenticationResult result = request.executeTokenRequest(); @@ -275,22 +274,21 @@ void testExecuteOAuth_Failure() throws SerializeException, new AADAuthority(new URL(TestConstants.ORGANIZATIONS_AUTHORITY)), acr, serviceBundle)); final OAuthHttpRequest msalOAuthHttpRequest = mock(OAuthHttpRequest.class); - final HTTPResponse httpResponse = mock(HTTPResponse.class); + final HttpResponse httpResponse = mock(HttpResponse.class); doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest(); doReturn(httpResponse).when(msalOAuthHttpRequest).send(); - lenient().doReturn(402).when(httpResponse).getStatusCode(); - doReturn("403 Forbidden").when(httpResponse).getStatusMessage(); - doReturn(new HashMap<>()).when(httpResponse).getHeaderMap(); - doReturn(TestConfiguration.HTTP_ERROR_RESPONSE).when(httpResponse).getContent(); + lenient().doReturn(402).when(httpResponse).statusCode(); + doReturn(new HashMap<>()).when(httpResponse).headers(); + doReturn(TestConfiguration.HTTP_ERROR_RESPONSE).when(httpResponse).body(); final ErrorResponse errorResponse = mock(ErrorResponse.class); lenient().doReturn("invalid_request").when(errorResponse).error(); - lenient().doReturn(null).when(httpResponse).getHeaderValue("User-Agent"); - lenient().doReturn(null).when(httpResponse).getHeaderValue("x-ms-request-id"); - lenient().doReturn(null).when(httpResponse).getHeaderValue("x-ms-clitelem"); - doReturn(402).when(httpResponse).getStatusCode(); + lenient().doReturn(null).when(httpResponse).getHeader("User-Agent"); + lenient().doReturn(null).when(httpResponse).getHeader("x-ms-request-id"); + lenient().doReturn(null).when(httpResponse).getHeader("x-ms-clitelem"); + doReturn(402).when(httpResponse).statusCode(); assertThrows(MsalException.class, request::executeTokenRequest); } From 82d0ec8b0ac844a67fa24a33ad44b29027bb4b2d Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 1 Apr 2025 13:43:56 -0700 Subject: [PATCH 07/31] Remove com.nimbusds's imports related to Tokens --- .../microsoft/aad/msal4j/HttpResponse.java | 17 +-- .../aad/msal4j/TokenRequestExecutor.java | 17 +-- .../microsoft/aad/msal4j/TokenResponse.java | 117 +++++------------- .../aad/msal4j/CacheFormatTests.java | 16 ++- .../com/microsoft/aad/msal4j/TestHelper.java | 11 ++ .../aad/msal4j/TokenRequestExecutorTest.java | 3 +- .../aad/msal4j/TokenResponseTest.java | 75 ++--------- 7 files changed, 81 insertions(+), 175 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpResponse.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpResponse.java index 2b7c5ca2..e1e766b3 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpResponse.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpResponse.java @@ -3,10 +3,10 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; -import net.minidev.json.JSONObject; -import com.nimbusds.oauth2.sdk.ParseException; +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -62,11 +62,12 @@ List getHeader(String key) { return headers.get(key); } - JSONObject getBodyAsJson() { - try { - return JSONObjectUtils.parse(this.body()); - } catch (ParseException e) { - throw new RuntimeException(e); + Map getBodyAsMap() { + try (JsonReader reader = JsonProviders.createReader(this.body)) { + reader.nextToken(); + return reader.readMap(JsonReader::getString); + } catch (IOException e) { + throw new MsalClientException("Could not parse JSON from HttpResponse body: " + e.getMessage(), AuthenticationErrorCode.INVALID_JSON); } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java index aa5d002e..b86c58a2 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java @@ -6,7 +6,6 @@ import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.SerializeException; import com.nimbusds.oauth2.sdk.util.URLUtils; -import com.nimbusds.openid.connect.sdk.token.OIDCTokens; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -121,17 +120,11 @@ private AuthenticationResult createAuthenticationResultFromOauthHttpResponse( if (oauthHttpResponse.statusCode() == HttpHelper.HTTP_STATUS_200) { final TokenResponse response = TokenResponse.parseHttpResponse(oauthHttpResponse); - OIDCTokens tokens = response.getOIDCTokens(); - String refreshToken = null; - if (tokens.getRefreshToken() != null) { - refreshToken = tokens.getRefreshToken().getValue(); - } - AccountCacheEntity accountCacheEntity = null; - if (!StringHelper.isNullOrBlank(tokens.getIDTokenString())) { + if (!StringHelper.isNullOrBlank(response.idToken())) { String idTokenJson; try { - idTokenJson = new String(Base64.getDecoder().decode(tokens.getIDTokenString().split("\\.")[1]), StandardCharsets.UTF_8); + idTokenJson = new String(Base64.getDecoder().decode(response.idToken().split("\\.")[1]), StandardCharsets.UTF_8); } catch (ArrayIndexOutOfBoundsException e) { throw new MsalServiceException("Error parsing ID token, missing payload section. Ensure that the ID token is following the JWT format.", AuthenticationErrorCode.INVALID_JWT); @@ -161,10 +154,10 @@ private AuthenticationResult createAuthenticationResultFromOauthHttpResponse( long currTimestampSec = new Date().getTime() / 1000; result = AuthenticationResult.builder(). - accessToken(tokens.getAccessToken().getValue()). - refreshToken(refreshToken). + accessToken(response.accessToken()). + refreshToken(response.refreshToken()). familyId(response.getFoci()). - idToken(tokens.getIDTokenString()). + idToken(response.idToken()). environment(requestAuthority.host()). expiresOn(currTimestampSec + response.getExpiresIn()). extExpiresOn(response.getExtExpiresIn() > 0 ? currTimestampSec + response.getExtExpiresIn() : 0). diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java index 0fa4ff66..f8243217 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java @@ -3,15 +3,9 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.token.AccessToken; -import com.nimbusds.oauth2.sdk.token.RefreshToken; -import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; -import com.nimbusds.openid.connect.sdk.OIDCTokenResponse; -import com.nimbusds.openid.connect.sdk.token.OIDCTokens; -import net.minidev.json.JSONObject; +import java.util.Map; -class TokenResponse extends OIDCTokenResponse { +class TokenResponse { private String scope; private String clientInfo; @@ -19,92 +13,29 @@ class TokenResponse extends OIDCTokenResponse { private long extExpiresIn; private String foci; private long refreshIn; - - TokenResponse(final AccessToken accessToken, final RefreshToken refreshToken, final String idToken, - final String scope, String clientInfo, long expiresIn, long extExpiresIn, String foci, - long refreshIn) { - super(new OIDCTokens(idToken, accessToken, refreshToken)); - this.scope = scope; - this.clientInfo = clientInfo; - this.expiresIn = expiresIn; - this.extExpiresIn = extExpiresIn; - this.refreshIn = refreshIn; - this.foci = foci; + private String accessToken; + private String idToken; + private String refreshToken; + + TokenResponse(Map jsonMap) { + this.accessToken = jsonMap.get("access_token"); + this.idToken = jsonMap.get("id_token"); + this.refreshToken = jsonMap.get("refresh_token"); + this.scope = jsonMap.get("scope"); + this.clientInfo = jsonMap.get("client_info"); + this.expiresIn = StringHelper.isNullOrBlank(jsonMap.get("expires_in")) ? 0 : Long.parseLong(jsonMap.get("expires_in")); + this.extExpiresIn = StringHelper.isNullOrBlank(jsonMap.get("ext_expires_in")) ? 0 : Long.parseLong(jsonMap.get("ext_expires_in")); + this.refreshIn = StringHelper.isNullOrBlank(jsonMap.get("refresh_in")) ? 0: Long.parseLong(jsonMap.get("refresh_in")); + this.foci = jsonMap.get("foci"); } - static TokenResponse parseHttpResponse(final HttpResponse httpResponse) throws ParseException { + static TokenResponse parseHttpResponse(final HttpResponse httpResponse) { if (httpResponse.statusCode() != HttpHelper.HTTP_STATUS_200) { throw MsalServiceExceptionFactory.fromHttpResponse(httpResponse); } - final JSONObject jsonObject = httpResponse.getBodyAsJson(); - - return parseJsonObject(jsonObject); - } - - static Long getLongValue(JSONObject jsonObject, String key) throws ParseException { - Object value = jsonObject.get(key); - - if (value instanceof Long) { - return JSONObjectUtils.getLong(jsonObject, key); - } else { - return Long.parseLong(JSONObjectUtils.getString(jsonObject, key)); - } - } - - static TokenResponse parseJsonObject(final JSONObject jsonObject) - throws ParseException { - - // In same cases such as client credentials there isn't an id token. Instead of a null value - // use an empty string in order to avoid an IllegalArgumentException from OIDCTokens. - String idTokenValue = ""; - if (jsonObject.containsKey("id_token")) { - idTokenValue = JSONObjectUtils.getString(jsonObject, "id_token"); - } - - // Parse value - String scopeValue = null; - if (jsonObject.containsKey("scope")) { - scopeValue = JSONObjectUtils.getString(jsonObject, "scope"); - } - - String clientInfo = null; - if (jsonObject.containsKey("client_info")) { - clientInfo = JSONObjectUtils.getString(jsonObject, "client_info"); - } - - long expiresIn = 0; - if (jsonObject.containsKey("expires_in")) { - expiresIn = getLongValue(jsonObject, "expires_in"); - } - - long ext_expires_in = 0; - if (jsonObject.containsKey("ext_expires_in")) { - ext_expires_in = getLongValue(jsonObject, "ext_expires_in"); - } - - String foci = null; - if (jsonObject.containsKey("foci")) { - foci = JSONObjectUtils.getString(jsonObject, "foci"); - } - - long refreshIn = 0; - if (jsonObject.containsKey("refresh_in")) { - refreshIn = getLongValue(jsonObject, "refresh_in"); - } - - try { - final AccessToken accessToken = AccessToken.parse(jsonObject); - final RefreshToken refreshToken = RefreshToken.parse(jsonObject); - return new TokenResponse(accessToken, refreshToken, idTokenValue, scopeValue, clientInfo, - expiresIn, ext_expires_in, foci, refreshIn); - } catch (ParseException e) { - throw new MsalClientException("Invalid or missing token, could not parse. If using B2C, information on a potential B2C issue and workaround can be found here: https://aka.ms/msal4j-b2c-known-issues", - AuthenticationErrorCode.INVALID_JSON); - } catch (Exception e) { - throw new MsalClientException(e); - } + return new TokenResponse(httpResponse.getBodyAsMap()); } String getScope() { @@ -130,4 +61,16 @@ String getFoci() { long getRefreshIn() { return this.refreshIn; } + + public String accessToken() { + return accessToken; + } + + public String idToken() { + return idToken; + } + + public String refreshToken() { + return refreshToken; + } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java index 619ebcb4..ef5333f5 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java @@ -158,7 +158,7 @@ public void tokenCacheEntitiesFormatTest(String folder) throws URISyntaxExceptio doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest(); doReturn(httpResponse).when(msalOAuthHttpRequest).send(); doReturn(200).when(httpResponse).statusCode(); - doReturn(JSONObjectUtils.parse(tokenResponse)).when(httpResponse).getBodyAsJson(); + doReturn(TestHelper.convertJsonToMap((tokenResponse))).when(httpResponse).getBodyAsMap(); final AuthenticationResult result = request.executeTokenRequest(); @@ -186,14 +186,24 @@ private void validateAccessTokenCacheEntity(String folder, String tokenResponse, String valueExpected = readResource(folder + AT_CACHE_ENTITY); JSONObject tokenResponseJsonObj = JSONObjectUtils.parse(tokenResponse); - long expireIn = TokenResponse.getLongValue(tokenResponseJsonObj, "expires_in"); + long expireIn = getLongValue(tokenResponseJsonObj, "expires_in"); - long extExpireIn = TokenResponse.getLongValue(tokenResponseJsonObj, "ext_expires_in"); + long extExpireIn = getLongValue(tokenResponseJsonObj, "ext_expires_in"); JSONAssert.assertEquals(valueExpected, valueActual, new DynamicTimestampsComparator(JSONCompareMode.STRICT, expireIn, extExpireIn)); } + static Long getLongValue(JSONObject jsonObject, String key) throws ParseException { + Object value = jsonObject.get(key); + + if (value instanceof Long) { + return JSONObjectUtils.getLong(jsonObject, key); + } else { + return Long.parseLong(JSONObjectUtils.getString(jsonObject, key)); + } + } + private void validateRefreshTokenCacheEntity(String folder, TokenCache tokenCache) throws IOException, URISyntaxException, JSONException { diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java index 8760ddca..ed51252f 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java @@ -3,6 +3,8 @@ package com.microsoft.aad.msal4j; +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; import com.nimbusds.jose.*; import com.nimbusds.jose.crypto.RSASSASigner; import com.nimbusds.jose.jwk.RSAKey; @@ -188,4 +190,13 @@ static PrivateKey getPrivateKey() { return privateKey; } + + static Map convertJsonToMap(String jsonString) { + try (JsonReader reader = JsonProviders.createReader(jsonString)) { + reader.nextToken(); + return reader.readMap(JsonReader::getString); + } catch (IOException e) { + throw new MsalClientException("Could not parse JSON from HttpResponse body: " + e.getMessage(), AuthenticationErrorCode.INVALID_JSON); + } + } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java index 5c4aa40c..beecf09b 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java @@ -5,7 +5,6 @@ import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.SerializeException; -import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -235,7 +234,7 @@ void testExecuteOAuth_Success() throws SerializeException, ParseException, MsalE doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest(); doReturn(httpResponse).when(msalOAuthHttpRequest).send(); - doReturn(JSONObjectUtils.parse(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE)).when(httpResponse).getBodyAsJson(); + doReturn(TestHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE)).when(httpResponse).getBodyAsMap(); doReturn(200).when(httpResponse).statusCode(); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenResponseTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenResponseTest.java index 0744c224..cab65277 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenResponseTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenResponseTest.java @@ -3,77 +3,26 @@ package com.microsoft.aad.msal4j; -import java.text.ParseException; - -import com.nimbusds.jwt.JWT; -import com.nimbusds.oauth2.sdk.token.AccessToken; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; -import com.nimbusds.oauth2.sdk.token.RefreshToken; -import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; -import com.nimbusds.openid.connect.sdk.token.OIDCTokens; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertFalse; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TokenResponseTest { - private final String idToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1THdqcHdBSk9NOW4tQSJ9." - + "eyJhdWQiOiIyMTZlZjgxZC1mM2IyLTQ3ZDQtYWQyMS1hNGRmNDliNTZkZWUiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5l" - + "dC9kM2VhYjEzMi1iM2Y3LTRkNzktOTM5Yy0zMDIyN2FlYjhjMjYvIiwiaWF0IjoxMzkzNDk2MDM3LCJuYmYiOjEzOTM0OTYwMzcsI" - + "mV4cCI6MTM5MzQ5OTkzNywidmVyIjoiMS4wIiwidGlkIjoiZDNlYWIxMzItYjNmNy00ZDc5LTkzOWMtMzAyMjdhZWI4YzI2Iiwib2l" - + "kIjoiMzZiNjE4MTMtM2EyYi00NTA4LWFlOGQtZmM3NTQyMDE3NTlhIiwidXBuIjoibWVAa2FuaXNoa3BhbndhcmhvdG1haWwub25ta" - + "WNyb3NvZnQuY29tIiwidW5pcXVlX25hbWUiOiJtZUBrYW5pc2hrcGFud2FyaG90bWFpbC5vbm1pY3Jvc29mdC5jb20iLCJzdWIiOiJ" - + "mZU40RU4wTW1vQ3ZubFZoRk1KeWozMzRSd0NaTGxrdTFfMVQ1VlNSN0xrIiwiZmFtaWx5X25hbWUiOiJQYW53YXIiLCJnaXZlbl9uYW" - + "1lIjoiS2FuaXNoayIsIm5vbmNlIjoiYTM1OWY0MGItNDJhOC00YTRjLTk2YWMtMTE0MjRhZDk2N2U5IiwiY19oYXNoIjoib05kOXE1e" - + "m1fWTZQaUNpYTg1MDZUQSJ9.iyGfoL0aKai-rZVGFwaCYm73h2Dk93M80CRAOoIwlxAKfGrQ2YDbvAPIvlQUrNQacqzenmkJvVEMqXT" - + "OYO5teyweUkxruod_iMgmhC6RZZZ603vMoqItUVu8c-4Y3KIEweRi17BYjdR2_tEowPlcEteRY52nwCmiNJRQnkqnQ2aZP89Jzhb9qw" - + "_G3CeYsOmV4f7jUp7anDT9hae7eGuvdUAf4LTDD6hFTBJP8MsyuMD6DkgBytlSxaXXJBKBJ5r5XPHdtStCTNF7edktlSufA2owTWVGw" - + "gWpKmnue_2Mgl3jBozTSJJ34r-R6lnWWeN6lqZ2Svw7saI5pmPtC8OZbw"; - - private final long expiresIn = 12345678910L; - private final long extExpiresIn = 12345678910L; - private final long refreshIn = 0; - - @Test - public void testConstructor() throws ParseException { - final TokenResponse response = new TokenResponse( - new BearerAccessToken("access_token"), new RefreshToken( - "refresh_token"), idToken, null, null, expiresIn, extExpiresIn, null, refreshIn); - assertNotNull(response); - OIDCTokens tokens = response.getOIDCTokens(); - assertNotNull(tokens); - final JWT jwt = tokens.getIDToken(); - assertTrue(jwt.getJWTClaimsSet().getClaims().size() >= 0); - } - @Test - public void testParseJsonObject() - throws com.nimbusds.oauth2.sdk.ParseException { - final TokenResponse response = TokenResponse - .parseJsonObject(JSONObjectUtils - .parse(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE)); - assertNotNull(response); - OIDCTokens tokens = response.getOIDCTokens(); - assertNotNull(tokens); - assertNotNull(tokens.getIDToken()); - assertFalse(StringHelper.isBlank(tokens.getIDTokenString())); - assertFalse(StringHelper.isBlank(response.getScope())); - } - - @Test - public void testEmptyIdToken() { - final TokenResponse response = new TokenResponse( - new BearerAccessToken(idToken), - new RefreshToken("refresh_token"), - "", null, null, expiresIn, extExpiresIn, null, refreshIn); - + void testConstructor() { + final Map jsonMap = TestHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE); + final TokenResponse response = new TokenResponse(TestHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE)); assertNotNull(response); - OIDCTokens tokens = response.getOIDCTokens(); - assertNotNull(tokens); - final AccessToken accessToken = tokens.getAccessToken(); - assertNotNull(accessToken); + assertEquals(jsonMap.get("access_token"), response.accessToken()); + assertEquals(jsonMap.get("id_token"), response.idToken()); + assertEquals(jsonMap.get("client_info"), response.getClientInfo()); + assertEquals(jsonMap.get("scope"), response.getScope()); + assertEquals(jsonMap.get("expires_in"), Long.toString(response.getExpiresIn())); } } From be2debcc5f21343606f1df7a6d5995cbf5cb10b4 Mon Sep 17 00:00:00 2001 From: avdunn Date: Fri, 4 Apr 2025 10:46:18 -0700 Subject: [PATCH 08/31] Address PR feedback --- .../com/microsoft/aad/msal4j/HttpResponse.java | 7 +------ .../com/microsoft/aad/msal4j/JsonHelper.java | 11 +++++++++++ .../microsoft/aad/msal4j/CacheFormatTests.java | 2 +- .../aad/msal4j/RequestThrottlingTest.java | 2 +- .../aad/msal4j/TestConfiguration.java | 17 ++++++++++++++++- .../com/microsoft/aad/msal4j/TestHelper.java | 9 --------- .../aad/msal4j/TokenRequestExecutorTest.java | 2 +- .../aad/msal4j/TokenResponseTest.java | 18 +++++++++++++++--- .../aad/msal4j/UIRequiredCacheTest.java | 2 +- 9 files changed, 47 insertions(+), 23 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpResponse.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpResponse.java index e1e766b3..136fb600 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpResponse.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpResponse.java @@ -63,12 +63,7 @@ List getHeader(String key) { } Map getBodyAsMap() { - try (JsonReader reader = JsonProviders.createReader(this.body)) { - reader.nextToken(); - return reader.readMap(JsonReader::getString); - } catch (IOException e) { - throw new MsalClientException("Could not parse JSON from HttpResponse body: " + e.getMessage(), AuthenticationErrorCode.INVALID_JSON); - } + return JsonHelper.convertJsonToMap(this.body); } public int statusCode() { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java index 0adbe3a6..09cf1ced 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; +import java.util.Map; import java.util.Set; class JsonHelper { @@ -51,6 +52,16 @@ static > T convertJsonStringToJsonSerializableObje } } + //Converts a JSON string to a Map + static Map convertJsonToMap(String jsonString) { + try (JsonReader reader = JsonProviders.createReader(jsonString)) { + reader.nextToken(); + return reader.readMap(JsonReader::getString); + } catch (IOException e) { + throw new MsalClientException("Could not parse JSON from HttpResponse body: " + e.getMessage(), AuthenticationErrorCode.INVALID_JSON); + } + } + /** * Throws exception if given String does not follow JSON syntax */ diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java index ef5333f5..7b208e29 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java @@ -158,7 +158,7 @@ public void tokenCacheEntitiesFormatTest(String folder) throws URISyntaxExceptio doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest(); doReturn(httpResponse).when(msalOAuthHttpRequest).send(); doReturn(200).when(httpResponse).statusCode(); - doReturn(TestHelper.convertJsonToMap((tokenResponse))).when(httpResponse).getBodyAsMap(); + doReturn(JsonHelper.convertJsonToMap((tokenResponse))).when(httpResponse).getBodyAsMap(); final AuthenticationResult result = request.executeTokenRequest(); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java index 990b59b4..71505e8a 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java @@ -98,7 +98,7 @@ private PublicClientApplication getClientApplicationMockedWithOneTokenEndpointRe switch (responseType) { case RETRY_AFTER_HEADER: httpResponse.statusCode(HTTPResponse.SC_OK); - httpResponse.body(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE); + httpResponse.body(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS); headers.put("Retry-After", Arrays.asList(THROTTLE_IN_SEC.toString())); break; diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestConfiguration.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestConfiguration.java index 2ddaec54..53e648bd 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestConfiguration.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestConfiguration.java @@ -43,7 +43,7 @@ public final class TestConfiguration { public final static String AAD_PREFERRED_NETWORK_ENV_ALIAS = "login.microsoftonline.com"; - public final static String TOKEN_ENDPOINT_OK_RESPONSE = "{\"access_token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6I" + public final static String TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS = "{\"access_token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6I" + "k5HVEZ2ZEstZnl0aEV1THdqcHdBSk9NOW4tQSJ9.eyJhdWQiOiJiN2E2NzFkOC1hNDA4LTQyZmYtODZlMC1hYWY0NDdmZDE3YzQiLCJpc3MiOiJod" + "HRwczovL3N0cy53aW5kb3dzLm5ldC8zMGJhYTY2Ni04ZGY4LTQ4ZTctOTdlNi03N2NmZDA5OTU5NjMvIiwiaWF0IjoxMzkzODQ0NTA0LCJuYmYiOj" + "EzOTM4NDQ1MDQsImV4cCI6MTM5Mzg0ODQwNCwidmVyIjoiMS4wIiwidGlkIjoiMzBiYWE2NjYtOGRmOC00OGU3LTk3ZTYtNzdjZmQwOTk1OTYzIiwi" @@ -66,6 +66,21 @@ public final class TestConfiguration { "WQiOiI5ZjQ4ODBkOC04MGJhLTRjNDAtOTdiYy1mN2EyM2M3MDMwODQiLCJ1dGlkIjoiZjY0NWFkOTItZTM4ZC00ZDFhLWI1MTAtZDFiMDlhNzRhOGNhIn0\"" + "}"; + public final static String TOKEN_ENDPOINT_OK_RESPONSE_ACCESS_ONLY = "{\"token_type\":\"Bearer\",\"expires_in\":3600," + + "\"access_token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1THdqcHdBSk9NOW4tQSJ9" + + ".eyJhdWQiOiJiN2E2NzFkOC1hNDA4LTQyZmYtODZlMC1hYWY0NDdmZDE3YzQiLCJpc3MiOiJodRwczovL3N0cy53aW5kb3dzLm5ldC8" + + "zMGJhYTY2Ni04ZGY4LTQ4ZTctOTdlNi03N2NmZDA5OTU5NjMvIiwiaWF0IjoxMzkzODQ0NTA0LCJuYmYiOjEzOTM4NDQ1MDQsImV4cC" + + "I6MTM5Mzg0ODQwNCwidmVyIjoiMS4wIiwidGlkIjoiMzBiYWE2NjYtOGRmOC00OGU3LTk3ZTYtNzdjZmQwOTk1OTYzIiwib2lkIjoiN" + + "GY4NTk5ODktYTJmZi00MTFlLTkwNDgtYzMyMjI0N2FjNjJjIiwidXBuIjoiYWRtaW5AYWFsdGVzdHMub25taWNyb3NvZnQuY29tIiwi" + + "dW5pcXVlX25hbWUiOiJhZG1pbkBhYWx0ZXN0cy5vbm1pY3Jvc29mdC5jb20iLCJzdWIiOiJqQ0ttUENWWEFzblN1MVBQUzRWblo4c2V" + + "ONTR3U3F0cW1RkpGbW14SEF3IiwiZmFtaWx5X25hbWUiOiJBZG1pbiIsImdpdmVuX25hbWUiOiJBREFMVGVzdHMiLCJhcHBpZCI6Ijk" + + "wODNjY2I4LThhNDYtNDNlNy04NDM5LTFkNjk2ZGY5ODRhZSIsImFwcGlkYWNyIjoiMSIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbi" + + "IsImFjciI6IjEifQ.lUfDlkLdNGuAGUukgTnS_uWeSFXljbhId1l9PDrr7AwOSbOzogLvO14TaU294T6HeOQ8e0dUAvxEAMvsK_800A" + + "-AGNvbHK363xDjgmu464ye3CQvwq73GoHkzuxILCJKo0DUj0_XsCpQ4TdkPepuhzaGc-zYsfMU1POuIOB87pzW7e_VDpCdxcN1fuk-7" + + "CECPQb8nrO0L8Du8y-TZDdTSe-i_A0Alv48Zll-6tDY9cxfAR0UyYKl_Kf45kuHAphCWwPsjUxv4rGHhgXZPRlKFq7bkXP2Es4ixCQz" + + "b3bVLLrtQaZjkQ1yn37ngJro8NR63EbHHjHTA9lRmf8KIQ\"" + + "}"; + public final static String HTTP_ERROR_RESPONSE = "{\"error\":\"invalid_request\",\"error_description\":\"AADSTS90011: Request " + "is ambiguous, multiple application identifiers found. Application identifiers: 'd09bb6da-4d46-4a16-880c-7885d8291fb9" + ", 216ef81d-f3b2-47d4-ad21-a4df49b56dee'.\r\nTrace ID: 428a1f68-767d-4a1c-ae8e-f710eeaf4e9b\r\nCorrelation ID: 1e0955" diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java index ed51252f..8bf51d4f 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java @@ -190,13 +190,4 @@ static PrivateKey getPrivateKey() { return privateKey; } - - static Map convertJsonToMap(String jsonString) { - try (JsonReader reader = JsonProviders.createReader(jsonString)) { - reader.nextToken(); - return reader.readMap(JsonReader::getString); - } catch (IOException e) { - throw new MsalClientException("Could not parse JSON from HttpResponse body: " + e.getMessage(), AuthenticationErrorCode.INVALID_JSON); - } - } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java index beecf09b..7d3668e3 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java @@ -234,7 +234,7 @@ void testExecuteOAuth_Success() throws SerializeException, ParseException, MsalE doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest(); doReturn(httpResponse).when(msalOAuthHttpRequest).send(); - doReturn(TestHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE)).when(httpResponse).getBodyAsMap(); + doReturn(JsonHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS)).when(httpResponse).getBodyAsMap(); doReturn(200).when(httpResponse).statusCode(); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenResponseTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenResponseTest.java index cab65277..adaf96eb 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenResponseTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenResponseTest.java @@ -10,14 +10,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TokenResponseTest { @Test - void testConstructor() { - final Map jsonMap = TestHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE); - final TokenResponse response = new TokenResponse(TestHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE)); + void testConstructor_PublicResponse() { + final Map jsonMap = JsonHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS); + final TokenResponse response = new TokenResponse(JsonHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS)); assertNotNull(response); assertEquals(jsonMap.get("access_token"), response.accessToken()); assertEquals(jsonMap.get("id_token"), response.idToken()); @@ -25,4 +26,15 @@ void testConstructor() { assertEquals(jsonMap.get("scope"), response.getScope()); assertEquals(jsonMap.get("expires_in"), Long.toString(response.getExpiresIn())); } + + @Test + void testConstructor_CLientCredentialResponse() { + final Map jsonMap = JsonHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ACCESS_ONLY); + final TokenResponse response = new TokenResponse(JsonHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ACCESS_ONLY)); + assertNotNull(response); + assertEquals(jsonMap.get("access_token"), response.accessToken()); + assertNull(response.idToken()); + assertNull(response.getClientInfo()); + assertEquals(jsonMap.get("expires_in"), Long.toString(response.getExpiresIn())); + } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java index 00e29882..d27c82a6 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java @@ -89,7 +89,7 @@ private PublicClientApplication getApp_MockedWith_OKTokenEndpointResponse_Invali IHttpClient httpClientMock = mock(IHttpClient.class); HttpResponse httpResponse = - getHttpResponse(HTTPResponse.SC_OK, TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE); + getHttpResponse(HTTPResponse.SC_OK, TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS); lenient().doReturn(httpResponse).when(httpClientMock).send(any()); httpResponse = getHttpResponse(HTTPResponse.SC_UNAUTHORIZED, From dc13c462ad64311d0f3dcd00c89004be03f8911c Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 10 Apr 2025 14:28:29 -0700 Subject: [PATCH 09/31] Address PR feedback --- ...uireTokenByAuthorizationGrantSupplier.java | 2 +- .../aad/msal4j/OAuthAuthorizationGrant.java | 33 ++++++++++++------- .../aad/msal4j/OnBehalfOfRequest.java | 2 +- .../MsalOauthAuthorizatonGrantTest.java | 2 +- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java index 85917b97..ce0f1234 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java @@ -104,7 +104,7 @@ private OAuthAuthorizationGrant processPasswordGrant( Map> params = getSAMLAuthGrantParameters(response); params.putAll(authGrant.toParameters()); - authGrant = new OAuthAuthorizationGrant(params, null, null); + authGrant = new OAuthAuthorizationGrant(params, null); } return authGrant; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java index 7bb5b819..1f9be3c4 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java @@ -19,36 +19,45 @@ class OAuthAuthorizationGrant extends AbstractMsalAuthorizationGrant { * * @param params parameters relevant for the specific authorization grant type * @param scopes additional scopes which will be added to a default set of common scopes - * @param claims optional claims */ - OAuthAuthorizationGrant(Map> params, Set scopes, ClaimsRequest claims) { + OAuthAuthorizationGrant(Map> params, Set scopes) { this.scopes = new HashSet<>(AbstractMsalAuthorizationGrant.COMMON_SCOPES); if (scopes != null) { this.scopes.addAll(scopes); } + // Default scopes that apply to most flows this.params.put(SCOPE_PARAM_NAME, Collections.singletonList(String.join(" ", this.scopes))); + // Parameter to request client info from the endpoint + this.params.put("client_info", Collections.singletonList("1")); if (params != null) { this.params.putAll(params); } + } - if (claims != null) { - this.claims = claims; - this.params.put("claims", Collections.singletonList(claims.formatAsJSONString())); + /** + * Constructor to create an OAuthAuthorizationGrant + * + * @param params parameters relevant for the specific authorization grant type + * @param scopes additional scopes which will be added to a default set of common scopes + * @param claims optional claims + */ + OAuthAuthorizationGrant(Map> params, Set scopes, ClaimsRequest claims) { + this(params, scopes); + + if (claims != null) { + this.claims = claims; + this.params.put("claims", Collections.singletonList(claims.formatAsJSONString())); + } } - } /** - * Returns an unmodifiable version of the parameters map, and adds the client_info parameter + * Returns an unmodifiable version of the parameters map */ @Override public Map> toParameters() { - final Map> outParams = new LinkedHashMap<>(params); - - outParams.put("client_info", Collections.singletonList("1")); - - return Collections.unmodifiableMap(outParams); + return Collections.unmodifiableMap(new LinkedHashMap<>(params)); } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java index 8f973ce5..6a5f0e9f 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java @@ -29,7 +29,7 @@ private static OAuthAuthorizationGrant createAuthenticationGrant(OnBehalfOfParam params.put("claims", Collections.singletonList(parameters.claims().formatAsJSONString())); } - return new OAuthAuthorizationGrant(params, parameters.scopes(), null); + return new OAuthAuthorizationGrant(params, parameters.scopes()); } OnBehalfOfParameters parameters() { diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java index a4b54fd2..807a857f 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java @@ -22,7 +22,7 @@ void testToParameters() { Map> params = new LinkedHashMap<>(); params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList("SomeGrantType")); - final OAuthAuthorizationGrant grant = new OAuthAuthorizationGrant(params, null, null); + final OAuthAuthorizationGrant grant = new OAuthAuthorizationGrant(params, null); assertNotNull(grant); assertNotNull(grant.toParameters()); From 16438908beaac33ccb9bef8868f4f47f9afefe0e Mon Sep 17 00:00:00 2001 From: avdunn Date: Wed, 16 Apr 2025 09:57:11 -0700 Subject: [PATCH 10/31] PR feedback, and correctly adjust parameters in ADFS username/password scenarios --- .../infrastructure/SeleniumExtensions.java | 2 +- ...uireTokenByAuthorizationGrantSupplier.java | 28 +++++-------- .../aad/msal4j/OAuthAuthorizationGrant.java | 42 ++++++++++++------- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/msal4j-sdk/src/integrationtest/java/infrastructure/SeleniumExtensions.java b/msal4j-sdk/src/integrationtest/java/infrastructure/SeleniumExtensions.java index ffcad117..1eb04821 100644 --- a/msal4j-sdk/src/integrationtest/java/infrastructure/SeleniumExtensions.java +++ b/msal4j-sdk/src/integrationtest/java/infrastructure/SeleniumExtensions.java @@ -40,7 +40,7 @@ public static WebDriver createDefaultWebDriver() { //No visual rendering, remove to see browser window when debugging options.addArguments("--headless"); //Add to avoid issues if your real browser's history/cookies are affecting tests, should not be needed in ADO pipelines - //options.addArguments("--incognito"); + options.addArguments("--incognito"); System.setProperty("webdriver.chrome.driver", "C:/Windows/chromedriver.exe"); ChromeDriver driver = new ChromeDriver(options); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java index ce0f1234..8c448e98 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java @@ -39,8 +39,7 @@ AuthenticationResult execute() throws Exception { } if (authGrant instanceof OAuthAuthorizationGrant) { - msalRequest.msalAuthorizationGrant = - processPasswordGrant((OAuthAuthorizationGrant) authGrant); + processPasswordGrant((OAuthAuthorizationGrant) authGrant); } if (authGrant instanceof IntegratedWindowsAuthorizationGrant) { @@ -74,19 +73,16 @@ private boolean IsUiRequiredCacheSupported() { clientApplication instanceof PublicClientApplication; } - private OAuthAuthorizationGrant processPasswordGrant( - OAuthAuthorizationGrant authGrant) throws Exception { - - if (!(authGrant.toParameters().get(GrantConstants.GRANT_TYPE_PARAMETER).get(0).equals(GrantConstants.PASSWORD))) { - return authGrant; - } + private void processPasswordGrant(OAuthAuthorizationGrant authGrant) throws Exception { - if (msalRequest.application().authenticationAuthority.authorityType != AuthorityType.AAD) { - return authGrant; + //Additional processing is only needed if it's a password grant with a non-AAD authority + if (!(authGrant.getParamValue(GrantConstants.GRANT_TYPE_PARAMETER).equals(GrantConstants.PASSWORD)) + || msalRequest.application().authenticationAuthority.authorityType != AuthorityType.AAD) { + return; } UserDiscoveryResponse userDiscoveryResponse = UserDiscoveryRequest.execute( - this.clientApplication.authenticationAuthority.getUserRealmEndpoint(authGrant.toParameters().get("username").get(0)), + this.clientApplication.authenticationAuthority.getUserRealmEndpoint(authGrant.getParamValue(GrantConstants.USERNAME_PARAMETER)), msalRequest.headers().getReadonlyHeaderMap(), msalRequest.requestContext(), this.clientApplication.serviceBundle()); @@ -94,19 +90,15 @@ private OAuthAuthorizationGrant processPasswordGrant( if (userDiscoveryResponse.isAccountFederated()) { WSTrustResponse response = WSTrustRequest.execute( userDiscoveryResponse.federationMetadataUrl(), - authGrant.toParameters().get(GrantConstants.USERNAME_PARAMETER).get(0), - authGrant.toParameters().get(GrantConstants.PASSWORD_PARAMETER).get(0), + authGrant.getParamValue(GrantConstants.USERNAME_PARAMETER), + authGrant.getParamValue(GrantConstants.PASSWORD_PARAMETER), userDiscoveryResponse.cloudAudienceUrn(), msalRequest.requestContext(), this.clientApplication.serviceBundle(), this.clientApplication.logPii()); - Map> params = getSAMLAuthGrantParameters(response); - params.putAll(authGrant.toParameters()); - - authGrant = new OAuthAuthorizationGrant(params, null); + authGrant.addAndReplaceParams(getSAMLAuthGrantParameters(response)); } - return authGrant; } private Map> getSAMLAuthGrantParameters(WSTrustResponse response) { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java index 1f9be3c4..3ae69f4c 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java @@ -37,21 +37,35 @@ class OAuthAuthorizationGrant extends AbstractMsalAuthorizationGrant { } } - /** - * Constructor to create an OAuthAuthorizationGrant - * - * @param params parameters relevant for the specific authorization grant type - * @param scopes additional scopes which will be added to a default set of common scopes - * @param claims optional claims - */ - OAuthAuthorizationGrant(Map> params, Set scopes, ClaimsRequest claims) { - this(params, scopes); - - if (claims != null) { - this.claims = claims; - this.params.put("claims", Collections.singletonList(claims.formatAsJSONString())); - } + /** + * Constructor to create an OAuthAuthorizationGrant + * + * @param params parameters relevant for the specific authorization grant type + * @param scopes additional scopes which will be added to a default set of common scopes + * @param claims optional claims + */ + OAuthAuthorizationGrant(Map> params, Set scopes, ClaimsRequest claims) { + this(params, scopes); + + if (claims != null) { + this.claims = claims; + this.params.put("claims", Collections.singletonList(claims.formatAsJSONString())); + } + } + + void addAndReplaceParams(Map> params) { + if (params != null) { + //putAll() will overwrite existing values if the key already exists in the map + this.params.putAll(params); } + } + + String getParamValue(String paramKey) { + if (this.params.containsKey(paramKey)) { + return this.params.get(paramKey).get(0); + } + return null; + } /** * Returns an unmodifiable version of the parameters map From f2644562396a2fcde0521be4948954540073580c Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 22 Apr 2025 17:18:48 -0700 Subject: [PATCH 11/31] Remove and replace various Nimbus imports --- .../aad/msal4j/AuthenticationErrorCode.java | 5 + .../microsoft/aad/msal4j/ClientAssertion.java | 3 +- .../com/microsoft/aad/msal4j/ClientInfo.java | 6 +- .../com/microsoft/aad/msal4j/IBroker.java | 18 ++-- .../com/microsoft/aad/msal4j/IdToken.java | 41 -------- .../com/microsoft/aad/msal4j/JwtHelper.java | 93 ++++++++++--------- .../aad/msal4j/SAML11BearerGrant.java | 33 ------- .../aad/msal4j/TokenRequestExecutor.java | 10 +- .../aad/msal4j/HelperAndUtilityTests.java | 72 ++++++++++++++ 9 files changed, 149 insertions(+), 132 deletions(-) delete mode 100644 msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SAML11BearerGrant.java create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/HelperAndUtilityTests.java diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java index 5c568831..690a1e2c 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java @@ -93,6 +93,11 @@ public class AuthenticationErrorCode { */ public final static String INVALID_REDIRECT_URI = "invalid_redirect_uri"; + /** + * Indicates token endpoint is invalid. Ensure authority and tenant are correctly set, as this endpoint is typically created using those values. + */ + public final static String INVALID_ENDPOINT_URI = "invalid_endpoint_uri"; + /** * MSAL was unable to open the user-default browser. This is either because the current platform * does not support {@link java.awt.Desktop} or {@link java.awt.Desktop.Action#BROWSE}. Interactive diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java index 12c9e566..7c4e456b 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java @@ -3,7 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.auth.JWTAuthentication; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.experimental.Accessors; @@ -13,7 +12,7 @@ @EqualsAndHashCode final class ClientAssertion implements IClientAssertion { - static final String assertionType = JWTAuthentication.CLIENT_ASSERTION_TYPE; + static final String ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; private final String assertion; ClientAssertion(final String assertion) { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientInfo.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientInfo.java index c9b2f83a..f09d2a93 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientInfo.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientInfo.java @@ -4,8 +4,8 @@ package com.microsoft.aad.msal4j; import com.fasterxml.jackson.annotation.JsonProperty; -import com.nimbusds.jose.util.StandardCharset; +import java.nio.charset.StandardCharsets; import java.util.Base64; import static com.microsoft.aad.msal4j.Constants.POINT_DELIMITER; @@ -23,9 +23,9 @@ public static ClientInfo createFromJson(String clientInfoJsonBase64Encoded) { return null; } - byte[] decodedInput = Base64.getUrlDecoder().decode(clientInfoJsonBase64Encoded.getBytes(StandardCharset.UTF_8)); + byte[] decodedInput = Base64.getUrlDecoder().decode(clientInfoJsonBase64Encoded.getBytes(StandardCharsets.UTF_8)); - return JsonHelper.convertJsonToObject(new String(decodedInput, StandardCharset.UTF_8), ClientInfo.class); + return JsonHelper.convertJsonToObject(new String(decodedInput, StandardCharsets.UTF_8), ClientInfo.class); } String toAccountIdentifier() { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java index ab3f0ce0..9f50fc7a 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java @@ -3,9 +3,9 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.jwt.JWTParser; - import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.concurrent.CompletableFuture; /** @@ -67,11 +67,17 @@ default IAuthenticationResult parseBrokerAuthResult(String authority, String idT if (idToken != null) { builder.idToken(idToken); if (accountId != null) { - String idTokenJson = - JWTParser.parse(idToken).getParsedParts()[1].decodeToString(); + String idTokenJson; + + try { + idTokenJson = new String(Base64.getDecoder().decode(idToken.split("\\.")[1]), StandardCharsets.UTF_8); + } catch (ArrayIndexOutOfBoundsException e) { + throw new MsalServiceException("Error parsing ID token, missing payload section. Ensure that the ID token is following the JWT format.", + AuthenticationErrorCode.INVALID_JWT); + } + builder.accountCacheEntity(AccountCacheEntity.create(clientInfo, - Authority.createAuthority(new URL(authority)), JsonHelper.convertJsonToObject(idTokenJson, - IdToken.class), null)); + Authority.createAuthority(new URL(authority)), JsonHelper.convertJsonToObject(idTokenJson, IdToken.class), null)); } } if (accessToken != null) { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdToken.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdToken.java index 04964b36..0425f368 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdToken.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdToken.java @@ -4,29 +4,10 @@ package com.microsoft.aad.msal4j; import com.fasterxml.jackson.annotation.JsonProperty; -import com.nimbusds.jwt.JWTClaimsSet; - -import java.text.ParseException; -import java.util.HashMap; -import java.util.Map; - import java.io.Serializable; class IdToken implements Serializable { - static final String ISSUER = "iss"; - static final String SUBJECT = "sub"; - static final String AUDIENCE = "aud"; - static final String EXPIRATION_TIME = "exp"; - static final String ISSUED_AT = "issuedAt"; - static final String NOT_BEFORE = "nbf"; - static final String NAME = "name"; - static final String PREFERRED_USERNAME = "preferred_username"; - static final String OBJECT_IDENTIFIER = "oid"; - static final String TENANT_IDENTIFIER = "tid"; - static final String UPN = "upn"; - static final String UNIQUE_NAME = "unique_name"; - @JsonProperty("iss") protected String issuer; @@ -62,26 +43,4 @@ class IdToken implements Serializable { @JsonProperty("unique_name") protected String uniqueName; - - static IdToken createFromJWTClaims(final JWTClaimsSet claims) throws ParseException { - IdToken idToken = new IdToken(); - - idToken.issuer = claims.getStringClaim(ISSUER); - idToken.subject = claims.getStringClaim(SUBJECT); - idToken.audience = claims.getStringClaim(AUDIENCE); - - idToken.expirationTime = claims.getLongClaim(EXPIRATION_TIME); - idToken.issuedAt = claims.getLongClaim(ISSUED_AT); - idToken.notBefore = claims.getLongClaim(NOT_BEFORE); - - idToken.name = claims.getStringClaim(NAME); - idToken.preferredUsername = claims.getStringClaim(PREFERRED_USERNAME); - idToken.objectIdentifier = claims.getStringClaim(OBJECT_IDENTIFIER); - idToken.tenantIdentifier = claims.getStringClaim(TENANT_IDENTIFIER); - - idToken.upn = claims.getStringClaim(UPN); - idToken.uniqueName = claims.getStringClaim(UNIQUE_NAME); - - return idToken; - } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java index 2e928809..bca0cb82 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java @@ -3,76 +3,85 @@ package com.microsoft.aad.msal4j; +import java.nio.charset.StandardCharsets; +import java.security.Signature; import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; +import java.util.Base64; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.JWSHeader.Builder; -import com.nimbusds.jose.crypto.RSASSASigner; -import com.nimbusds.jose.util.Base64; -import com.nimbusds.jose.util.Base64URL; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.SignedJWT; - final class JwtHelper { static ClientAssertion buildJwt(String clientId, final ClientCertificate credential, final String jwtAudience, boolean sendX5c, boolean useSha1) throws MsalClientException { - if (StringHelper.isBlank(clientId)) { - throw new IllegalArgumentException("clientId is null or empty"); - } - - if (credential == null) { - throw new IllegalArgumentException("credential is null"); - } - final long time = System.currentTimeMillis(); + ParameterValidationUtils.validateNotBlank("clientId", clientId); + ParameterValidationUtils.validateNotNull("credential", clientId); - final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() - .audience(Collections.singletonList(jwtAudience)) - .issuer(clientId) - .jwtID(UUID.randomUUID().toString()) - .notBeforeTime(new Date(time)) - .expirationTime(new Date(time - + Constants.AAD_JWT_TOKEN_LIFETIME_SECONDS - * 1000)) - .subject(clientId) - .build(); - - SignedJWT jwt; try { - JWSHeader.Builder builder = new Builder(JWSAlgorithm.RS256); + final long time = System.currentTimeMillis(); + + // Build header + Map header = new HashMap<>(); + header.put("alg", "RS256"); + header.put("typ", "JWT"); if (sendX5c) { - List certs = new ArrayList<>(); + List certs = new ArrayList<>(); for (String cert : credential.getEncodedPublicKeyCertificateChain()) { - certs.add(new Base64(cert)); + certs.add(cert); } - builder.x509CertChain(certs); + header.put("x5c", certs); } //SHA-256 is preferred, however certain flows still require SHA-1 due to what is supported server-side. If SHA-256 // is not supported or the IClientCredential.publicCertificateHash256() method is not implemented, the library will default to SHA-1. String hash256 = credential.publicCertificateHash256(); if (useSha1 || hash256 == null) { - builder.x509CertThumbprint(new Base64URL(credential.publicCertificateHash())); + header.put("x5t", credential.publicCertificateHash()); } else { - builder.x509CertSHA256Thumbprint(new Base64URL(hash256)); + header.put("x5t#S256", hash256); } - jwt = new SignedJWT(builder.build(), claimsSet); - final RSASSASigner signer = new RSASSASigner(credential.privateKey()); + // Build payload + Map payload = new HashMap<>(); + payload.put("aud", jwtAudience); + payload.put("iss", clientId); + payload.put("jti", UUID.randomUUID().toString()); + payload.put("nbf", time / 1000); + payload.put("exp", time / 1000 + Constants.AAD_JWT_TOKEN_LIFETIME_SECONDS); + payload.put("sub", clientId); + + // Concatenate header and payload + String jsonHeader = JsonHelper.mapper.writeValueAsString(header); + String jsonPayload = JsonHelper.mapper.writeValueAsString(payload); - jwt.sign(signer); + String encodedHeader = base64UrlEncode(jsonHeader.getBytes(StandardCharsets.UTF_8)); + String encodedPayload = base64UrlEncode(jsonPayload.getBytes(StandardCharsets.UTF_8)); + + // Create signature + String dataToSign = encodedHeader + "." + encodedPayload; + + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initSign(credential.privateKey()); + sig.update(dataToSign.getBytes(StandardCharsets.UTF_8)); + byte[] signatureBytes = sig.sign(); + + String encodedSignature = base64UrlEncode(signatureBytes); + + // Build the JWT + String jwt = dataToSign + "." + encodedSignature; + + return new ClientAssertion(jwt); } catch (final Exception e) { throw new MsalClientException(e); } + } - return new ClientAssertion(jwt.serialize()); + private static String base64UrlEncode(byte[] data) { + return Base64.getUrlEncoder().withoutPadding().encodeToString(data); } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SAML11BearerGrant.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SAML11BearerGrant.java deleted file mode 100644 index 54f10d49..00000000 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SAML11BearerGrant.java +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.aad.msal4j; - -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import com.nimbusds.jose.util.Base64URL; -import com.nimbusds.oauth2.sdk.GrantType; -import com.nimbusds.oauth2.sdk.SAML2BearerGrant; - -class SAML11BearerGrant extends SAML2BearerGrant { - - /** - * The grant type. - */ - public static GrantType grantType = new GrantType( - "urn:ietf:params:oauth:grant-type:saml1_1-bearer"); - - public SAML11BearerGrant(Base64URL assertion) { - super(assertion); - } - - @Override - public Map> toParameters() { - - Map> params = super.toParameters(); - params.put("grant_type", Collections.singletonList(grantType.getValue())); - return params; - } -} diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java index b86c58a2..ad0ac1d7 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java @@ -31,7 +31,7 @@ class TokenRequestExecutor { msalRequest.requestContext().apiParameters().tenant() ; } - AuthenticationResult executeTokenRequest() throws ParseException, IOException { + AuthenticationResult executeTokenRequest() throws IOException { log.debug("Sending token request to: {}", requestAuthority.canonicalAuthorityUrl()); OAuthHttpRequest oAuthHttpRequest = createOauthHttpRequest(); @@ -39,10 +39,11 @@ AuthenticationResult executeTokenRequest() throws ParseException, IOException { return createAuthenticationResultFromOauthHttpResponse(oauthHttpResponse); } - OAuthHttpRequest createOauthHttpRequest() throws SerializeException, MalformedURLException { + OAuthHttpRequest createOauthHttpRequest() throws MalformedURLException { if (requestAuthority.tokenEndpointUrl() == null) { - throw new SerializeException("The endpoint URI is not specified"); + throw new MsalClientException("The endpoint URI is not specified", + AuthenticationErrorCode.INVALID_ENDPOINT_URI); } final OAuthHttpRequest oauthHttpRequest = new OAuthHttpRequest( @@ -113,8 +114,7 @@ private void addJWTBearerAssertionParams(Map> queryParamete queryParameters.put("client_assertion_type", Collections.singletonList("urn:ietf:params:oauth:client-assertion-type:jwt-bearer")); } - private AuthenticationResult createAuthenticationResultFromOauthHttpResponse( - HttpResponse oauthHttpResponse) throws ParseException { + private AuthenticationResult createAuthenticationResultFromOauthHttpResponse(HttpResponse oauthHttpResponse) { AuthenticationResult result; if (oauthHttpResponse.statusCode() == HttpHelper.HTTP_STATUS_200) { diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/HelperAndUtilityTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/HelperAndUtilityTests.java new file mode 100644 index 00000000..b81f9831 --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/HelperAndUtilityTests.java @@ -0,0 +1,72 @@ +package com.microsoft.aad.msal4j; + +import org.junit.jupiter.api.Test; + +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +class HelperAndUtilityTests { + + @Test + void JwtHelper_buildJwt_ValidSha1AndSha256Assertions() throws MsalClientException, CertificateEncodingException, NoSuchAlgorithmException { + ClientCertificate clientCertificateMock = mock(ClientCertificate.class); + when(clientCertificateMock.privateKey()).thenReturn(TestHelper.getPrivateKey()); + when(clientCertificateMock.publicCertificateHash()).thenReturn("certificateHash"); + when(clientCertificateMock.publicCertificateHash256()).thenReturn("certificateHash256"); + when(clientCertificateMock.getEncodedPublicKeyCertificateChain()).thenReturn(Arrays.asList("cert1", "cert2")); + + String clientId = "clientId"; + String audience = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; + + //Sha256 assertion + ClientAssertion clientAssertion = JwtHelper.buildJwt(clientId, clientCertificateMock, audience, true, false); + + assertNotNull(clientAssertion); + assertNotNull(clientAssertion.assertion()); + + // Verify JWT structure (header.payload.signature) + String jwt = clientAssertion.assertion(); + String[] jwtParts = jwt.split("\\."); + assertEquals(3, jwtParts.length, "JWT should have three parts"); + + // Decode and verify headers + String headerJson = new String(Base64.getUrlDecoder().decode(jwtParts[0])); + assertTrue(headerJson.contains("\"alg\":\"RS256\""), "Header should specify RS256 algorithm"); + assertTrue(headerJson.contains("\"typ\":\"JWT\""), "Header should specify JWT type"); + assertTrue(headerJson.contains("\"x5t#S256\":\"certificateHash256\""), "Header should contain x5t#S256"); + assertTrue(headerJson.contains("\"x5c\":[\"cert1\",\"cert2\"]"), "Header should contain x5c"); + + // Decode and verify payload + String payloadJson = new String(Base64.getUrlDecoder().decode(jwtParts[1])); + assertTrue(payloadJson.contains("\"aud\":\"" + audience + "\""), "Payload should contain correct audience"); + assertTrue(payloadJson.contains("\"iss\":\"" + clientId + "\""), "Payload should contain correct issuer"); + assertTrue(payloadJson.contains("\"sub\":\"" + clientId + "\""), "Payload should contain correct subject"); + assertTrue(payloadJson.contains("\"nbf\":"), "Payload should contain nbf claim"); + assertTrue(payloadJson.contains("\"exp\":"), "Payload should contain exp claim"); + assertTrue(payloadJson.contains("\"jti\":"), "Payload should contain jti claim"); + + // Verify certificate parameters were accessed + verify(clientCertificateMock).privateKey(); + verify(clientCertificateMock).publicCertificateHash256(); + verify(clientCertificateMock).getEncodedPublicKeyCertificateChain(); + + // Sha1 assertion, used in certain legacy flows + clientAssertion = JwtHelper.buildJwt(clientId, clientCertificateMock, audience, true, true); + + jwt = clientAssertion.assertion(); + jwtParts = jwt.split("\\."); + + // Verify header uses SHA1 hash/x5t header and not SHA256/x5t#S256 + headerJson = new String(Base64.getUrlDecoder().decode(jwtParts[0])); + assertTrue(headerJson.contains("\"x5t\":\"certificateHash\""), "Header should contain x5t (SHA1)"); + assertFalse(headerJson.contains("\"x5t#S256\""), "Header should not contain x5t#S256"); + + // Verify the correct certificate hash method was called + verify(clientCertificateMock).publicCertificateHash(); + } +} From efbf08e97981326d200dff3d84d6b18d1c3cc59f Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 22 Apr 2025 17:25:01 -0700 Subject: [PATCH 12/31] Represent query parameters with Map instead of Map> --- .../msal4j/AbstractClientApplicationBase.java | 11 +- .../AbstractMsalAuthorizationGrant.java | 2 +- ...uireTokenByAuthorizationGrantSupplier.java | 18 ++- .../AppServiceManagedIdentitySource.java | 4 +- .../aad/msal4j/AuthorizationCodeRequest.java | 10 +- .../AuthorizationRequestUrlParameters.java | 55 +++++----- .../msal4j/AzureArcManagedIdentitySource.java | 4 +- .../aad/msal4j/ClientCredentialRequest.java | 4 +- .../CloudShellManagedIdentitySource.java | 2 +- .../aad/msal4j/DeviceCodeFlowRequest.java | 19 ++-- .../aad/msal4j/IMDSManagedIdentitySource.java | 4 +- .../IntegratedWindowsAuthorizationGrant.java | 2 +- .../aad/msal4j/ManagedIdentityRequest.java | 17 ++- .../aad/msal4j/OAuthAuthorizationGrant.java | 24 ++-- .../aad/msal4j/OnBehalfOfRequest.java | 10 +- .../aad/msal4j/RefreshTokenRequest.java | 6 +- .../ServiceFabricManagedIdentitySource.java | 4 +- .../microsoft/aad/msal4j/StringHelper.java | 103 +++++++++++++++++- .../aad/msal4j/TokenRequestExecutor.java | 27 +++-- .../aad/msal4j/UserNamePasswordRequest.java | 8 +- .../aad/msal4j/HelperAndUtilityTests.java | 89 +++++++++++++++ .../MsalOauthAuthorizatonGrantTest.java | 6 +- 22 files changed, 297 insertions(+), 132 deletions(-) create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/HelperAndUtilityTests.java diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java index cf4a2407..a15ae15f 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java @@ -7,7 +7,6 @@ import java.net.MalformedURLException; import java.net.Proxy; import java.net.URL; -import java.util.Collections; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; @@ -134,22 +133,22 @@ public URL getAuthorizationRequestUrl(AuthorizationRequestUrlParameters paramete validateNotNull("parameters", parameters); - parameters.requestParameters.put("client_id", Collections.singletonList(this.clientId)); + parameters.requestParameters.put("client_id", this.clientId); //If the client application has any client capabilities set, they must be merged into the claims parameter if (this.clientCapabilities != null) { if (parameters.requestParameters.containsKey("claims")) { - String claims = String.valueOf(parameters.requestParameters.get("claims").get(0)); + String claims = String.valueOf(parameters.requestParameters.get("claims")); String mergedClaimsCapabilities = JsonHelper.mergeJSONString(claims, this.clientCapabilities); - parameters.requestParameters.put("claims", Collections.singletonList(mergedClaimsCapabilities)); + parameters.requestParameters.put("claims", mergedClaimsCapabilities); } else { - parameters.requestParameters.put("claims", Collections.singletonList(this.clientCapabilities)); + parameters.requestParameters.put("claims", this.clientCapabilities); } } return parameters.createAuthorizationURL( this.authenticationAuthority, - parameters.requestParameters()); + parameters.requestParameters); } @Override diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractMsalAuthorizationGrant.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractMsalAuthorizationGrant.java index a99b6b99..85baf4eb 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractMsalAuthorizationGrant.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractMsalAuthorizationGrant.java @@ -20,7 +20,7 @@ abstract class AbstractMsalAuthorizationGrant { * * @return A map contains the HTTP parameters */ - abstract Map> toParameters(); + abstract Map toParameters(); static final String SCOPE_PARAM_NAME = "scope"; static final String SCOPES_DELIMITER = " "; diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java index 8c448e98..1f9a49a0 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java @@ -3,8 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.jose.util.Base64URL; - import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Base64; @@ -101,24 +99,22 @@ private void processPasswordGrant(OAuthAuthorizationGrant authGrant) throws Exce } } - private Map> getSAMLAuthGrantParameters(WSTrustResponse response) { - Map> params = new LinkedHashMap<>(); + private Map getSAMLAuthGrantParameters(WSTrustResponse response) { + Map params = new LinkedHashMap<>(); if (response.isTokenSaml2()) { - params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.SAML_2_BEARER)); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, GrantConstants.SAML_2_BEARER); } else { - params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.SAML_1_1_BEARER)); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, GrantConstants.SAML_1_1_BEARER); } - params.put(GrantConstants.ASSERTION_PARAMETER, Collections.singletonList(new Base64URL( - Base64.getEncoder().encodeToString(response.getToken() - .getBytes(StandardCharsets.UTF_8))).toString())); + params.put(GrantConstants.ASSERTION_PARAMETER, Base64.getUrlEncoder().encodeToString(response.getToken().getBytes(StandardCharsets.UTF_8))); return params; } - private Map> getAuthorizationGrantIntegrated(String userName) throws Exception { - Map> params; + private Map getAuthorizationGrantIntegrated(String userName) throws Exception { + Map params; String userRealmEndpoint = this.clientApplication.authenticationAuthority. getUserRealmEndpoint(URLEncoder.encode(userName, StandardCharsets.UTF_8.name())); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AppServiceManagedIdentitySource.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AppServiceManagedIdentitySource.java index bb0f3112..612e4339 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AppServiceManagedIdentitySource.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AppServiceManagedIdentitySource.java @@ -31,8 +31,8 @@ public void createManagedIdentityRequest(String resource) { managedIdentityRequest.headers.put(SECRET_HEADER_NAME, identityHeader); managedIdentityRequest.queryParameters = new HashMap<>(); - managedIdentityRequest.queryParameters.put("api-version", Collections.singletonList(APP_SERVICE_MSI_API_VERSION)); - managedIdentityRequest.queryParameters.put("resource", Collections.singletonList(resource)); + managedIdentityRequest.queryParameters.put("api-version", APP_SERVICE_MSI_API_VERSION); + managedIdentityRequest.queryParameters.put("resource", resource); if (this.idType != null && !StringHelper.isNullOrBlank(this.userAssignedId)) { LOG.info("[Managed Identity] Adding user assigned ID to the request for App Service Managed Identity."); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java index 57ff658a..227c68a3 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java @@ -17,17 +17,17 @@ class AuthorizationCodeRequest extends MsalRequest { } private static AbstractMsalAuthorizationGrant createMsalGrant(AuthorizationCodeParameters parameters) { - Map> params = new LinkedHashMap<>(); + Map params = new LinkedHashMap<>(); - params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.AUTHORIZATION_CODE)); - params.put("code", Collections.singletonList(parameters.authorizationCode())); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, GrantConstants.AUTHORIZATION_CODE); + params.put("code", parameters.authorizationCode()); if (parameters.redirectUri() != null) { - params.put("redirect_uri", Collections.singletonList(parameters.redirectUri().toString())); + params.put("redirect_uri", parameters.redirectUri().toString()); } if (parameters.codeVerifier() != null) { - params.put("code_verifier", Collections.singletonList(parameters.codeVerifier())); + params.put("code_verifier", parameters.codeVerifier()); } return new OAuthAuthorizationGrant(params, parameters.scopes(), parameters.claims()); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java index 58e1a0c1..2d6dd0f9 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java @@ -3,7 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.util.URLUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,7 +35,7 @@ public class AuthorizationRequestUrlParameters { Map extraQueryParameters; - Map> requestParameters = new HashMap<>(); + Map requestParameters = new HashMap<>(); Logger log = LoggerFactory.getLogger(AuthorizationRequestUrlParameters.class); @@ -58,7 +57,7 @@ private static Builder builder() { private AuthorizationRequestUrlParameters(Builder builder) { //required parameters this.redirectUri = builder.redirectUri; - requestParameters.put("redirect_uri", Collections.singletonList(this.redirectUri)); + requestParameters.put("redirect_uri", this.redirectUri); this.scopes = builder.scopes; Set scopesParam = new LinkedHashSet<>(AbstractMsalAuthorizationGrant.COMMON_SCOPES); @@ -70,86 +69,86 @@ private AuthorizationRequestUrlParameters(Builder builder) { } this.scopes = scopesParam; - requestParameters.put("scope", Collections.singletonList(String.join(" ", scopesParam))); - requestParameters.put("response_type", Collections.singletonList("code")); + requestParameters.put("scope", String.join(" ", scopesParam)); + requestParameters.put("response_type", "code"); // Optional parameters if (builder.claims != null) { String claimsParam = String.join(" ", builder.claims); - requestParameters.put("claims", Collections.singletonList(claimsParam)); + requestParameters.put("claims", claimsParam); } if (builder.claimsChallenge != null && builder.claimsChallenge.trim().length() > 0) { JsonHelper.validateJsonFormat(builder.claimsChallenge); - requestParameters.put("claims", Collections.singletonList(builder.claimsChallenge)); + requestParameters.put("claims", builder.claimsChallenge); } if (builder.claimsRequest != null) { String claimsRequest = builder.claimsRequest.formatAsJSONString(); //If there are other claims (such as part of a claims challenge), merge them with this claims request. if (requestParameters.get("claims") != null) { - claimsRequest = JsonHelper.mergeJSONString(claimsRequest, requestParameters.get("claims").get(0)); + claimsRequest = JsonHelper.mergeJSONString(claimsRequest, requestParameters.get("claims")); } - requestParameters.put("claims", Collections.singletonList(claimsRequest)); + requestParameters.put("claims", claimsRequest); } if (builder.codeChallenge != null) { this.codeChallenge = builder.codeChallenge; - requestParameters.put("code_challenge", Collections.singletonList(builder.codeChallenge)); + requestParameters.put("code_challenge", builder.codeChallenge); } if (builder.codeChallengeMethod != null) { this.codeChallengeMethod = builder.codeChallengeMethod; - requestParameters.put("code_challenge_method", Collections.singletonList(builder.codeChallengeMethod)); + requestParameters.put("code_challenge_method", builder.codeChallengeMethod); } if (builder.state != null) { this.state = builder.state; - requestParameters.put("state", Collections.singletonList(builder.state)); + requestParameters.put("state", builder.state); } if (builder.nonce != null) { this.nonce = builder.nonce; - requestParameters.put("nonce", Collections.singletonList(builder.nonce)); + requestParameters.put("nonce", builder.nonce); } if (builder.responseMode != null) { this.responseMode = builder.responseMode; - requestParameters.put("response_mode", Collections.singletonList( - builder.responseMode.toString())); + requestParameters.put("response_mode", + builder.responseMode.toString()); } else { this.responseMode = ResponseMode.FORM_POST; - requestParameters.put("response_mode", Collections.singletonList( - ResponseMode.FORM_POST.toString())); + requestParameters.put("response_mode", + ResponseMode.FORM_POST.toString()); } if (builder.loginHint != null) { this.loginHint = loginHint(); - requestParameters.put("login_hint", Collections.singletonList(builder.loginHint)); + requestParameters.put("login_hint", builder.loginHint); // For CCS routing - requestParameters.put(HttpHeaders.X_ANCHOR_MAILBOX, Collections.singletonList( - String.format(HttpHeaders.X_ANCHOR_MAILBOX_UPN_FORMAT, builder.loginHint))); + requestParameters.put(HttpHeaders.X_ANCHOR_MAILBOX, + String.format(HttpHeaders.X_ANCHOR_MAILBOX_UPN_FORMAT, builder.loginHint)); } if (builder.domainHint != null) { this.domainHint = domainHint(); - requestParameters.put("domain_hint", Collections.singletonList(builder.domainHint)); + requestParameters.put("domain_hint", builder.domainHint); } if (builder.prompt != null) { this.prompt = builder.prompt; - requestParameters.put("prompt", Collections.singletonList(builder.prompt.toString())); + requestParameters.put("prompt", builder.prompt.toString()); } if (builder.correlationId != null) { this.correlationId = builder.correlationId; - requestParameters.put("correlation_id", Collections.singletonList(builder.correlationId)); + requestParameters.put("correlation_id", builder.correlationId); } if (builder.instanceAware) { this.instanceAware = builder.instanceAware; - requestParameters.put("instance_aware", Collections.singletonList(String.valueOf(instanceAware))); + requestParameters.put("instance_aware", String.valueOf(instanceAware)); } if(null != builder.extraQueryParameters && !builder.extraQueryParameters.isEmpty()){ @@ -160,13 +159,13 @@ private AuthorizationRequestUrlParameters(Builder builder) { if(requestParameters.containsKey(key)){ log.warn("A query parameter {} has been provided with values multiple times.", key); } - requestParameters.put(key, Collections.singletonList(value)); + requestParameters.put(key, value); } } } URL createAuthorizationURL(Authority authority, - Map> requestParameters) { + Map requestParameters) { URL authorizationRequestUrl; try { String authorizationCodeEndpoint; @@ -178,7 +177,7 @@ URL createAuthorizationURL(Authority authority, } String uriString = authorizationCodeEndpoint + "?" + - URLUtils.serializeParameters(requestParameters); + StringHelper.serializeQueryParameters(requestParameters); authorizationRequestUrl = new URL(uriString); } catch (MalformedURLException ex) { @@ -240,7 +239,7 @@ public Map extraQueryParameters() { } public Map> requestParameters() { - return this.requestParameters; + return StringHelper.convertToMultiValueMap(this.requestParameters); } public Logger log() { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AzureArcManagedIdentitySource.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AzureArcManagedIdentitySource.java index c6ffb45a..3e504764 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AzureArcManagedIdentitySource.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AzureArcManagedIdentitySource.java @@ -83,8 +83,8 @@ public void createManagedIdentityRequest(String resource) managedIdentityRequest.headers.put("Metadata", "true"); managedIdentityRequest.queryParameters = new HashMap<>(); - managedIdentityRequest.queryParameters.put("api-version", Collections.singletonList(ARC_API_VERSION)); - managedIdentityRequest.queryParameters.put("resource", Collections.singletonList(resource)); + managedIdentityRequest.queryParameters.put("api-version", ARC_API_VERSION); + managedIdentityRequest.queryParameters.put("resource", resource); } @Override diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialRequest.java index a03ee8ea..b108a9a8 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialRequest.java @@ -28,9 +28,9 @@ class ClientCredentialRequest extends MsalRequest { } private static OAuthAuthorizationGrant createMsalGrant(ClientCredentialParameters parameters) { - Map> params = new LinkedHashMap<>(); + Map params = new LinkedHashMap<>(); - params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.CLIENT_CREDENTIALS)); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, GrantConstants.CLIENT_CREDENTIALS); return new OAuthAuthorizationGrant(params, parameters.scopes(), parameters.claims()); } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CloudShellManagedIdentitySource.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CloudShellManagedIdentitySource.java index 11d75bb3..72a5e367 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CloudShellManagedIdentitySource.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CloudShellManagedIdentitySource.java @@ -27,7 +27,7 @@ public void createManagedIdentityRequest(String resource) { managedIdentityRequest.headers.put("Metadata", "true"); managedIdentityRequest.queryParameters = new HashMap<>(); - managedIdentityRequest.queryParameters.put("resource", Collections.singletonList(resource)); + managedIdentityRequest.queryParameters.put("resource", resource); } private CloudShellManagedIdentitySource(MsalRequest msalRequest, ServiceBundle serviceBundle, URI msiEndpoint) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java index 9bb7327e..fe5565cf 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java @@ -3,12 +3,9 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.util.URLUtils; - import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; @@ -55,28 +52,28 @@ DeviceCode acquireDeviceCode(String url, } void createAuthenticationGrant(DeviceCode deviceCode) { - final Map> params = new LinkedHashMap<>(); + final Map params = new LinkedHashMap<>(); - params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.DEVICE_CODE)); - params.put("device_code", Collections.singletonList(deviceCode.deviceCode())); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, GrantConstants.DEVICE_CODE); + params.put("device_code", deviceCode.deviceCode()); if (parameters.claims() != null) { - params.put("claims", Collections.singletonList(parameters.claims().formatAsJSONString())); + params.put("claims", parameters.claims().formatAsJSONString()); } msalAuthorizationGrant = new OAuthAuthorizationGrant(params, Collections.singleton(deviceCode.scopes()), parameters.claims()); } private String createQueryParams(String clientId) { - Map> queryParameters = new HashMap<>(); - queryParameters.put("client_id", Collections.singletonList(clientId)); + Map queryParameters = new HashMap<>(); + queryParameters.put("client_id", clientId); String scopesParam = String.join(AbstractMsalAuthorizationGrant.SCOPES_DELIMITER, AbstractMsalAuthorizationGrant.COMMON_SCOPES) + AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + scopesStr; - queryParameters.put("scope", Collections.singletonList(scopesParam)); + queryParameters.put("scope", scopesParam); - return URLUtils.serializeParameters(queryParameters); + return StringHelper.serializeQueryParameters(queryParameters); } private Map appendToHeaders(Map clientDataHeaders) { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IMDSManagedIdentitySource.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IMDSManagedIdentitySource.java index c1973ee1..d2e37fc8 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IMDSManagedIdentitySource.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IMDSManagedIdentitySource.java @@ -74,8 +74,8 @@ public void createManagedIdentityRequest(String resource) { managedIdentityRequest.headers.put("Metadata", "true"); managedIdentityRequest.queryParameters = new HashMap<>(); - managedIdentityRequest.queryParameters.put("api-version", Collections.singletonList(IMDS_API_VERSION)); - managedIdentityRequest.queryParameters.put("resource", Collections.singletonList(resource)); + managedIdentityRequest.queryParameters.put("api-version", IMDS_API_VERSION); + managedIdentityRequest.queryParameters.put("resource", resource); if (this.idType != null && !StringHelper.isNullOrBlank(this.userAssignedId)) { LOG.info("[Managed Identity] Adding user assigned ID to the request for IMDS Managed Identity."); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthorizationGrant.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthorizationGrant.java index ee963ed1..53c10b94 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthorizationGrant.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthorizationGrant.java @@ -18,7 +18,7 @@ class IntegratedWindowsAuthorizationGrant extends AbstractMsalAuthorizationGrant } @Override - Map> toParameters() { + Map toParameters() { return null; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityRequest.java index 2076c37b..483f47a7 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityRequest.java @@ -3,7 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.util.URLUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,8 +10,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.util.Collections; -import java.util.List; import java.util.Map; class ManagedIdentityRequest extends MsalRequest { @@ -25,9 +22,9 @@ class ManagedIdentityRequest extends MsalRequest { Map headers; - Map> bodyParameters; + Map bodyParameters; - Map> queryParameters; + Map queryParameters; public ManagedIdentityRequest(ManagedIdentityApplication managedIdentityApplication, RequestContext requestContext) { super(managedIdentityApplication, requestContext); @@ -37,7 +34,7 @@ public String getBodyAsString() { if (bodyParameters == null || bodyParameters.isEmpty()) return ""; - return URLUtils.serializeParameters(bodyParameters); + return StringHelper.serializeQueryParameters(bodyParameters); } public URL computeURI() throws URISyntaxException { @@ -54,7 +51,7 @@ private String appendQueryParametersToBaseEndpoint() { return baseEndpoint.toString(); } - String queryString = URLUtils.serializeParameters(queryParameters); + String queryString = StringHelper.serializeQueryParameters(queryParameters); return baseEndpoint.toString() + "?" + queryString; } @@ -63,15 +60,15 @@ void addUserAssignedIdToQuery(ManagedIdentityIdType idType, String userAssignedI switch (idType) { case CLIENT_ID: LOG.info("[Managed Identity] Adding user assigned client id to the request."); - queryParameters.put(Constants.MANAGED_IDENTITY_CLIENT_ID, Collections.singletonList(userAssignedId)); + queryParameters.put(Constants.MANAGED_IDENTITY_CLIENT_ID, userAssignedId); break; case RESOURCE_ID: LOG.info("[Managed Identity] Adding user assigned resource id to the request."); - queryParameters.put(Constants.MANAGED_IDENTITY_RESOURCE_ID, Collections.singletonList(userAssignedId)); + queryParameters.put(Constants.MANAGED_IDENTITY_RESOURCE_ID, userAssignedId); break; case OBJECT_ID: LOG.info("[Managed Identity] Adding user assigned object id to the request."); - queryParameters.put(Constants.MANAGED_IDENTITY_OBJECT_ID, Collections.singletonList(userAssignedId)); + queryParameters.put(Constants.MANAGED_IDENTITY_OBJECT_ID, userAssignedId); break; } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java index 3ae69f4c..8929656c 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthAuthorizationGrant.java @@ -6,13 +6,12 @@ import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.Set; class OAuthAuthorizationGrant extends AbstractMsalAuthorizationGrant { - private final Map> params = new LinkedHashMap<>(); + private final Map params = new LinkedHashMap<>(); /** * Constructor to create an OAuthAuthorizationGrant @@ -20,7 +19,7 @@ class OAuthAuthorizationGrant extends AbstractMsalAuthorizationGrant { * @param params parameters relevant for the specific authorization grant type * @param scopes additional scopes which will be added to a default set of common scopes */ - OAuthAuthorizationGrant(Map> params, Set scopes) { + OAuthAuthorizationGrant(Map params, Set scopes) { this.scopes = new HashSet<>(AbstractMsalAuthorizationGrant.COMMON_SCOPES); if (scopes != null) { @@ -28,9 +27,9 @@ class OAuthAuthorizationGrant extends AbstractMsalAuthorizationGrant { } // Default scopes that apply to most flows - this.params.put(SCOPE_PARAM_NAME, Collections.singletonList(String.join(" ", this.scopes))); + this.params.put(SCOPE_PARAM_NAME, String.join(" ", this.scopes)); // Parameter to request client info from the endpoint - this.params.put("client_info", Collections.singletonList("1")); + this.params.put("client_info", "1"); if (params != null) { this.params.putAll(params); @@ -44,16 +43,16 @@ class OAuthAuthorizationGrant extends AbstractMsalAuthorizationGrant { * @param scopes additional scopes which will be added to a default set of common scopes * @param claims optional claims */ - OAuthAuthorizationGrant(Map> params, Set scopes, ClaimsRequest claims) { + OAuthAuthorizationGrant(Map params, Set scopes, ClaimsRequest claims) { this(params, scopes); if (claims != null) { this.claims = claims; - this.params.put("claims", Collections.singletonList(claims.formatAsJSONString())); + this.params.put("claims", claims.formatAsJSONString()); } } - void addAndReplaceParams(Map> params) { + void addAndReplaceParams(Map params) { if (params != null) { //putAll() will overwrite existing values if the key already exists in the map this.params.putAll(params); @@ -61,17 +60,14 @@ void addAndReplaceParams(Map> params) { } String getParamValue(String paramKey) { - if (this.params.containsKey(paramKey)) { - return this.params.get(paramKey).get(0); - } - return null; + return this.params.get(paramKey); } /** * Returns an unmodifiable version of the parameters map */ @Override - public Map> toParameters() { + public Map toParameters() { return Collections.unmodifiableMap(new LinkedHashMap<>(params)); } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java index 6a5f0e9f..00abde07 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfRequest.java @@ -19,14 +19,14 @@ class OnBehalfOfRequest extends MsalRequest { } private static OAuthAuthorizationGrant createAuthenticationGrant(OnBehalfOfParameters parameters) { - Map> params = new LinkedHashMap<>(); + Map params = new LinkedHashMap<>(); - params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.JWT_BEARER)); - params.put(GrantConstants.ASSERTION_PARAMETER, Collections.singletonList(parameters.userAssertion().getAssertion())); - params.put("requested_token_use", Collections.singletonList("on_behalf_of")); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, GrantConstants.JWT_BEARER); + params.put(GrantConstants.ASSERTION_PARAMETER, parameters.userAssertion().getAssertion()); + params.put("requested_token_use", "on_behalf_of"); if (parameters.claims() != null) { - params.put("claims", Collections.singletonList(parameters.claims().formatAsJSONString())); + params.put("claims", parameters.claims().formatAsJSONString()); } return new OAuthAuthorizationGrant(params, parameters.scopes()); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RefreshTokenRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RefreshTokenRequest.java index 7f7a79a2..4bbc4180 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RefreshTokenRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RefreshTokenRequest.java @@ -33,10 +33,10 @@ class RefreshTokenRequest extends MsalRequest { } private static AbstractMsalAuthorizationGrant createAuthenticationGrant(RefreshTokenParameters parameters) { - Map> params = new LinkedHashMap<>(); + Map params = new LinkedHashMap<>(); - params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.REFRESH_TOKEN)); - params.put("refresh_token", Collections.singletonList(parameters.refreshToken())); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, GrantConstants.REFRESH_TOKEN); + params.put("refresh_token", parameters.refreshToken()); return new OAuthAuthorizationGrant(params, parameters.scopes(), parameters.claims()); } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ServiceFabricManagedIdentitySource.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ServiceFabricManagedIdentitySource.java index 5368eff2..0714db3d 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ServiceFabricManagedIdentitySource.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ServiceFabricManagedIdentitySource.java @@ -36,8 +36,8 @@ public void createManagedIdentityRequest(String resource) { managedIdentityRequest.headers.put("secret", identityHeader); managedIdentityRequest.queryParameters = new HashMap<>(); - managedIdentityRequest.queryParameters.put("resource", Collections.singletonList(resource)); - managedIdentityRequest.queryParameters.put("api-version", Collections.singletonList(SERVICE_FABRIC_MSI_API_VERSION)); + managedIdentityRequest.queryParameters.put("resource", resource); + managedIdentityRequest.queryParameters.put("api-version", SERVICE_FABRIC_MSI_API_VERSION); if (this.idType != null && !StringHelper.isNullOrBlank(this.userAssignedId)) { LOG.info("[Managed Identity] Adding user assigned ID to the request for Service Fabric Managed Identity."); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java index 1fe4f7d6..c31f0660 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java @@ -3,17 +3,20 @@ package com.microsoft.aad.msal4j; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Base64; +import java.util.*; final class StringHelper { static String EMPTY_STRING = ""; static boolean isBlank(final String str) { - return str == null || str.trim().length() == 0; + return str == null || str.trim().isEmpty(); } static String createBase64EncodedSha256Hash(String stringToHash) { @@ -24,7 +27,7 @@ static String createSha256Hash(String stringToHash) { return createSha256Hash(stringToHash, false); } - static private String createSha256Hash(String stringToHash, boolean base64Encode) { + private static String createSha256Hash(String stringToHash, boolean base64Encode) { String res; try { MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); @@ -42,6 +45,96 @@ static private String createSha256Hash(String stringToHash, boolean base64Encode } public static boolean isNullOrBlank(final String str) { - return str == null || str.trim().length() == 0; + return str == null || str.trim().isEmpty(); } -} + + //Converts a map of parameters into a URL query string + static String serializeQueryParameters(Map params) { + if (params != null && !params.isEmpty()) { + Map encodedParams = urlEncodeMap(params); + StringBuilder sb = new StringBuilder(); + + for (Map.Entry entry : encodedParams.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + continue; + } + + String value = entry.getValue(); + + if (sb.length() > 0) { + sb.append('&'); + } + + sb.append(entry.getKey()); + sb.append('='); + sb.append(value); + } + + return sb.toString(); + } else { + return ""; + } + } + + static Map urlEncodeMap(Map params) { + if (params == null || params.isEmpty()) { + return params; + } else { + Map out = new LinkedHashMap<>(); + + for (Map.Entry entry : params.entrySet()) { + try { + String newKey = entry.getKey() != null ? URLEncoder.encode(entry.getKey(), "utf-8") : null; + String newValue = entry.getValue() != null ? URLEncoder.encode(entry.getValue(), "utf-8") : null; + + out.put(newKey, newValue); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + return out; + } + } + + static Map parseQueryParameters(String query) { + Map params = new LinkedHashMap<>(); + if (StringHelper.isBlank(query)) { + return params; + } else { + StringTokenizer st = new StringTokenizer(query.trim(), "&"); + + while (st.hasMoreTokens()) { + String param = st.nextToken(); + String[] pair = param.split("=", 2); + + String key; + String value; + try { + key = URLDecoder.decode(pair[0], "utf-8"); + value = pair.length > 1 ? URLDecoder.decode(pair[1], "utf-8") : ""; + } catch (UnsupportedEncodingException | IllegalArgumentException e) { + continue; + } + + params.put(key, value); + } + + return params; + } + } + + //Much of the library once used Map> for query parameters due to reliance on a specific dependency. + // Although Map is now used internally, some public APIs still return Map> + static Map> convertToMultiValueMap(Map singleValueMap) { + Map> multiValueMap = new HashMap<>(); + + if (singleValueMap != null) { + for (Map.Entry entry : singleValueMap.entrySet()) { + multiValueMap.put(entry.getKey(), Collections.singletonList(entry.getValue())); + } + } + + return multiValueMap; + } +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java index b86c58a2..e6627bd3 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java @@ -5,7 +5,6 @@ import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.SerializeException; -import com.nimbusds.oauth2.sdk.util.URLUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,18 +51,18 @@ OAuthHttpRequest createOauthHttpRequest() throws SerializeException, MalformedUR msalRequest.requestContext(), this.serviceBundle); - final Map> params = new HashMap<>(msalRequest.msalAuthorizationGrant().toParameters()); + final Map params = new HashMap<>(msalRequest.msalAuthorizationGrant().toParameters()); if (msalRequest.application() instanceof AbstractClientApplicationBase && ((AbstractClientApplicationBase) msalRequest.application()).clientCapabilities() != null) { - params.put("claims", Collections.singletonList(((AbstractClientApplicationBase) msalRequest.application()).clientCapabilities())); + params.put("claims", ((AbstractClientApplicationBase) msalRequest.application()).clientCapabilities()); } if (msalRequest.msalAuthorizationGrant.getClaims() != null) { String claimsRequest = msalRequest.msalAuthorizationGrant.getClaims().formatAsJSONString(); if (params.get("claims") != null) { - claimsRequest = JsonHelper.mergeJSONString(params.get("claims").get(0), claimsRequest); + claimsRequest = JsonHelper.mergeJSONString(params.get("claims"), claimsRequest); } - params.put("claims", Collections.singletonList(claimsRequest)); + params.put("claims", claimsRequest); } if(msalRequest.requestContext().apiParameters().extraQueryParameters() != null ){ @@ -71,11 +70,11 @@ OAuthHttpRequest createOauthHttpRequest() throws SerializeException, MalformedUR if(params.containsKey(key)){ log.warn("A query parameter {} has been provided with values multiple times.", key); } - params.put(key, Collections.singletonList(msalRequest.requestContext().apiParameters().extraQueryParameters().get(key))); + params.put(key, msalRequest.requestContext().apiParameters().extraQueryParameters().get(key)); } } - oauthHttpRequest.setQuery(URLUtils.serializeParameters(params)); + oauthHttpRequest.setQuery(StringHelper.serializeQueryParameters(params)); //Certain query parameters are required by Public and Confidential client applications, but not Managed Identity if (msalRequest.application() instanceof AbstractClientApplicationBase) { @@ -85,9 +84,9 @@ OAuthHttpRequest createOauthHttpRequest() throws SerializeException, MalformedUR } private void addQueryParameters(OAuthHttpRequest oauthHttpRequest) { - Map> queryParameters = URLUtils.parseParameters(oauthHttpRequest.query); + Map queryParameters = StringHelper.parseQueryParameters(oauthHttpRequest.query); String clientID = msalRequest.application().clientId(); - queryParameters.put("client_id", Arrays.asList(clientID)); + queryParameters.put("client_id", clientID); // If the client application has a client assertion to apply to the request, check if a new client assertion // was supplied as a request parameter. If so, use the request's assertion instead of the application's @@ -100,17 +99,17 @@ private void addQueryParameters(OAuthHttpRequest oauthHttpRequest) { addJWTBearerAssertionParams(queryParameters, ((ConfidentialClientApplication) msalRequest.application()).assertion); } else if (((ConfidentialClientApplication) msalRequest.application()).secret != null) { // Client secrets have a different parameter than bearer assertions - queryParameters.put("client_secret", Collections.singletonList(((ConfidentialClientApplication) msalRequest.application()).secret)); + queryParameters.put("client_secret", ((ConfidentialClientApplication) msalRequest.application()).secret); } } } - oauthHttpRequest.setQuery(URLUtils.serializeParameters(queryParameters)); + oauthHttpRequest.setQuery(StringHelper.serializeQueryParameters(queryParameters)); } - private void addJWTBearerAssertionParams(Map> queryParameters, String assertion) { - queryParameters.put("client_assertion", Collections.singletonList(assertion)); - queryParameters.put("client_assertion_type", Collections.singletonList("urn:ietf:params:oauth:client-assertion-type:jwt-bearer")); + private void addJWTBearerAssertionParams(Map queryParameters, String assertion) { + queryParameters.put("client_assertion", assertion); + queryParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); } private AuthenticationResult createAuthenticationResultFromOauthHttpResponse( diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordRequest.java index 6b4d0133..3a1dc672 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordRequest.java @@ -17,11 +17,11 @@ class UserNamePasswordRequest extends MsalRequest { } private static OAuthAuthorizationGrant createAuthenticationGrant(UserNamePasswordParameters parameters) { - Map> params = new LinkedHashMap<>(); + Map params = new LinkedHashMap<>(); - params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList(GrantConstants.PASSWORD)); - params.put(GrantConstants.USERNAME_PARAMETER, Collections.singletonList(parameters.username())); - params.put(GrantConstants.PASSWORD_PARAMETER, Collections.singletonList(new String(parameters.password()))); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, GrantConstants.PASSWORD); + params.put(GrantConstants.USERNAME_PARAMETER, parameters.username()); + params.put(GrantConstants.PASSWORD_PARAMETER, new String(parameters.password())); return new OAuthAuthorizationGrant(params, parameters.scopes(), parameters.claims()); } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/HelperAndUtilityTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/HelperAndUtilityTests.java new file mode 100644 index 00000000..7b2d8473 --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/HelperAndUtilityTests.java @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HelperAndUtilityTests { + + @Test + void StringHelper_serializeQueryParameters_ValidUrlQueryStrings() { + //Empty map + Map params = new LinkedHashMap<>(); + String result = StringHelper.serializeQueryParameters(params); + assertEquals("", result); + + //Null map + result = StringHelper.serializeQueryParameters(null); + assertEquals("", result); + + //Basic parameters + params = new LinkedHashMap<>(); + params.put("client_id", "abc123"); + params.put("scope", "openid profile"); + params.put("redirect_uri", "https://example.com/auth"); + + result = StringHelper.serializeQueryParameters(params); + assertEquals("client_id=abc123&scope=openid+profile&redirect_uri=https%3A%2F%2Fexample.com%2Fauth", result); + + //(Unrealistic) parameters with special characters + params = new LinkedHashMap<>(); + params.put("client_id", "special@client"); + params.put("scope", "openid offline_access"); + params.put("redirect_uri", "https://example.com/query?key=value"); + + result = StringHelper.serializeQueryParameters(params); + assertEquals("client_id=special%40client&scope=openid+offline_access&redirect_uri=https%3A%2F%2Fexample.com%2Fquery%3Fkey%3Dvalue", result); + + //Null values in map + params = new LinkedHashMap<>(); + params.put("client_id", "abc123"); + params.put("scope", null); + params.put("redirect_uri", "https://example.com/auth"); + + result = StringHelper.serializeQueryParameters(params); + assertEquals("client_id=abc123&redirect_uri=https%3A%2F%2Fexample.com%2Fauth", result); + } + + @Test + void StringHelper_convertToMultiValueMap() { + //Historically, much of the library once used Map> to represent URL query params, though it now uses Map. + // AuthorizationRequestUrlParameters unfortunately has a public API returning Map>, + // so this test helps ensure we still return an equivalent map to what we're using internally. + AuthorizationRequestUrlParameters params = new AuthorizationRequestUrlParameters.Builder() + .redirectUri("https://myapp.com/callback") + .scopes(Collections.singleton("openid profile email")) + .state("state123") + .nonce("nonce123") + .loginHint("user@example.com") + .correlationId("correlation-id-123").build(); + + Map internalRequestParams = params.requestParameters; + Map> convertedInternalMap = StringHelper.convertToMultiValueMap(internalRequestParams); + + // Get the map returned by the method + Map> methodReturnedMap = params.requestParameters(); + + // Assert + assertNotNull(convertedInternalMap, "Converted map should not be null"); + assertNotNull(methodReturnedMap, "Method returned map should not be null"); + + assertEquals(convertedInternalMap.size(), methodReturnedMap.size(), "Maps should have the same size"); + + for (String key : convertedInternalMap.keySet()) { + assertTrue(methodReturnedMap.containsKey(key), "Method returned map should contain key: " + key); + + List convertedValues = convertedInternalMap.get(key); + List methodValues = methodReturnedMap.get(key); + + assertEquals(convertedValues, methodValues, + "Values for key '" + key + "' should be equal"); + } + } +} diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java index 807a857f..a8d8f3de 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MsalOauthAuthorizatonGrantTest.java @@ -19,13 +19,13 @@ class MsalOauthAuthorizatonGrantTest { @Test void testToParameters() { - Map> params = new LinkedHashMap<>(); - params.put(GrantConstants.GRANT_TYPE_PARAMETER, Collections.singletonList("SomeGrantType")); + Map params = new LinkedHashMap<>(); + params.put(GrantConstants.GRANT_TYPE_PARAMETER, "SomeGrantType"); final OAuthAuthorizationGrant grant = new OAuthAuthorizationGrant(params, null); assertNotNull(grant); assertNotNull(grant.toParameters()); - assertEquals("SomeGrantType", grant.toParameters().get(GrantConstants.GRANT_TYPE_PARAMETER).get(0)); + assertEquals("SomeGrantType", grant.toParameters().get(GrantConstants.GRANT_TYPE_PARAMETER)); } } From 8be97d5876d5b082069258e456e52822db022e43 Mon Sep 17 00:00:00 2001 From: avdunn Date: Fri, 25 Apr 2025 13:33:45 -0700 Subject: [PATCH 13/31] Merge latest dev --- .../java/com/microsoft/aad/msal4j/TokenRequestExecutor.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java index e2bcbca6..9a53b1de 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java @@ -124,8 +124,7 @@ private AuthenticationResult createAuthenticationResultFromOauthHttpResponse( if (!StringHelper.isNullOrBlank(response.idToken())) { String idTokenJson; try { - idTokenJson = new String(Base64.getUrlDecoder().decode(tokens.getIDTokenString().split("\\.")[1]), StandardCharsets.UTF_8); - idTokenJson = new String(Base64.getDecoder().decode(response.idToken().split("\\.")[1]), StandardCharsets.UTF_8); + idTokenJson = new String(Base64.getUrlDecoder().decode(response.idToken().split("\\.")[1]), StandardCharsets.UTF_8); } catch (ArrayIndexOutOfBoundsException e) { throw new MsalServiceException("Error parsing ID token, missing payload section. Ensure that the ID token is following the JWT format.", AuthenticationErrorCode.INVALID_JWT); From cac03fc185cd09cece595e99bd5cf0ddf9bee10e Mon Sep 17 00:00:00 2001 From: avdunn Date: Fri, 25 Apr 2025 14:11:15 -0700 Subject: [PATCH 14/31] Fix comment --- .../aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java index 8c448e98..7a480d95 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java @@ -75,7 +75,7 @@ private boolean IsUiRequiredCacheSupported() { private void processPasswordGrant(OAuthAuthorizationGrant authGrant) throws Exception { - //Additional processing is only needed if it's a password grant with a non-AAD authority + //Additional processing is only needed if it's a password grant with an AAD authority if (!(authGrant.getParamValue(GrantConstants.GRANT_TYPE_PARAMETER).equals(GrantConstants.PASSWORD)) || msalRequest.application().authenticationAuthority.authorityType != AuthorityType.AAD) { return; From 03ae326085d63111317c968f895f6ae918be3d98 Mon Sep 17 00:00:00 2001 From: avdunn Date: Fri, 25 Apr 2025 14:15:59 -0700 Subject: [PATCH 15/31] Fix comment --- .../src/main/java/com/microsoft/aad/msal4j/StringHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java index c31f0660..ad042810 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java @@ -125,7 +125,7 @@ static Map parseQueryParameters(String query) { } //Much of the library once used Map> for query parameters due to reliance on a specific dependency. - // Although Map is now used internally, some public APIs still return Map> + // Although Map is now used internally, some public APIs in AuthorizationRequestUrlParameters still return Map> static Map> convertToMultiValueMap(Map singleValueMap) { Map> multiValueMap = new HashMap<>(); From 8d1f418a51c13bd8d271281dbf44734e5dcc3380 Mon Sep 17 00:00:00 2001 From: avdunn Date: Fri, 25 Apr 2025 15:45:30 -0700 Subject: [PATCH 16/31] Address PR feedback --- .../microsoft/aad/msal4j/ClientAssertion.java | 2 +- .../com/microsoft/aad/msal4j/IBroker.java | 13 +----- .../com/microsoft/aad/msal4j/JsonHelper.java | 18 +++++++-- .../com/microsoft/aad/msal4j/JwtHelper.java | 5 +-- .../aad/msal4j/TokenRequestExecutor.java | 15 +------ .../aad/msal4j/HelperAndUtilityTests.java | 40 +++++++++++++++++++ 6 files changed, 60 insertions(+), 33 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java index 7c4e456b..f5fe64e9 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java @@ -12,7 +12,7 @@ @EqualsAndHashCode final class ClientAssertion implements IClientAssertion { - static final String ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + static final String ASSERTION_TYPE_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; private final String assertion; ClientAssertion(final String assertion) { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java index 9f50fc7a..7175d8d6 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java @@ -4,8 +4,6 @@ package com.microsoft.aad.msal4j; import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Base64; import java.util.concurrent.CompletableFuture; /** @@ -67,17 +65,10 @@ default IAuthenticationResult parseBrokerAuthResult(String authority, String idT if (idToken != null) { builder.idToken(idToken); if (accountId != null) { - String idTokenJson; - - try { - idTokenJson = new String(Base64.getDecoder().decode(idToken.split("\\.")[1]), StandardCharsets.UTF_8); - } catch (ArrayIndexOutOfBoundsException e) { - throw new MsalServiceException("Error parsing ID token, missing payload section. Ensure that the ID token is following the JWT format.", - AuthenticationErrorCode.INVALID_JWT); - } + IdToken idTokenObj = JsonHelper.createIdTokenFromEncodedTokenString(idToken); builder.accountCacheEntity(AccountCacheEntity.create(clientInfo, - Authority.createAuthority(new URL(authority)), JsonHelper.convertJsonToObject(idTokenJson, IdToken.class), null)); + Authority.createAuthority(new URL(authority)), idTokenObj, null)); } } if (accessToken != null) { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java index d0038014..aa6a74e1 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java @@ -15,10 +15,8 @@ import com.fasterxml.jackson.databind.JsonNode; import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; +import java.nio.charset.StandardCharsets; +import java.util.*; class JsonHelper { static ObjectMapper mapper; @@ -41,6 +39,18 @@ static T convertJsonToObject(final String json, final Class tClass) { } } + static IdToken createIdTokenFromEncodedTokenString(String token) { + String idTokenJson; + try { + idTokenJson = new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8); + } catch (ArrayIndexOutOfBoundsException e) { + throw new MsalClientException("Error parsing ID token, missing payload section.", + AuthenticationErrorCode.INVALID_JWT); + } + + return JsonHelper.convertJsonToObject(idTokenJson, IdToken.class); + } + //This method is used to convert a JSON string to an object which implements the JsonSerializable interface from com.azure.json static > T convertJsonStringToJsonSerializableObject(String jsonResponse, ReadValueCallback readFunction) { try (JsonReader jsonReader = JsonProviders.createReader(jsonResponse)) { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java index bca0cb82..9d1a4fca 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java @@ -30,10 +30,7 @@ static ClientAssertion buildJwt(String clientId, final ClientCertificate credent header.put("typ", "JWT"); if (sendX5c) { - List certs = new ArrayList<>(); - for (String cert : credential.getEncodedPublicKeyCertificateChain()) { - certs.add(cert); - } + List certs = new ArrayList<>(credential.getEncodedPublicKeyCertificateChain()); header.put("x5c", certs); } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java index e08744fc..75342c38 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java @@ -3,14 +3,11 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.SerializeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.MalformedURLException; -import java.nio.charset.StandardCharsets; import java.util.*; class TokenRequestExecutor { @@ -110,7 +107,7 @@ private void addQueryParameters(OAuthHttpRequest oauthHttpRequest) { private void addJWTBearerAssertionParams(Map queryParameters, String assertion) { queryParameters.put("client_assertion", assertion); - queryParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); + queryParameters.put("client_assertion_type", ClientAssertion.ASSERTION_TYPE_JWT_BEARER); } private AuthenticationResult createAuthenticationResultFromOauthHttpResponse(HttpResponse oauthHttpResponse) { @@ -121,15 +118,7 @@ private AuthenticationResult createAuthenticationResultFromOauthHttpResponse(Htt AccountCacheEntity accountCacheEntity = null; if (!StringHelper.isNullOrBlank(response.idToken())) { - String idTokenJson; - try { - idTokenJson = new String(Base64.getUrlDecoder().decode(response.idToken().split("\\.")[1]), StandardCharsets.UTF_8); - } catch (ArrayIndexOutOfBoundsException e) { - throw new MsalServiceException("Error parsing ID token, missing payload section. Ensure that the ID token is following the JWT format.", - AuthenticationErrorCode.INVALID_JWT); - } - - IdToken idToken = JsonHelper.convertJsonToObject(idTokenJson, IdToken.class); + IdToken idToken = JsonHelper.createIdTokenFromEncodedTokenString(response.idToken()); AuthorityType type = msalRequest.application().authenticationAuthority.authorityType; if (!StringHelper.isBlank(response.getClientInfo())) { diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/HelperAndUtilityTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/HelperAndUtilityTests.java index 32939513..08ca2e39 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/HelperAndUtilityTests.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/HelperAndUtilityTests.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; +import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateEncodingException; import java.util.*; @@ -147,4 +148,43 @@ void JwtHelper_buildJwt_ValidSha1AndSha256Assertions() throws MsalClientExceptio // Verify the correct certificate hash method was called verify(clientCertificateMock).publicCertificateHash(); } + + @Test + void JsonHelper_createIdTokenFromEncodedTokenString_Base64URLCharacters() { + HashMap tokenParameters = new HashMap<>(); + tokenParameters.put("preferred_username", "~nameWith~specialChars"); + String encodedIDToken = TestHelper.createIdToken(tokenParameters); + + try { + //TestHelper.createIdToken() should use Base64URL encoding, so first we prove that the encoded token cannot be decoded with Base64 decoder + Base64.getDecoder().decode(encodedIDToken); + + fail("IllegalArgumentException was expected but not thrown."); + } catch (IllegalArgumentException e) { + //Encoded token should have some "-" characters in it + assertTrue(e.getMessage().contains("Illegal base64 character 2e")); + } + + // Act + IdToken idToken = JsonHelper.createIdTokenFromEncodedTokenString(encodedIDToken); + + // Assert + assertNotNull(idToken); + assertEquals("~nameWith~specialChars", idToken.preferredUsername); + } + + @Test + void JsonHelper_createIdTokenFromEncodedTokenString_InvalidJsonInToken() { + // Arrange + String invalidPayload = "{not-valid-json}"; + String encodedPayload = Base64.getUrlEncoder().withoutPadding() + .encodeToString(invalidPayload.getBytes(StandardCharsets.UTF_8)); + String invalidToken = "header." + encodedPayload + ".signature"; + + // Act & Assert + MsalJsonParsingException exception = assertThrows(MsalJsonParsingException.class, + () -> JsonHelper.createIdTokenFromEncodedTokenString(invalidToken)); + + assertEquals(AuthenticationErrorCode.INVALID_JSON, exception.errorCode()); + } } From 19aaaf156530c558cb65f48e3ce414c000140b40 Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 29 Apr 2025 09:55:15 -0700 Subject: [PATCH 17/31] Remove nimbus from main library --- msal4j-sdk/pom.xml | 1 + .../aad/msal4j/AuthenticationResult.java | 18 +---- .../aad/msal4j/CustomJWTAuthentication.java | 63 ----------------- .../com/microsoft/aad/msal4j/HttpHelper.java | 1 + .../com/microsoft/aad/msal4j/JsonHelper.java | 69 ++++++++++++++++++- .../com/microsoft/aad/msal4j/TokenCache.java | 6 +- .../aad/msal4j/ClientCertificateTest.java | 1 - .../aad/msal4j/ManagedIdentityTests.java | 37 +++++----- .../aad/msal4j/RequestThrottlingTest.java | 3 +- .../com/microsoft/aad/msal4j/TestHelper.java | 43 +++++++----- .../aad/msal4j/TokenRequestExecutorTest.java | 20 ++---- .../aad/msal4j/UIRequiredCacheTest.java | 7 +- 12 files changed, 122 insertions(+), 147 deletions(-) delete mode 100644 msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CustomJWTAuthentication.java diff --git a/msal4j-sdk/pom.xml b/msal4j-sdk/pom.xml index c38f2023..f16c9386 100644 --- a/msal4j-sdk/pom.xml +++ b/msal4j-sdk/pom.xml @@ -37,6 +37,7 @@ com.nimbusds oauth2-oidc-sdk 11.23 + test net.minidev diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java index 9d3562b1..0e6ce4d7 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java @@ -3,15 +3,12 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.jwt.JWTParser; import lombok.AccessLevel; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.experimental.Accessors; -import java.io.Serializable; -import java.text.ParseException; import java.util.Date; @Accessors(fluent = true) @@ -45,14 +42,8 @@ private IdToken getIdTokenObj() { if (StringHelper.isBlank(idToken)) { return null; } - try { - String idTokenJson = JWTParser.parse(idToken).getParsedParts()[1].decodeToString(); - return JsonHelper.convertJsonToObject(idTokenJson, IdToken.class); - } catch (ParseException e) { - e.printStackTrace(); - } - return null; + return JsonHelper.createIdTokenFromEncodedTokenString(idToken); } @Getter(value = AccessLevel.PACKAGE) @@ -76,12 +67,7 @@ private ITenantProfile getTenantProfile() { return null; } - try { - return new TenantProfile(JWTParser.parse(idToken).getJWTClaimsSet().getClaims(), - getAccount().environment()); - } catch (ParseException e) { - throw new MsalClientException("Cached JWT could not be parsed: " + e.getMessage(), AuthenticationErrorCode.INVALID_JWT); - } + return new TenantProfile(JsonHelper.parseJsonToMap(JsonHelper.getTokenPayloadClaims(idToken)), getAccount().environment()); } private String environment; diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CustomJWTAuthentication.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CustomJWTAuthentication.java deleted file mode 100644 index b5aad36a..00000000 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CustomJWTAuthentication.java +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.aad.msal4j; - -import com.nimbusds.common.contenttype.ContentType; -import com.nimbusds.oauth2.sdk.SerializeException; -import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; -import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; -import com.nimbusds.oauth2.sdk.auth.JWTAuthentication; -import com.nimbusds.oauth2.sdk.http.HTTPRequest; -import com.nimbusds.oauth2.sdk.id.ClientID; -import com.nimbusds.oauth2.sdk.util.URLUtils; - -import java.util.*; - -public class CustomJWTAuthentication extends ClientAuthentication { - private ClientAssertion clientAssertion; - - protected CustomJWTAuthentication(ClientAuthenticationMethod method, ClientAssertion clientAssertion, ClientID clientID) { - super(method, clientID); - this.clientAssertion = clientAssertion; - } - - @Override - public Set getFormParameterNames() { - return Collections.unmodifiableSet(new HashSet(Arrays.asList("client_assertion", "client_assertion_type", "client_id"))); - - } - - @Override - public void applyTo(HTTPRequest httpRequest) { - if (httpRequest.getMethod() != HTTPRequest.Method.POST) { - throw new SerializeException("The HTTP request method must be POST"); - } else { - ContentType ct = httpRequest.getEntityContentType(); - if (ct == null) { - throw new SerializeException("Missing HTTP Content-Type header"); - } else if (!ct.matches(ContentType.APPLICATION_URLENCODED)) { - throw new SerializeException("The HTTP Content-Type header must be " + ContentType.APPLICATION_URLENCODED); - } else { - Map> params = httpRequest.getQueryParameters(); - params.putAll(this.toParameters()); - String queryString = URLUtils.serializeParameters(params); - httpRequest.setQuery(queryString); - } - } - } - - public Map> toParameters() { - HashMap> params = new HashMap<>(); - - try { - params.put("client_assertion", Collections.singletonList(this.clientAssertion.assertion())); - } catch (IllegalStateException var3) { - throw new SerializeException("Couldn't serialize JWT to a client assertion string: " + var3.getMessage(), var3); - } - - params.put("client_assertion_type", Collections.singletonList(JWTAuthentication.CLIENT_ASSERTION_TYPE)); - params.put("client_id", Collections.singletonList(getClientID().getValue())); - return params; - } -} diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java index f514f519..a1c12a8e 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java @@ -23,6 +23,7 @@ class HttpHelper implements IHttpHelper { public static final int HTTP_STATUS_200 = 200; public static final int HTTP_STATUS_400 = 400; + public static final int HTTP_STATUS_401 = 401; public static final int HTTP_STATUS_429 = 429; public static final int HTTP_STATUS_500 = 500; diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java index aa6a74e1..27963121 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java @@ -6,6 +6,7 @@ import com.azure.json.JsonProviders; import com.azure.json.JsonReader; import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; import com.azure.json.ReadValueCallback; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonParser; @@ -40,15 +41,77 @@ static T convertJsonToObject(final String json, final Class tClass) { } static IdToken createIdTokenFromEncodedTokenString(String token) { - String idTokenJson; + return JsonHelper.convertJsonToObject(getTokenPayloadClaims(token), IdToken.class); + } + + static String getTokenPayloadClaims(String token) { try { - idTokenJson = new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8); + return new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8); } catch (ArrayIndexOutOfBoundsException e) { throw new MsalClientException("Error parsing ID token, missing payload section.", AuthenticationErrorCode.INVALID_JWT); } + } + + //Converts a generic JSON string to a Map with relevant types + static Map parseJsonToMap(String jsonString) { + if (StringHelper.isBlank(jsonString)) { + return new HashMap<>(); + } + + try (JsonReader jsonReader = JsonProviders.createReader(jsonString)) { + jsonReader.nextToken(); + return parseJsonObject(jsonReader); + } catch (IOException e) { + throw new MsalJsonParsingException(e.getMessage(), AuthenticationErrorCode.INVALID_JSON); + } + } + + private static List parseJsonArray(JsonReader jsonReader) throws IOException { + List array = new ArrayList<>(); + + while (jsonReader.nextToken() != JsonToken.END_ARRAY) { + array.add(parseValue(jsonReader)); + } + + return array; + } + + private static Map parseJsonObject(JsonReader jsonReader) throws IOException { + Map object = new HashMap<>(); - return JsonHelper.convertJsonToObject(idTokenJson, IdToken.class); + while (jsonReader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = jsonReader.getFieldName(); + object.put(fieldName, parseValue(jsonReader)); + } + + return object; + } + + private static Object parseValue(JsonReader jsonReader) throws IOException { + JsonToken token = jsonReader.currentToken(); + + switch (token) { + case STRING: + return jsonReader.getString(); + case NUMBER: + try { + return jsonReader.getLong(); + } catch (ArithmeticException e) { + return jsonReader.getDouble(); + } + case BOOLEAN: + return jsonReader.getBoolean(); + case NULL: + return null; + case START_ARRAY: + return parseJsonArray(jsonReader); + case START_OBJECT: + return parseJsonObject(jsonReader); + default: + jsonReader.skipChildren(); + return null; + } } //This method is used to convert a JSON string to an object which implements the JsonSerializable interface from com.azure.json diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java index 28b6c6e7..6fd1f4a0 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java @@ -6,10 +6,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.nimbusds.jwt.JWTParser; import java.io.IOException; -import java.text.ParseException; import java.util.*; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -331,7 +329,7 @@ Set getAccounts(String clientId) { ITenantProfile profile = null; if (idToken != null) { - Map idTokenClaims = JWTParser.parse(idToken.secret()).getJWTClaimsSet().getClaims(); + Map idTokenClaims = JsonHelper.parseJsonToMap(JsonHelper.getTokenPayloadClaims(idToken.secret)); profile = new TenantProfile(idTokenClaims, accCached.environment()); } @@ -352,8 +350,6 @@ Set getAccounts(String clientId) { } return new HashSet<>(rootAccounts.values()); - } catch (ParseException e) { - throw new MsalClientException("Cached JWT could not be parsed: " + e.getMessage(), AuthenticationErrorCode.INVALID_JWT); } finally { lock.readLock().unlock(); } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java index d7979767..45ff96fb 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java @@ -3,7 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.auth.PrivateKeyJWT; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java index b006347f..7df0a121 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java @@ -3,8 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.util.URLUtils; -import labapi.App; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -12,7 +10,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; import org.mockito.junit.jupiter.MockitoExtension; import java.net.SocketException; @@ -78,69 +75,69 @@ private HttpRequest expectedRequest(ManagedIdentitySourceType source, String res ManagedIdentityId id) { String endpoint = null; Map headers = new HashMap<>(); - Map> queryParameters = new HashMap<>(); + Map queryParameters = new HashMap<>(); switch (source) { case APP_SERVICE: endpoint = appServiceEndpoint; - queryParameters.put("api-version", Collections.singletonList("2019-08-01")); - queryParameters.put("resource", Collections.singletonList(resource)); + queryParameters.put("api-version", "2019-08-01"); + queryParameters.put("resource", resource); headers.put("X-IDENTITY-HEADER", "secret"); break; case CLOUD_SHELL: endpoint = cloudShellEndpoint; headers.put("ContentType", "application/x-www-form-urlencoded"); headers.put("Metadata", "true"); - queryParameters.put("resource", Collections.singletonList(resource)); + queryParameters.put("resource", resource); break; case IMDS: endpoint = IMDS_ENDPOINT; - queryParameters.put("api-version", Collections.singletonList("2018-02-01")); - queryParameters.put("resource", Collections.singletonList(resource)); + queryParameters.put("api-version", "2018-02-01"); + queryParameters.put("resource", resource); headers.put("Metadata", "true"); break; case AZURE_ARC: endpoint = azureArcEndpoint; - queryParameters.put("api-version", Collections.singletonList("2019-11-01")); - queryParameters.put("resource", Collections.singletonList(resource)); + queryParameters.put("api-version", "2019-11-01"); + queryParameters.put("resource", resource); headers.put("Metadata", "true"); break; case SERVICE_FABRIC: endpoint = serviceFabricEndpoint; - queryParameters.put("api-version", Collections.singletonList("2019-07-01-preview")); - queryParameters.put("resource", Collections.singletonList(resource)); + queryParameters.put("api-version", "2019-07-01-preview"); + queryParameters.put("resource", resource); headers.put("secret", "secret"); break; case NONE: case DEFAULT_TO_IMDS: endpoint = IMDS_ENDPOINT; - queryParameters.put("api-version", Collections.singletonList("2018-02-01")); - queryParameters.put("resource", Collections.singletonList(resource)); + queryParameters.put("api-version", "2018-02-01"); + queryParameters.put("resource", resource); headers.put("Metadata", "true"); break; } switch (id.getIdType()) { case CLIENT_ID: - queryParameters.put("client_id", Collections.singletonList(id.getUserAssignedId())); + queryParameters.put("client_id", id.getUserAssignedId()); break; case RESOURCE_ID: - queryParameters.put("mi_res_id", Collections.singletonList(id.getUserAssignedId())); + queryParameters.put("mi_res_id", id.getUserAssignedId()); break; case OBJECT_ID: - queryParameters.put("object_id", singletonList(id.getUserAssignedId())); + queryParameters.put("object_id", id.getUserAssignedId()); break; } return new HttpRequest(HttpMethod.GET, computeUri(endpoint, queryParameters), headers); } - private String computeUri(String endpoint, Map> queryParameters) { + private String computeUri(String endpoint, Map queryParameters) { if (queryParameters.isEmpty()) { return endpoint; } - String queryString = URLUtils.serializeParameters(queryParameters); + String queryString = StringHelper.serializeQueryParameters(queryParameters); return endpoint + "?" + queryString; } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java index 71505e8a..81cd9812 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java @@ -3,7 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -97,7 +96,7 @@ private PublicClientApplication getClientApplicationMockedWithOneTokenEndpointRe switch (responseType) { case RETRY_AFTER_HEADER: - httpResponse.statusCode(HTTPResponse.SC_OK); + httpResponse.statusCode(HttpHelper.HTTP_STATUS_200); httpResponse.body(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS); headers.put("Retry-After", Arrays.asList(THROTTLE_IN_SEC.toString())); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java index 8bf51d4f..c7813544 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java @@ -3,17 +3,11 @@ package com.microsoft.aad.msal4j; -import com.azure.json.JsonProviders; -import com.azure.json.JsonReader; -import com.nimbusds.jose.*; -import com.nimbusds.jose.crypto.RSASSASigner; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; - import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.security.*; @@ -76,18 +70,29 @@ static void deleteFileContent(Class classInstance, String resource) static String generateToken() { try { - RSAKey rsaJWK = new RSAKeyGenerator(2048) - .keyID("kid") - .generate(); - JWSObject jwsObject = new JWSObject( - new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(), - new Payload("payload")); - - jwsObject.sign(new RSASSASigner(rsaJWK)); - - return jwsObject.serialize(); - } catch (JOSEException e) { - throw new RuntimeException(e); + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair keyPair = keyGen.generateKeyPair(); + + String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\",\"kid\":\"kid\"}"; + String encodedHeader = Base64.getUrlEncoder().withoutPadding() + .encodeToString(header.getBytes(StandardCharsets.UTF_8)); + + String payload = "payload"; + String encodedPayload = Base64.getUrlEncoder().withoutPadding() + .encodeToString(payload.getBytes(StandardCharsets.UTF_8)); + + String dataToSign = encodedHeader + "." + encodedPayload; + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initSign(keyPair.getPrivate()); + signature.update(dataToSign.getBytes(StandardCharsets.UTF_8)); + byte[] signatureBytes = signature.sign(); + String encodedSignature = Base64.getUrlEncoder().withoutPadding() + .encodeToString(signatureBytes); + + return encodedHeader + "." + encodedPayload + "." + encodedSignature; + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new RuntimeException("Error generating token: " + e.getMessage(), e); } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java index db7b67ed..83b178dd 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java @@ -3,8 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.SerializeException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -36,7 +34,7 @@ class TokenRequestExecutorTest { @Test void executeOAuthRequest_SCBadRequestErrorInvalidGrant_InteractionRequiredException() - throws SerializeException, ParseException, MsalException, + throws MsalException, IOException, URISyntaxException { TokenRequestExecutor request = createMockedTokenRequest(); @@ -73,7 +71,7 @@ void executeOAuthRequest_SCBadRequestErrorInvalidGrant_InteractionRequiredExcept @Test void executeOAuthRequest_SCBadRequestErrorInvalidGrant_SubErrorFilteredServiceExceptionThrown() - throws SerializeException, ParseException, MsalException, + throws MsalException, IOException, URISyntaxException { TokenRequestExecutor request = createMockedTokenRequest(); @@ -108,7 +106,7 @@ void executeOAuthRequest_SCBadRequestErrorInvalidGrant_SubErrorFilteredServiceEx } } - private TokenRequestExecutor createMockedTokenRequest() throws URISyntaxException, MalformedURLException { + private TokenRequestExecutor createMockedTokenRequest() throws MalformedURLException { PublicClientApplication app = PublicClientApplication.builder("id") .correlationId("corr_id").build(); @@ -154,7 +152,7 @@ void testConstructor() throws MalformedURLException, @Test void testToOAuthRequestNonEmptyCorrelationId() - throws MalformedURLException, SerializeException, URISyntaxException, ParseException { + throws MalformedURLException, URISyntaxException { PublicClientApplication app = PublicClientApplication.builder("id").correlationId("corr-id").build(); @@ -181,9 +179,7 @@ void testToOAuthRequestNonEmptyCorrelationId() } @Test - void testToOAuthRequestNullCorrelationId_NullClientAuth() - throws MalformedURLException, SerializeException, - URISyntaxException { + void testToOAuthRequestNullCorrelationId_NullClientAuth() throws MalformedURLException, URISyntaxException { PublicClientApplication app = PublicClientApplication.builder("id").correlationId("corr-id").build(); @@ -207,8 +203,7 @@ void testToOAuthRequestNullCorrelationId_NullClientAuth() } @Test - void testExecuteOAuth_Success() throws SerializeException, ParseException, MsalException, - IOException, URISyntaxException { + void testExecuteOAuth_Success() throws MsalException, IOException, URISyntaxException { PublicClientApplication app = PublicClientApplication.builder("id").correlationId("corr-id").build(); @@ -251,8 +246,7 @@ void testExecuteOAuth_Success() throws SerializeException, ParseException, MsalE } @Test - void testExecuteOAuth_Failure() throws SerializeException, - ParseException, MsalException, IOException, URISyntaxException { + void testExecuteOAuth_Failure() throws MsalException, IOException, URISyntaxException { PublicClientApplication app = PublicClientApplication.builder("id").correlationId("corr-id").build(); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java index d27c82a6..efd56cfa 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java @@ -3,7 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -88,12 +87,10 @@ private PublicClientApplication getApp_MockedWith_OKTokenEndpointResponse_Invali throws Exception { IHttpClient httpClientMock = mock(IHttpClient.class); - HttpResponse httpResponse = - getHttpResponse(HTTPResponse.SC_OK, TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS); + HttpResponse httpResponse = getHttpResponse(HttpHelper.HTTP_STATUS_200, TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS); lenient().doReturn(httpResponse).when(httpClientMock).send(any()); - httpResponse = getHttpResponse(HTTPResponse.SC_UNAUTHORIZED, - TestConfiguration.TOKEN_ENDPOINT_INVALID_GRANT_ERROR_RESPONSE); + httpResponse = getHttpResponse(HttpHelper.HTTP_STATUS_401, TestConfiguration.TOKEN_ENDPOINT_INVALID_GRANT_ERROR_RESPONSE); lenient().doReturn(httpResponse).when(httpClientMock).send(any()); PublicClientApplication app = getPublicClientApp(httpClientMock); From 67d3131adc0c56baf44f9ac46a05491c8e8b4634 Mon Sep 17 00:00:00 2001 From: avdunn Date: Wed, 30 Apr 2025 11:16:19 -0700 Subject: [PATCH 18/31] Final Lombok removal --- msal4j-sdk/pom.xml | 2 +- .../com/microsoft/aad/msal4j/Account.java | 57 +++- .../aad/msal4j/AccountCacheEntity.java | 115 +++++++- .../aad/msal4j/AuthenticationResult.java | 261 +++++++++++++++--- .../microsoft/aad/msal4j/ClientAssertion.java | 29 +- .../microsoft/aad/msal4j/ClientSecret.java | 29 +- .../com/microsoft/aad/msal4j/HttpRequest.java | 54 +++- 7 files changed, 468 insertions(+), 79 deletions(-) diff --git a/msal4j-sdk/pom.xml b/msal4j-sdk/pom.xml index f16c9386..725dc062 100644 --- a/msal4j-sdk/pom.xml +++ b/msal4j-sdk/pom.xml @@ -59,7 +59,7 @@ org.projectlombok lombok 1.18.36 - provided + test com.azure diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Account.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Account.java index 1c541d21..f2347547 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Account.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Account.java @@ -3,34 +3,63 @@ package com.microsoft.aad.msal4j; -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - import java.util.Map; +import java.util.Objects; /** * Representation of a single user account. If modifying this object, ensure it is compliant with * cache persistent model */ -@Accessors(fluent = true) -@Getter -@Setter -@EqualsAndHashCode(of = {"homeAccountId"}) -@AllArgsConstructor class Account implements IAccount { String homeAccountId; - String environment; - String username; - Map tenantProfiles; + Account(String homeAccountId, String environment, String username, Map tenantProfiles) { + this.homeAccountId = homeAccountId; + this.environment = environment; + this.username = username; + this.tenantProfiles = tenantProfiles; + } + public Map getTenantProfiles() { return tenantProfiles; } + + public String homeAccountId() { + return this.homeAccountId; + } + + public String environment() { + return this.environment; + } + + public String username() { + return this.username; + } + + void username(String username) { + this.username = username; + } + + //These methods are based on those generated by Lombok's @EqualsAndHashCode annotation. + //They have the same functionality as the generated methods, but were refactored for readability. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Account)) return false; + + Account other = (Account) o; + + return Objects.equals(homeAccountId(), other.homeAccountId()); + } + + @Override + public int hashCode() { + int result = 1; + result = result * 59 + (this.homeAccountId == null ? 43 : this.homeAccountId.hashCode()); + return result; + } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AccountCacheEntity.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AccountCacheEntity.java index e3ac2578..f404b2f0 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AccountCacheEntity.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AccountCacheEntity.java @@ -4,18 +4,12 @@ package com.microsoft.aad.msal4j; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; -import lombok.experimental.Accessors; import java.io.Serializable; import java.util.ArrayList; import java.util.List; -import java.util.Map; +import java.util.Objects; -@Accessors(fluent = true) -@Getter -@Setter -@EqualsAndHashCode class AccountCacheEntity implements Serializable { static final String MSSTS_ACCOUNT_TYPE = "MSSTS"; @@ -106,4 +100,111 @@ static AccountCacheEntity create(String clientInfoStr, Authority requestAuthorit IAccount toAccount() { return new Account(homeAccountId, environment, username, null); } + + String homeAccountId() { + return this.homeAccountId; + } + + String environment() { + return this.environment; + } + + String realm() { + return this.realm; + } + + String localAccountId() { + return this.localAccountId; + } + + String username() { + return this.username; + } + + String name() { + return this.name; + } + + String clientInfoStr() { + return this.clientInfoStr; + } + + String userAssertionHash() { + return this.userAssertionHash; + } + + String authorityType() { + return this.authorityType; + } + + void homeAccountId(String homeAccountId) { + this.homeAccountId = homeAccountId; + } + + void environment(String environment) { + this.environment = environment; + } + + void realm(String realm) { + this.realm = realm; + } + + void localAccountId(String localAccountId) { + this.localAccountId = localAccountId; + } + + void username(String username) { + this.username = username; + } + + void name(String name) { + this.name = name; + } + + void clientInfoStr(String clientInfoStr) { + this.clientInfoStr = clientInfoStr; + } + + void userAssertionHash(String userAssertionHash) { + this.userAssertionHash = userAssertionHash; + } + + void authorityType(String authorityType) { + this.authorityType = authorityType; + } + + //These methods are based on those generated by Lombok's @EqualsAndHashCode annotation. + //They have the same functionality as the generated methods, but were refactored for readability. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AccountCacheEntity)) return false; + + AccountCacheEntity other = (AccountCacheEntity) o; + + if (!Objects.equals(homeAccountId(), other.homeAccountId())) return false; + if (!Objects.equals(environment(), other.environment())) return false; + if (!Objects.equals(realm(), other.realm())) return false; + if (!Objects.equals(localAccountId(), other.localAccountId())) return false; + if (!Objects.equals(username(), other.username())) return false; + if (!Objects.equals(name(), other.name())) return false; + if (!Objects.equals(clientInfoStr(), other.clientInfoStr())) return false; + if (!Objects.equals(userAssertionHash(), other.userAssertionHash())) return false; + return Objects.equals(authorityType(), other.authorityType()); + } + + @Override + public int hashCode() { + int result = 1; + result = result * 59 + (this.homeAccountId == null ? 43 : this.homeAccountId.hashCode()); + result = result * 59 + (this.environment == null ? 43 : this.environment.hashCode()); + result = result * 59 + (this.realm == null ? 43 : this.realm.hashCode()); + result = result * 59 + (this.localAccountId == null ? 43 : this.localAccountId.hashCode()); + result = result * 59 + (this.username == null ? 43 : this.username.hashCode()); + result = result * 59 + (this.name() == null ? 43 : this.name().hashCode()); + result = result * 59 + (this.clientInfoStr == null ? 43 : this.clientInfoStr.hashCode()); + result = result * 59 + (this.userAssertionHash == null ? 43 : this.userAssertionHash.hashCode()); + result = result * 59 + (this.authorityType == null ? 43 : this.authorityType.hashCode()); + return result; + } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java index 0e6ce4d7..2419edab 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java @@ -3,40 +3,44 @@ package com.microsoft.aad.msal4j; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.experimental.Accessors; - import java.util.Date; +import java.util.Objects; -@Accessors(fluent = true) -@Getter -@EqualsAndHashCode -@Builder final class AuthenticationResult implements IAuthenticationResult { private static final long serialVersionUID = 1L; private final String accessToken; - - @Getter(value = AccessLevel.PACKAGE) private final long expiresOn; - - @Getter(value = AccessLevel.PACKAGE) private final long extExpiresOn; - private final String refreshToken; - private final Long refreshOn; - - @Getter(value = AccessLevel.PACKAGE) private final String familyId; - private final String idToken; - - @Getter(value = AccessLevel.PACKAGE, lazy = true) private final IdToken idTokenObject = getIdTokenObj(); + private final AccountCacheEntity accountCacheEntity; + private final IAccount account = getAccount(); + private final ITenantProfile tenantProfile = getTenantProfile(); + private String environment; + private final Date expiresOnDate; + private final String scopes; + private final AuthenticationResultMetadata metadata; + private final Boolean isPopAuthorization; + + AuthenticationResult(String accessToken, long expiresOn, long extExpiresOn, String refreshToken, Long refreshOn, String familyId, String idToken, AccountCacheEntity accountCacheEntity, String environment, String scopes, AuthenticationResultMetadata metadata, Boolean isPopAuthorization) { + this.accessToken = accessToken; + this.expiresOn = expiresOn; + this.extExpiresOn = extExpiresOn; + this.refreshToken = refreshToken; + this.refreshOn = refreshOn; + this.familyId = familyId; + this.idToken = idToken; + this.accountCacheEntity = accountCacheEntity; + this.environment = environment; + this.scopes = scopes; + this.metadata = metadata == null ? AuthenticationResultMetadata.builder().build() : metadata; + this.isPopAuthorization = isPopAuthorization; + this.expiresOnDate = new Date(expiresOn * 1000); + } private IdToken getIdTokenObj() { if (StringHelper.isBlank(idToken)) { @@ -46,12 +50,6 @@ private IdToken getIdTokenObj() { return JsonHelper.createIdTokenFromEncodedTokenString(idToken); } - @Getter(value = AccessLevel.PACKAGE) - private final AccountCacheEntity accountCacheEntity; - - @Getter(lazy = true) - private final IAccount account = getAccount(); - private IAccount getAccount() { if (accountCacheEntity == null) { return null; @@ -59,9 +57,6 @@ private IAccount getAccount() { return accountCacheEntity.toAccount(); } - @Getter(lazy = true) - private final ITenantProfile tenantProfile = getTenantProfile(); - private ITenantProfile getTenantProfile() { if (StringHelper.isBlank(idToken)) { return null; @@ -70,16 +65,206 @@ private ITenantProfile getTenantProfile() { return new TenantProfile(JsonHelper.parseJsonToMap(JsonHelper.getTokenPayloadClaims(idToken)), getAccount().environment()); } - private String environment; + public String accessToken() { + return this.accessToken; + } - @Getter(lazy = true) - private final Date expiresOnDate = new Date(expiresOn * 1000); + String refreshToken() { + return this.refreshToken; + } - private final String scopes; + Long refreshOn() { + return this.refreshOn; + } - @Builder.Default - private final AuthenticationResultMetadata metadata = AuthenticationResultMetadata.builder().build(); + public String idToken() { + return this.idToken; + } - @Getter(value = AccessLevel.PACKAGE) - private final Boolean isPopAuthorization; + public String environment() { + return this.environment; + } + + public String scopes() { + return this.scopes; + } + + public AuthenticationResultMetadata metadata() { + return this.metadata; + } + + long expiresOn() { + return this.expiresOn; + } + + long extExpiresOn() { + return this.extExpiresOn; + } + + String familyId() { + return this.familyId; + } + + IdToken idTokenObject() { + return this.idTokenObject; + } + + AccountCacheEntity accountCacheEntity() { + return this.accountCacheEntity; + } + + public IAccount account() { + return getAccount(); + } + + public ITenantProfile tenantProfile() { + return this.tenantProfile; + } + + public Date expiresOnDate() { + return this.expiresOnDate; + } + + Boolean isPopAuthorization() { + return this.isPopAuthorization; + } + + static AuthenticationResultBuilder builder() { + return new AuthenticationResultBuilder(); + } + + static class AuthenticationResultBuilder { + private String accessToken; + private long expiresOn; + private long extExpiresOn; + private String refreshToken; + private Long refreshOn; + private String familyId; + private String idToken; + private AccountCacheEntity accountCacheEntity; + private String environment; + private String scopes; + private AuthenticationResultMetadata metadata; + private Boolean isPopAuthorization; + + AuthenticationResultBuilder() { + } + + public AuthenticationResultBuilder accessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + public AuthenticationResultBuilder expiresOn(long expiresOn) { + this.expiresOn = expiresOn; + return this; + } + + public AuthenticationResultBuilder extExpiresOn(long extExpiresOn) { + this.extExpiresOn = extExpiresOn; + return this; + } + + public AuthenticationResultBuilder refreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + public AuthenticationResultBuilder refreshOn(Long refreshOn) { + this.refreshOn = refreshOn; + return this; + } + + public AuthenticationResultBuilder familyId(String familyId) { + this.familyId = familyId; + return this; + } + + public AuthenticationResultBuilder idToken(String idToken) { + this.idToken = idToken; + return this; + } + + public AuthenticationResultBuilder accountCacheEntity(AccountCacheEntity accountCacheEntity) { + this.accountCacheEntity = accountCacheEntity; + return this; + } + + public AuthenticationResultBuilder environment(String environment) { + this.environment = environment; + return this; + } + + public AuthenticationResultBuilder scopes(String scopes) { + this.scopes = scopes; + return this; + } + + public AuthenticationResultBuilder metadata(AuthenticationResultMetadata metadata) { + this.metadata = metadata; + return this; + } + + public AuthenticationResultBuilder isPopAuthorization(Boolean isPopAuthorization) { + this.isPopAuthorization = isPopAuthorization; + return this; + } + + public AuthenticationResult build() { + return new AuthenticationResult(this.accessToken, this.expiresOn, this.extExpiresOn, this.refreshToken, this.refreshOn, this.familyId, this.idToken, this.accountCacheEntity, this.environment, this.scopes, this.metadata, this.isPopAuthorization); + } + + public String toString() { + return "AuthenticationResult.AuthenticationResultBuilder(accessToken=" + this.accessToken + ", expiresOn=" + this.expiresOn + ", extExpiresOn=" + this.extExpiresOn + ", refreshToken=" + this.refreshToken + ", refreshOn=" + this.refreshOn + ", familyId=" + this.familyId + ", idToken=" + this.idToken + ", accountCacheEntity=" + this.accountCacheEntity + ", environment=" + this.environment + ", scopes=" + this.scopes + ", metadata" + this.metadata + ", isPopAuthorization=" + this.isPopAuthorization + ")"; + } + } + + //These methods are based on those generated by Lombok's @EqualsAndHashCode annotation. + //They have the same functionality as the generated methods, but were refactored for readability. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AuthenticationResult)) return false; + + AuthenticationResult other = (AuthenticationResult) o; + + if (this.expiresOn() != other.expiresOn()) return false; + if (this.extExpiresOn() != other.extExpiresOn()) return false; + if (!Objects.equals(refreshOn, other.refreshOn)) return false; + if (!Objects.equals(isPopAuthorization, other.isPopAuthorization)) return false; + if (!Objects.equals(accessToken, other.accessToken)) return false; + if (!Objects.equals(refreshToken, other.refreshToken)) return false; + if (!Objects.equals(familyId, other.familyId)) return false; + if (!Objects.equals(idToken, other.idToken)) return false; + if (!Objects.equals(idTokenObject, other.idTokenObject)) return false; + if (!Objects.equals(accountCacheEntity, other.accountCacheEntity)) return false; + if (!Objects.equals(account, other.account)) return false; + if (!Objects.equals(tenantProfile, other.tenantProfile)) return false; + if (!Objects.equals(environment, other.environment)) return false; + if (!Objects.equals(expiresOnDate, other.expiresOnDate)) return false; + if (!Objects.equals(scopes, other.scopes)) return false; + return Objects.equals(metadata, other.metadata); + } + + @Override + public int hashCode() { + int result = 1; + result = result * 59 + (int) (this.expiresOn >>> 32 ^ this.expiresOn); + result = result * 59 + (int) (this.extExpiresOn >>> 32 ^ this.extExpiresOn); + result = result * 59 + (this.refreshOn == null ? 43 : this.refreshOn.hashCode()); + result = result * 59 + (this.isPopAuthorization == null ? 43 : this.isPopAuthorization.hashCode()); + result = result * 59 + (this.accessToken == null ? 43 : this.accessToken.hashCode()); + result = result * 59 + (this.refreshToken == null ? 43 : this.refreshToken.hashCode()); + result = result * 59 + (this.familyId == null ? 43 : this.familyId.hashCode()); + result = result * 59 + (this.idToken == null ? 43 : this.idToken.hashCode()); + result = result * 59 + (this.idTokenObject == null ? 43 : this.idTokenObject.hashCode()); + result = result * 59 + (this.accountCacheEntity == null ? 43 : this.accountCacheEntity.hashCode()); + result = result * 59 + (this.account == null ? 43 : this.account.hashCode()); + result = result * 59 + (this.tenantProfile == null ? 43 : this.tenantProfile.hashCode()); + result = result * 59 + (this.environment == null ? 43 : this.environment.hashCode()); + result = result * 59 + (this.expiresOnDate == null ? 43 : this.expiresOnDate.hashCode()); + result = result * 59 + (this.scopes == null ? 43 : this.scopes.hashCode()); + result = result * 59 + (this.metadata == null ? 43 : this.metadata.hashCode()); + return result; + } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java index f5fe64e9..39f24ad4 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java @@ -3,13 +3,8 @@ package com.microsoft.aad.msal4j; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.experimental.Accessors; +import java.util.Objects; -@Accessors(fluent = true) -@Getter -@EqualsAndHashCode final class ClientAssertion implements IClientAssertion { static final String ASSERTION_TYPE_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; @@ -22,4 +17,26 @@ final class ClientAssertion implements IClientAssertion { this.assertion = assertion; } + + public String assertion() { + return this.assertion; + } + + //These methods are based on those generated by Lombok's @EqualsAndHashCode annotation. + //They have the same functionality as the generated methods, but were refactored for readability. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ClientAssertion)) return false; + + ClientAssertion other = (ClientAssertion) o; + return Objects.equals(assertion(), other.assertion()); + } + + @Override + public int hashCode() { + int result = 1; + result = result * 59 + (this.assertion == null ? 43 : this.assertion.hashCode()); + return result; + } } \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientSecret.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientSecret.java index 99f714bc..3d7767ba 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientSecret.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientSecret.java @@ -3,15 +3,10 @@ package com.microsoft.aad.msal4j; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.experimental.Accessors; +import java.util.Objects; -@EqualsAndHashCode final class ClientSecret implements IClientSecret { - @Accessors(fluent = true) - @Getter private final String clientSecret; /** @@ -26,4 +21,26 @@ final class ClientSecret implements IClientSecret { this.clientSecret = clientSecret; } + + public String clientSecret() { + return this.clientSecret; + } + + //These methods are based on those generated by Lombok's @EqualsAndHashCode annotation. + //They have the same functionality as the generated methods, but were refactored for readability. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ClientSecret)) return false; + + ClientSecret other = (ClientSecret) o; + return Objects.equals(clientSecret, other.clientSecret); + } + + @Override + public int hashCode() { + int result = 1; + result = result * 59 + (this.clientSecret == null ? 43 : this.clientSecret.hashCode()); + return result; + } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpRequest.java index f64b447e..2923f799 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpRequest.java @@ -3,21 +3,15 @@ package com.microsoft.aad.msal4j; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.experimental.Accessors; - import java.net.MalformedURLException; import java.net.URL; import java.util.Map; +import java.util.Objects; /** * Contains information about outgoing HTTP request. Should be adapted to HTTP request for HTTP * client of choice */ -@Getter -@Accessors(fluent = true) -@EqualsAndHashCode public class HttpRequest { /** @@ -89,4 +83,50 @@ private URL createUrlFromString(String stringUrl) { return url; } + + public HttpMethod httpMethod() { + return this.httpMethod; + } + + public URL url() { + return this.url; + } + + public Map headers() { + return this.headers; + } + + public String body() { + return this.body; + } + + //These methods are based on those generated by Lombok's @EqualsAndHashCode annotation. + //They have the same functionality as the generated methods, but were refactored for readability. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof HttpRequest)) return false; + + HttpRequest other = (HttpRequest) o; + if (!other.canEqual(this)) return false; + + if (!Objects.equals(httpMethod(), other.httpMethod())) return false; + if (!Objects.equals(url(), other.url())) return false; + if (!Objects.equals(headers(), other.headers())) return false; + return Objects.equals(body(), other.body()); + } + + protected boolean canEqual(Object other) { + return other instanceof HttpRequest; + } + + @Override + public int hashCode() { + int result = 1; + result = result * 59 + (this.httpMethod == null ? 43 : this.httpMethod.hashCode()); + result = result * 59 + (this.url == null ? 43 : this.url.hashCode()); + result = result * 59 + (this.headers == null ? 43 : this.headers.hashCode()); + result = result * 59 + (this.body == null ? 43 : this.body.hashCode()); + return result; + } } From 0ea43624c918eb422d2baed5caf9581449f88c93 Mon Sep 17 00:00:00 2001 From: avdunn Date: Mon, 5 May 2025 15:23:24 -0700 Subject: [PATCH 19/31] Use azure-json in claims classes --- .../microsoft/aad/msal4j/ClaimsRequest.java | 149 +++++++++++------- .../microsoft/aad/msal4j/RequestedClaim.java | 55 +++++-- .../msal4j/RequestedClaimAdditionalInfo.java | 83 ++++++++-- .../com/microsoft/aad/msal4j/ClaimsTest.java | 107 ++++++++++++- 4 files changed, 311 insertions(+), 83 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClaimsRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClaimsRequest.java index ebf1a0e8..3093008c 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClaimsRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClaimsRequest.java @@ -3,16 +3,16 @@ package com.microsoft.aad.msal4j; +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; -import com.fasterxml.jackson.databind.node.ObjectNode; - +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; /** @@ -20,7 +20,7 @@ * * @see https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter */ -public class ClaimsRequest { +public class ClaimsRequest implements JsonSerializable { List idTokenRequestedClaims = new ArrayList<>(); List userInfoRequestedClaims = new ArrayList<>(); @@ -62,31 +62,47 @@ protected void requestClaimInAccessToken(String claim, RequestedClaimAdditionalI * @return a String following JSON formatting */ public String formatAsJSONString() { - ObjectMapper mapper = new ObjectMapper(); - ObjectNode rootNode = mapper.createObjectNode(); + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + toJson(jsonWriter); - if (!idTokenRequestedClaims.isEmpty()) { - rootNode.set("id_token", convertClaimsToObjectNode(idTokenRequestedClaims)); - } - if (!userInfoRequestedClaims.isEmpty()) { - rootNode.set("userinfo", convertClaimsToObjectNode(userInfoRequestedClaims)); - } - if (!accessTokenRequestedClaims.isEmpty()) { - rootNode.set("access_token", convertClaimsToObjectNode(accessTokenRequestedClaims)); + jsonWriter.flush(); + return outputStream.toString(StandardCharsets.UTF_8.name()); + } catch (IOException e) { + throw new MsalClientException("Could not convert ClaimsRequest to string: " + e.getMessage(), AuthenticationErrorCode.INVALID_JSON); } + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); - return mapper.valueToTree(rootNode).toString(); + writeClaimsToJsonWriter(jsonWriter, "id_token", idTokenRequestedClaims); + writeClaimsToJsonWriter(jsonWriter, "userinfo", userInfoRequestedClaims); + writeClaimsToJsonWriter(jsonWriter, "access_token", accessTokenRequestedClaims); + + jsonWriter.writeEndObject(); + return jsonWriter; } - private ObjectNode convertClaimsToObjectNode(List claims) { - ObjectMapper mapper = new ObjectMapper(); - ObjectNode claimsNode = mapper.createObjectNode(); + private void writeClaimsToJsonWriter(JsonWriter jsonWriter, String sectionName, List claims) throws IOException { + if (claims.isEmpty()) { + return; + } + jsonWriter.writeStartObject(sectionName); for (RequestedClaim claim : claims) { - claimsNode.setAll((ObjectNode) mapper.valueToTree(claim)); + if (claim.name != null) { + if (claim.getRequestedClaimAdditionalInfo() != null) { + jsonWriter.writeJsonField(claim.name, claim.getRequestedClaimAdditionalInfo()); + } else { + jsonWriter.writeNullField(claim.name); + } + } } - return claimsNode; + + jsonWriter.writeEndObject(); } /** @@ -96,49 +112,74 @@ private ObjectNode convertClaimsToObjectNode(List claims) { * @return a ClaimsRequest instance */ public static ClaimsRequest formatAsClaimsRequest(String claims) { - try { - ClaimsRequest cr = new ClaimsRequest(); + try (JsonReader jsonReader = JsonProviders.createReader(claims)) { + ClaimsRequest claimsRequest = new ClaimsRequest(); - ObjectMapper mapper = new ObjectMapper(); - ObjectReader reader = mapper.readerFor(new TypeReference>() { - }); - - JsonNode jsonClaims = mapper.readTree(claims); + return jsonReader.readObject(reader -> { + if (reader.currentToken() != JsonToken.START_OBJECT) { + throw new IllegalStateException("Expected start of object but was " + reader.currentToken()); + } - addClaimsFromJsonNode(jsonClaims.get("id_token"), "id_token", cr, reader); - addClaimsFromJsonNode(jsonClaims.get("userinfo"), "userinfo", cr, reader); - addClaimsFromJsonNode(jsonClaims.get("access_token"), "access_token", cr, reader); + while (reader.nextToken() != JsonToken.END_OBJECT) { + parseClaims(reader, claimsRequest, reader.getFieldName()); + } - return cr; + return claimsRequest; + }); } catch (IOException e) { - throw new MsalClientException("Could not convert string to ClaimsRequest: " + e.getMessage(), AuthenticationErrorCode.INVALID_JSON); + throw new MsalClientException("Could not convert string to ClaimsRequest: " + e.getMessage(), + AuthenticationErrorCode.INVALID_JSON); } } - private static void addClaimsFromJsonNode(JsonNode claims, String group, ClaimsRequest cr, ObjectReader reader) throws IOException { - Iterator claimsIterator; + private static void parseClaims(JsonReader jsonReader, ClaimsRequest claimsRequest, String section) throws IOException { + if (jsonReader.currentToken() != JsonToken.FIELD_NAME) { + jsonReader.nextToken(); + } - if (claims != null) { - claimsIterator = claims.fieldNames(); - while (claimsIterator.hasNext()) { - String claim = claimsIterator.next(); - Boolean essential = null; + jsonReader.nextToken(); + if (jsonReader.currentToken() != JsonToken.START_OBJECT) { + throw new IllegalStateException("Expected start of object but was " + jsonReader.currentToken()); + } + + while (jsonReader.nextToken() != JsonToken.END_OBJECT) { + String claimName = jsonReader.getFieldName(); + jsonReader.nextToken(); + + RequestedClaimAdditionalInfo claimInfo = null; + if (jsonReader.currentToken() == JsonToken.START_OBJECT) { + boolean essential = false; String value = null; List values = null; - RequestedClaimAdditionalInfo claimInfo = null; - if (claims.get(claim).has("essential")) essential = claims.get(claim).get("essential").asBoolean(); - if (claims.get(claim).has("value")) value = claims.get(claim).get("value").textValue(); - if (claims.get(claim).has("values")) values = reader.readValue(claims.get(claim).get("values")); + while (jsonReader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = jsonReader.getFieldName(); + jsonReader.nextToken(); + + switch (fieldName) { + case "essential": essential = jsonReader.getBoolean(); break; + case "value": value = jsonReader.getString(); break; + case "values": + values = new ArrayList<>(); + if (jsonReader.currentToken() == JsonToken.START_ARRAY) { + while (jsonReader.nextToken() != JsonToken.END_ARRAY) { + values.add(jsonReader.getString()); + } + } + break; + default: jsonReader.skipChildren(); break; + } + } - //'null' is a valid value for RequestedClaimAdditionalInfo, so only initialize it if one of the parameters is not null - if (essential != null || value != null || values != null) { - claimInfo = new RequestedClaimAdditionalInfo(essential == null ? false : essential, value, values); + if (essential || value != null || values != null) { + claimInfo = new RequestedClaimAdditionalInfo(essential, value, values); } + } - if (group.equals("id_token")) cr.requestClaimInIdToken(claim, claimInfo); - if (group.equals("userinfo")) cr.requestClaimInUserInfo(claim, claimInfo); - if (group.equals("access_token")) cr.requestClaimInAccessToken(claim, claimInfo); + switch (section) { + case "access_token": claimsRequest.requestClaimInAccessToken(claimName, claimInfo); break; + case "id_token": claimsRequest.requestClaimInIdToken(claimName, claimInfo); break; + case "userinfo": claimsRequest.requestClaimInUserInfo(claimName, claimInfo); break; } } } @@ -150,4 +191,4 @@ public List getIdTokenRequestedClaims() { public void setIdTokenRequestedClaims(List idTokenRequestedClaims) { this.idTokenRequestedClaims = idTokenRequestedClaims; } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RequestedClaim.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RequestedClaim.java index 94cc358b..e9e779da 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RequestedClaim.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RequestedClaim.java @@ -3,10 +3,12 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; +import java.io.IOException; import java.util.Collections; import java.util.Map; @@ -15,21 +17,56 @@ * * @see https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class RequestedClaim { +public class RequestedClaim implements JsonSerializable { - @JsonIgnore public String name; + private RequestedClaimAdditionalInfo requestedClaimAdditionalInfo; - RequestedClaimAdditionalInfo requestedClaimAdditionalInfo; + RequestedClaim() {} public RequestedClaim(String name, RequestedClaimAdditionalInfo requestedClaimAdditionalInfo) { this.name = name; this.requestedClaimAdditionalInfo = requestedClaimAdditionalInfo; } - @JsonAnyGetter + static RequestedClaim fromJson(JsonReader jsonReader) throws IOException { + RequestedClaim claim = new RequestedClaim(); + return jsonReader.readObject(reader -> { + if (reader.currentToken() != JsonToken.START_OBJECT) { + throw new IllegalStateException("Expected start of object but was " + reader.currentToken()); + } + + claim.name = reader.getFieldName(); + + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(false, null, null); + claim.requestedClaimAdditionalInfo = info.fromJson(reader); + + return claim; + }); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + if (name != null && requestedClaimAdditionalInfo != null) { + jsonWriter.writeString(name); + requestedClaimAdditionalInfo.toJson(jsonWriter); + } + + jsonWriter.writeEndObject(); + return jsonWriter; + } + + RequestedClaimAdditionalInfo getRequestedClaimAdditionalInfo() { + return requestedClaimAdditionalInfo; + } + + void setRequestedClaimAdditionalInfo(RequestedClaimAdditionalInfo requestedClaimAdditionalInfo) { + this.requestedClaimAdditionalInfo = requestedClaimAdditionalInfo; + } + protected Map any() { return Collections.singletonMap(name, requestedClaimAdditionalInfo); } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RequestedClaimAdditionalInfo.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RequestedClaimAdditionalInfo.java index f1485e36..2428f969 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RequestedClaimAdditionalInfo.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RequestedClaimAdditionalInfo.java @@ -3,9 +3,13 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; +import java.io.IOException; +import java.util.ArrayList; import java.util.List; /** @@ -13,18 +17,11 @@ * * @see https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class RequestedClaimAdditionalInfo { +public class RequestedClaimAdditionalInfo implements JsonSerializable { - @JsonInclude(JsonInclude.Include.NON_DEFAULT) - @JsonProperty("essential") - boolean essential; - - @JsonProperty("value") - String value; - - @JsonProperty("values") - List values; + private boolean essential; + private String value; + private List values; public RequestedClaimAdditionalInfo(boolean essential, String value, List values) { this.essential = essential; @@ -32,6 +29,64 @@ public RequestedClaimAdditionalInfo(boolean essential, String value, List(); + while (jsonReader.nextToken() != JsonToken.END_ARRAY) { + values.add(jsonReader.getString()); + } + break; + default: + jsonReader.skipChildren(); + break; + } + } + + return this; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + if (essential) { + jsonWriter.writeBooleanField("essential", essential); + } + + if (value != null) { + jsonWriter.writeStringField("value", value); + } + + if (values != null && !values.isEmpty()) { + jsonWriter.writeStartArray("values"); + for (String val : values) { + jsonWriter.writeString(val); + } + jsonWriter.writeEndArray(); + } + + jsonWriter.writeEndObject(); + return jsonWriter; + } + public boolean isEssential() { return this.essential; } @@ -55,4 +110,4 @@ public void setValue(String value) { public void setValues(List values) { this.values = values; } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClaimsTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClaimsTest.java index 791f7a53..feb95d39 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClaimsTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClaimsTest.java @@ -3,16 +3,19 @@ package com.microsoft.aad.msal4j; +import com.azure.json.JsonProviders; +import com.azure.json.JsonWriter; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; -import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ClaimsTest { @@ -74,4 +77,96 @@ void testClaimsRequest_StringToClaimsRequest() { assertEquals(TestConfiguration.CLAIMS_CHALLENGE, cr.formatAsJSONString()); } + + @Test + void testClaimsRequest_SerializationWithNullAdditionalInfo() { + // Setup a claims request with null additional info + ClaimsRequest request = new ClaimsRequest(); + request.requestClaimInIdToken("email", null); + + // Convert to JSON string + String jsonString = request.formatAsJSONString(); + + // Verify the output format + assertNotNull(jsonString); + assertEquals("{\"id_token\":{\"email\":null}}", jsonString); + } + + @Test + void testClaimsRequest_RoundTrip() { + // Create original request with various claims + ClaimsRequest originalRequest = new ClaimsRequest(); + originalRequest.requestClaimInIdToken("email", new RequestedClaimAdditionalInfo(true, null, null)); + originalRequest.requestClaimInUserInfo("name", new RequestedClaimAdditionalInfo(false, "John", null)); + + List groups = Arrays.asList("admin", "user"); + originalRequest.requestClaimInAccessToken("groups", new RequestedClaimAdditionalInfo(false, null, groups)); + + // Convert to JSON string + String jsonString = originalRequest.formatAsJSONString(); + + // Parse back to a ClaimsRequest + ClaimsRequest parsedRequest = ClaimsRequest.formatAsClaimsRequest(jsonString); + + // Verify the claims were preserved + assertEquals(1, parsedRequest.getIdTokenRequestedClaims().size()); + assertEquals("email", parsedRequest.getIdTokenRequestedClaims().get(0).name); + assertTrue(parsedRequest.getIdTokenRequestedClaims().get(0).getRequestedClaimAdditionalInfo().isEssential()); + + // Check userinfo claims + List userInfoClaims = parsedRequest.userInfoRequestedClaims; + assertEquals(1, userInfoClaims.size()); + assertEquals("name", userInfoClaims.get(0).name); + assertEquals("John", userInfoClaims.get(0).getRequestedClaimAdditionalInfo().getValue()); + + // Check access token claims + List accessTokenClaims = parsedRequest.accessTokenRequestedClaims; + assertEquals(1, accessTokenClaims.size()); + assertEquals("groups", accessTokenClaims.get(0).name); + assertEquals(2, accessTokenClaims.get(0).getRequestedClaimAdditionalInfo().getValues().size()); + assertTrue(accessTokenClaims.get(0).getRequestedClaimAdditionalInfo().getValues().contains("admin")); + } + + @Test + void testRequestedClaimAdditionalInfo_Serialization() { + RequestedClaimAdditionalInfo info1 = new RequestedClaimAdditionalInfo(true, null, null); + String json1 = serializeAdditionalInfo(info1); + assertTrue(json1.contains("\"essential\":true")); + assertFalse(json1.contains("\"value\"")); + assertFalse(json1.contains("\"values\"")); + + RequestedClaimAdditionalInfo info2 = new RequestedClaimAdditionalInfo(false, "test", null); + String json2 = serializeAdditionalInfo(info2); + assertFalse(json2.contains("\"essential\"")); + assertTrue(json2.contains("\"value\":\"test\"")); + + List valuesList = Arrays.asList("one", "two"); + RequestedClaimAdditionalInfo info3 = new RequestedClaimAdditionalInfo(false, null, valuesList); + String json3 = serializeAdditionalInfo(info3); + assertTrue(json3.contains("\"values\":[\"one\",\"two\"]")); + } + + @Test + void testInvalidJsonHandling() { + try { + ClaimsRequest.formatAsClaimsRequest("{invalid json}"); + fail("Should have thrown MsalClientException"); + } catch (MsalClientException e) { + assertTrue(e.getMessage().contains("Could not convert string to ClaimsRequest")); + assertEquals(AuthenticationErrorCode.INVALID_JSON, e.errorCode()); + } + } + + // Helper method to serialize RequestedClaimAdditionalInfo for testing + private String serializeAdditionalInfo(RequestedClaimAdditionalInfo info) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + + info.toJson(jsonWriter); + jsonWriter.flush(); + return outputStream.toString(StandardCharsets.UTF_8.name()); + } catch (IOException e) { + throw new RuntimeException("Failed to serialize", e); + } + } } From 1ae637ecd6a73446910381d6df03ad36ae70e5d0 Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 6 May 2025 18:11:39 -0700 Subject: [PATCH 20/31] Replace com.fasterxml.jackson with com.azure.json --- .../CachePersistenceIT.java | 8 +- .../TokenCacheIT.java | 2 +- .../msal4j/AbstractManagedIdentitySource.java | 2 +- .../aad/msal4j/AccessTokenCacheEntity.java | 99 +++++-- .../aad/msal4j/AccountCacheEntity.java | 98 +++++-- .../aad/msal4j/AppMetadataCacheEntity.java | 56 +++- .../com/microsoft/aad/msal4j/ClientInfo.java | 45 ++- .../com/microsoft/aad/msal4j/Credential.java | 66 ++++- .../com/microsoft/aad/msal4j/DeviceCode.java | 74 ++++- .../aad/msal4j/DeviceCodeFlowRequest.java | 2 +- .../microsoft/aad/msal4j/ErrorResponse.java | 99 +++++-- .../com/microsoft/aad/msal4j/IdToken.java | 110 ++++++-- .../aad/msal4j/IdTokenCacheEntity.java | 66 ++++- .../com/microsoft/aad/msal4j/JsonHelper.java | 160 +++++------ .../com/microsoft/aad/msal4j/JwtHelper.java | 4 +- .../msal4j/ManagedIdentityErrorResponse.java | 102 +++++-- .../msal4j/MsalServiceExceptionFactory.java | 4 +- .../aad/msal4j/RefreshTokenCacheEntity.java | 62 ++++- .../com/microsoft/aad/msal4j/TokenCache.java | 193 ++++++++----- ...AuthorizationRequestUrlParametersTest.java | 2 +- .../aad/msal4j/CacheFormatTests.java | 10 +- .../com/microsoft/aad/msal4j/CacheTests.java | 263 +++++++++++++++++- .../aad/msal4j/ManagedIdentityTests.java | 4 +- .../aad/msal4j/TestConfiguration.java | 6 +- .../aad/msal4j/TokenRequestExecutorTest.java | 8 - .../cache_data/serialized_cache.json | 9 - 26 files changed, 1194 insertions(+), 360 deletions(-) diff --git a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/CachePersistenceIT.java b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/CachePersistenceIT.java index d6dc51b5..3b945d18 100644 --- a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/CachePersistenceIT.java +++ b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/CachePersistenceIT.java @@ -54,7 +54,7 @@ void cacheDeserializationSerializationTest() throws IOException, URISyntaxExcept assertEquals(app.getAccounts().join().size(), 1); assertEquals(app.tokenCache.accounts.size(), 1); - assertEquals(app.tokenCache.accessTokens.size(), 2); + assertEquals(app.tokenCache.accessTokens.size(), 1); assertEquals(app.tokenCache.refreshTokens.size(), 1); assertEquals(app.tokenCache.idTokens.size(), 1); assertEquals(app.tokenCache.appMetadata.size(), 1); @@ -65,7 +65,7 @@ void cacheDeserializationSerializationTest() throws IOException, URISyntaxExcept assertEquals(app.getAccounts().join().size(), 1); assertEquals(app.tokenCache.accounts.size(), 1); - assertEquals(app.tokenCache.accessTokens.size(), 2); + assertEquals(app.tokenCache.accessTokens.size(), 1); assertEquals(app.tokenCache.refreshTokens.size(), 1); assertEquals(app.tokenCache.idTokens.size(), 1); assertEquals(app.tokenCache.appMetadata.size(), 1); @@ -74,7 +74,7 @@ void cacheDeserializationSerializationTest() throws IOException, URISyntaxExcept assertEquals(app.getAccounts().join().size(), 0); assertEquals(app.tokenCache.accounts.size(), 0); - assertEquals(app.tokenCache.accessTokens.size(), 1); + assertEquals(app.tokenCache.accessTokens.size(), 0); assertEquals(app.tokenCache.refreshTokens.size(), 0); assertEquals(app.tokenCache.idTokens.size(), 0); assertEquals(app.tokenCache.appMetadata.size(), 1); @@ -84,7 +84,7 @@ void cacheDeserializationSerializationTest() throws IOException, URISyntaxExcept assertEquals(app.getAccounts().join().size(), 0); assertEquals(app.tokenCache.accounts.size(), 0); - assertEquals(app.tokenCache.accessTokens.size(), 1); + assertEquals(app.tokenCache.accessTokens.size(), 0); assertEquals(app.tokenCache.refreshTokens.size(), 0); assertEquals(app.tokenCache.idTokens.size(), 0); assertEquals(app.tokenCache.appMetadata.size(), 1); diff --git a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/TokenCacheIT.java b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/TokenCacheIT.java index d1192f2c..8d7274c6 100644 --- a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/TokenCacheIT.java +++ b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/TokenCacheIT.java @@ -163,7 +163,7 @@ void twoAccountsInCache_SameUserDifferentTenants_RemoveAccountTest() throws Exce // RemoveAccount should remove both cache entities pca2.removeAccount(account).join(); - assertEquals(pca.getAccounts().join().size(), 0); + assertEquals(0, pca2.getAccounts().join().size()); //clean up file TestHelper.deleteFileContent( diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractManagedIdentitySource.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractManagedIdentitySource.java index c90fc8e1..5b5b7a38 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractManagedIdentitySource.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractManagedIdentitySource.java @@ -113,7 +113,7 @@ protected String getMessageFromErrorResponse(IHttpResponse response) { ManagedIdentityErrorResponse managedIdentityErrorResponse; try { - managedIdentityErrorResponse = JsonHelper.convertJsonToObject(response.body(), ManagedIdentityErrorResponse.class); + managedIdentityErrorResponse = JsonHelper.convertJsonStringToJsonSerializableObject(response.body(), ManagedIdentityErrorResponse::fromJson); } catch (MsalJsonParsingException e) { throw new MsalJsonParsingException(String.format(MsalErrorMessage.MANAGED_IDENTITY_RESPONSE_PARSE_FAILURE, response.statusCode(), e.getMessage()), MsalError.MANAGED_IDENTITY_RESPONSE_PARSE_FAILURE, managedIdentitySourceType); } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AccessTokenCacheEntity.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AccessTokenCacheEntity.java index 0fe690ee..92904da8 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AccessTokenCacheEntity.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AccessTokenCacheEntity.java @@ -3,32 +3,23 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; +import java.io.IOException; import java.util.ArrayList; import java.util.List; -class AccessTokenCacheEntity extends Credential { +class AccessTokenCacheEntity extends Credential implements JsonSerializable { - @JsonProperty("credential_type") private String credentialType; - - @JsonProperty("realm") protected String realm; - - @JsonProperty("target") private String target; - - @JsonProperty("cached_at") private String cachedAt; - - @JsonProperty("expires_on") private String expiresOn; - - @JsonProperty("extended_expires_on") private String extExpiresOn; - - @JsonProperty("refresh_on") private String refreshOn; String getKey() { @@ -44,12 +35,80 @@ String getKey() { return String.join(Constants.CACHE_KEY_SEPARATOR, keyParts).toLowerCase(); } - String credentialType() { - return this.credentialType; + static AccessTokenCacheEntity fromJson(JsonReader jsonReader) throws IOException { + AccessTokenCacheEntity entity = new AccessTokenCacheEntity(); + + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + switch (fieldName) { + case "home_account_id": + entity.homeAccountId = reader.getString(); + break; + case "environment": + entity.environment = reader.getString(); + break; + case "credential_type": + entity.credentialType = reader.getString(); + break; + case "client_id": + entity.clientId = reader.getString(); + break; + case "secret": + entity.secret = reader.getString(); + break; + case "realm": + entity.realm = reader.getString(); + break; + case "target": + entity.target = reader.getString(); + break; + case "cached_at": + entity.cachedAt = reader.getString(); + break; + case "expires_on": + entity.expiresOn = reader.getString(); + break; + case "extended_expires_on": + entity.extExpiresOn = reader.getString(); + break; + case "refresh_on": + entity.refreshOn = reader.getString(); + break; + case "user_assertion_hash": + entity.userAssertionHash = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return entity; + }); } - String realm() { - return this.realm; + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + jsonWriter.writeStringField("home_account_id", homeAccountId); + jsonWriter.writeStringField("environment", environment); + jsonWriter.writeStringField("credential_type", credentialType); + jsonWriter.writeStringField("client_id", clientId); + jsonWriter.writeStringField("secret", secret); + jsonWriter.writeStringField("realm", realm); + jsonWriter.writeStringField("target", target); + jsonWriter.writeStringField("cached_at", cachedAt); + jsonWriter.writeStringField("expires_on", expiresOn); + jsonWriter.writeStringField("extended_expires_on", extExpiresOn); + jsonWriter.writeStringField("refresh_on", refreshOn); + jsonWriter.writeStringField("user_assertion_hash", userAssertionHash); + + jsonWriter.writeEndObject(); + + return jsonWriter; } String target() { @@ -99,4 +158,4 @@ void extExpiresOn(String extExpiresOn) { void refreshOn(String refreshOn) { this.refreshOn = refreshOn; } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AccountCacheEntity.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AccountCacheEntity.java index f404b2f0..b060e6fd 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AccountCacheEntity.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AccountCacheEntity.java @@ -3,51 +3,101 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; +import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Objects; -class AccountCacheEntity implements Serializable { +class AccountCacheEntity implements JsonSerializable, Serializable { static final String MSSTS_ACCOUNT_TYPE = "MSSTS"; static final String ADFS_ACCOUNT_TYPE = "ADFS"; - @JsonProperty("home_account_id") protected String homeAccountId; - - @JsonProperty("environment") protected String environment; - - @JsonProperty("realm") protected String realm; - - @JsonProperty("local_account_id") protected String localAccountId; - - @JsonProperty("username") protected String username; - - @JsonProperty("name") protected String name; - - @JsonProperty("client_info") protected String clientInfoStr; - - @JsonProperty("user_assertion_hash") protected String userAssertionHash; + protected String authorityType; + + static AccountCacheEntity fromJson(JsonReader jsonReader) throws IOException { + AccountCacheEntity entity = new AccountCacheEntity(); + + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + switch (fieldName) { + case "home_account_id": + entity.homeAccountId = reader.getString(); + break; + case "environment": + entity.environment = reader.getString(); + break; + case "realm": + entity.realm = reader.getString(); + break; + case "local_account_id": + entity.localAccountId = reader.getString(); + break; + case "username": + entity.username = reader.getString(); + break; + case "name": + entity.name = reader.getString(); + break; + case "client_info": + entity.clientInfoStr = reader.getString(); + break; + case "user_assertion_hash": + entity.userAssertionHash = reader.getString(); + break; + case "authority_type": + entity.authorityType = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return entity; + }); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + jsonWriter.writeStringField("home_account_id", homeAccountId); + jsonWriter.writeStringField("environment", environment); + jsonWriter.writeStringField("realm", realm); + jsonWriter.writeStringField("local_account_id", localAccountId); + jsonWriter.writeStringField("username", username); + jsonWriter.writeStringField("name", name); + jsonWriter.writeStringField("client_info", clientInfoStr); + jsonWriter.writeStringField("user_assertion_hash", userAssertionHash); + jsonWriter.writeStringField("authority_type", authorityType); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } ClientInfo clientInfo() { return ClientInfo.createFromJson(clientInfoStr); } - @JsonProperty("authority_type") - protected String authorityType; - String getKey() { - List keyParts = new ArrayList<>(); keyParts.add(homeAccountId); @@ -58,7 +108,6 @@ String getKey() { } static AccountCacheEntity create(String clientInfoStr, Authority requestAuthority, IdToken idToken, String policy) { - AccountCacheEntity account = new AccountCacheEntity(); account.authorityType(MSSTS_ACCOUNT_TYPE); account.clientInfoStr = clientInfoStr; @@ -80,7 +129,6 @@ static AccountCacheEntity create(String clientInfoStr, Authority requestAuthorit } static AccountCacheEntity createADFSAccount(Authority requestAuthority, IdToken idToken) { - AccountCacheEntity account = new AccountCacheEntity(); account.authorityType(ADFS_ACCOUNT_TYPE); account.homeAccountId(idToken.subject); @@ -173,8 +221,6 @@ void authorityType(String authorityType) { this.authorityType = authorityType; } - //These methods are based on those generated by Lombok's @EqualsAndHashCode annotation. - //They have the same functionality as the generated methods, but were refactored for readability. @Override public boolean equals(Object o) { if (this == o) return true; @@ -207,4 +253,4 @@ public int hashCode() { result = result * 59 + (this.authorityType == null ? 43 : this.authorityType.hashCode()); return result; } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AppMetadataCacheEntity.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AppMetadataCacheEntity.java index 7e8b999f..9d20238a 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AppMetadataCacheEntity.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AppMetadataCacheEntity.java @@ -3,23 +3,63 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; + +import java.io.IOException; import java.util.ArrayList; import java.util.List; -class AppMetadataCacheEntity { +class AppMetadataCacheEntity implements JsonSerializable { public static final String APP_METADATA_CACHE_ENTITY_ID = "appmetadata"; - @JsonProperty("client_id") private String clientId; - - @JsonProperty("environment") private String environment; - - @JsonProperty("family_id") private String familyId; + static AppMetadataCacheEntity fromJson(JsonReader jsonReader) throws IOException { + AppMetadataCacheEntity entity = new AppMetadataCacheEntity(); + + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + switch (fieldName) { + case "client_id": + entity.clientId = reader.getString(); + break; + case "environment": + entity.environment = reader.getString(); + break; + case "family_id": + entity.familyId = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return entity; + }); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + jsonWriter.writeStringField("client_id", clientId); + jsonWriter.writeStringField( "environment", environment); + jsonWriter.writeStringField("family_id", familyId); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } + String getKey() { List keyParts = new ArrayList<>(); @@ -53,4 +93,4 @@ void environment(String environment) { void familyId(String familyId) { this.familyId = familyId; } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientInfo.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientInfo.java index f09d2a93..be8a7c74 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientInfo.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientInfo.java @@ -3,19 +3,17 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.*; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; import static com.microsoft.aad.msal4j.Constants.POINT_DELIMITER; -class ClientInfo { +class ClientInfo implements JsonSerializable { - @JsonProperty("uid") private String uniqueIdentifier; - - @JsonProperty("utid") private String uniqueTenantIdentifier; public static ClientInfo createFromJson(String clientInfoJsonBase64Encoded) { @@ -25,7 +23,40 @@ public static ClientInfo createFromJson(String clientInfoJsonBase64Encoded) { byte[] decodedInput = Base64.getUrlDecoder().decode(clientInfoJsonBase64Encoded.getBytes(StandardCharsets.UTF_8)); - return JsonHelper.convertJsonToObject(new String(decodedInput, StandardCharsets.UTF_8), ClientInfo.class); + return JsonHelper.convertJsonStringToJsonSerializableObject(new String(decodedInput, StandardCharsets.UTF_8), ClientInfo::fromJson); + } + + static ClientInfo fromJson(JsonReader jsonReader) throws IOException { + ClientInfo clientInfo = new ClientInfo(); + + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + switch (fieldName) { + case "uid": + clientInfo.uniqueIdentifier = reader.getString(); + break; + case "utid": + clientInfo.uniqueTenantIdentifier = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return clientInfo; + }); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + jsonWriter.writeStringField("uid", uniqueIdentifier); + jsonWriter.writeStringField("utid", uniqueTenantIdentifier); + jsonWriter.writeEndObject(); + return jsonWriter; } String toAccountIdentifier() { @@ -39,4 +70,4 @@ String getUniqueIdentifier() { String getUniqueTenantIdentifier() { return this.uniqueTenantIdentifier; } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Credential.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Credential.java index 55c3cf92..0530ff92 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Credential.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Credential.java @@ -3,25 +3,67 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; -class Credential { +import java.io.IOException; - @JsonProperty("home_account_id") - protected String homeAccountId; +class Credential implements JsonSerializable { - @JsonProperty("environment") + protected String homeAccountId; protected String environment; - - @JsonProperty("client_id") protected String clientId; - - @JsonProperty("secret") protected String secret; - - @JsonProperty("user_assertion_hash") protected String userAssertionHash; + static Credential fromJson(JsonReader jsonReader) throws IOException { + Credential credential = new Credential(); + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + switch (fieldName) { + case "home_account_id": + credential.homeAccountId = reader.getString(); + break; + case "environment": + credential.environment = reader.getString(); + break; + case "client_id": + credential.clientId = reader.getString(); + break; + case "secret": + credential.secret = reader.getString(); + break; + case "user_assertion_hash": + credential.userAssertionHash = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return credential; + }); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + jsonWriter.writeStringField("home_account_id", homeAccountId); + jsonWriter.writeStringField("environment", environment); + jsonWriter.writeStringField("client_id", clientId); + jsonWriter.writeStringField("secret", secret); + jsonWriter.writeStringField("user_assertion_hash", userAssertionHash); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } + String homeAccountId() { return this.homeAccountId; } @@ -61,4 +103,4 @@ void secret(String secret) { void userAssertionHash(String userAssertionHash) { this.userAssertionHash = userAssertionHash; } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCode.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCode.java index ef8a212e..da72c3e9 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCode.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCode.java @@ -3,36 +3,82 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; + +import java.io.IOException; /** * Response returned from the STS device code endpoint containing information necessary for * device code flow */ -public final class DeviceCode { +public final class DeviceCode implements JsonSerializable { - @JsonProperty("user_code") private String userCode; - - @JsonProperty("device_code") private String deviceCode; - - @JsonProperty("verification_uri") private String verificationUri; - - @JsonProperty("expires_in") private long expiresIn; - - @JsonProperty("interval") private long interval; - - @JsonProperty("message") private String message; private transient String correlationId = null; private transient String clientId = null; private transient String scopes = null; + public static DeviceCode fromJson(JsonReader jsonReader) throws IOException { + DeviceCode deviceCode = new DeviceCode(); + + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + switch (fieldName) { + case "user_code": + deviceCode.userCode = reader.getString(); + break; + case "device_code": + deviceCode.deviceCode = reader.getString(); + break; + case "verification_uri": + deviceCode.verificationUri = reader.getString(); + break; + case "expires_in": + deviceCode.expiresIn = reader.getLong(); + break; + case "interval": + deviceCode.interval = reader.getLong(); + break; + case "message": + deviceCode.message = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return deviceCode; + }); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + jsonWriter.writeStringField("user_code", userCode); + jsonWriter.writeStringField("device_code", deviceCode); + jsonWriter.writeStringField("verification_uri", verificationUri); + jsonWriter.writeNumberField("expires_in", expiresIn); + jsonWriter.writeNumberField("interval", interval); + jsonWriter.writeStringField("message", message); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } + /** * code which user needs to provide when authenticating at the verification URI */ @@ -101,4 +147,4 @@ protected DeviceCode scopes(String scopes) { this.scopes = scopes; return this; } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java index fe5565cf..01be0fec 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java @@ -89,7 +89,7 @@ private DeviceCode parseJsonToDeviceCodeAndSetParameters( String clientId) { DeviceCode result; - result = JsonHelper.convertJsonToObject(json, DeviceCode.class); + result = JsonHelper.convertJsonStringToJsonSerializableObject(json, DeviceCode::fromJson); String correlationIdHeader = headers.get(HttpHeaders.CORRELATION_ID_HEADER_NAME); if (correlationIdHeader != null) { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ErrorResponse.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ErrorResponse.java index 737c7104..bc55dcba 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ErrorResponse.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ErrorResponse.java @@ -3,37 +3,98 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; -class ErrorResponse { +import java.io.IOException; + +class ErrorResponse implements JsonSerializable { private Integer statusCode; private String statusMessage; - - @JsonProperty("error") protected String error; - - @JsonProperty("error_description") protected String errorDescription; - - @JsonProperty("error_codes") protected long[] errorCodes; - - @JsonProperty("suberror") protected String subError; - - @JsonProperty("trace_id") protected String traceId; - - @JsonProperty("timestamp") protected String timestamp; - - @JsonProperty("correlation_id") protected String correlation_id; - - @JsonProperty("claims") private String claims; + static ErrorResponse fromJson(JsonReader jsonReader) throws IOException { + ErrorResponse entity = new ErrorResponse(); + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + switch (fieldName) { + case "error": + entity.error = reader.getString(); + break; + case "error_description": + entity.errorDescription = reader.getString(); + break; + case "error_codes": + entity.errorCodes = reader.readArray(JsonReader::getLong).stream().mapToLong(Long::longValue).toArray(); + break; + case "suberror": + entity.subError = reader.getString(); + break; + case "trace_id": + entity.traceId = reader.getString(); + break; + case "timestamp": + entity.timestamp = reader.getString(); + break; + case "correlation_id": + entity.correlation_id = reader.getString(); + break; + case "claims": + entity.claims = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return entity; + }); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + jsonWriter.writeStartObject(); + + jsonWriter.writeNumberField("statusCode", statusCode); + jsonWriter.writeStringField("statusMessage", statusMessage); + jsonWriter.writeStringField("error", error); + jsonWriter.writeStringField("error_description", errorDescription); + + if (errorCodes != null) { + jsonWriter.writeStartArray("error_codes"); + for (long code : errorCodes) { + jsonWriter.writeNumber(code); + } + jsonWriter.writeEndArray(); + } else { + jsonWriter.writeNullField("error_codes"); + } + + jsonWriter.writeStringField("suberror", subError); + jsonWriter.writeStringField("trace_id", traceId); + jsonWriter.writeStringField("timestamp", timestamp); + jsonWriter.writeStringField("correlation_id", correlation_id); + jsonWriter.writeStringField("claims", claims); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } + Integer statusCode() { return this.statusCode; } @@ -113,4 +174,4 @@ void correlation_id(String correlation_id) { void claims(String claims) { this.claims = claims; } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdToken.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdToken.java index 0425f368..f9992a71 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdToken.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdToken.java @@ -3,44 +3,102 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; + +import java.io.IOException; import java.io.Serializable; -class IdToken implements Serializable { +class IdToken implements Serializable, JsonSerializable { - @JsonProperty("iss") protected String issuer; - - @JsonProperty("sub") protected String subject; - - @JsonProperty("aud") protected String audience; - - @JsonProperty("exp") protected Long expirationTime; - - @JsonProperty("iat") protected Long issuedAt; - - @JsonProperty("nbf") protected Long notBefore; - - @JsonProperty("name") protected String name; - - @JsonProperty("preferred_username") protected String preferredUsername; - - @JsonProperty("oid") protected String objectIdentifier; - - @JsonProperty("tid") protected String tenantIdentifier; - - @JsonProperty("upn") protected String upn; - - @JsonProperty("unique_name") protected String uniqueName; -} + + static IdToken fromJson(JsonReader jsonReader) throws IOException { + IdToken idToken = new IdToken(); + + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + switch (fieldName) { + case "iss": + idToken.issuer = reader.getString(); + break; + case "sub": + idToken.subject = reader.getString(); + break; + case "aud": + idToken.audience = reader.getString(); + break; + case "exp": + idToken.expirationTime = reader.getLong(); + break; + case "iat": + idToken.issuedAt = reader.getLong(); + break; + case "nbf": + idToken.notBefore = reader.getLong(); + break; + case "name": + idToken.name = reader.getString(); + break; + case "preferred_username": + idToken.preferredUsername = reader.getString(); + break; + case "oid": + idToken.objectIdentifier = reader.getString(); + break; + case "tid": + idToken.tenantIdentifier = reader.getString(); + break; + case "upn": + idToken.upn = reader.getString(); + break; + case "unique_name": + idToken.uniqueName = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return idToken; + }); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + jsonWriter.writeStringField("iss", issuer); + jsonWriter.writeStringField("sub", subject); + jsonWriter.writeStringField("aud", audience); + jsonWriter.writeNumberField("exp", expirationTime); + jsonWriter.writeNumberField("iat", issuedAt); + jsonWriter.writeNumberField("nbf", notBefore); + jsonWriter.writeStringField("name", name); + jsonWriter.writeStringField("preferred_username", preferredUsername); + jsonWriter.writeStringField("oid", objectIdentifier); + jsonWriter.writeStringField("tid", tenantIdentifier); + jsonWriter.writeStringField("upn", upn); + jsonWriter.writeStringField("unique_name", uniqueName); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdTokenCacheEntity.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdTokenCacheEntity.java index db6662a9..a7304c79 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdTokenCacheEntity.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdTokenCacheEntity.java @@ -3,17 +3,17 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.JsonReader; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; +import java.io.IOException; import java.util.ArrayList; import java.util.List; class IdTokenCacheEntity extends Credential { - @JsonProperty("credential_type") private String credentialType; - - @JsonProperty("realm") protected String realm; String getKey() { @@ -31,6 +31,62 @@ String getKey() { return String.join(Constants.CACHE_KEY_SEPARATOR, keyParts).toLowerCase(); } + static IdTokenCacheEntity fromJson(JsonReader jsonReader) throws IOException { + IdTokenCacheEntity entity = new IdTokenCacheEntity(); + + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + switch (fieldName) { + case "home_account_id": + entity.homeAccountId = reader.getString(); + break; + case "environment": + entity.environment = reader.getString(); + break; + case "credential_type": + entity.credentialType = reader.getString(); + break; + case "client_id": + entity.clientId = reader.getString(); + break; + case "secret": + entity.secret = reader.getString(); + break; + case "realm": + entity.realm = reader.getString(); + break; + case "user_assertion_hash": + entity.userAssertionHash = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return entity; + }); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + jsonWriter.writeStringField("home_account_id", homeAccountId); + jsonWriter.writeStringField("environment", environment); + jsonWriter.writeStringField("credential_type", credentialType); + jsonWriter.writeStringField("client_id", clientId); + jsonWriter.writeStringField("secret", secret); + jsonWriter.writeStringField("realm", realm); + jsonWriter.writeStringField("user_assertion_hash", userAssertionHash); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } + String credentialType() { return this.credentialType; } @@ -46,4 +102,4 @@ void credentialType(String credentialType) { void realm(String realm) { this.realm = realm; } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java index 27963121..ac57e2c6 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java @@ -7,41 +7,22 @@ import com.azure.json.JsonReader; import com.azure.json.JsonSerializable; import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; import com.azure.json.ReadValueCallback; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.JsonNode; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.*; class JsonHelper { - static ObjectMapper mapper; - - static { - mapper = new ObjectMapper(); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - } private JsonHelper() { } - static T convertJsonToObject(final String json, final Class tClass) { - try { - return mapper.readValue(json, tClass); - } catch (Exception e) { - throw new MsalJsonParsingException(e.getMessage(), AuthenticationErrorCode.INVALID_JSON); - } - } - static IdToken createIdTokenFromEncodedTokenString(String token) { - return JsonHelper.convertJsonToObject(getTokenPayloadClaims(token), IdToken.class); + return convertJsonStringToJsonSerializableObject(getTokenPayloadClaims(token), IdToken::fromJson); } static String getTokenPayloadClaims(String token) { @@ -53,7 +34,6 @@ static String getTokenPayloadClaims(String token) { } } - //Converts a generic JSON string to a Map with relevant types static Map parseJsonToMap(String jsonString) { if (StringHelper.isBlank(jsonString)) { return new HashMap<>(); @@ -92,29 +72,23 @@ private static Object parseValue(JsonReader jsonReader) throws IOException { JsonToken token = jsonReader.currentToken(); switch (token) { - case STRING: - return jsonReader.getString(); + case STRING: return jsonReader.getString(); case NUMBER: try { return jsonReader.getLong(); } catch (ArithmeticException e) { return jsonReader.getDouble(); } - case BOOLEAN: - return jsonReader.getBoolean(); - case NULL: - return null; - case START_ARRAY: - return parseJsonArray(jsonReader); - case START_OBJECT: - return parseJsonObject(jsonReader); + case BOOLEAN: return jsonReader.getBoolean(); + case NULL: return null; + case START_ARRAY: return parseJsonArray(jsonReader); + case START_OBJECT: return parseJsonObject(jsonReader); default: jsonReader.skipChildren(); return null; } } - //This method is used to convert a JSON string to an object which implements the JsonSerializable interface from com.azure.json static > T convertJsonStringToJsonSerializableObject(String jsonResponse, ReadValueCallback readFunction) { try (JsonReader jsonReader = JsonProviders.createReader(jsonResponse)) { return readFunction.read(jsonReader); @@ -123,90 +97,102 @@ static > T convertJsonStringToJsonSerializableObje } } - //Converts a JSON string to a Map + static > String convertJsonSerializableObjectToString(T jsonSerializable) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream); + + jsonSerializable.toJson(jsonWriter); + jsonWriter.flush(); + + return outputStream.toString(StandardCharsets.UTF_8.name()); + } catch (Exception e) { + throw new MsalClientException("Error serializing object to JSON: " + e.getMessage(), + AuthenticationErrorCode.INVALID_JSON); + } + } + static Map convertJsonToMap(String jsonString) { try (JsonReader reader = JsonProviders.createReader(jsonString)) { reader.nextToken(); return reader.readMap(JsonReader::getString); } catch (IOException e) { - throw new MsalClientException("Could not parse JSON from HttpResponse body: " + e.getMessage(), AuthenticationErrorCode.INVALID_JSON); + throw new MsalClientException("Could not parse JSON from HttpResponse body: " + e.getMessage(), + AuthenticationErrorCode.INVALID_JSON); } } - /** - * Throws exception if given String does not follow JSON syntax - */ static void validateJsonFormat(String jsonString) { - try { - mapper.readTree(jsonString); + try (JsonReader reader = JsonProviders.createReader(jsonString)) { + while (reader.nextToken() != JsonToken.END_DOCUMENT) { + reader.skipChildren(); + } } catch (IOException e) { throw new MsalClientException(e.getMessage(), AuthenticationErrorCode.INVALID_JSON); } } - /** - * Take a set of Strings and return a String representing a JSON object of the format: - * { - * "access_token": { - * "xms_cc": { - * "values": [ clientCapabilities ] - * } - * } - * } - */ public static String formCapabilitiesJson(Set clientCapabilities) { - if (clientCapabilities != null && !clientCapabilities.isEmpty()) { - ClaimsRequest cr = new ClaimsRequest(); - RequestedClaimAdditionalInfo capabilitiesValues = new RequestedClaimAdditionalInfo(false, null, new ArrayList<>(clientCapabilities)); - cr.requestClaimInAccessToken("xms_cc", capabilitiesValues); - - return cr.formatAsJSONString(); - } else { + if (clientCapabilities == null || clientCapabilities.isEmpty()) { return null; } + + ClaimsRequest cr = new ClaimsRequest(); + RequestedClaimAdditionalInfo capabilitiesValues = new RequestedClaimAdditionalInfo( + false, null, new ArrayList<>(clientCapabilities)); + cr.requestClaimInAccessToken("xms_cc", capabilitiesValues); + + return cr.formatAsJSONString(); } - /** - * Merges given JSON strings into one Jackson JSONNode object, which is returned as a String - */ static String mergeJSONString(String mainJsonString, String addJsonString) { - JsonNode mainJson; - JsonNode addJson; - try { - mainJson = mapper.readTree(mainJsonString); - addJson = mapper.readTree(addJsonString); + Map mainMap = parseJsonToMap(mainJsonString); + Map addMap = parseJsonToMap(addJsonString); + + mergeJsonMaps(mainMap, addMap); + + return writeJsonMap(mainMap); } catch (IOException e) { throw new MsalClientException(e.getMessage(), AuthenticationErrorCode.INVALID_JSON); } - - mergeJSONNode(mainJson, addJson); - - return mainJson.toString(); } - /** - * Merges given Jackson JsonNode object into another JsonNode - */ - static void mergeJSONNode(JsonNode mainNode, JsonNode addNode) { - if (addNode == null) { + @SuppressWarnings("unchecked") + private static void mergeJsonMaps(Map mainMap, Map addMap) { + if (addMap == null) { return; } - Iterator fieldNames = addNode.fieldNames(); - while (fieldNames.hasNext()) { + for (Map.Entry entry : addMap.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); - String fieldName = fieldNames.next(); - JsonNode jsonNode = mainNode.get(fieldName); - - if (jsonNode != null && jsonNode.isObject()) { - mergeJSONNode(jsonNode, addNode.get(fieldName)); + if (mainMap.containsKey(key) && mainMap.get(key) instanceof Map && value instanceof Map) { + mergeJsonMaps((Map) mainMap.get(key), (Map) value); } else { - if (mainNode instanceof ObjectNode) { - JsonNode value = addNode.get(fieldName); - ((ObjectNode) mainNode).put(fieldName, value); - } + mainMap.put(key, value); } } } + + static String writeJsonMap(Map map) throws IOException { + StringWriter stringWriter = new StringWriter(); + try (JsonWriter jsonWriter = JsonProviders.createWriter(stringWriter)) { + + jsonWriter.writeStartObject(); + + for (Map.Entry entry : map.entrySet()) { + jsonWriter.writeUntypedField(entry.getKey(), entry.getValue()); + } + + jsonWriter.writeEndObject(); + jsonWriter.flush(); + + return stringWriter.toString(); + } catch (Exception e) { + throw new MsalClientException("Error writing JSON map to string: " + e.getMessage(), + AuthenticationErrorCode.INVALID_JSON); + } + } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java index 9d1a4fca..3c2f20f2 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java @@ -53,8 +53,8 @@ static ClientAssertion buildJwt(String clientId, final ClientCertificate credent payload.put("sub", clientId); // Concatenate header and payload - String jsonHeader = JsonHelper.mapper.writeValueAsString(header); - String jsonPayload = JsonHelper.mapper.writeValueAsString(payload); + String jsonHeader = JsonHelper.writeJsonMap(header); + String jsonPayload = JsonHelper.writeJsonMap(payload); String encodedHeader = base64UrlEncode(jsonHeader.getBytes(StandardCharsets.UTF_8)); String encodedPayload = base64UrlEncode(jsonPayload.getBytes(StandardCharsets.UTF_8)); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityErrorResponse.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityErrorResponse.java index 7454b0a7..ccecef35 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityErrorResponse.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityErrorResponse.java @@ -3,29 +3,70 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; -public class ManagedIdentityErrorResponse { +import java.io.IOException; - @JsonProperty("message") - private String message; +public class ManagedIdentityErrorResponse implements JsonSerializable { - @JsonProperty("correlationId") + private String message; private String correlationId; + private String error; + private String errorDescription; - //In some MSI scenarios such as Cloud Shell, the actual error info is in a JSON within the main JSON. To parse that second - // JSON layer, we need to first pass it into a subclass, parse it using the usual @JsonProperty annotation, and then retrieve the values. - @JsonProperty("error") - private void parseErrorField(ErrorField errorResponse) { - this.error = errorResponse.code; - this.message = errorResponse.message; + public static ManagedIdentityErrorResponse fromJson(JsonReader jsonReader) throws IOException { + ManagedIdentityErrorResponse response = new ManagedIdentityErrorResponse(); + + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + switch (fieldName) { + case "message": + response.message = reader.getString(); + break; + case "correlationId": + response.correlationId = reader.getString(); + break; + case "error": + if (reader.currentToken() == JsonToken.START_OBJECT) { + // Handle nested JSON error object + ErrorField errorField = ErrorField.fromJson(reader); + response.error = errorField.getCode(); + response.message = errorField.getMessage(); + } else { + response.error = reader.getString(); + } + break; + case "error_description": + response.errorDescription = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return response; + }); } - @JsonProperty("error") - private String error; + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); - @JsonProperty("error_description") - private String errorDescription; + jsonWriter.writeStringField("message", message); + jsonWriter.writeStringField("correlationId", correlationId); + jsonWriter.writeStringField("error", error); + jsonWriter.writeStringField("error_description", errorDescription); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } public String getMessage() { return this.message; @@ -44,13 +85,34 @@ public String getErrorDescription() { } private static class ErrorField { - @JsonProperty("code") private String code; - - @JsonProperty("message") private String message; - String getCode() { + static ErrorField fromJson(JsonReader jsonReader) throws IOException { + ErrorField errorField = new ErrorField(); + + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + switch (fieldName) { + case "code": + errorField.code = reader.getString(); + break; + case "message": + errorField.message = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return errorField; + }); + } + + String getCode() { return this.code; } @@ -58,4 +120,4 @@ String getMessage() { return this.message; } } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java index dc1398e5..a2ebfd6f 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java @@ -22,9 +22,7 @@ static MsalServiceException fromHttpResponse(IHttpResponse response) { AuthenticationErrorCode.UNKNOWN); } - ErrorResponse errorResponse = JsonHelper.convertJsonToObject( - responseBody, - ErrorResponse.class); + ErrorResponse errorResponse = JsonHelper.convertJsonStringToJsonSerializableObject(responseBody, ErrorResponse::fromJson); if (errorResponse.error() != null && errorResponse.error().equalsIgnoreCase(AuthenticationErrorCode.INVALID_GRANT) && isInteractionRequired(errorResponse.subError)) { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RefreshTokenCacheEntity.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RefreshTokenCacheEntity.java index 8e439aab..8bf6c4b6 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RefreshTokenCacheEntity.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RefreshTokenCacheEntity.java @@ -3,19 +3,71 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.JsonReader; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; +import java.io.IOException; import java.util.ArrayList; import java.util.List; class RefreshTokenCacheEntity extends Credential { - @JsonProperty("credential_type") private String credentialType; - - @JsonProperty("family_id") private String family_id; + static RefreshTokenCacheEntity fromJson(JsonReader jsonReader) throws IOException { + RefreshTokenCacheEntity entity = new RefreshTokenCacheEntity(); + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + switch (fieldName) { + case "credential_type": + entity.credentialType = reader.getString(); + break; + case "family_id": + entity.family_id = reader.getString(); + break; + // Include other fields from the Credential parent class + case "home_account_id": + entity.homeAccountId = reader.getString(); + break; + case "environment": + entity.environment = reader.getString(); + break; + case "client_id": + entity.clientId = reader.getString(); + break; + case "secret": + entity.secret = reader.getString(); + break; + // Add other Credential fields as needed + default: + reader.skipChildren(); + break; + } + } + return entity; + }); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + jsonWriter.writeStringField("credential_type", credentialType); + jsonWriter.writeStringField("family_id", family_id); + jsonWriter.writeStringField("home_account_id", homeAccountId); + jsonWriter.writeStringField("environment", environment); + jsonWriter.writeStringField("client_id", clientId); + jsonWriter.writeStringField("secret", secret); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } + boolean isFamilyRT() { return !StringHelper.isBlank(family_id); } @@ -56,4 +108,4 @@ void credentialType(String credentialType) { void family_id(String family_id) { this.family_id = family_id; } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java index 6fd1f4a0..ef6fbaf8 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java @@ -3,11 +3,11 @@ package com.microsoft.aad.msal4j; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import com.azure.json.*; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -41,19 +41,10 @@ public TokenCache(ITokenCacheAccessAspect tokenCacheAccessAspect) { public TokenCache() { } - @JsonProperty("AccessToken") Map accessTokens = new LinkedHashMap<>(); - - @JsonProperty("RefreshToken") Map refreshTokens = new LinkedHashMap<>(); - - @JsonProperty("IdToken") Map idTokens = new LinkedHashMap<>(); - - @JsonProperty("Account") Map accounts = new LinkedHashMap<>(); - - @JsonProperty("AppMetadata") Map appMetadata = new LinkedHashMap<>(); transient ITokenCacheAccessAspect tokenCacheAccessAspect; @@ -67,92 +58,154 @@ public void deserialize(String data) { } serializedCachedSnapshot = data; - TokenCache deserializedCache = JsonHelper.convertJsonToObject(data, TokenCache.class); + try { + JsonReader jsonReader = JsonProviders.createReader(data); + deserializeFromJson(jsonReader); + } catch (IOException e) { + throw new MsalClientException(e); + } + } + private void deserializeFromJson(JsonReader jsonReader) throws IOException { lock.writeLock().lock(); try { - this.accounts = deserializedCache.accounts; - this.accessTokens = deserializedCache.accessTokens; - this.refreshTokens = deserializedCache.refreshTokens; - this.idTokens = deserializedCache.idTokens; - this.appMetadata = deserializedCache.appMetadata; + jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + switch (fieldName) { + case "AccessToken": + deserializeCollection(reader, accessTokens, AccessTokenCacheEntity::fromJson); + break; + case "RefreshToken": + deserializeCollection(reader, refreshTokens, RefreshTokenCacheEntity::fromJson); + break; + case "IdToken": + deserializeCollection(reader, idTokens, IdTokenCacheEntity::fromJson); + break; + case "Account": + deserializeCollection(reader, accounts, AccountCacheEntity::fromJson); + break; + case "AppMetadata": + deserializeCollection(reader, appMetadata, AppMetadataCacheEntity::fromJson); + break; + default: + reader.skipChildren(); + break; + } + } + return null; + }); } finally { lock.writeLock().unlock(); } } - private static void mergeJsonObjects(JsonNode old, JsonNode update) { - mergeRemovals(old, update); - mergeUpdates(old, update); - } - - private static void mergeUpdates(JsonNode old, JsonNode update) { - Iterator fieldNames = update.fieldNames(); - while (fieldNames.hasNext()) { - String uKey = fieldNames.next(); - JsonNode uValue = update.get(uKey); + private void deserializeCollection( + JsonReader reader, + Map targetCollection, + ReadValueCallback deserializer) throws IOException { - // add new property - if (!old.has(uKey)) { - if (!uValue.isNull() && - !(uValue.isObject() && uValue.size() == 0)) { - ((ObjectNode) old).set(uKey, uValue); - } + reader.readObject(entityReader -> { + while (entityReader.nextToken() != JsonToken.END_OBJECT) { + String key = entityReader.getFieldName(); + entityReader.nextToken(); + T entity = deserializer.read(entityReader); + targetCollection.put(key, entity); } - // merge old and new property - else { - JsonNode oValue = old.get(uKey); - if (uValue.isObject()) { - mergeUpdates(oValue, uValue); - } else { - ((ObjectNode) old).set(uKey, uValue); + return null; + }); + } + + @Override + public String serialize() { + lock.readLock().lock(); + try { + if (!StringHelper.isBlank(serializedCachedSnapshot)) { + String updatedCache = mergeWithExistingCache(); + if (updatedCache != null) { + return updatedCache; } } + return serializeToJson(); + } finally { + lock.readLock().unlock(); } } - private static void mergeRemovals(JsonNode old, JsonNode update) { - Set msalEntities = - new HashSet<>(Arrays.asList("Account", "AccessToken", "RefreshToken", "IdToken", "AppMetadata")); + private String serializeToJson() { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { - for (String msalEntity : msalEntities) { - JsonNode oldEntries = old.get(msalEntity); - JsonNode newEntries = update.get(msalEntity); - if (oldEntries != null) { - Iterator> iterator = oldEntries.fields(); + jsonWriter.writeStartObject(); - while (iterator.hasNext()) { - Map.Entry oEntry = iterator.next(); + // Write all collections + writeCollection(jsonWriter, "AccessToken", accessTokens); + writeCollection(jsonWriter, "RefreshToken", refreshTokens); + writeCollection(jsonWriter, "IdToken", idTokens); + writeCollection(jsonWriter, "Account", accounts); + writeCollection(jsonWriter, "AppMetadata", appMetadata); - String key = oEntry.getKey(); - if (newEntries == null || !newEntries.has(key)) { - iterator.remove(); - } - } + jsonWriter.writeEndObject(); + jsonWriter.flush(); + + return outputStream.toString(StandardCharsets.UTF_8.name()); + } catch (IOException e) { + throw new MsalClientException(e); + } + } + + private void writeCollection( + JsonWriter jsonWriter, + String collectionName, + Map collection) throws IOException { + + jsonWriter.writeFieldName(collectionName); + + jsonWriter.writeStartObject(); + + for (Map.Entry entry : collection.entrySet()) { + jsonWriter.writeFieldName(entry.getKey()); + if (entry.getValue() instanceof JsonSerializable) { + ((JsonSerializable) entry.getValue()).toJson(jsonWriter); } } + + jsonWriter.writeEndObject(); } - @Override - public String serialize() { - lock.readLock().lock(); + private String mergeWithExistingCache() { try { - if (!StringHelper.isBlank(serializedCachedSnapshot)) { - JsonNode cache = JsonHelper.mapper.readTree(serializedCachedSnapshot); - JsonNode update = JsonHelper.mapper.valueToTree(this); + // Parse existing cache snapshot + TokenCache updatedCache = new TokenCache(); + updatedCache.deserialize(serializedCachedSnapshot); - mergeJsonObjects(cache, update); + // Merge current in-memory cache with the snapshot + mergeCache(updatedCache); - return JsonHelper.mapper.writeValueAsString(cache); - } - return JsonHelper.mapper.writeValueAsString(this); - } catch (IOException e) { - throw new MsalClientException(e); - } finally { - lock.readLock().unlock(); + // Serialize merged cache + return updatedCache.serializeToJson(); + } catch (Exception e) { + return null; } } + private void mergeCache(TokenCache targetCache) { + targetCache.accessTokens.putAll(accessTokens); + targetCache.refreshTokens.putAll(refreshTokens); + targetCache.idTokens.putAll(idTokens); + targetCache.accounts.putAll(accounts); + targetCache.appMetadata.putAll(appMetadata); + + // Handle removals by removing keys that are not in the current cache + targetCache.accessTokens.keySet().retainAll(accessTokens.keySet()); + targetCache.refreshTokens.keySet().retainAll(refreshTokens.keySet()); + targetCache.idTokens.keySet().retainAll(idTokens.keySet()); + targetCache.accounts.keySet().retainAll(accounts.keySet()); + targetCache.appMetadata.keySet().retainAll(appMetadata.keySet()); + } + private class CacheAspect implements AutoCloseable { ITokenCacheAccessContext context; diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java index 8c854be1..fe6d874e 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java @@ -153,7 +153,7 @@ void testBuilder_optionalParameters() throws UnsupportedEncodingException { assertEquals(queryParameters.get("correlation_id"), "corr_id"); assertEquals(queryParameters.get("login_hint"), "hint"); assertEquals(queryParameters.get("domain_hint"), "domain_hint"); - assertEquals(queryParameters.get("claims"), "{\"id_token\":{\"auth_time\":{\"essential\":true}},\"access_token\":{\"auth_time\":{\"essential\":true},\"xms_cc\":{\"values\":[\"llt\",\"ssm\"]}}}"); + assertEquals(queryParameters.get("claims"), "{\"access_token\":{\"auth_time\":{\"essential\":true},\"xms_cc\":{\"values\":[\"llt\",\"ssm\"]}},\"id_token\":{\"auth_time\":{\"essential\":true}}}"); // CCS routing assertEquals(queryParameters.get(HttpHeaders.X_ANCHOR_MAILBOX), String.format(HttpHeaders.X_ANCHOR_MAILBOX_UPN_FORMAT, "hint")); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java index 7b208e29..3c3d8cc6 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java @@ -182,7 +182,7 @@ private void validateAccessTokenCacheEntity(String folder, String tokenResponse, String keyExpected = readResource(folder + AT_CACHE_ENTITY_KEY); assertEquals(keyActual, keyExpected); - String valueActual = JsonHelper.mapper.writeValueAsString(tokenCache.accessTokens.get(keyActual)); + String valueActual = JsonHelper.convertJsonSerializableObjectToString(tokenCache.accessTokens.get(keyActual)); String valueExpected = readResource(folder + AT_CACHE_ENTITY); JSONObject tokenResponseJsonObj = JSONObjectUtils.parse(tokenResponse); @@ -213,7 +213,7 @@ private void validateRefreshTokenCacheEntity(String folder, TokenCache tokenCach String keyExpected = readResource(folder + RT_CACHE_ENTITY_KEY); assertEquals(actualKey, keyExpected); - String actualValue = JsonHelper.mapper.writeValueAsString(tokenCache.refreshTokens.get(actualKey)); + String actualValue = JsonHelper.convertJsonSerializableObjectToString(tokenCache.refreshTokens.get(actualKey)); String valueExpected = readResource(folder + RT_CACHE_ENTITY); JSONAssert.assertEquals(valueExpected, actualValue, JSONCompareMode.STRICT); } @@ -249,7 +249,7 @@ private void validateIdTokenCacheEntity(String folder, TokenCache tokenCache) String keyExpected = readResource(folder + ID_TOKEN_CACHE_ENTITY_KEY); assertEquals(actualKey, keyExpected); - String actualValue = JsonHelper.mapper.writeValueAsString(tokenCache.idTokens.get(actualKey)); + String actualValue = JsonHelper.convertJsonSerializableObjectToString(tokenCache.idTokens.get(actualKey)); String valueExpected = readResource(folder + ID_TOKEN_CACHE_ENTITY); JSONAssert.assertEquals(valueExpected, actualValue, new IdTokenComparator(JSONCompareMode.STRICT, folder)); @@ -264,7 +264,7 @@ private void validateAccountCacheEntity(String folder, TokenCache tokenCache) String keyExpected = readResource(folder + ACCOUNT_CACHE_ENTITY_KEY); assertEquals(actualKey, keyExpected); - String actualValue = JsonHelper.mapper.writeValueAsString(tokenCache.accounts.get(actualKey)); + String actualValue = JsonHelper.convertJsonSerializableObjectToString(tokenCache.accounts.get(actualKey)); String valueExpected = readResource(folder + ACCOUNT_CACHE_ENTITY); JSONAssert.assertEquals(valueExpected, actualValue, JSONCompareMode.STRICT); @@ -283,7 +283,7 @@ private void validateAppMetadataCacheEntity(String folder, TokenCache tokenCache String keyExpected = readResource(folder + APP_METADATA_ENTITY_KEY); assertEquals(actualKey, keyExpected); - String actualValue = JsonHelper.mapper.writeValueAsString(tokenCache.appMetadata.get(actualKey)); + String actualValue = JsonHelper.convertJsonSerializableObjectToString(tokenCache.appMetadata.get(actualKey)); String valueExpected = readResource(folder + APP_METADATA_CACHE_ENTITY); JSONAssert.assertEquals(valueExpected, actualValue, JSONCompareMode.STRICT); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheTests.java index 856b10d6..d7538761 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheTests.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheTests.java @@ -3,13 +3,18 @@ package com.microsoft.aad.msal4j; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.HashMap; +import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -18,6 +23,13 @@ class CacheTests { + private TokenCache tokenCache; + + @BeforeEach + void setUp() { + tokenCache = new TokenCache(); + } + @Test void cacheLookup_MixAccountBasedAndAssertionBasedSilentFlows() throws Exception { DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class); @@ -67,4 +79,251 @@ void cacheLookup_MixAccountBasedAndAssertionBasedSilentFlows() throws Exception assertEquals("accessTokenNoAccount", resultNoAccount.accessToken()); assertEquals("accessTokenWithAccount", resultWithAccount.accessToken()); } + + @Test + void serializeDeserialize_EmptyCache_Success() { + // Serialize empty cache + String serializedCache = tokenCache.serialize(); + + // Should have all required sections but be empty + assertTrue(serializedCache.contains("\"Account\":{}")); + assertTrue(serializedCache.contains("\"AccessToken\":{}")); + assertTrue(serializedCache.contains("\"RefreshToken\":{}")); + assertTrue(serializedCache.contains("\"IdToken\":{}")); + assertTrue(serializedCache.contains("\"AppMetadata\":{}")); + + // Create new cache and deserialize + TokenCache newCache = new TokenCache(); + newCache.deserialize(serializedCache); + + // Verify all collections are empty + assertTrue(newCache.accessTokens.isEmpty()); + assertTrue(newCache.refreshTokens.isEmpty()); + assertTrue(newCache.idTokens.isEmpty()); + assertTrue(newCache.accounts.isEmpty()); + assertTrue(newCache.appMetadata.isEmpty()); + } + + @Test + void serializeDeserialize_WithAccountData_PreservesData() { + // Add an account to the cache + AccountCacheEntity account = new AccountCacheEntity(); + account.homeAccountId("home-account-id"); + account.environment("login.microsoftonline.com"); + account.realm("tenant-id"); + account.localAccountId("local-id"); + account.username("user@example.com"); + account.name("Test User"); + + tokenCache.accounts.put(account.getKey(), account); + + // Serialize the cache + String serializedCache = tokenCache.serialize(); + + // Create new cache and deserialize + TokenCache newCache = new TokenCache(); + newCache.deserialize(serializedCache); + + // Verify account was preserved + assertEquals(1, newCache.accounts.size()); + AccountCacheEntity deserializedAccount = newCache.accounts.get(account.getKey()); + assertNotNull(deserializedAccount); + assertEquals("home-account-id", deserializedAccount.homeAccountId()); + assertEquals("login.microsoftonline.com", deserializedAccount.environment()); + assertEquals("tenant-id", deserializedAccount.realm()); + assertEquals("user@example.com", deserializedAccount.username()); + assertEquals("Test User", deserializedAccount.name()); + } + + @Test + void removeAccount_RemovesAllRelatedEntities() { + // Setup test data + String homeAccountId = "home-account-id"; + String environment = "login.microsoftonline.com"; + String clientId = "client-id"; + String realm = "tenant-id"; + String username = "user@example.com"; + + // Create account + AccountCacheEntity account = new AccountCacheEntity(); + account.homeAccountId(homeAccountId); + account.environment(environment); + account.realm(realm); + account.username(username); + tokenCache.accounts.put(account.getKey(), account); + + // Create access token + AccessTokenCacheEntity accessToken = new AccessTokenCacheEntity(); + accessToken.homeAccountId(homeAccountId); + accessToken.environment(environment); + accessToken.clientId(clientId); + accessToken.realm(realm); + accessToken.target("scope1 scope2"); + accessToken.secret("access-token-secret"); + accessToken.cachedAt("1600000000"); + accessToken.expiresOn("1600100000"); + tokenCache.accessTokens.put(accessToken.getKey(), accessToken); + + // Create refresh token + RefreshTokenCacheEntity refreshToken = new RefreshTokenCacheEntity(); + refreshToken.homeAccountId(homeAccountId); + refreshToken.environment(environment); + refreshToken.clientId(clientId); + refreshToken.secret("refresh-token-secret"); + tokenCache.refreshTokens.put(refreshToken.getKey(), refreshToken); + + // Create ID token + IdTokenCacheEntity idToken = new IdTokenCacheEntity(); + idToken.homeAccountId(homeAccountId); + idToken.environment(environment); + idToken.clientId(clientId); + idToken.realm(realm); + idToken.secret("id-token-secret"); + tokenCache.idTokens.put(idToken.getKey(), idToken); + + // Remove the account + tokenCache.removeAccount(clientId, new Account(homeAccountId, environment, username, null)); + + // Verify all related entities are removed + assertTrue(tokenCache.accounts.isEmpty()); + assertTrue(tokenCache.accessTokens.isEmpty()); + assertTrue(tokenCache.refreshTokens.isEmpty()); + assertTrue(tokenCache.idTokens.isEmpty()); + } + + @Test + void removeAccount_WithMultipleAccounts_OnlyRemovesSpecificAccount() { + // Create two accounts with different homeAccountIds + String homeAccountId1 = "home-account-id-1"; + String homeAccountId2 = "home-account-id-2"; + String environment = "login.microsoftonline.com"; + String clientId = "client-id"; + String username = "user@example.com"; + + // Account 1 + AccountCacheEntity account1 = new AccountCacheEntity(); + account1.homeAccountId(homeAccountId1); + account1.environment(environment); + tokenCache.accounts.put(account1.getKey(), account1); + + // Account 2 + AccountCacheEntity account2 = new AccountCacheEntity(); + account2.homeAccountId(homeAccountId2); + account2.environment(environment); + tokenCache.accounts.put(account2.getKey(), account2); + + // Add tokens for both accounts + AccessTokenCacheEntity accessToken1 = new AccessTokenCacheEntity(); + accessToken1.homeAccountId(homeAccountId1); + accessToken1.environment(environment); + accessToken1.clientId(clientId); + accessToken1.expiresOn("1600100000"); + tokenCache.accessTokens.put(accessToken1.getKey(), accessToken1); + + AccessTokenCacheEntity accessToken2 = new AccessTokenCacheEntity(); + accessToken2.homeAccountId(homeAccountId2); + accessToken2.environment(environment); + accessToken2.clientId(clientId); + accessToken2.expiresOn("1600100000"); + tokenCache.accessTokens.put(accessToken2.getKey(), accessToken2); + + // Remove account1 + tokenCache.removeAccount(clientId, new Account(homeAccountId1, environment, username, null)); + + // Verify only account1 and its tokens are removed + assertEquals(1, tokenCache.accounts.size()); + assertEquals(1, tokenCache.accessTokens.size()); + assertTrue(tokenCache.accounts.containsKey(account2.getKey())); + assertFalse(tokenCache.accounts.containsKey(account1.getKey())); + } + + @Test + void mergeCache_PreservesRemovals() { + // Setup initial cache with two accounts + String homeAccountId1 = "home-account-id-1"; + String homeAccountId2 = "home-account-id-2"; + String environment = "login.microsoftonline.com"; + String clientId = "client-id"; + String username = "user@example.com"; + + // Account 1 + AccountCacheEntity account1 = new AccountCacheEntity(); + account1.homeAccountId(homeAccountId1); + account1.environment(environment); + tokenCache.accounts.put(account1.getKey(), account1); + + // Account 2 + AccountCacheEntity account2 = new AccountCacheEntity(); + account2.homeAccountId(homeAccountId2); + account2.environment(environment); + tokenCache.accounts.put(account2.getKey(), account2); + + // Serialize the cache with both accounts + String serializedWithTwoAccounts = tokenCache.serialize(); + + // Create a new cache with this serialized data + TokenCache deserializedCache = new TokenCache(); + deserializedCache.deserialize(serializedWithTwoAccounts); + assertEquals(2, deserializedCache.accounts.size()); + + // Now remove account1 from the original cache + tokenCache.removeAccount(clientId, new Account(homeAccountId1, environment, username, null)); + + // Serialize the cache with just one account + String serializedWithOneAccount = tokenCache.serialize(); + + // Deserialize into a new cache + TokenCache finalCache = new TokenCache(); + finalCache.deserialize(serializedWithOneAccount); + + // Verify only one account exists + assertEquals(1, finalCache.accounts.size()); + assertTrue(finalCache.accounts.containsKey(account2.getKey())); + assertFalse(finalCache.accounts.containsKey(account1.getKey())); + } + + @Test + void getAccounts_ReturnsCorrectAccounts() { + // Setup multiple accounts in the cache + String homeAccountId1 = "home-account-id-1"; + String localAccountId1 = "local-id-1"; + String homeAccountId2 = "home-account-id-2"; + String environment = "login.microsoftonline.com"; + String clientId = "client-id"; + String realm = "tenant-id"; + + // Account 1 + AccountCacheEntity account1 = new AccountCacheEntity(); + account1.homeAccountId(homeAccountId1); + account1.localAccountId(localAccountId1); + account1.environment(environment); + account1.realm(realm); + account1.username("user1@example.com"); + tokenCache.accounts.put(account1.getKey(), account1); + + // Account 2 + AccountCacheEntity account2 = new AccountCacheEntity(); + account2.homeAccountId(homeAccountId2); + account2.environment(environment); + account2.realm(realm); + account2.username("user2@example.com"); + tokenCache.accounts.put(account2.getKey(), account2); + + // Get accounts + Set accounts = tokenCache.getAccounts(clientId); + + // Verify correct accounts are returned + assertEquals(2, accounts.size()); + + // Verify account details + for (IAccount account : accounts) { + if (account.homeAccountId().equals(homeAccountId1)) { + assertEquals("user1@example.com", account.username()); + } else if (account.homeAccountId().equals(homeAccountId2)) { + assertEquals("user2@example.com", account.username()); + } else { + fail("Unexpected account returned"); + } + } + } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java index 7df0a121..5ac73848 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java @@ -396,6 +396,8 @@ void managedIdentityTest_WrongScopes(ManagedIdentitySourceType source, String en assert(exception.getCause() instanceof MsalServiceException); MsalServiceException miException = (MsalServiceException) exception.getCause(); + System.out.println(miException.getMessage()); + System.out.println(miException.errorCode()); assertEquals(source.name(), miException.managedIdentitySource()); assertEquals(AuthenticationErrorCode.MANAGED_IDENTITY_REQUEST_FAILED, miException.errorCode()); return; @@ -488,7 +490,7 @@ void managedIdentity_RequestFailed_NoPayload(ManagedIdentitySourceType source, S MsalServiceException miException = (MsalServiceException) exception.getCause(); assertEquals(source.name(), miException.managedIdentitySource()); - assertEquals(MsalError.MANAGED_IDENTITY_RESPONSE_PARSE_FAILURE, miException.errorCode()); + assertEquals(MANAGED_IDENTITY_REQUEST_FAILED, miException.errorCode()); return; } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestConfiguration.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestConfiguration.java index 53e648bd..5878afac 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestConfiguration.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestConfiguration.java @@ -97,7 +97,7 @@ public final class TestConfiguration { public final static String CLAIMS_REQUEST = "{\"id_token\":{\"acr\":{\"values\":[\"urn:mace:incommon:iap:silver\",\"urn:mace:incommon:iap:bronze\"]},\"sub\":{\"essential\":true,\"value\":\"248289761001\"},\"auth_time\":{}},\"access_token\":{\"given_name\":{\"essential\":true},\"email\":null}}"; public final static String CLAIMS_CHALLENGE = "{\"access_token\":{\"nbf\":{\"essential\":true,\"value\":\"1701477303\"}}}"; public final static String CLIENT_CAPABILITIES = "{\"access_token\":{\"xms_cc\":{\"values\":[\"cp1\"]}}}"; - public final static String MERGED_CLAIMS_AND_CAPABILITIES = "{\"access_token\":{\"nbf\":{\"essential\":true,\"value\":\"1701477303\"},\"xms_cc\":{\"values\":[\"cp1\"]}}}"; - public final static String MERGED_CLAIMS_AND_CHALLENGE = "{\"access_token\":{\"nbf\":{\"essential\":true,\"value\":\"1701477303\"}}}"; - public final static String MERGED_CLAIMS_CAPABILITIES_AND_CHALLENGE = "{\"access_token\":{\"nbf\":{\"essential\":true,\"value\":\"1701477303\"},\"xms_cc\":{\"values\":[\"cp1\"]}}}"; + public final static String MERGED_CLAIMS_AND_CAPABILITIES = "{\"access_token\":{\"nbf\":{\"value\":\"1701477303\",\"essential\":true},\"xms_cc\":{\"values\":[\"cp1\"]}}}"; + public final static String MERGED_CLAIMS_AND_CHALLENGE = "{\"access_token\":{\"nbf\":{\"value\":\"1701477303\",\"essential\":true}}}"; + public final static String MERGED_CLAIMS_CAPABILITIES_AND_CHALLENGE = "{\"access_token\":{\"nbf\":{\"value\":\"1701477303\",\"essential\":true},\"xms_cc\":{\"values\":[\"cp1\"]}}}"; } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java index 83b178dd..a68410d1 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java @@ -273,16 +273,8 @@ void testExecuteOAuth_Failure() throws MsalException, IOException, URISyntaxExce doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest(); doReturn(httpResponse).when(msalOAuthHttpRequest).send(); - lenient().doReturn(402).when(httpResponse).statusCode(); - doReturn(new HashMap<>()).when(httpResponse).headers(); doReturn(TestConfiguration.HTTP_ERROR_RESPONSE).when(httpResponse).body(); - final ErrorResponse errorResponse = mock(ErrorResponse.class); - - lenient().doReturn("invalid_request").when(errorResponse).error(); - lenient().doReturn(null).when(httpResponse).getHeader("User-Agent"); - lenient().doReturn(null).when(httpResponse).getHeader("x-ms-request-id"); - lenient().doReturn(null).when(httpResponse).getHeader("x-ms-clitelem"); doReturn(402).when(httpResponse).statusCode(); assertThrows(MsalException.class, request::executeTokenRequest); diff --git a/msal4j-sdk/src/test/resources/cache_data/serialized_cache.json b/msal4j-sdk/src/test/resources/cache_data/serialized_cache.json index db008bfd..d4acadf9 100644 --- a/msal4j-sdk/src/test/resources/cache_data/serialized_cache.json +++ b/msal4j-sdk/src/test/resources/cache_data/serialized_cache.json @@ -11,7 +11,6 @@ }, "RefreshToken": { "uid.utid-login.windows.net-refreshtoken-my_client_id--s2 s1 s3": { - "target": "s2 s1 s3", "environment": "login.windows.net", "credential_type": "RefreshToken", "secret": "a refresh token", @@ -20,9 +19,6 @@ } }, "AccessToken": { - "an-entry": { - "foo": "bar" - }, "uid.utid-login.windows.net-accesstoken-my_client_id-contoso-s2 s1 s3": { "environment": "login.windows.net", "credential_type": "AccessToken", @@ -46,14 +42,9 @@ "home_account_id": "uid.utid" } }, - "unknownEntity": { - "field1": "1", - "field2": "whats" - }, "AppMetadata": { "appmetadata-login.windows.net-my_client_id": { "environment": "login.windows.net", - "family_id": null, "client_id": "my_client_id" } } From 1fa1939708f03f1df3efcfb470e330755777e3a0 Mon Sep 17 00:00:00 2001 From: avdunn Date: Wed, 7 May 2025 13:32:09 -0700 Subject: [PATCH 21/31] Add more tests and improve helper methods to better match nimbus behavior --- .../com/microsoft/aad/msal4j/JsonHelper.java | 27 ++- .../aad/msal4j/JsonCompatibilityTests.java | 154 ++++++++++++++++++ .../com/microsoft/aad/msal4j/TestHelper.java | 40 +++++ 3 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/JsonCompatibilityTests.java diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java index 27963121..7177e7d0 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java @@ -82,12 +82,37 @@ private static Map parseJsonObject(JsonReader jsonReader) throws while (jsonReader.nextToken() != JsonToken.END_OBJECT) { String fieldName = jsonReader.getFieldName(); - object.put(fieldName, parseValue(jsonReader)); + Object value = parseValue(jsonReader); + object.put(fieldName, handleSpecialFields(fieldName, value)); } return object; } + //Due to the old usage of com.nimbusds for JWT parsing, customers may be relying on certain fields being treated as specific types. + // This method handles those special cases to help ensure backwards compatibility. + private static Object handleSpecialFields(String fieldName, Object value) { + //nimbus always treated the "aud" field as an ArrayList, even when it was a single string + if ("aud".equals(fieldName) && value instanceof String) { + ArrayList list = new ArrayList<>(); + list.add((String) value); + return list; + } + + //nimbus converted certain unix timestamps to Date objects + if (isTimestampField(fieldName) && value instanceof Number) { + // Convert seconds to milliseconds for Date constructor + return new Date(((Number) value).longValue() * 1000); + } + + return value; + } + + private static boolean isTimestampField(String fieldName) { + return "exp".equals(fieldName) || "iat".equals(fieldName) || + "nbf".equals(fieldName); + } + private static Object parseValue(JsonReader jsonReader) throws IOException { JsonToken token = jsonReader.currentToken(); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/JsonCompatibilityTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/JsonCompatibilityTests.java new file mode 100644 index 00000000..cc15ee94 --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/JsonCompatibilityTests.java @@ -0,0 +1,154 @@ +package com.microsoft.aad.msal4j; + +import com.nimbusds.jwt.JWTParser; +import org.junit.jupiter.api.Test; + +import java.text.ParseException; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +//These tests were added to ensure the new usages of com.azure.json are functionally the same as the old usages of com.nimbusds packages. +//Once we are confident in the new behavior these should no longer be necessary. +class JsonCompatibilityTests { + + //New style, using helper methods in JsonHelper that use com.azure.json + private final Map newStyleParsedClaims = JsonHelper.parseJsonToMap(JsonHelper.getTokenPayloadClaims(TestHelper.ENCODED_JWT)); + //Old style, using com.nimbusds methods + private final Map oldStyleParsedClaims = JWTParser.parse(TestHelper.ENCODED_JWT).getJWTClaimsSet().getClaims(); + + JsonCompatibilityTests() throws ParseException { + } + + @Test + void testBasicClaimsMatching() { + // Test basic string claims + assertEquals(oldStyleParsedClaims.get("aud"), newStyleParsedClaims.get("aud")); + assertEquals(oldStyleParsedClaims.get("iss"), newStyleParsedClaims.get("iss")); + assertEquals(oldStyleParsedClaims.get("name"), newStyleParsedClaims.get("name")); + assertEquals(oldStyleParsedClaims.get("oid"), newStyleParsedClaims.get("oid")); + assertEquals(oldStyleParsedClaims.get("preferred_username"), newStyleParsedClaims.get("preferred_username")); + assertEquals(oldStyleParsedClaims.get("sub"), newStyleParsedClaims.get("sub")); + assertEquals(oldStyleParsedClaims.get("tid"), newStyleParsedClaims.get("tid")); + assertEquals(oldStyleParsedClaims.get("ver"), newStyleParsedClaims.get("ver")); + } + + @Test + void testNullValues() { + // Check null values are handled the same + assertEquals(oldStyleParsedClaims.get("email"), newStyleParsedClaims.get("email")); + } + + @Test + void testNumericValues() { + // Test numeric claims + assertEquals(oldStyleParsedClaims.get("exp"), newStyleParsedClaims.get("exp")); + assertEquals(oldStyleParsedClaims.get("iat"), newStyleParsedClaims.get("iat")); + assertEquals(oldStyleParsedClaims.get("nbf"), newStyleParsedClaims.get("nbf")); + assertEquals(oldStyleParsedClaims.get("auth_time"), newStyleParsedClaims.get("auth_time")); + } + + @Test + void testListValues() { + // Test list claims + List oldGroups = (List) oldStyleParsedClaims.get("groups"); + List newGroups = (List) newStyleParsedClaims.get("groups"); + assertEquals(oldGroups, newGroups); + + List oldAmr = (List) oldStyleParsedClaims.get("amr"); + List newAmr = (List) newStyleParsedClaims.get("amr"); + assertEquals(oldAmr, newAmr); + + List oldRoles = (List) oldStyleParsedClaims.get("roles"); + List newRoles = (List) newStyleParsedClaims.get("roles"); + assertEquals(oldRoles, newRoles); + } + + @Test + void testNestedObjects() { + // Test nested objects + Map oldExtensionData = (Map) oldStyleParsedClaims.get("extension_data"); + Map newExtensionData = (Map) newStyleParsedClaims.get("extension_data"); + + assertEquals(oldExtensionData.get("department"), newExtensionData.get("department")); + assertEquals(oldExtensionData.get("manager"), newExtensionData.get("manager")); + assertEquals(oldExtensionData.get("employeeId"), newExtensionData.get("employeeId")); + } + + @Test + void testCompleteTokenParsing() { + assertEquals(oldStyleParsedClaims.size(), newStyleParsedClaims.size()); + + // Check that all keys and values match + for (String key : oldStyleParsedClaims.keySet()) { + assertTrue(newStyleParsedClaims.containsKey(key), "New claims should contain key: " + key); + + Object oldValue = oldStyleParsedClaims.get(key); + Object newValue = newStyleParsedClaims.get(key); + + if (oldValue instanceof List) { + assertListsEqual((List) oldValue, (List) newValue); + } else if (oldValue instanceof Map) { + assertMapsEqual((Map) oldValue, (Map) newValue); + } else { + assertEquals(oldValue, newValue, "Value mismatch for key: " + key); + } + } + } + + @Test + void testInvalidJSONHandling() { + // Test that both parsers throw exceptions for invalid JSON + String invalidJson = "{ this is not valid json }"; + + assertThrows(Exception.class, () -> JsonHelper.parseJsonToMap(invalidJson)); + assertThrows(Exception.class, () -> JWTParser.parse("header." + + Base64.getUrlEncoder().encodeToString(invalidJson.getBytes()) + ".signature")); + } + + /** + * Utility method to compare lists deeply + */ + private void assertListsEqual(List list1, List list2) { + assertEquals(list1.size(), list2.size()); + for (int i = 0; i < list1.size(); i++) { + Object item1 = list1.get(i); + Object item2 = list2.get(i); + + if (item1 == null) { + assertNull(item2); + } else if (item1 instanceof List) { + assertListsEqual((List) item1, (List) item2); + } else if (item1 instanceof Map) { + assertMapsEqual((Map) item1, (Map) item2); + } else { + assertEquals(item1, item2); + } + } + } + + /** + * Utility method to compare maps deeply + */ + private void assertMapsEqual(Map map1, Map map2) { + assertEquals(map1.size(), map2.size()); + for (Object key : map1.keySet()) { + assertTrue(map2.containsKey(key)); + + Object value1 = map1.get(key); + Object value2 = map2.get(key); + + if (value1 == null) { + assertNull(value2); + } else if (value1 instanceof List) { + assertListsEqual((List) value1, (List) value2); + } else if (value1 instanceof Map) { + assertMapsEqual((Map) value1, (Map) value2); + } else { + assertEquals(value1, value2); + } + } + } +} \ No newline at end of file diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java index c7813544..6a01e86b 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java @@ -26,6 +26,38 @@ class TestHelper { //Signed JWT which should be enough to pass the parsing/validation in the library, useful if a unit test needs an // assertion but that is not the focus of the test static String signedAssertion = generateToken(); + + // Realistic token header + static final String TOKEN_HEADER = "{\"alg\":\"PS256\",\"typ\":\"JWT\"}"; + + // Realistic JWT payload, containing strings, numbers, timestamps, arrays, nested JSON objects, and nulls + static final String TOKEN_PAYLOAD = "{\n" + + "\"aud\": \"e854a4a7-6c34-449c-b237-fc7a28093d84\",\n" + + "\"iss\": \"https://login.microsoftonline.com/6c3d51dd-f0e5-4959-b4ea-a80c4e36fe5e/v2.0/\",\n" + + "\"name\": \"John Doe\",\n" + + "\"oid\": \"00000000-0000-0000-66f3-3332eca7ea81\",\n" + + "\"preferred_username\": \"john.doe@example.com\",\n" + + "\"sub\": \"K4_SGGxKqW1SxUAmhg6C1F6VPiFzcx-Qd80ehIEdFus\",\n" + + "\"tid\": \"6c3d51dd-f0e5-4959-b4ea-a80c4e36fe5e\",\n" + + "\"ver\": \"2.0\",\n" + + "\"exp\": 1735689600,\n" + + "\"iat\": 1516239022,\n" + + "\"nbf\": 1516239022,\n" + + "\"email\": null,\n" + + "\"groups\": [\"admin\", \"user\", null],\n" + + "\"roles\": [],\n" + + "\"amr\": [\"pwd\", \"mfa\"],\n" + + "\"nonce\": \"12345\",\n" + + "\"auth_time\": 1516239022,\n" + + "\"given_name\": \"John\",\n" + + "\"family_name\": \"Doe\",\n" + + "\"at_hash\": \"jT3s9ygOQRtifgrpJgGz_w\",\n" + + "\"extension_data\": {\"department\": \"Engineering\", \"manager\": null, \"employeeId\": 12345}\n" + + "}"; + + // Base64URL encoded ID token, has a junk signature but is otherwise realistic and parsable + static final String ENCODED_JWT = createEncodedJwt(TOKEN_HEADER, TOKEN_PAYLOAD); + private static final String successfulResponseFormat = "{\"access_token\":\"%s\",\"id_token\":\"%s\",\"refresh_token\":\"%s\"," + "\"client_id\":\"%s\",\"client_info\":\"%s\"," + "\"refresh_in\": %d,\"expires_on\": %d,\"expires_in\": %d," + @@ -96,6 +128,14 @@ static String generateToken() { } } + private static String createEncodedJwt(String headerJson, String payloadJson) { + String encodedHeader = Base64.getUrlEncoder().withoutPadding().encodeToString(headerJson.getBytes()); + String encodedPayload = Base64.getUrlEncoder().withoutPadding().encodeToString(payloadJson.getBytes()); + String signature = "signature"; // Simple signature for testing purposes + + return encodedHeader + "." + encodedPayload + "." + signature; + } + //Maps various values to the successfulResponseFormat string to create a valid token response static String getSuccessfulTokenResponse(HashMap responseValues) { //Will default to expiring in one hour if expiry time values are not set From 979ff0ce7ceaccdb3c7b41b7ab7637f76c0be84b Mon Sep 17 00:00:00 2001 From: avdunn Date: Wed, 7 May 2025 13:54:14 -0700 Subject: [PATCH 22/31] Very minor issue, but in a public method nonetheless --- .../java/com/microsoft/aad/msal4j/AuthenticationResult.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java index 2419edab..d87dfc4b 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java @@ -215,7 +215,7 @@ public AuthenticationResult build() { } public String toString() { - return "AuthenticationResult.AuthenticationResultBuilder(accessToken=" + this.accessToken + ", expiresOn=" + this.expiresOn + ", extExpiresOn=" + this.extExpiresOn + ", refreshToken=" + this.refreshToken + ", refreshOn=" + this.refreshOn + ", familyId=" + this.familyId + ", idToken=" + this.idToken + ", accountCacheEntity=" + this.accountCacheEntity + ", environment=" + this.environment + ", scopes=" + this.scopes + ", metadata" + this.metadata + ", isPopAuthorization=" + this.isPopAuthorization + ")"; + return "AuthenticationResult.AuthenticationResultBuilder(accessToken=" + this.accessToken + ", expiresOn=" + this.expiresOn + ", extExpiresOn=" + this.extExpiresOn + ", refreshToken=" + this.refreshToken + ", refreshOn=" + this.refreshOn + ", familyId=" + this.familyId + ", idToken=" + this.idToken + ", accountCacheEntity=" + this.accountCacheEntity + ", environment=" + this.environment + ", scopes=" + this.scopes + ", metadata=" + this.metadata + ", isPopAuthorization=" + this.isPopAuthorization + ")"; } } From 9c26b835358395c35e89af538ff0bfa2eecf2489 Mon Sep 17 00:00:00 2001 From: avdunn Date: Wed, 7 May 2025 14:53:23 -0700 Subject: [PATCH 23/31] Address PR feedback --- .../src/main/java/com/microsoft/aad/msal4j/DeviceCode.java | 6 +++--- .../src/main/java/com/microsoft/aad/msal4j/TokenCache.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCode.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCode.java index da72c3e9..c5c93e9b 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCode.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DeviceCode.java @@ -23,9 +23,9 @@ public final class DeviceCode implements JsonSerializable { private long interval; private String message; - private transient String correlationId = null; - private transient String clientId = null; - private transient String scopes = null; + private String correlationId = null; + private String clientId = null; + private String scopes = null; public static DeviceCode fromJson(JsonReader jsonReader) throws IOException { DeviceCode deviceCode = new DeviceCode(); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java index ef6fbaf8..c54f75cf 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java @@ -23,7 +23,7 @@ public class TokenCache implements ITokenCache { protected static final int MIN_ACCESS_TOKEN_EXPIRE_IN_SEC = 5 * 60; - transient private ReadWriteLock lock = new ReentrantReadWriteLock(); + private ReadWriteLock lock = new ReentrantReadWriteLock(); /** * Constructor for token cache @@ -47,9 +47,9 @@ public TokenCache() { Map accounts = new LinkedHashMap<>(); Map appMetadata = new LinkedHashMap<>(); - transient ITokenCacheAccessAspect tokenCacheAccessAspect; + ITokenCacheAccessAspect tokenCacheAccessAspect; - private transient String serializedCachedSnapshot; + private String serializedCachedSnapshot; @Override public void deserialize(String data) { From 4cf5133fee6553009641a79308cfada5dbae7873 Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 8 May 2025 09:21:22 -0700 Subject: [PATCH 24/31] Address PR feedback --- .../src/main/java/com/microsoft/aad/msal4j/ClaimsRequest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClaimsRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClaimsRequest.java index 3093008c..a0fb33b0 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClaimsRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClaimsRequest.java @@ -138,6 +138,10 @@ private static void parseClaims(JsonReader jsonReader, ClaimsRequest claimsReque } jsonReader.nextToken(); + if (jsonReader.currentToken() == JsonToken.NULL) { + return; + } + if (jsonReader.currentToken() != JsonToken.START_OBJECT) { throw new IllegalStateException("Expected start of object but was " + jsonReader.currentToken()); } From 9de706c5263ec05f97cb5bfffa9a1f05177f35ae Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 8 May 2025 09:39:23 -0700 Subject: [PATCH 25/31] Address PR feedback --- .../com/microsoft/aad/msal4j/JsonHelper.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java index 7177e7d0..1b9c801a 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java @@ -14,12 +14,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.JsonNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.*; class JsonHelper { + private static final Logger LOG = LoggerFactory.getLogger(JsonHelper.class); + static ObjectMapper mapper; static { @@ -36,6 +40,7 @@ static T convertJsonToObject(final String json, final Class tClass) { try { return mapper.readValue(json, tClass); } catch (Exception e) { + LOG.error(String.format("Error converting JSON string into %s: %s", tClass, e.getMessage())); throw new MsalJsonParsingException(e.getMessage(), AuthenticationErrorCode.INVALID_JSON); } } @@ -48,6 +53,7 @@ static String getTokenPayloadClaims(String token) { try { return new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8); } catch (ArrayIndexOutOfBoundsException e) { + LOG.error("Error parsing ID token, missing payload section."); throw new MsalClientException("Error parsing ID token, missing payload section.", AuthenticationErrorCode.INVALID_JWT); } @@ -63,20 +69,11 @@ static Map parseJsonToMap(String jsonString) { jsonReader.nextToken(); return parseJsonObject(jsonReader); } catch (IOException e) { + LOG.error("JSON parsing error when attempting to convert JSON into a Map."); throw new MsalJsonParsingException(e.getMessage(), AuthenticationErrorCode.INVALID_JSON); } } - private static List parseJsonArray(JsonReader jsonReader) throws IOException { - List array = new ArrayList<>(); - - while (jsonReader.nextToken() != JsonToken.END_ARRAY) { - array.add(parseValue(jsonReader)); - } - - return array; - } - private static Map parseJsonObject(JsonReader jsonReader) throws IOException { Map object = new HashMap<>(); @@ -130,7 +127,7 @@ private static Object parseValue(JsonReader jsonReader) throws IOException { case NULL: return null; case START_ARRAY: - return parseJsonArray(jsonReader); + return jsonReader.readArray(JsonReader::readUntyped); case START_OBJECT: return parseJsonObject(jsonReader); default: From 75abea00b6d7a426d0ae62ed3607ce18faf6a49b Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 8 May 2025 10:41:17 -0700 Subject: [PATCH 26/31] Resolve merge conflicts --- .../java/com/microsoft/aad/msal4j/JsonHelper.java | 12 +++--------- .../microsoft/aad/msal4j/ManagedIdentityTests.java | 7 ++----- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java index d6432542..d566cfb6 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java @@ -9,6 +9,8 @@ import com.azure.json.JsonToken; import com.azure.json.JsonWriter; import com.azure.json.ReadValueCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -17,19 +19,11 @@ import java.util.*; class JsonHelper { + private static final Logger LOG = LoggerFactory.getLogger(JsonHelper.class); private JsonHelper() { } - static T convertJsonToObject(final String json, final Class tClass) { - try { - return mapper.readValue(json, tClass); - } catch (Exception e) { - LOG.error(String.format("Error converting JSON string into %s: %s", tClass, e.getMessage())); - throw new MsalJsonParsingException(e.getMessage(), AuthenticationErrorCode.INVALID_JSON); - } - } - static IdToken createIdTokenFromEncodedTokenString(String token) { return convertJsonStringToJsonSerializableObject(getTokenPayloadClaims(token), IdToken::fromJson); } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java index 5ac73848..a1456880 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java @@ -638,8 +638,6 @@ void managedIdentityTest_WithClaims(ManagedIdentitySourceType source, String end // Clear caching to avoid cross test pollution. miApp.tokenCache().accessTokens.clear(); - String claimsJson = "{\"default\":\"claim\"}"; - // First call, get the token from the identity provider. IAuthenticationResult result = miApp.acquireTokenForManagedIdentity( ManagedIdentityParameters.builder(resource) @@ -659,7 +657,7 @@ void managedIdentityTest_WithClaims(ManagedIdentitySourceType source, String end // Third call, when claims are passed bypass the cache. result = miApp.acquireTokenForManagedIdentity( ManagedIdentityParameters.builder(resource) - .claims(claimsJson) + .claims(TestConfiguration.CLAIMS_REQUEST) .build()).get(); assertNotNull(result.accessToken()); @@ -689,7 +687,6 @@ void managedIdentity_ClaimsAndCapabilities(ManagedIdentitySourceType source, Str // Clear caching to avoid cross test pollution. miApp.tokenCache().accessTokens.clear(); - String claimsJson = "{\"default\":\"claim\"}"; // First call, get the token from the identity provider. IAuthenticationResult result = miApp.acquireTokenForManagedIdentity( ManagedIdentityParameters.builder(resource) @@ -709,7 +706,7 @@ void managedIdentity_ClaimsAndCapabilities(ManagedIdentitySourceType source, Str // Third call, when claims are passed bypass the cache. result = miApp.acquireTokenForManagedIdentity( ManagedIdentityParameters.builder(resource) - .claims(claimsJson) + .claims(TestConfiguration.CLAIMS_REQUEST) .build()).get(); assertNotNull(result.accessToken()); From b4d9b86d5f0eac296cd6d3b6bea55d74b7751a0a Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 8 May 2025 12:37:26 -0700 Subject: [PATCH 27/31] Final dependency changes in test classes, and cleanup of various unused code --- msal4j-sdk/pom.xml | 69 ++----- .../AcquireTokenInteractiveIT.java | 36 ---- .../CachePersistenceIT.java | 67 +++---- .../java/com.microsoft.aad.msal4j/Config.java | 24 ++- .../infrastructure/SeleniumConstants.java | 8 - .../src/integrationtest/java/labapi/App.java | 93 +++++++-- .../java/labapi/AppCredentialProvider.java | 2 +- .../java/labapi/AzureEnvironment.java | 2 - .../java/labapi/B2CProvider.java | 4 - .../java/labapi/FederationProvider.java | 1 - .../src/integrationtest/java/labapi/Lab.java | 82 ++++++-- .../java/labapi/LabService.java | 67 ++++--- .../java/labapi/LabUserProvider.java | 2 - .../src/integrationtest/java/labapi/User.java | 188 ++++++++++++++---- .../java/labapi/UserQueryParameters.java | 8 - .../java/labapi/UserSecret.java | 50 ++++- .../integrationtest/java/labapi/UserType.java | 1 - ...AuthorizationRequestUrlParametersTest.java | 2 - .../aad/msal4j/CacheFormatTests.java | 46 ++--- .../aad/msal4j/RequestThrottlingTest.java | 4 - .../aad/msal4j/TestConfiguration.java | 11 - .../aad/msal4j/UIRequiredCacheTest.java | 4 - 22 files changed, 451 insertions(+), 320 deletions(-) diff --git a/msal4j-sdk/pom.xml b/msal4j-sdk/pom.xml index 725dc062..57efe257 100644 --- a/msal4j-sdk/pom.xml +++ b/msal4j-sdk/pom.xml @@ -33,46 +33,30 @@ - - com.nimbusds - oauth2-oidc-sdk - 11.23 - test - - - net.minidev - json-smart - 2.5.2 - org.slf4j slf4j-api 1.7.36 - - org.slf4j - slf4j-simple - 1.6.2 - test - - - org.projectlombok - lombok - 1.18.36 - test - com.azure azure-json 1.4.0 - - com.fasterxml.jackson.core - jackson-databind - 2.18.1 - + + com.nimbusds + oauth2-oidc-sdk + 11.23 + test + + + org.slf4j + slf4j-simple + 1.6.2 + test + org.apache.commons commons-text @@ -115,7 +99,6 @@ 1.14.5 test - org.skyscreamer jsonassert @@ -181,7 +164,7 @@ - ${project.build.directory}/delombok + src/main/java org.revapi @@ -210,30 +193,6 @@ - - org.projectlombok - lombok-maven-plugin - 1.18.20.0 - - - org.projectlombok - lombok - 1.18.36 - - - - - - delombok - - - - - src/main/java - ${project.build.directory}/delombok - false - - org.apache.maven.plugins @@ -263,7 +222,7 @@ maven-javadoc-plugin 3.1.0 - ${project.build.directory}/delombok + src/main/java diff --git a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java index d8f9f1fb..4bc403e4 100644 --- a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java +++ b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java @@ -208,42 +208,6 @@ private IAuthenticationResult acquireTokenSilently(IPublicClientApplication pca, .get(); } - public void acquireTokensInHomeAndGuestClouds(String homeCloud) throws MalformedURLException { - - User user = labUserProvider.getUserByGuestHomeAzureEnvironments - (AzureEnvironment.AZURE, homeCloud); - - // use user`s upn from home cloud - user.setUpn(user.getHomeUPN()); - - ITokenCacheAccessAspect persistenceAspect = new ITokenCacheAccessAspect() { - String data; - - @Override - public void beforeCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) { - iTokenCacheAccessContext.tokenCache().deserialize(data); - } - - @Override - public void afterCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) { - data = iTokenCacheAccessContext.tokenCache().serialize(); - } - }; - - PublicClientApplication publicCloudPca = PublicClientApplication.builder( - user.getAppId()). - authority(TestConstants.AUTHORITY_PUBLIC_TENANT_SPECIFIC).setTokenCacheAccessAspect(persistenceAspect). - build(); - - IAuthenticationResult result = acquireTokenInteractive(user, publicCloudPca, TestConstants.USER_READ_SCOPE); - IntegrationTestHelper.assertAccessAndIdTokensNotNull(result); - assertEquals(user.getHomeUPN(), result.account().username()); - - publicCloudPca.removeAccount(publicCloudPca.getAccounts().join().iterator().next()).join(); - - assertEquals(publicCloudPca.getAccounts().join().size(), 0); - } - private IAuthenticationResult acquireTokenInteractive( User user, PublicClientApplication pca, diff --git a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/CachePersistenceIT.java b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/CachePersistenceIT.java index 3b945d18..15324afa 100644 --- a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/CachePersistenceIT.java +++ b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/CachePersistenceIT.java @@ -2,16 +2,10 @@ // Licensed under the MIT License. package com.microsoft.aad.msal4j; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.PlainJWT; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.Collections; - @TestInstance(TestInstance.Lifecycle.PER_CLASS) class CachePersistenceIT { @@ -34,59 +28,50 @@ public void afterCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) } @Test - void cacheDeserializationSerializationTest() throws IOException, URISyntaxException { + void cacheDeserializationSerializationTest() { String dataToInitCache = TestHelper.readResource(this.getClass(), "/cache_data/serialized_cache.json"); - - String ID_TOKEN_PLACEHOLDER = ""; - JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() - .audience(Collections.singletonList("jwtAudience")) - .issuer("issuer") - .subject("subject") - .build(); - PlainJWT jwt = new PlainJWT(claimsSet); - - dataToInitCache = dataToInitCache.replace(ID_TOKEN_PLACEHOLDER, jwt.serialize()); + dataToInitCache = dataToInitCache.replace("", TestHelper.ENCODED_JWT); ITokenCacheAccessAspect persistenceAspect = new TokenPersistence(dataToInitCache); PublicClientApplication app = PublicClientApplication.builder("my_client_id") .setTokenCacheAccessAspect(persistenceAspect).build(); - assertEquals(app.getAccounts().join().size(), 1); - assertEquals(app.tokenCache.accounts.size(), 1); - assertEquals(app.tokenCache.accessTokens.size(), 1); - assertEquals(app.tokenCache.refreshTokens.size(), 1); - assertEquals(app.tokenCache.idTokens.size(), 1); - assertEquals(app.tokenCache.appMetadata.size(), 1); + assertEquals(1, app.getAccounts().join().size()); + assertEquals(1, app.tokenCache.accounts.size()); + assertEquals(1, app.tokenCache.accessTokens.size()); + assertEquals(1, app.tokenCache.refreshTokens.size()); + assertEquals(1, app.tokenCache.idTokens.size()); + assertEquals(1, app.tokenCache.appMetadata.size()); // create new instance of app to make sure in memory cache cleared app = PublicClientApplication.builder("my_client_id") .setTokenCacheAccessAspect(persistenceAspect).build(); - assertEquals(app.getAccounts().join().size(), 1); - assertEquals(app.tokenCache.accounts.size(), 1); - assertEquals(app.tokenCache.accessTokens.size(), 1); - assertEquals(app.tokenCache.refreshTokens.size(), 1); - assertEquals(app.tokenCache.idTokens.size(), 1); - assertEquals(app.tokenCache.appMetadata.size(), 1); + assertEquals(1, app.getAccounts().join().size()); + assertEquals(1, app.tokenCache.accounts.size()); + assertEquals(1, app.tokenCache.accessTokens.size()); + assertEquals(1, app.tokenCache.refreshTokens.size()); + assertEquals(1, app.tokenCache.idTokens.size()); + assertEquals(1, app.tokenCache.appMetadata.size()); app.removeAccount(app.getAccounts().join().iterator().next()).join(); - assertEquals(app.getAccounts().join().size(), 0); - assertEquals(app.tokenCache.accounts.size(), 0); - assertEquals(app.tokenCache.accessTokens.size(), 0); - assertEquals(app.tokenCache.refreshTokens.size(), 0); - assertEquals(app.tokenCache.idTokens.size(), 0); - assertEquals(app.tokenCache.appMetadata.size(), 1); + assertEquals(0, app.getAccounts().join().size()); + assertEquals(0, app.tokenCache.accounts.size()); + assertEquals(0, app.tokenCache.accessTokens.size()); + assertEquals(0, app.tokenCache.refreshTokens.size()); + assertEquals(0, app.tokenCache.idTokens.size()); + assertEquals(1, app.tokenCache.appMetadata.size()); app = PublicClientApplication.builder("my_client_id") .setTokenCacheAccessAspect(persistenceAspect).build(); - assertEquals(app.getAccounts().join().size(), 0); - assertEquals(app.tokenCache.accounts.size(), 0); - assertEquals(app.tokenCache.accessTokens.size(), 0); - assertEquals(app.tokenCache.refreshTokens.size(), 0); - assertEquals(app.tokenCache.idTokens.size(), 0); - assertEquals(app.tokenCache.appMetadata.size(), 1); + assertEquals(0, app.getAccounts().join().size()); + assertEquals(0, app.tokenCache.accounts.size()); + assertEquals(0, app.tokenCache.accessTokens.size()); + assertEquals(0, app.tokenCache.refreshTokens.size()); + assertEquals(0, app.tokenCache.idTokens.size()); + assertEquals(1, app.tokenCache.appMetadata.size()); } } diff --git a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/Config.java b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/Config.java index afd05c21..883855fd 100644 --- a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/Config.java +++ b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/Config.java @@ -5,11 +5,7 @@ import labapi.AppCredentialProvider; import labapi.AzureEnvironment; -import lombok.Getter; -import lombok.experimental.Accessors; -@Accessors(fluent = true) -@Getter() public class Config { private String organizationsAuthority; private String tenantSpecificAuthority; @@ -44,4 +40,24 @@ public class Config { throw new UnsupportedOperationException("Azure Environment - " + azureEnvironment + " unsupported"); } } + + public String organizationsAuthority() { + return this.organizationsAuthority; + } + + public String tenantSpecificAuthority() { + return this.tenantSpecificAuthority; + } + + public String commonAuthority() { + return this.commonAuthority; + } + + public String graphDefaultScope() { + return this.graphDefaultScope; + } + + public String tenant() { + return this.tenant; + } } diff --git a/msal4j-sdk/src/integrationtest/java/infrastructure/SeleniumConstants.java b/msal4j-sdk/src/integrationtest/java/infrastructure/SeleniumConstants.java index b42834d8..51430609 100644 --- a/msal4j-sdk/src/integrationtest/java/infrastructure/SeleniumConstants.java +++ b/msal4j-sdk/src/integrationtest/java/infrastructure/SeleniumConstants.java @@ -21,14 +21,6 @@ public class SeleniumConstants { final static String ADFS2019_PASSWORD_ID = "passwordInput"; final static String ADFS2019_SUBMIT_ID = "submitButton"; - // ADFSv2 fields - final static String ADFSV2_WEB_USERNAME_INPUT_ID = "ContentPlaceHolder1_UsernameTextBox"; - final static String ADFSV2_WEB_PASSWORD_INPUT_ID = "ContentPlaceHolder1_PasswordTextBox"; - final static String ADFSV2_ARLINGTON_WEB_PASSWORD_INPUT_ID = "passwordInput"; - final static String ADFSV2_WEB_SUBMIT_BUTTON_ID = "ContentPlaceHolder1_SubmitButton"; - final static String ADFSV2_ARLINGTON_WEB_SUBMIT_BUTTON_ID = "submitButton"; - - //B2C Facebook final static String FACEBOOK_ACCOUNT_ID = "FacebookExchange"; final static String FACEBOOK_USERNAME_ID = "email"; diff --git a/msal4j-sdk/src/integrationtest/java/labapi/App.java b/msal4j-sdk/src/integrationtest/java/labapi/App.java index 2d43ab1e..eb918303 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/App.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/App.java @@ -3,30 +3,85 @@ package labapi; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; -@Getter -public class App { +import java.io.IOException; - @JsonProperty("appType") - String appType; +public class App implements JsonSerializable { - @JsonProperty("appName") - String appName; + private String appType; + private String appName; + private String appId; + private String redirectUri; + private String authority; + private String labName; + private String clientSecret; - @JsonProperty("appId") - String appId; + static App fromJson(JsonReader jsonReader) throws IOException { + App app = new App(); - @JsonProperty("redirectUri") - String redirectUri; + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); - @JsonProperty("authority") - String authority; + switch (fieldName) { + case "appType": + app.appType = reader.getString(); + break; + case "appName": + app.appName = reader.getString(); + break; + case "appId": + app.appId = reader.getString(); + break; + case "redirectUri": + app.redirectUri = reader.getString(); + break; + case "authority": + app.authority = reader.getString(); + break; + case "labName": + app.labName = reader.getString(); + break; + case "clientSecret": + app.clientSecret = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return app; + }); + } - @JsonProperty("labName") - String labName; + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); - @JsonProperty("clientSecret") - String clientSecret; -} + jsonWriter.writeStringField("appType", appType); + jsonWriter.writeStringField("appName", appName); + jsonWriter.writeStringField("appId", appId); + jsonWriter.writeStringField("redirectUri", redirectUri); + jsonWriter.writeStringField("authority", authority); + jsonWriter.writeStringField("labName", labName); + jsonWriter.writeStringField("clientSecret", clientSecret); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } + + public String getAuthority() { + return authority; + } + + public String getClientSecret() { + return clientSecret; + } + +} \ No newline at end of file diff --git a/msal4j-sdk/src/integrationtest/java/labapi/AppCredentialProvider.java b/msal4j-sdk/src/integrationtest/java/labapi/AppCredentialProvider.java index 9aca04bb..a0bdf593 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/AppCredentialProvider.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/AppCredentialProvider.java @@ -33,7 +33,7 @@ public AppCredentialProvider(String azureEnvironment) { oboClientId = LabConstants.ARLINGTON_OBO_APP_ID; oboAppIdURI = "https://arlmsidlab1.us/IDLABS_APP_Confidential_Client"; - oboPassword = keyVaultSecretsProvider.getSecret(LabService.getApp(oboClientId).clientSecret); + oboPassword = keyVaultSecretsProvider.getSecret(LabService.getApp(oboClientId).getClientSecret()); break; case AzureEnvironment.CIAM: oboPassword = keyVaultSecretsProvider.getSecret(LabConstants.CIAM_KEY_VAULT_SECRET_KEY); diff --git a/msal4j-sdk/src/integrationtest/java/labapi/AzureEnvironment.java b/msal4j-sdk/src/integrationtest/java/labapi/AzureEnvironment.java index 6faa0e54..b42ed52e 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/AzureEnvironment.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/AzureEnvironment.java @@ -5,10 +5,8 @@ public class AzureEnvironment { - public static final String AZURE_B2C = "azureb2ccloud"; public static final String AZURE_CHINA = "azurechinacloud"; public static final String AZURE = "azurecloud"; - public static final String AZURE_PPE = "azureppe"; public static final String AZURE_US_GOVERNMENT = "azureusgovernment"; public static final String CIAM = "ciam"; } diff --git a/msal4j-sdk/src/integrationtest/java/labapi/B2CProvider.java b/msal4j-sdk/src/integrationtest/java/labapi/B2CProvider.java index 1293734c..03dc1983 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/B2CProvider.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/B2CProvider.java @@ -4,11 +4,7 @@ package labapi; public class B2CProvider { - public static final String NONE = "none"; - public static final String AMAZON = "amazon"; public static final String FACEBOOK = "facebook"; public static final String GOOGLE = "google"; public static final String LOCAL = "local"; - public static final String MICROSOFT = "microsoft"; - public static final String TWITTER = "twitter"; } diff --git a/msal4j-sdk/src/integrationtest/java/labapi/FederationProvider.java b/msal4j-sdk/src/integrationtest/java/labapi/FederationProvider.java index a4eed1bf..f969040e 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/FederationProvider.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/FederationProvider.java @@ -8,7 +8,6 @@ public class FederationProvider { public static final String NONE = "none"; public static final String ADFS_4 = "adfsv4"; public static final String ADFS_2019 = "adfsv2019"; - public static final String CIAM = "ciam"; public static final String CIAMCUD = "ciamcud"; } diff --git a/msal4j-sdk/src/integrationtest/java/labapi/Lab.java b/msal4j-sdk/src/integrationtest/java/labapi/Lab.java index bae558e8..5712d62d 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/Lab.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/Lab.java @@ -3,26 +3,74 @@ package labapi; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; -@Getter -public class Lab { - @JsonProperty("labName") - String labName; +import java.io.IOException; - @JsonProperty("domain") - String domain; +public class Lab implements JsonSerializable { + private String labName; + private String domain; + private String tenantId; + private String federationProvider; + private String azureEnvironment; + private String authority; - @JsonProperty("tenantId") - String tenantId; + static Lab fromJson(JsonReader jsonReader) throws IOException { + Lab lab = new Lab(); - @JsonProperty("federationProvider") - String federationProvider; + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); - @JsonProperty("azureEnvironment") - String azureEnvironment; + switch (fieldName) { + case "labName": + lab.labName = reader.getString(); + break; + case "domain": + lab.domain = reader.getString(); + break; + case "tenantId": + lab.tenantId = reader.getString(); + break; + case "federationProvider": + lab.federationProvider = reader.getString(); + break; + case "azureEnvironment": + lab.azureEnvironment = reader.getString(); + break; + case "authority": + lab.authority = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return lab; + }); + } - @JsonProperty("authority") - String authority; -} + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + jsonWriter.writeStringField("labName", labName); + jsonWriter.writeStringField("domain", domain); + jsonWriter.writeStringField("tenantId", tenantId); + jsonWriter.writeStringField("federationProvider", federationProvider); + jsonWriter.writeStringField("azureEnvironment", azureEnvironment); + jsonWriter.writeStringField("authority", authority); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } + + public String getTenantId() { + return this.tenantId; + } +} \ No newline at end of file diff --git a/msal4j-sdk/src/integrationtest/java/labapi/LabService.java b/msal4j-sdk/src/integrationtest/java/labapi/LabService.java index f95b7338..70f6c9d8 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/LabService.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/LabService.java @@ -3,8 +3,7 @@ package labapi; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.azure.json.*; import com.microsoft.aad.msal4j.*; import java.io.IOException; @@ -18,17 +17,6 @@ public class LabService { static ConfidentialClientApplication labApp; - static ObjectMapper mapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - static T convertJsonToObject(final String json, final Class clazz) { - try { - return mapper.readValue(json, clazz); - } catch (IOException e) { - throw new RuntimeException("JSON processing error: " + e.getMessage(), e); - } - } - static void initLabApp() throws MalformedURLException { KeyVaultSecretsProvider keyVaultSecretsProvider = new KeyVaultSecretsProvider(); @@ -56,7 +44,7 @@ User getUser(UserQueryParameters query) { String result = HttpClientHelper.sendRequestToLab( LabConstants.LAB_USER_ENDPOINT, queryMap, getLabAccessToken()); - User[] users = convertJsonToObject(result, User[].class); + User[] users = parseUserArray(result); User user = users[0]; if (user.getUserType().equals("Guest")) { String secretId = user.getHomeDomain().split("\\.")[0]; @@ -64,11 +52,7 @@ User getUser(UserQueryParameters query) { } else { user.setPassword(getSecret(user.getLabName())); } - if (query.parameters.containsKey(UserQueryParameters.FEDERATION_PROVIDER)) { - user.setFederationProvider(query.parameters.get(UserQueryParameters.FEDERATION_PROVIDER)); - } else { - user.setFederationProvider(FederationProvider.NONE); - } + user.setFederationProvider(query.parameters.getOrDefault(UserQueryParameters.FEDERATION_PROVIDER, FederationProvider.NONE)); return user; } catch (Exception ex) { throw new RuntimeException("Error getting user from lab: " + ex.getMessage()); @@ -79,7 +63,7 @@ public static App getApp(String appId) { try { String result = HttpClientHelper.sendRequestToLab( LabConstants.LAB_APP_ENDPOINT, appId, getLabAccessToken()); - App[] apps = convertJsonToObject(result, App[].class); + App[] apps = parseAppArray(result); return apps[0]; } catch (Exception ex) { throw new RuntimeException("Error getting app from lab: " + ex.getMessage()); @@ -91,7 +75,7 @@ public static Lab getLab(String labId) { try { result = HttpClientHelper.sendRequestToLab( LabConstants.LAB_LAB_ENDPOINT, labId, getLabAccessToken()); - Lab[] labs = convertJsonToObject(result, Lab[].class); + Lab[] labs = parseLabArray(result); return labs[0]; } catch (Exception ex) { throw new RuntimeException("Error getting lab from lab: " + ex.getMessage()); @@ -106,9 +90,46 @@ public static String getSecret(String labName) { result = HttpClientHelper.sendRequestToLab( LabConstants.LAB_USER_SECRET_ENDPOINT, queryMap, getLabAccessToken()); - return convertJsonToObject(result, UserSecret.class).value; + UserSecret userSecret = parseUserSecret(result); + return userSecret.value; } catch (Exception ex) { throw new RuntimeException("Error getting user secret from lab: " + ex.getMessage()); } } -} + + // Helper methods for parsing JSON responses into specific objects + private static User[] parseUserArray(String json) { + try (JsonReader reader = JsonProviders.createReader(json)) { + reader.nextToken(); + return reader.readArray(User::fromJson).toArray(new User[0]); + } catch (IOException e) { + throw new RuntimeException("Error parsing User array: " + e.getMessage(), e); + } + } + + private static App[] parseAppArray(String json) { + try (JsonReader reader = JsonProviders.createReader(json)) { + reader.nextToken(); + return reader.readArray(App::fromJson).toArray(new App[0]); + } catch (IOException e) { + throw new RuntimeException("Error parsing App array: " + e.getMessage(), e); + } + } + + private static Lab[] parseLabArray(String json) { + try (JsonReader reader = JsonProviders.createReader(json)) { + reader.nextToken(); + return reader.readArray(Lab::fromJson).toArray(new Lab[0]); + } catch (IOException e) { + throw new RuntimeException("Error parsing Lab array: " + e.getMessage(), e); + } + } + + private static UserSecret parseUserSecret(String json) { + try (JsonReader reader = JsonProviders.createReader(json)) { + return UserSecret.fromJson(reader); + } catch (IOException e) { + throw new RuntimeException("Error parsing UserSecret: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/msal4j-sdk/src/integrationtest/java/labapi/LabUserProvider.java b/msal4j-sdk/src/integrationtest/java/labapi/LabUserProvider.java index 0fae5d42..d7b13f03 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/LabUserProvider.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/LabUserProvider.java @@ -14,12 +14,10 @@ public class LabUserProvider { private static LabUserProvider instance; - private final KeyVaultSecretsProvider keyVaultSecretsProvider; private final LabService labService; private Map userCache; private LabUserProvider() { - keyVaultSecretsProvider = new KeyVaultSecretsProvider(); labService = new LabService(); userCache = new HashMap<>(); } diff --git a/msal4j-sdk/src/integrationtest/java/labapi/User.java b/msal4j-sdk/src/integrationtest/java/labapi/User.java index 5582c915..9c189e0f 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/User.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/User.java @@ -3,61 +3,169 @@ package labapi; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import lombok.Setter; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; -@Getter -public class User { - @JsonProperty("appId") - private String appId; +import java.io.IOException; - @JsonProperty("objectId") +public class User implements JsonSerializable { + private String appId; private String objectId; - - @JsonProperty("userType") private String userType; - - @JsonProperty("displayName") private String displayName; - - @JsonProperty("licenses") private String licenses; - - @JsonProperty("upn") - @Setter private String upn; - - @JsonProperty("mfa") private String mfa; - - @JsonProperty("protectionPolicy") private String protectionPolicy; - - @JsonProperty("homeDomain") private String homeDomain; - - @JsonProperty("homeUPN") private String homeUPN; - - @JsonProperty("b2cProvider") private String b2cProvider; - - @JsonProperty("labName") private String labName; - - @JsonProperty("lastUpdatedBy") private String lastUpdatedBy; - - @JsonProperty("lastUpdatedDate") private String lastUpdatedDate; - - @JsonProperty("tenantID") private String tenantID; - - @Setter private String password; - - @Setter private String federationProvider; -} + + static User fromJson(JsonReader jsonReader) throws IOException { + User user = new User(); + + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + switch (fieldName) { + case "appId": + user.appId = reader.getString(); + break; + case "objectId": + user.objectId = reader.getString(); + break; + case "userType": + user.userType = reader.getString(); + break; + case "displayName": + user.displayName = reader.getString(); + break; + case "licenses": + user.licenses = reader.getString(); + break; + case "upn": + user.upn = reader.getString(); + break; + case "mfa": + user.mfa = reader.getString(); + break; + case "protectionPolicy": + user.protectionPolicy = reader.getString(); + break; + case "homeDomain": + user.homeDomain = reader.getString(); + break; + case "homeUPN": + user.homeUPN = reader.getString(); + break; + case "b2cProvider": + user.b2cProvider = reader.getString(); + break; + case "labName": + user.labName = reader.getString(); + break; + case "lastUpdatedBy": + user.lastUpdatedBy = reader.getString(); + break; + case "lastUpdatedDate": + user.lastUpdatedDate = reader.getString(); + break; + case "tenantID": + user.tenantID = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return user; + }); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + jsonWriter.writeStringField("appId", appId); + jsonWriter.writeStringField("objectId", objectId); + jsonWriter.writeStringField("userType", userType); + jsonWriter.writeStringField("displayName", displayName); + jsonWriter.writeStringField("licenses", licenses); + jsonWriter.writeStringField("upn", upn); + jsonWriter.writeStringField("mfa", mfa); + jsonWriter.writeStringField("protectionPolicy", protectionPolicy); + jsonWriter.writeStringField("homeDomain", homeDomain); + jsonWriter.writeStringField("homeUPN", homeUPN); + jsonWriter.writeStringField("b2cProvider", b2cProvider); + jsonWriter.writeStringField("labName", labName); + jsonWriter.writeStringField("lastUpdatedBy", lastUpdatedBy); + jsonWriter.writeStringField("lastUpdatedDate", lastUpdatedDate); + jsonWriter.writeStringField("tenantID", tenantID); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } + + public String getAppId() { + return this.appId; + } + + public String getUserType() { + return this.userType; + } + + public String getUpn() { + return this.upn; + } + + public String getHomeDomain() { + return this.homeDomain; + } + + public String getHomeUPN() { + return this.homeUPN; + } + + public String getB2cProvider() { + return this.b2cProvider; + } + + public String getLabName() { + return this.labName; + } + + public String getTenantID() { + return this.tenantID; + } + + public String getPassword() { + return this.password; + } + + public String getFederationProvider() { + return this.federationProvider; + } + + public void setUpn(String upn) { + this.upn = upn; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setFederationProvider(String federationProvider) { + this.federationProvider = federationProvider; + } +} \ No newline at end of file diff --git a/msal4j-sdk/src/integrationtest/java/labapi/UserQueryParameters.java b/msal4j-sdk/src/integrationtest/java/labapi/UserQueryParameters.java index b25c46cb..fb638c78 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/UserQueryParameters.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/UserQueryParameters.java @@ -3,26 +3,18 @@ package labapi; -import lombok.EqualsAndHashCode; - import java.util.HashMap; import java.util.Map; -@EqualsAndHashCode public class UserQueryParameters { public static final String USER_TYPE = "usertype"; - public static final String MFA = "mfa"; - public static final String PROTECTION_POLICEY = "protectionpolicy"; - public static final String HOME_DOMAIN = "homedomain"; - public static final String HOME_UPN = "homeupn"; public static final String B2C_PROVIDER = "b2cprovider"; public static final String FEDERATION_PROVIDER = "federationprovider"; public static final String AZURE_ENVIRONMENT = "azureenvironment"; public static final String HOME_AZURE_ENVIRONMENT = "guesthomeazureenvironment"; public static final String GUEST_HOME_DIN = "guesthomedin"; public static final String SIGN_IN_AUDIENCE = "signInAudience"; - public static final String PUBLIC_CLIENT = "publicClient"; public Map parameters = new HashMap<>(); } diff --git a/msal4j-sdk/src/integrationtest/java/labapi/UserSecret.java b/msal4j-sdk/src/integrationtest/java/labapi/UserSecret.java index e33f47e2..9475b65f 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/UserSecret.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/UserSecret.java @@ -3,13 +3,51 @@ package labapi; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; -public class UserSecret { +import java.io.IOException; - @JsonProperty("secret") - String secret; +public class UserSecret implements JsonSerializable { - @JsonProperty("value") + String secret; String value; -} + + static UserSecret fromJson(JsonReader jsonReader) throws IOException { + UserSecret userSecret = new UserSecret(); + + return jsonReader.readObject(reader -> { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + switch (fieldName) { + case "secret": + userSecret.secret = reader.getString(); + break; + case "value": + userSecret.value = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + } + return userSecret; + }); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + + jsonWriter.writeStringField("secret", secret); + jsonWriter.writeStringField("value", value); + + jsonWriter.writeEndObject(); + + return jsonWriter; + } +} \ No newline at end of file diff --git a/msal4j-sdk/src/integrationtest/java/labapi/UserType.java b/msal4j-sdk/src/integrationtest/java/labapi/UserType.java index 45000479..31a3cb58 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/UserType.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/UserType.java @@ -4,7 +4,6 @@ package labapi; public class UserType { - public static final String CLOUD = "cloud"; public static final String FEDERATED = "federated"; public static final String ON_PREM = "onprem"; public static final String GUEST = "guest"; diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java index fe6d874e..e7976478 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java @@ -85,8 +85,6 @@ void testBuilder_invalidRequiredParameters() { @Test void testBuilder_conflictingParameters() { - PublicClientApplication app = PublicClientApplication.builder("client_id").build(); - String redirectUri = "http://localhost:8080"; Set scope = Collections.singleton("scope"); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java index 3c3d8cc6..d624a36b 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CacheFormatTests.java @@ -3,9 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; -import net.minidev.json.JSONObject; import org.json.JSONException; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; @@ -76,7 +73,7 @@ String readResource(String resource) throws IOException, URISyntaxException { Paths.get(getClass().getResource(resource).toURI()))); } - boolean doesResourceExist(String resource) throws IOException, URISyntaxException { + boolean doesResourceExist(String resource) { return getClass().getResource(resource) != null; } @@ -113,21 +110,21 @@ public void compareValues(String s, Object o, Object o1, JSONCompareResult jsonC } @Test - void AADTokenCacheEntitiesFormatTest() throws JSONException, IOException, ParseException, URISyntaxException { + void AADTokenCacheEntitiesFormatTest() throws JSONException, IOException, URISyntaxException { tokenCacheEntitiesFormatTest("/AAD_cache_data"); } @Test - void MSATokenCacheEntitiesFormatTest() throws JSONException, IOException, ParseException, URISyntaxException { + void MSATokenCacheEntitiesFormatTest() throws JSONException, IOException, URISyntaxException { tokenCacheEntitiesFormatTest("/MSA_cache_data"); } @Test - void FociTokenCacheEntitiesFormatTest() throws JSONException, IOException, ParseException, URISyntaxException { + void FociTokenCacheEntitiesFormatTest() throws JSONException, IOException, URISyntaxException { tokenCacheEntitiesFormatTest("/Foci_cache_data"); } - public void tokenCacheEntitiesFormatTest(String folder) throws URISyntaxException, IOException, ParseException, JSONException { + public void tokenCacheEntitiesFormatTest(String folder) throws URISyntaxException, IOException, JSONException { String CLIENT_ID = "b6c69a37-df96-4db0-9088-2ab96e1d8215"; String AUTHORIZE_REQUEST_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; @@ -158,7 +155,7 @@ public void tokenCacheEntitiesFormatTest(String folder) throws URISyntaxExceptio doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest(); doReturn(httpResponse).when(msalOAuthHttpRequest).send(); doReturn(200).when(httpResponse).statusCode(); - doReturn(JsonHelper.convertJsonToMap((tokenResponse))).when(httpResponse).getBodyAsMap(); + doReturn(JsonHelper.convertJsonToMap(tokenResponse)).when(httpResponse).getBodyAsMap(); final AuthenticationResult result = request.executeTokenRequest(); @@ -174,9 +171,9 @@ public void tokenCacheEntitiesFormatTest(String folder) throws URISyntaxExceptio } private void validateAccessTokenCacheEntity(String folder, String tokenResponse, TokenCache tokenCache) - throws IOException, URISyntaxException, ParseException, JSONException { + throws IOException, URISyntaxException, JSONException { - assertEquals(tokenCache.accessTokens.size(), 1); + assertEquals(1, tokenCache.accessTokens.size()); String keyActual = tokenCache.accessTokens.keySet().stream().findFirst().get(); String keyExpected = readResource(folder + AT_CACHE_ENTITY_KEY); @@ -185,29 +182,16 @@ private void validateAccessTokenCacheEntity(String folder, String tokenResponse, String valueActual = JsonHelper.convertJsonSerializableObjectToString(tokenCache.accessTokens.get(keyActual)); String valueExpected = readResource(folder + AT_CACHE_ENTITY); - JSONObject tokenResponseJsonObj = JSONObjectUtils.parse(tokenResponse); - long expireIn = getLongValue(tokenResponseJsonObj, "expires_in"); - - long extExpireIn = getLongValue(tokenResponseJsonObj, "ext_expires_in"); + Map tokenResponseMap = JsonHelper.convertJsonToMap(tokenResponse); JSONAssert.assertEquals(valueExpected, valueActual, - new DynamicTimestampsComparator(JSONCompareMode.STRICT, expireIn, extExpireIn)); - } - - static Long getLongValue(JSONObject jsonObject, String key) throws ParseException { - Object value = jsonObject.get(key); - - if (value instanceof Long) { - return JSONObjectUtils.getLong(jsonObject, key); - } else { - return Long.parseLong(JSONObjectUtils.getString(jsonObject, key)); - } + new DynamicTimestampsComparator(JSONCompareMode.STRICT, Long.parseLong(tokenResponseMap.get("expires_in")), Long.parseLong(tokenResponseMap.get("ext_expires_in")))); } private void validateRefreshTokenCacheEntity(String folder, TokenCache tokenCache) throws IOException, URISyntaxException, JSONException { - assertEquals(tokenCache.refreshTokens.size(), 1); + assertEquals(1, tokenCache.refreshTokens.size()); String actualKey = tokenCache.refreshTokens.keySet().stream().findFirst().get(); String keyExpected = readResource(folder + RT_CACHE_ENTITY_KEY); @@ -243,7 +227,7 @@ public void compareValues(String s, Object o, Object o1, JSONCompareResult jsonC private void validateIdTokenCacheEntity(String folder, TokenCache tokenCache) throws IOException, URISyntaxException, JSONException { - assertEquals(tokenCache.idTokens.size(), 1); + assertEquals(1, tokenCache.idTokens.size()); String actualKey = tokenCache.idTokens.keySet().stream().findFirst().get(); String keyExpected = readResource(folder + ID_TOKEN_CACHE_ENTITY_KEY); @@ -258,7 +242,7 @@ private void validateIdTokenCacheEntity(String folder, TokenCache tokenCache) private void validateAccountCacheEntity(String folder, TokenCache tokenCache) throws IOException, URISyntaxException, JSONException { - assertEquals(tokenCache.accounts.size(), 1); + assertEquals(1, tokenCache.accounts.size()); String actualKey = tokenCache.accounts.keySet().stream().findFirst().get(); String keyExpected = readResource(folder + ACCOUNT_CACHE_ENTITY_KEY); @@ -277,7 +261,7 @@ private void validateAppMetadataCacheEntity(String folder, TokenCache tokenCache return; } - assertEquals(tokenCache.appMetadata.size(), 1); + assertEquals(1, tokenCache.appMetadata.size()); String actualKey = tokenCache.appMetadata.keySet().stream().findFirst().get(); String keyExpected = readResource(folder + APP_METADATA_ENTITY_KEY); @@ -314,4 +298,4 @@ private String getIdToken(String folder) throws IOException, URISyntaxException return encodedIdToken; } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java index 81cd9812..83c67c22 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java @@ -54,10 +54,6 @@ private AuthorizationCodeParameters getAcquireTokenApiParameters(String scope) t .build(); } - private AuthorizationCodeParameters getAcquireTokenApiParameters() throws URISyntaxException { - return getAcquireTokenApiParameters("default-scope"); - } - private PublicClientApplication getPublicClientApp() throws Exception { return getPublicClientApp(null); } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestConfiguration.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestConfiguration.java index 5878afac..859c471d 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestConfiguration.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestConfiguration.java @@ -11,11 +11,9 @@ public final class TestConfiguration { + "/" + AAD_TENANT_NAME + "/"; public final static String AAD_CLIENT_ID = "9083ccb8-8a46-43e7-8439-1d696df984ae"; public final static String AAD_CLIENT_DUMMYSECRET = "client_dummysecret"; - public final static String AAD_RESOURCE_ID = "b7a671d8-a408-42ff-86e0-aaf447fd17c4"; public final static String AAD_MEX_RESPONSE_FILE = "/mex-response.xml"; public final static String AAD_MEX_RESPONSE_FILE_INTEGRATED = "/mex-response-integrated.xml"; public final static String AAD_MEX_2005_RESPONSE_FILE = "/mex-2005-response.xml"; - public final static String AAD_TOKEN_ERROR_FILE = "/token-error.xml"; public final static String AAD_TOKEN_SUCCESS_FILE = "/token.xml"; public final static String AAD_DEFAULT_REDIRECT_URI = "https://non_existing_uri.windows.com/"; public final static String AAD_COMMON_AUTHORITY = "https://login.microsoftonline.com/common/"; @@ -23,8 +21,6 @@ public final class TestConfiguration { public final static String ADFS_HOST_NAME = "fs.ade2eadfs30.com"; public final static String ADFS_TENANT_ENDPOINT = "https://" + ADFS_HOST_NAME + "/adfs/"; - public final static String AAD_UNKNOWN_TENANT_ENDPOINT = "https://lgn.windows.net/" - + AAD_TENANT_NAME + "/"; public final static String B2C_HOST_NAME = "msidlabb2c.b2clogin.com"; public final static String B2C_SIGN_IN_POLICY = "B2C_1_SignInPolicy"; @@ -36,13 +32,6 @@ public final class TestConfiguration { public final static String B2C_AUTHORITY_CUSTOM_PORT = "https://login.microsoftonline.in:444/tfp/tenant/policy"; public final static String B2C_AUTHORITY_CUSTOM_PORT_TAIL_SLASH = "https://login.microsoftonline.in:444/tfp/tenant/policy/"; - public static String INSTANCE_DISCOVERY_RESPONSE = "{" + - "\"tenant_discovery_endpoint\":\"https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-appConfiguration\"," + - "\"api-version\":\"1.1\"," + - "\"metadata\":[{\"preferred_network\":\"login.microsoftonline.com\",\"preferred_cache\":\"login.windows.net\",\"aliases\":[\"login.microsoftonline.com\",\"login.windows.net\",\"login.microsoft.com\",\"sts.windows.net\"]},{\"preferred_network\":\"login.partner.microsoftonline.cn\",\"preferred_cache\":\"login.partner.microsoftonline.cn\",\"aliases\":[\"login.partner.microsoftonline.cn\",\"login.chinacloudapi.cn\"]},{\"preferred_network\":\"login.microsoftonline.de\",\"preferred_cache\":\"login.microsoftonline.de\",\"aliases\":[\"login.microsoftonline.de\"]},{\"preferred_network\":\"login.microsoftonline.us\",\"preferred_cache\":\"login.microsoftonline.us\",\"aliases\":[\"login.microsoftonline.us\",\"login.usgovcloudapi.net\"]},{\"preferred_network\":\"login-us.microsoftonline.com\",\"preferred_cache\":\"login-us.microsoftonline.com\",\"aliases\":[\"login-us.microsoftonline.com\"]}]}"; - - public final static String AAD_PREFERRED_NETWORK_ENV_ALIAS = "login.microsoftonline.com"; - public final static String TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS = "{\"access_token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6I" + "k5HVEZ2ZEstZnl0aEV1THdqcHdBSk9NOW4tQSJ9.eyJhdWQiOiJiN2E2NzFkOC1hNDA4LTQyZmYtODZlMC1hYWY0NDdmZDE3YzQiLCJpc3MiOiJod" + "HRwczovL3N0cy53aW5kb3dzLm5ldC8zMGJhYTY2Ni04ZGY4LTQ4ZTctOTdlNi03N2NmZDA5OTU5NjMvIiwiaWF0IjoxMzkzODQ0NTA0LCJuYmYiOj" diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java index efd56cfa..04ef696d 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java @@ -34,10 +34,6 @@ private RefreshTokenParameters getAcquireTokenApiParameters(String scope) { .build(); } - private RefreshTokenParameters getAcquireTokenApiParameters() { - return getAcquireTokenApiParameters("default-scope"); - } - private PublicClientApplication getPublicClientApp() throws Exception { return getPublicClientApp(null); } From 4f5a10c4a415ee0511b9274a073491c91c2801be Mon Sep 17 00:00:00 2001 From: avdunn Date: Mon, 12 May 2025 11:13:37 -0700 Subject: [PATCH 28/31] Version updates for 1.30.0-beta --- README.md | 6 +++--- changelog.txt | 8 +++++++- msal4j-sdk/README.md | 6 +++--- msal4j-sdk/bnd.bnd | 2 +- msal4j-sdk/pom.xml | 2 +- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9457930e..c5683911 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Quick links: The library supports the following Java environments: - Java 8 (or higher) -Current version - 1.20.1 +Current version - 1.30.0-beta You can find the changes for each version in the [change log](https://github.com/AzureAD/microsoft-authentication-library-for-java/blob/main/msal4j-sdk/changelog.txt). @@ -28,13 +28,13 @@ Find [the latest package in the Maven repository](https://mvnrepository.com/arti com.microsoft.azure msal4j - 1.20.1 + 1.30.0-beta ``` ### Gradle ```gradle -implementation group: 'com.microsoft.azure', name: 'com.microsoft.aad.msal4j', version: '1.20.1' +implementation group: 'com.microsoft.azure', name: 'com.microsoft.aad.msal4j', version: '1.30.0-beta' ``` ## Usage diff --git a/changelog.txt b/changelog.txt index 5a31166d..282107b4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,10 @@ -Version 1.20.1 +Version 1.30.0-beta +============= +- Replace org.projectlombok dependencies with implementations of generated code (#946) +- Replace com.nimbusds dependencies with implementations of OAuth behavior (#926, #927, #928, #941, #945) +- Replace com.fasterxml.jackson with com.azure.json for JSON behavior (#947, #948) + + 1.20.1 ============= - Fix Base64URL decoding bug (#938) diff --git a/msal4j-sdk/README.md b/msal4j-sdk/README.md index e6d4f8da..3a9e20ee 100644 --- a/msal4j-sdk/README.md +++ b/msal4j-sdk/README.md @@ -16,7 +16,7 @@ Quick links: The library supports the following Java environments: - Java 8 (or higher) -Current version - 1.20.1 +Current version - 1.30.0-beta You can find the changes for each version in the [change log](https://github.com/AzureAD/microsoft-authentication-library-for-java/blob/master/changelog.txt). @@ -28,13 +28,13 @@ Find [the latest package in the Maven repository](https://mvnrepository.com/arti com.microsoft.azure msal4j - 1.20.1 + 1.30.0-beta ``` ### Gradle ```gradle -compile group: 'com.microsoft.azure', name: 'msal4j', version: '1.20.1' +compile group: 'com.microsoft.azure', name: 'msal4j', version: '1.30.0-beta' ``` ## Usage diff --git a/msal4j-sdk/bnd.bnd b/msal4j-sdk/bnd.bnd index 11dae51e..2e637dcb 100644 --- a/msal4j-sdk/bnd.bnd +++ b/msal4j-sdk/bnd.bnd @@ -1,2 +1,2 @@ -Export-Package: com.microsoft.aad.msal4j;version="1.20.1" +Export-Package: com.microsoft.aad.msal4j;version="1.30.0-beta" Automatic-Module-Name: com.microsoft.aad.msal4j diff --git a/msal4j-sdk/pom.xml b/msal4j-sdk/pom.xml index c38f2023..21af8602 100644 --- a/msal4j-sdk/pom.xml +++ b/msal4j-sdk/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.microsoft.azure msal4j - 1.20.1 + 1.30.0-beta jar msal4j From dfae289f8187f6901592a7d6b448ff78c482de65 Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 29 Jul 2025 14:33:05 -0700 Subject: [PATCH 29/31] Resolve merge conflicts --- .../aad/msal4j/ManagedIdentityRequest.java | 4 +-- .../microsoft/aad/msal4j/StringHelper.java | 1 + .../aad/msal4j/TokenRequestExecutor.java | 4 +-- .../microsoft/aad/msal4j/TokenResponse.java | 2 +- .../aad/msal4j/ClientCertificateTest.java | 15 +++++--- .../aad/msal4j/ManagedIdentityTests.java | 36 +++++++++---------- .../aad/msal4j/RequestThrottlingTest.java | 2 +- .../aad/msal4j/TokenRequestExecutorTest.java | 8 ++--- .../aad/msal4j/UIRequiredCacheTest.java | 4 +-- 9 files changed, 38 insertions(+), 38 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityRequest.java index 9f1dbe6c..42a2fbe1 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityRequest.java @@ -98,7 +98,7 @@ void addTokenRevocationParametersToQuery(ManagedIdentityParameters parameters) { // Add client capabilities as a comma separated string for all the values in client capabilities String clientCapabilities = String.join(",", managedIdentityApplication.getClientCapabilities()); - queryParameters.put(Constants.CLIENT_CAPABILITY_REQUEST_PARAM, Collections.singletonList(clientCapabilities.toString())); + queryParameters.put(Constants.CLIENT_CAPABILITY_REQUEST_PARAM, clientCapabilities.toString()); } // Pass the token revocation parameter if the claims are present and there is a token to revoke @@ -107,7 +107,7 @@ void addTokenRevocationParametersToQuery(ManagedIdentityParameters parameters) { if (queryParameters == null) { queryParameters = new HashMap<>(); } - queryParameters.put(Constants.TOKEN_HASH_CLAIM, Collections.singletonList(parameters.revokedTokenHash())); + queryParameters.put(Constants.TOKEN_HASH_CLAIM, parameters.revokedTokenHash()); } } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java index 3e807fbf..d8b2d9e7 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java @@ -79,6 +79,7 @@ static String createSha256HashHexString(String stringToHash) { static boolean isNullOrBlank(final String str) { return str == null || str.trim().isEmpty(); + } //Converts a map of parameters into a URL query string static String serializeQueryParameters(Map params) { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java index d9fb8a26..dfb6f6e5 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java @@ -113,7 +113,7 @@ private void addJWTBearerAssertionParams(Map queryParameters, St private AuthenticationResult createAuthenticationResultFromOauthHttpResponse(HttpResponse oauthHttpResponse) { AuthenticationResult result; - if (oauthHttpResponse.statusCode() == HttpHelper.HTTP_STATUS_200) { + if (oauthHttpResponse.statusCode() == HttpStatus.HTTP_OK) { final TokenResponse response = TokenResponse.parseHttpResponse(oauthHttpResponse); AccountCacheEntity accountCacheEntity = null; @@ -160,7 +160,7 @@ private AuthenticationResult createAuthenticationResultFromOauthHttpResponse(Htt } else { // http codes indicating that STS did not log request - if (oauthHttpResponse.getStatusCode() == HttpStatus.HTTP_TOO_MANY_REQUESTS || oauthHttpResponse.getStatusCode() >= HttpStatus.HTTP_INTERNAL_ERROR) { + if (oauthHttpResponse.statusCode() == HttpStatus.HTTP_TOO_MANY_REQUESTS || oauthHttpResponse.statusCode() >= HttpStatus.HTTP_INTERNAL_ERROR) { serviceBundle.getServerSideTelemetry().previousRequests.putAll( serviceBundle.getServerSideTelemetry().previousRequestInProgress); } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java index f8243217..b314bb77 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java @@ -31,7 +31,7 @@ class TokenResponse { static TokenResponse parseHttpResponse(final HttpResponse httpResponse) { - if (httpResponse.statusCode() != HttpHelper.HTTP_STATUS_200) { + if (httpResponse.statusCode() != HttpStatus.HTTP_OK) { throw MsalServiceExceptionFactory.fromHttpResponse(httpResponse); } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java index 299231da..99eb8649 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java @@ -4,6 +4,7 @@ package com.microsoft.aad.msal4j; import com.nimbusds.oauth2.sdk.auth.PrivateKeyJWT; +import com.nimbusds.jwt.SignedJWT; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -11,6 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -18,6 +20,7 @@ import java.security.*; import java.security.cert.CertificateException; import java.security.interfaces.RSAPrivateKey; +import java.text.ParseException; import java.util.*; @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -70,12 +73,14 @@ void testIClientCertificateInterface_CredentialFactoryUsesSha256() throws Except HashMap tokenResponseValues = new HashMap<>(); tokenResponseValues.put("access_token", "accessTokenSha256"); - when(httpClientMock.send(any(HttpRequest.class))).thenAnswer( parameters -> { + when(httpClientMock.send(any(HttpRequest.class))).thenAnswer(parameters -> { HttpRequest request = parameters.getArgument(0); - Set headerParams = ((PrivateKeyJWT) cca.clientAuthentication()).getClientAssertion().getHeader().getIncludedParams(); -//TODO - if (request.body().contains(cca.assertion) - && headerParams.contains("x5t#S256")) { + String requestBody = request.body(); + + SignedJWT signedJWT = SignedJWT.parse(cca.assertion); + + if (requestBody.contains(cca.assertion) + && signedJWT.getHeader().toJSONObject().containsKey("x5t#S256")) { return TestHelper.expectedResponse(200, TestHelper.getSuccessfulTokenResponse(tokenResponseValues)); } return null; diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java index cf3ca701..f2f3951e 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java @@ -3,7 +3,6 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.util.URLUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; @@ -89,15 +88,15 @@ private HttpRequest expectedRequest(ManagedIdentitySourceType source, String res Map queryParameters = new HashMap<>(); // Add resource to query parameters (common for all sources) - queryParameters.put("resource", singletonList(resource)); + queryParameters.put("resource", resource); // Handle claims and capabilities if supported if (Constants.TOKEN_REVOCATION_SUPPORTED_ENVIRONMENTS.contains(source)) { if (hasCapabilities) { - queryParameters.put(Constants.CLIENT_CAPABILITY_REQUEST_PARAM, singletonList("cp1")); + queryParameters.put(Constants.CLIENT_CAPABILITY_REQUEST_PARAM, "cp1"); } if (hasClaims) { - queryParameters.put(Constants.TOKEN_HASH_CLAIM, singletonList(expectedTokenHash)); + queryParameters.put(Constants.TOKEN_HASH_CLAIM, expectedTokenHash); } } @@ -110,7 +109,7 @@ private HttpRequest expectedRequest(ManagedIdentitySourceType source, String res } if (!queryParameters.isEmpty()) { - endpoint = endpoint + "?" + URLUtils.serializeParameters(queryParameters); + endpoint = endpoint + "?" + StringHelper.serializeQueryParameters(queryParameters); } return new HttpRequest(HttpMethod.GET, endpoint, headers); @@ -118,10 +117,10 @@ private HttpRequest expectedRequest(ManagedIdentitySourceType source, String res private String configureSourceSpecificParameters(ManagedIdentitySourceType source, Map headers, - Map> queryParameters) { + Map queryParameters) { switch (source) { case APP_SERVICE: - queryParameters.put("api-version", singletonList("2019-08-01")); + queryParameters.put("api-version", "2019-08-01"); headers.put("X-IDENTITY-HEADER", "secret"); return ManagedIdentityTestConstants.APP_SERVICE_ENDPOINT; @@ -131,12 +130,12 @@ private String configureSourceSpecificParameters(ManagedIdentitySourceType sourc return ManagedIdentityTestConstants.CLOUDSHELL_ENDPOINT; case AZURE_ARC: - queryParameters.put("api-version", singletonList("2019-11-01")); + queryParameters.put("api-version", "2019-11-01"); headers.put("Metadata", "true"); return ManagedIdentityTestConstants.AZURE_ARC_ENDPOINT; case SERVICE_FABRIC: - queryParameters.put("api-version", singletonList("2019-07-01-preview")); + queryParameters.put("api-version", "2019-07-01-preview"); headers.put("secret", "secret"); return ManagedIdentityTestConstants.SERVICE_FABRIC_ENDPOINT; @@ -144,24 +143,24 @@ private String configureSourceSpecificParameters(ManagedIdentitySourceType sourc case NONE: case DEFAULT_TO_IMDS: default: - queryParameters.put("api-version", singletonList("2018-02-01")); + queryParameters.put("api-version", "2018-02-01"); headers.put("Metadata", "true"); return ManagedIdentityTestConstants.IMDS_ENDPOINT; } } - private void configureIdentitySpecificParameters(ManagedIdentityId id, Map> queryParameters) { + private void configureIdentitySpecificParameters(ManagedIdentityId id, Map queryParameters) { switch (id.getIdType()) { case SYSTEM_ASSIGNED: break; case CLIENT_ID: - queryParameters.put("client_id", singletonList(id.getUserAssignedId())); + queryParameters.put("client_id", id.getUserAssignedId()); break; case RESOURCE_ID: if (ManagedIdentityClient.getManagedIdentitySource() == ManagedIdentitySourceType.IMDS) { - queryParameters.put(Constants.MANAGED_IDENTITY_RESOURCE_ID_IMDS, Collections.singletonList(id.getUserAssignedId())); + queryParameters.put(Constants.MANAGED_IDENTITY_RESOURCE_ID_IMDS, id.getUserAssignedId()); } else { - queryParameters.put(Constants.MANAGED_IDENTITY_RESOURCE_ID, Collections.singletonList(id.getUserAssignedId())); + queryParameters.put(Constants.MANAGED_IDENTITY_RESOURCE_ID, id.getUserAssignedId()); } break; case OBJECT_ID: @@ -359,8 +358,6 @@ void managedIdentityTest_WithClaims(ManagedIdentitySourceType source, String end when(httpClientMock.send(any())).thenReturn(expectedResponse(HttpStatus.HTTP_OK, getSuccessfulResponse(ManagedIdentityTestConstants.RESOURCE))); - String claimsJson = "{\"default\":\"claim\"}"; - // First call, get the token from the identity provider. IAuthenticationResult result = acquireTokenCommon(ManagedIdentityTestConstants.RESOURCE).get(); @@ -378,7 +375,7 @@ void managedIdentityTest_WithClaims(ManagedIdentitySourceType source, String end // Third call, when claims are passed bypass the cache. result = miApp.acquireTokenForManagedIdentity( ManagedIdentityParameters.builder(ManagedIdentityTestConstants.RESOURCE) - .claims(claimsJson) + .claims(TestConfiguration.CLAIMS_REQUEST) .build()).get(); assertTokenFromIdentityProvider(result); @@ -430,7 +427,6 @@ void managedIdentity_ClaimsAndCapabilities(ManagedIdentitySourceType source, Str .httpClient(httpClientMock) .build(); - String claimsJson = "{\"default\":\"claim\"}"; // First call, get the token from the identity provider. IAuthenticationResult result = acquireTokenCommon(ManagedIdentityTestConstants.RESOURCE).get(); @@ -448,7 +444,7 @@ void managedIdentity_ClaimsAndCapabilities(ManagedIdentitySourceType source, Str // Third call, when claims are passed bypass the cache. result = miApp.acquireTokenForManagedIdentity( ManagedIdentityParameters.builder(ManagedIdentityTestConstants.RESOURCE) - .claims(claimsJson) + .claims(TestConfiguration.CLAIMS_REQUEST) .build()).get(); assertTokenFromIdentityProvider(result); @@ -770,7 +766,7 @@ void managedIdentity_RequestFailed_NoPayload(ManagedIdentitySourceType source, S when(httpClientMock.send(expectedRequest(source, ManagedIdentityTestConstants.RESOURCE))).thenReturn(expectedResponse(500, "")); - assertMsalServiceException(acquireTokenCommon(ManagedIdentityTestConstants.RESOURCE), source, MsalError.MANAGED_IDENTITY_RESPONSE_PARSE_FAILURE); + assertMsalServiceException(acquireTokenCommon(ManagedIdentityTestConstants.RESOURCE), source, MsalError.MANAGED_IDENTITY_REQUEST_FAILED); } @ParameterizedTest diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java index 8fda82f3..7544f6a4 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RequestThrottlingTest.java @@ -92,7 +92,7 @@ private PublicClientApplication getClientApplicationMockedWithOneTokenEndpointRe switch (responseType) { case RETRY_AFTER_HEADER: - httpResponse.statusCode(HttpHelper.HTTP_STATUS_200); + httpResponse.statusCode(HttpStatus.HTTP_OK); httpResponse.body(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS); headers.put("Retry-After", Arrays.asList(THROTTLE_IN_SEC.toString())); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java index d5a1215e..ac743755 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java @@ -42,7 +42,7 @@ void executeOAuthRequest_SCBadRequestErrorInvalidGrant_InteractionRequiredExcept OAuthHttpRequest msalOAuthHttpRequest = mock(OAuthHttpRequest.class); HttpResponse httpResponse = new HttpResponse(); - httpResponse.statusCode(HttpHelper.HTTP_STATUS_400); + httpResponse.statusCode(HttpStatus.HTTP_BAD_REQUEST); String claims = "{\\\"access_token\\\":{\\\"polids\\\":{\\\"essential\\\":true,\\\"values\\\":[\\\"5ce770ea-8690-4747-aa73-c5b3cd509cd4\\\"]}}}"; @@ -79,7 +79,7 @@ void executeOAuthRequest_SCBadRequestErrorInvalidGrant_SubErrorFilteredServiceEx OAuthHttpRequest msalOAuthHttpRequest = mock(OAuthHttpRequest.class); HttpResponse httpResponse = new HttpResponse(); - httpResponse.statusCode(HttpHelper.HTTP_STATUS_400); + httpResponse.statusCode(HttpStatus.HTTP_BAD_REQUEST); String claims = "{\\\"access_token\\\":{\\\"polids\\\":{\\\"essential\\\":true,\\\"values\\\":[\\\"5ce770ea-8690-4747-aa73-c5b3cd509cd4\\\"]}}}"; @@ -233,9 +233,7 @@ void testExecuteOAuth_Success() throws MsalException, IOException, URISyntaxExce doReturn(httpResponse).when(msalOAuthHttpRequest).send(); doReturn(JsonHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS)).when(httpResponse).getBodyAsMap(); - httpResponse.ensureStatusCode(HttpStatus.HTTP_OK); - - doReturn(HttpStatus.HTTP_OK).when(httpResponse).getStatusCode(); + doReturn(HttpStatus.HTTP_OK).when(httpResponse).statusCode(); final AuthenticationResult result = request.executeTokenRequest(); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java index 1ddc8450..55dbede7 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UIRequiredCacheTest.java @@ -83,10 +83,10 @@ private PublicClientApplication getApp_MockedWith_OKTokenEndpointResponse_Invali throws Exception { IHttpClient httpClientMock = mock(IHttpClient.class); - HttpResponse httpResponse = getHttpResponse(HttpHelper.HTTP_STATUS_200, TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS); + HttpResponse httpResponse = getHttpResponse(HttpStatus.HTTP_OK, TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS); lenient().doReturn(httpResponse).when(httpClientMock).send(any()); - httpResponse = getHttpResponse(HttpHelper.HTTP_STATUS_401, TestConfiguration.TOKEN_ENDPOINT_INVALID_GRANT_ERROR_RESPONSE); + httpResponse = getHttpResponse(HttpStatus.HTTP_UNAUTHORIZED, TestConfiguration.TOKEN_ENDPOINT_INVALID_GRANT_ERROR_RESPONSE); lenient().doReturn(httpResponse).when(httpClientMock).send(any()); PublicClientApplication app = getPublicClientApp(httpClientMock); From 62a66225478e0aed423901833ad97e47c9d10e30 Mon Sep 17 00:00:00 2001 From: avdunn Date: Wed, 6 Aug 2025 13:00:19 -0700 Subject: [PATCH 30/31] Resolve merge conflicts --- msal4j-sdk/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/msal4j-sdk/pom.xml b/msal4j-sdk/pom.xml index 7a0942a0..00bbd18c 100644 --- a/msal4j-sdk/pom.xml +++ b/msal4j-sdk/pom.xml @@ -35,6 +35,11 @@ + + org.slf4j + slf4j-api + 1.7.36 + com.azure azure-json From 5fc9dae10b88ab2871780689a89955ca0b6c0dfa Mon Sep 17 00:00:00 2001 From: avdunn Date: Wed, 6 Aug 2025 13:01:12 -0700 Subject: [PATCH 31/31] Remove duplicated dependency --- msal4j-sdk/pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/msal4j-sdk/pom.xml b/msal4j-sdk/pom.xml index 00bbd18c..4278726f 100644 --- a/msal4j-sdk/pom.xml +++ b/msal4j-sdk/pom.xml @@ -59,12 +59,6 @@ 1.6.2 test - - org.apache.commons - commons-text - 1.10.0 - test - org.junit.jupiter junit-jupiter-api