diff --git a/changelog.txt b/changelog.txt index 44f98d7cc6..b16cf62403 100644 --- a/changelog.txt +++ b/changelog.txt @@ -5,6 +5,7 @@ vNext - [MINOR] Remove LruCache from SharedPreferencesFileManager (#2910) - [MINOR] Edge TB: Claims (#2925) - [PATCH] Update Moshi to 1.15.2 to resolve okio CVE-2023-3635 vulnerability (#3005) +- [MINOR] Edge TB: PoP support (#3006) - [MINOR] Handle target="_blank" links in authorization WebView (#3010) - [MINOR] Handle openid-vc urls in webview (#3013) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapter.java b/common/src/main/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapter.java index 5756631455..52ea9ab768 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapter.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapter.java @@ -26,6 +26,7 @@ import static com.microsoft.identity.common.java.authscheme.BearerAuthenticationSchemeInternal.SCHEME_BEARER; import static com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal.SCHEME_POP; import static com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal.SCHEME_POP_WITH_CLIENT_KEY; +import static com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED; import androidx.annotation.NonNull; @@ -42,6 +43,7 @@ import com.microsoft.identity.common.java.authscheme.BearerAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal; +import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal; import com.microsoft.identity.common.logging.Logger; import java.lang.reflect.Type; @@ -85,6 +87,9 @@ public AbstractAuthenticationScheme deserialize(@NonNull final JsonElement json, case SCHEME_POP_WITH_CLIENT_KEY: return context.deserialize(json, PopAuthenticationSchemeWithClientKeyInternal.class); + case SCHEME_POP_PREGENERATED: + return context.deserialize(json, WebAppsPopAuthenticationSchemeInternal.class); + default: Logger.warn( methodTag, @@ -111,6 +116,9 @@ public JsonElement serialize(@NonNull final AbstractAuthenticationScheme src, case SCHEME_POP_WITH_CLIENT_KEY: return context.serialize(src, PopAuthenticationSchemeWithClientKeyInternal.class); + case SCHEME_POP_PREGENERATED: + return context.serialize(src, WebAppsPopAuthenticationSchemeInternal.class); + default: Logger.warn( methodTag, diff --git a/common/src/test/java/com/microsoft/identity/common/internal/controllers/BrokerMsalControllerTest.java b/common/src/test/java/com/microsoft/identity/common/internal/controllers/BrokerMsalControllerTest.java index 28cdc8955b..bd8664ab3b 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/controllers/BrokerMsalControllerTest.java +++ b/common/src/test/java/com/microsoft/identity/common/internal/controllers/BrokerMsalControllerTest.java @@ -335,7 +335,9 @@ private static String buildInteractiveGetTokenRequestJson(final boolean isSts) { null, false, null, - null + null, + null, // tokenType + null // reqCnf ); WebAppsGetTokenSubOperationEnvelope envelope = new WebAppsGetTokenSubOperationEnvelope( @@ -361,7 +363,9 @@ private String buildStrictlySilentGetTokenRequestJson(final boolean isSts) throw null, // loginHint false, // instanceAware null, // extraParameters - null // claims + null, // claims + null, // tokenType + null // reqCnf ); WebAppsGetTokenSubOperationEnvelope envelope = diff --git a/common/src/test/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapterTests.java b/common/src/test/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapterTests.java index d4e63d6aee..5538604250 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapterTests.java +++ b/common/src/test/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapterTests.java @@ -26,6 +26,7 @@ import com.microsoft.identity.common.java.authscheme.BearerAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal; +import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.crypto.IDevicePopManager; import org.junit.Assert; @@ -76,6 +77,17 @@ public void testSerialize_PopAuthenticationSchemeWithClientKeyInternal() throws Assert.assertEquals(expectedJson, json); } + @Test + public void testSerialize_WebAppsPopAuthenticationSchemeInternal() throws MalformedURLException, IllegalArgumentException { + final String expectedJson = + "{\"req_cnf\":\"eyJraWQiOiJteS1rZXktaWQifQ\",\"kid\":\"my-key-id\",\"name\":\"PoP_Pregenerated\"}"; + final WebAppsPopAuthenticationSchemeInternal scheme = + new WebAppsPopAuthenticationSchemeInternal("eyJraWQiOiJteS1rZXktaWQifQ"); + + final String json = AuthenticationSchemeTypeAdapter.getGsonInstance().toJson(scheme); + Assert.assertEquals(expectedJson, json); + } + @Test public void testDeserialize_BearerAuthenticationSchemeInternal() { final String json = "{\"name\":\"Bearer\"}"; @@ -103,4 +115,14 @@ public void testDeserialize_PopAuthenticationSchemeWithClientKeyInternal() { Assert.assertTrue(authenticationScheme instanceof PopAuthenticationSchemeWithClientKeyInternal); } + + @Test + public void testDeserialize_WebAppsPopAuthenticationSchemeInternal() { + final String json = + "{\"http_method\":\"GET\",\"url\":\"https://xyz.com\",\"nonce\":\"nonce_test\",\"client_claims\":\"clientClaims_test\",\"req_cnf\":\"eyJraWQiOiJteS1rZXktaWQifQ\",\"name\":\"PoP_Pregenerated\"}"; + final AbstractAuthenticationScheme authenticationScheme = + AuthenticationSchemeTypeAdapter.getGsonInstance().fromJson(json, AbstractAuthenticationScheme.class); + + Assert.assertTrue(authenticationScheme instanceof WebAppsPopAuthenticationSchemeInternal); + } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/authscheme/AuthenticationSchemeFactory.java b/common4j/src/main/com/microsoft/identity/common/java/authscheme/AuthenticationSchemeFactory.java index afc9a13544..c80ac5d54b 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/authscheme/AuthenticationSchemeFactory.java +++ b/common4j/src/main/com/microsoft/identity/common/java/authscheme/AuthenticationSchemeFactory.java @@ -106,6 +106,7 @@ public static AbstractAuthenticationScheme createScheme(@NonNull final IPlatform * @return boolean indicating if the the authentication scheme is a PoP authentication scheme */ public static boolean isPopAuthenticationScheme(@NonNull final AbstractAuthenticationScheme authenticationScheme) { - return authenticationScheme instanceof IPoPAuthenticationSchemeParams; + return authenticationScheme instanceof IPoPAuthenticationSchemeParams + || authenticationScheme instanceof WebAppsPopAuthenticationSchemeInternal; } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java b/common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java new file mode 100644 index 0000000000..ebee5a5e85 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.authscheme; + +import static com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal.SerializedNames.KID; +import static com.microsoft.identity.common.java.util.StringUtil.isNullOrEmpty; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.annotations.SerializedName; +import com.microsoft.identity.common.java.base64.Base64Util; +import com.microsoft.identity.common.java.logging.Logger; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.experimental.Accessors; + +/** + * Internal representation of a PoP Authentication Scheme for WebApps (Edge Token Broker). + * Unlike {@link PopAuthenticationSchemeInternal}, this scheme uses a pre-generated req_cnf + * provided in the token request, rather than one generated from the device's key store. + */ +@Getter +@EqualsAndHashCode(callSuper = true) +@Accessors(prefix = "m") +public class WebAppsPopAuthenticationSchemeInternal + extends TokenAuthenticationScheme + implements ITokenAuthenticationSchemeInternal { + private static final String TAG = WebAppsPopAuthenticationSchemeInternal.class.getSimpleName(); + private static final long serialVersionUID = 1L; + + /** + * The name of this auth scheme. Used to distinguish it from other PoP schemes. + */ + public static final String SCHEME_POP_PREGENERATED = "PoP_Pregenerated"; + + public static class SerializedNames { + public static final String REQUEST_CONFIRMATION = "req_cnf"; + } + + @SerializedName(SerializedNames.REQUEST_CONFIRMATION) + private String mRequestConfirmation; + + @SerializedName(KID) + private String mKid; + + /** + * Constructor for gson use. Package-private to restrict direct instantiation + * while allowing Gson's reflective deserialization to construct instances. + * {@code mRequestConfirmation} will be populated by Gson during deserialization + * and must not be used until the object is fully initialized. + */ + WebAppsPopAuthenticationSchemeInternal() { + super(SCHEME_POP_PREGENERATED); + } + + /** + * Constructs a new WebAppsPopAuthenticationSchemeInternal. + * + * @param requestConfirmation The pre-generated request confirmation (req_cnf) value. + * @throws IllegalArgumentException if requestConfirmation is null or empty. + */ + public WebAppsPopAuthenticationSchemeInternal(@NonNull final String requestConfirmation) { + super(SCHEME_POP_PREGENERATED); + + if (isNullOrEmpty(requestConfirmation)) { + throw new IllegalArgumentException( + "requestConfirmation (req_cnf) cannot be null or empty for WebAppsPopAuthenticationSchemeInternal" + ); + } + + mRequestConfirmation = requestConfirmation; + try { + final String reqCnfJson = Base64Util.decodeUrlSafeStringToString(requestConfirmation); + final JsonObject jsonObject = JsonParser.parseString(reqCnfJson).getAsJsonObject(); + if (jsonObject != null && jsonObject.has(KID)) { + mKid = jsonObject.get(KID).getAsString(); + } + } catch (Exception e) { + Logger.warn(TAG, "Failed to parse kid from reqCnf. Exception type and message: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + } + + /** + * Returns the access token as-is. For WebApps PoP, the access token returned from ESTS + * is already in the appropriate PoP format. + * + * @param accessToken The access token to return. + * @return The access token unchanged. + */ + @Override + public String getAccessTokenForScheme(@NonNull final String accessToken) { + return accessToken; + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/base64/Base64Util.kt b/common4j/src/main/com/microsoft/identity/common/java/base64/Base64Util.kt index 78b4534208..17c75c664d 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/base64/Base64Util.kt +++ b/common4j/src/main/com/microsoft/identity/common/java/base64/Base64Util.kt @@ -208,6 +208,31 @@ class Base64Util { } } + /** + * Decodes a base64url-encoded string (URL-safe, no padding, no wrap). + * + * @param input The base64url-encoded string + * @return The decoded byte array + */ + @JvmStatic + fun decodeUrlSafeString(input: String): ByteArray { + return decode( + input, + Base64Flags.URL_SAFE, Base64Flags.NO_PADDING, Base64Flags.NO_WRAP + ) + } + + /** + * Decodes a base64url-encoded string and returns it as a UTF-8 string. + * + * @param input The base64url-encoded string + * @return The decoded string + */ + @JvmStatic + fun decodeUrlSafeStringToString(input: String): String { + return String(decodeUrlSafeString(input), StandardCharsets.UTF_8) + } + //endregion } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/cache/AbstractAccountCredentialCache.java b/common4j/src/main/com/microsoft/identity/common/java/cache/AbstractAccountCredentialCache.java index 34b1c3e018..de0a162fdb 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/cache/AbstractAccountCredentialCache.java +++ b/common4j/src/main/com/microsoft/identity/common/java/cache/AbstractAccountCredentialCache.java @@ -26,6 +26,7 @@ import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal; +import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.dto.AccessTokenRecord; import com.microsoft.identity.common.java.dto.AccountRecord; import com.microsoft.identity.common.java.dto.Credential; @@ -239,7 +240,8 @@ protected List getCredentialsFilteredByInternal(@NonNull final List< if (TokenRequest.TokenType.POP.equalsIgnoreCase(atType)) { if (!(authScheme.equalsIgnoreCase(PopAuthenticationSchemeWithClientKeyInternal.SCHEME_POP_WITH_CLIENT_KEY) - || authScheme.equalsIgnoreCase(PopAuthenticationSchemeInternal.SCHEME_POP))) { + || authScheme.equalsIgnoreCase(PopAuthenticationSchemeInternal.SCHEME_POP) + || authScheme.equalsIgnoreCase(WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED))) { continue; } } else if (!authScheme.equalsIgnoreCase(atType)) continue; diff --git a/common4j/src/main/com/microsoft/identity/common/java/cache/MicrosoftStsAccountCredentialAdapter.java b/common4j/src/main/com/microsoft/identity/common/java/cache/MicrosoftStsAccountCredentialAdapter.java index 05cdc998aa..340b4a3850 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/cache/MicrosoftStsAccountCredentialAdapter.java +++ b/common4j/src/main/com/microsoft/identity/common/java/cache/MicrosoftStsAccountCredentialAdapter.java @@ -24,10 +24,13 @@ import static com.microsoft.identity.common.java.AuthenticationConstants.DEFAULT_SCOPES; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.microsoft.identity.common.java.authorities.Authority; import com.microsoft.identity.common.java.authscheme.AbstractAuthenticationScheme; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal; +import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.commands.parameters.TokenCommandParameters; import com.microsoft.identity.common.java.crypto.IDevicePopManager; import com.microsoft.identity.common.java.dto.AccessTokenRecord; @@ -355,6 +358,9 @@ public AccessTokenRecord createAccessTokenRecord( } else if (authenticationScheme instanceof PopAuthenticationSchemeWithClientKeyInternal) { accessTokenRecord.setCredentialType(CredentialType.AccessToken_With_AuthScheme.name()); accessTokenRecord.setKid(((PopAuthenticationSchemeWithClientKeyInternal) authenticationScheme).getKid()); + } else if (authenticationScheme instanceof WebAppsPopAuthenticationSchemeInternal) { + accessTokenRecord.setCredentialType(CredentialType.AccessToken_With_AuthScheme.name()); + accessTokenRecord.setKid(((WebAppsPopAuthenticationSchemeInternal) authenticationScheme).getKid()); } else { accessTokenRecord.setCredentialType(CredentialType.AccessToken.name()); } diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationRequest.kt b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationRequest.kt index fc35ac5d34..cb94c65cbc 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationRequest.kt +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationRequest.kt @@ -77,13 +77,23 @@ data class WebAppsGetTokenSubOperationRequest( val instanceAware : Boolean = false, // Optional; additional extra query parameters to include in the token request. - // Note: PoP token parameters will come through here. + // Note: PoP token parameters may come through here as a fallback if not set at the top level. @SerializedName(FIELD_EXTRA_PARAMETERS) val extraParameters: Map? = null, // Optional. @SerializedName(FIELD_CLAIMS) - val claims: String? = null + val claims: String? = null, + + // Optional; token type for PoP support. Values: "pop" or "bearer" (case-insensitive). + // Top-level value takes priority over extraParameters. + @SerializedName(FIELD_TOKEN_TYPE) + val tokenType: String? = null, + + // Optional; request confirmation value for PoP support. + // Top-level value takes priority over extraParameters. + @SerializedName(FIELD_REQ_CNF) + val reqCnf: String? = null ) { companion object { const val FIELD_HOME_ACCOUNT_ID = "accountId" @@ -101,5 +111,7 @@ data class WebAppsGetTokenSubOperationRequest( const val FIELD_EXTRA_PARAMETERS = "extraParameters" const val DEFAULT_AUTHORITY = "https://login.microsoftonline.com/common" const val FIELD_CLAIMS = "claims" + const val FIELD_TOKEN_TYPE = "tokenType" + const val FIELD_REQ_CNF = "reqCnf" } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsOAuth2Strategy.java b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsOAuth2Strategy.java index f0a940b344..1409949d61 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsOAuth2Strategy.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsOAuth2Strategy.java @@ -35,6 +35,7 @@ import com.microsoft.identity.common.java.authscheme.AuthenticationSchemeFactory; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal; +import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.cache.ICacheRecord; import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallenge; import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallengeFactory; @@ -427,6 +428,9 @@ public MicrosoftStsTokenRequest createTokenRequest(@NonNull final MicrosoftStsAu } else if (authScheme instanceof PopAuthenticationSchemeWithClientKeyInternal) { tokenRequest.setTokenType(TokenRequest.TokenType.POP); tokenRequest.setRequestConfirmation(((PopAuthenticationSchemeWithClientKeyInternal) authScheme).getRequestConfirmation()); + } else if (authScheme instanceof WebAppsPopAuthenticationSchemeInternal) { + tokenRequest.setTokenType(TokenRequest.TokenType.POP); + tokenRequest.setRequestConfirmation(((WebAppsPopAuthenticationSchemeInternal) authScheme).getRequestConfirmation()); } return tokenRequest; @@ -476,6 +480,9 @@ public MicrosoftStsTokenRequest createRefreshTokenRequest(@NonNull final Abstrac } else if (authScheme instanceof PopAuthenticationSchemeWithClientKeyInternal) { request.setTokenType(TokenRequest.TokenType.POP); request.setRequestConfirmation(((PopAuthenticationSchemeWithClientKeyInternal) authScheme).getRequestConfirmation()); + } else if (authScheme instanceof WebAppsPopAuthenticationSchemeInternal) { + request.setTokenType(TokenRequest.TokenType.POP); + request.setRequestConfirmation(((WebAppsPopAuthenticationSchemeInternal) authScheme).getRequestConfirmation()); } return request; diff --git a/common4j/src/test/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternalTest.java b/common4j/src/test/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternalTest.java new file mode 100644 index 0000000000..4b88421e4c --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternalTest.java @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.authscheme; + +import com.microsoft.identity.common.java.base64.Base64Util; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class WebAppsPopAuthenticationSchemeInternalTest { + + // Base64url-encoded req_cnf with kid: {"kid":"test-kid-1"} + private static final String REQ_CNF_ONE = Base64Util.encodeUrlSafeString("{\"kid\":\"test-kid-1\"}".getBytes()); + + // Base64url-encoded req_cnf with kid: {"kid":"test-kid-1"} (same as ONE) + private static final String REQ_CNF_ONE_CLONE = Base64Util.encodeUrlSafeString("{\"kid\":\"test-kid-1\"}".getBytes()); + + // Base64url-encoded req_cnf with kid: {"kid":"test-kid-2"} + private static final String REQ_CNF_TWO = Base64Util.encodeUrlSafeString("{\"kid\":\"test-kid-2\"}".getBytes()); + + private static final WebAppsPopAuthenticationSchemeInternal AUTHSCHEME_ONE = + new WebAppsPopAuthenticationSchemeInternal(REQ_CNF_ONE); + + private static final WebAppsPopAuthenticationSchemeInternal AUTHSCHEME_ONE_CLONE = + new WebAppsPopAuthenticationSchemeInternal(REQ_CNF_ONE_CLONE); + + private static final WebAppsPopAuthenticationSchemeInternal AUTHSCHEME_TWO = + new WebAppsPopAuthenticationSchemeInternal(REQ_CNF_TWO); + + @Test + public void testConstructor_validRequestConfirmation() { + final String reqCnf = Base64Util.encodeUrlSafeString("{\"kid\":\"my-key-id\"}".getBytes()); + final WebAppsPopAuthenticationSchemeInternal scheme = new WebAppsPopAuthenticationSchemeInternal(reqCnf); + + Assert.assertNotNull(scheme); + Assert.assertEquals(reqCnf, scheme.getRequestConfirmation()); + Assert.assertEquals("my-key-id", scheme.getKid()); + Assert.assertEquals(WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED, scheme.getName()); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructor_emptyRequestConfirmation() { + new WebAppsPopAuthenticationSchemeInternal(""); + } + + @Test + public void testConstructor_invalidBase64() { + // Invalid base64 should not throw, but kid will be null + final WebAppsPopAuthenticationSchemeInternal scheme = + new WebAppsPopAuthenticationSchemeInternal("not-valid-base64!@#$"); + + Assert.assertNotNull(scheme); + Assert.assertEquals("not-valid-base64!@#$", scheme.getRequestConfirmation()); + Assert.assertNull(scheme.getKid()); + } + + @Test + public void testConstructor_invalidJson() { + // Invalid JSON should not throw, but kid will be null + final String invalidJson = Base64Util.encodeUrlSafeString("{invalid json}".getBytes()); + final WebAppsPopAuthenticationSchemeInternal scheme = + new WebAppsPopAuthenticationSchemeInternal(invalidJson); + + Assert.assertNotNull(scheme); + Assert.assertEquals(invalidJson, scheme.getRequestConfirmation()); + Assert.assertNull(scheme.getKid()); + } + + @Test + public void testConstructor_reqCnfWithoutKid() { + final String reqCnfWithoutKid = Base64Util.encodeUrlSafeString("{\"other\":\"value\"}".getBytes()); + final WebAppsPopAuthenticationSchemeInternal scheme = new WebAppsPopAuthenticationSchemeInternal(reqCnfWithoutKid); + + Assert.assertNotNull(scheme); + Assert.assertEquals(reqCnfWithoutKid, scheme.getRequestConfirmation()); + Assert.assertNull(scheme.getKid()); + } + + @Test + public void testGetAccessTokenForScheme() { + final String accessToken = "test-access-token"; + final String result = AUTHSCHEME_ONE.getAccessTokenForScheme(accessToken); + + Assert.assertEquals("Access token should be returned as-is", accessToken, result); + } + + @Test + public void testMappability() { + Map testMap = new HashMap<>(); + + testMap.put(AUTHSCHEME_ONE, true); + Assert.assertEquals(1, testMap.size()); + testMap.put(AUTHSCHEME_TWO, true); + Assert.assertEquals(2, testMap.size()); + } + + @Test + public void testHashCode_equals() { + Assert.assertEquals(AUTHSCHEME_ONE.hashCode(), AUTHSCHEME_ONE_CLONE.hashCode()); + } + + @Test + public void testHashCode_notEquals() { + Assert.assertNotEquals(AUTHSCHEME_ONE.hashCode(), AUTHSCHEME_TWO.hashCode()); + } + + @Test + public void testEquals_equals() { + Assert.assertEquals(AUTHSCHEME_ONE, AUTHSCHEME_ONE_CLONE); + } + + @Test + public void testEquals_notEqualNull() { + Assert.assertNotEquals(AUTHSCHEME_ONE, null); + } + + @Test + public void testEquals_equalsSame() { + Assert.assertEquals(AUTHSCHEME_ONE, AUTHSCHEME_ONE); + } + + @Test + public void testEquals_notEqualDifferenceInKid() { + Assert.assertNotEquals(AUTHSCHEME_ONE, AUTHSCHEME_TWO); + } + + @Test + public void testEquals_notEqualDifferentType() { + Assert.assertNotEquals(AUTHSCHEME_ONE, new BearerAuthenticationSchemeInternal()); + } + + @Test + public void testSchemeName() { + Assert.assertEquals(WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED, + AUTHSCHEME_ONE.getName()); + } + + @Test + public void testBase64UrlDecoding() { + // Test with various req_cnf values including special characters + final String jsonWithSpecialChars = "{\"kid\":\"test+kid/with=special\"}"; + final String reqCnf = Base64Util.encodeUrlSafeString(jsonWithSpecialChars.getBytes()); + final WebAppsPopAuthenticationSchemeInternal scheme = new WebAppsPopAuthenticationSchemeInternal(reqCnf); + + Assert.assertNotNull(scheme); + Assert.assertEquals("test+kid/with=special", scheme.getKid()); + } +}