From 1af2ebf2e540379835feb6b40c1ae91fbbe163e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:30:21 +0000 Subject: [PATCH 1/7] Initial plan From e3237b6a5ea8bd6aedca458c1aa375dc868cc8a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:53:13 +0000 Subject: [PATCH 2/7] Edge TB: PoP support for WebApps, Fixes AB#3501329 Co-authored-by: melissaahn <97474059+melissaahn@users.noreply.github.com> --- changelog.txt | 1 + .../AuthenticationSchemeTypeAdapter.java | 8 + .../common/internal/util/WebAppsUtil.kt | 72 +++++++ .../controllers/BrokerMsalControllerTest.java | 8 +- .../internal/util/WebAppsUtilPopParseTest.kt | 189 ++++++++++++++++++ .../AuthenticationSchemeFactory.java | 3 +- ...ebAppsPopAuthenticationSchemeInternal.java | 89 +++++++++ .../cache/AbstractAccountCredentialCache.java | 4 +- .../MicrosoftStsAccountCredentialAdapter.java | 4 + .../WebAppsGetTokenSubOperationRequest.kt | 16 +- .../MicrosoftStsOAuth2Strategy.java | 7 + 11 files changed, 395 insertions(+), 6 deletions(-) create mode 100644 common/src/test/java/com/microsoft/identity/common/internal/util/WebAppsUtilPopParseTest.kt create mode 100644 common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java diff --git a/changelog.txt b/changelog.txt index 385921bcf8..5de0541dfd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -2,6 +2,7 @@ vNext ---------- - [MINOR] Remove LruCache from SharedPreferencesFileManager (#2910) - [MINOR] Edge TB: Claims (#2925) +- [MINOR] Edge TB: PoP support for WebApps (Edge Token Broker) Version 24.0.1 ---------- 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/main/java/com/microsoft/identity/common/internal/util/WebAppsUtil.kt b/common/src/main/java/com/microsoft/identity/common/internal/util/WebAppsUtil.kt index d522381fc9..f3271b9143 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/util/WebAppsUtil.kt +++ b/common/src/main/java/com/microsoft/identity/common/internal/util/WebAppsUtil.kt @@ -25,7 +25,9 @@ package com.microsoft.identity.common.internal.util import android.os.Bundle import com.microsoft.identity.common.java.commands.webapps.WebAppError import com.microsoft.identity.common.adal.internal.AuthenticationConstants +import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal import com.microsoft.identity.common.java.base64.Base64Util +import com.microsoft.identity.common.java.commands.webapps.WebAppsGetTokenSubOperationRequest import com.microsoft.identity.common.java.exception.ClientException import com.microsoft.identity.common.java.exception.ErrorStrings import com.microsoft.identity.common.java.util.ObjectMapper @@ -176,5 +178,75 @@ class WebAppsUtil { fun hasSameSchemeAndHost(urlA: String, urlB: String): Boolean { return getSchemeAndHost(urlA) == getSchemeAndHost(urlB) } + + /** + * Parses and validates PoP (Proof of Possession) parameters from a + * [WebAppsGetTokenSubOperationRequest], returning a [WebAppsPopAuthenticationSchemeInternal] + * if the request specifies PoP, or null if Bearer/unspecified. + * + * Top-level fields (`tokenType`, `reqCnf`) take priority over values found in + * `extraParameters`. + * + * @param request The token sub-operation request to parse. + * @return A [WebAppsPopAuthenticationSchemeInternal] if tokenType is "pop" with a valid + * reqCnf, or null if no PoP params are present (Bearer is assumed). + * @throws ClientException if PoP params are partially specified or otherwise invalid. + */ + @JvmStatic + @Throws(ClientException::class) + fun parsePopAuthSchemeFromRequest(request: WebAppsGetTokenSubOperationRequest): WebAppsPopAuthenticationSchemeInternal? { + try { + // Check top-level fields first (higher priority than extraParameters). + // Each field is resolved independently: use top-level if present, else fall back + // to extraParameters. + val tokenType = if (!request.tokenType.isNullOrBlank()) { + request.tokenType + } else { + request.extraParameters?.get(WebAppsGetTokenSubOperationRequest.FIELD_TOKEN_TYPE) + } + + val reqCnf = if (!request.reqCnf.isNullOrBlank()) { + request.reqCnf + } else { + request.extraParameters?.get(WebAppsGetTokenSubOperationRequest.FIELD_REQ_CNF) + } + + // If neither tokenType nor reqCnf is present, no PoP scheme needed. + if (tokenType.isNullOrBlank() && reqCnf.isNullOrBlank()) { + return null + } + + // Validate: tokenType is "pop" but reqCnf is missing or empty. + if ("pop".equals(tokenType, ignoreCase = true) && reqCnf.isNullOrBlank()) { + throw ClientException( + ClientException.MISSING_PARAMETER, + "tokenType is 'pop' but reqCnf is missing or empty." + ) + } + + // Validate: reqCnf is provided but tokenType is missing or empty. + if (!reqCnf.isNullOrBlank() && tokenType.isNullOrBlank()) { + throw ClientException( + ClientException.MISSING_PARAMETER, + "reqCnf is provided but tokenType is missing or empty." + ) + } + + // If tokenType is "pop" with a valid reqCnf, create and return the scheme. + if ("pop".equals(tokenType, ignoreCase = true)) { + return WebAppsPopAuthenticationSchemeInternal(reqCnf!!) + } + + // tokenType is present but not "pop" (e.g., "bearer"); no PoP scheme needed. + return null + } catch (e: Exception) { + if (e is ClientException) throw e + throw ClientException( + ErrorStrings.UNKNOWN_ERROR, + "Failed to parse PoP parameters from request: ${e.message}", + e + ) + } + } } } 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/util/WebAppsUtilPopParseTest.kt b/common/src/test/java/com/microsoft/identity/common/internal/util/WebAppsUtilPopParseTest.kt new file mode 100644 index 0000000000..8f3e5d859b --- /dev/null +++ b/common/src/test/java/com/microsoft/identity/common/internal/util/WebAppsUtilPopParseTest.kt @@ -0,0 +1,189 @@ +// 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.internal.util + +import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal +import com.microsoft.identity.common.java.commands.webapps.WebAppsGetTokenSubOperationRequest +import com.microsoft.identity.common.java.exception.ClientException +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class WebAppsUtilPopParseTest { + + private fun buildRequest( + tokenType: String? = null, + reqCnf: String? = null, + extraParameters: Map? = null + ): WebAppsGetTokenSubOperationRequest { + return WebAppsGetTokenSubOperationRequest( + homeAccountId = null, + clientId = "clientId", + authority = "https://login.microsoftonline.com/common", + scopes = "User.Read", + redirectUri = "https://app.example.com/", + tokenType = tokenType, + reqCnf = reqCnf, + extraParameters = extraParameters + ) + } + + @Test + fun testParsePopAuthScheme_noPopParams_returnsNull() { + val request = buildRequest() + val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) + assertNull(result) + } + + @Test + fun testParsePopAuthScheme_bearerTokenType_returnsNull() { + val request = buildRequest(tokenType = "bearer") + val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) + assertNull(result) + } + + @Test + fun testParsePopAuthScheme_bearerTokenTypeCaseInsensitive_returnsNull() { + val request = buildRequest(tokenType = "BEARER") + val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) + assertNull(result) + } + + @Test + fun testParsePopAuthScheme_popWithReqCnf_returnsScheme() { + val reqCnfValue = "test-req-cnf-value" + val request = buildRequest(tokenType = "pop", reqCnf = reqCnfValue) + val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) + assertNotNull(result) + assertEquals(reqCnfValue, result!!.requestConfirmation) + assertEquals(WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED, result.name) + } + + @Test + fun testParsePopAuthScheme_popTokenTypeCaseInsensitive_returnsScheme() { + val reqCnfValue = "test-req-cnf-value" + val request = buildRequest(tokenType = "POP", reqCnf = reqCnfValue) + val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) + assertNotNull(result) + assertEquals(reqCnfValue, result!!.requestConfirmation) + } + + @Test + fun testParsePopAuthScheme_popTokenTypeMixedCase_returnsScheme() { + val reqCnfValue = "test-req-cnf-value" + val request = buildRequest(tokenType = "Pop", reqCnf = reqCnfValue) + val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) + assertNotNull(result) + assertEquals(reqCnfValue, result!!.requestConfirmation) + } + + @Test(expected = ClientException::class) + fun testParsePopAuthScheme_popWithoutReqCnf_throwsException() { + val request = buildRequest(tokenType = "pop") + WebAppsUtil.parsePopAuthSchemeFromRequest(request) + } + + @Test(expected = ClientException::class) + fun testParsePopAuthScheme_reqCnfWithoutTokenType_throwsException() { + val request = buildRequest(reqCnf = "some-cnf-value") + WebAppsUtil.parsePopAuthSchemeFromRequest(request) + } + + @Test + fun testParsePopAuthScheme_popInExtraParameters_returnsScheme() { + val reqCnfValue = "extra-param-cnf" + val request = buildRequest( + extraParameters = mapOf( + WebAppsGetTokenSubOperationRequest.FIELD_TOKEN_TYPE to "pop", + WebAppsGetTokenSubOperationRequest.FIELD_REQ_CNF to reqCnfValue + ) + ) + val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) + assertNotNull(result) + assertEquals(reqCnfValue, result!!.requestConfirmation) + } + + @Test + fun testParsePopAuthScheme_topLevelTakesPriorityOverExtraParams() { + val topLevelReqCnf = "top-level-cnf" + val extraParamReqCnf = "extra-param-cnf" + val request = buildRequest( + tokenType = "pop", + reqCnf = topLevelReqCnf, + extraParameters = mapOf( + WebAppsGetTokenSubOperationRequest.FIELD_TOKEN_TYPE to "pop", + WebAppsGetTokenSubOperationRequest.FIELD_REQ_CNF to extraParamReqCnf + ) + ) + val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) + assertNotNull(result) + // Top-level reqCnf should take priority + assertEquals(topLevelReqCnf, result!!.requestConfirmation) + } + + @Test(expected = ClientException::class) + fun testParsePopAuthScheme_popInExtraParamsWithoutReqCnf_throwsException() { + val request = buildRequest( + extraParameters = mapOf( + WebAppsGetTokenSubOperationRequest.FIELD_TOKEN_TYPE to "pop" + ) + ) + WebAppsUtil.parsePopAuthSchemeFromRequest(request) + } + + @Test(expected = ClientException::class) + fun testParsePopAuthScheme_reqCnfInExtraParamsWithoutTokenType_throwsException() { + val request = buildRequest( + extraParameters = mapOf( + WebAppsGetTokenSubOperationRequest.FIELD_REQ_CNF to "some-cnf" + ) + ) + WebAppsUtil.parsePopAuthSchemeFromRequest(request) + } + + @Test + fun testParsePopAuthScheme_tokenTypeTopLevel_reqCnfInExtraParams_returnsScheme() { + val reqCnfValue = "extra-param-cnf" + val request = buildRequest( + tokenType = "pop", + extraParameters = mapOf( + WebAppsGetTokenSubOperationRequest.FIELD_REQ_CNF to reqCnfValue + ) + ) + val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) + assertNotNull(result) + assertEquals(reqCnfValue, result!!.requestConfirmation) + } + + @Test + fun testParsePopAuthScheme_emptyTokenType_returnsNull() { + val request = buildRequest(tokenType = "") + val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) + assertNull(result) + } +} 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..86044f192d --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java @@ -0,0 +1,89 @@ +// 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.google.gson.annotations.SerializedName; + +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 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; + + /** + * 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. + */ + public WebAppsPopAuthenticationSchemeInternal(@NonNull final String requestConfirmation) { + super(SCHEME_POP_PREGENERATED); + mRequestConfirmation = requestConfirmation; + } + + /** + * 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/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..bc2366bde6 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 @@ -28,6 +28,7 @@ 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 +356,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) { + // For WebApps PoP, we use a pre-generated req_cnf; no device key/kid is associated. + accessTokenRecord.setCredentialType(CredentialType.AccessToken_With_AuthScheme.name()); } 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; From 6eb1795f49ef279b96a8b6668f6b31b3cf9684c4 Mon Sep 17 00:00:00 2001 From: Melissa Ahn Date: Fri, 6 Mar 2026 16:27:15 -0800 Subject: [PATCH 3/7] removing unnecessary code --- .../common/internal/util/WebAppsUtil.kt | 72 ------- .../internal/util/WebAppsUtilPopParseTest.kt | 189 ------------------ 2 files changed, 261 deletions(-) delete mode 100644 common/src/test/java/com/microsoft/identity/common/internal/util/WebAppsUtilPopParseTest.kt diff --git a/common/src/main/java/com/microsoft/identity/common/internal/util/WebAppsUtil.kt b/common/src/main/java/com/microsoft/identity/common/internal/util/WebAppsUtil.kt index f3271b9143..d522381fc9 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/util/WebAppsUtil.kt +++ b/common/src/main/java/com/microsoft/identity/common/internal/util/WebAppsUtil.kt @@ -25,9 +25,7 @@ package com.microsoft.identity.common.internal.util import android.os.Bundle import com.microsoft.identity.common.java.commands.webapps.WebAppError import com.microsoft.identity.common.adal.internal.AuthenticationConstants -import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal import com.microsoft.identity.common.java.base64.Base64Util -import com.microsoft.identity.common.java.commands.webapps.WebAppsGetTokenSubOperationRequest import com.microsoft.identity.common.java.exception.ClientException import com.microsoft.identity.common.java.exception.ErrorStrings import com.microsoft.identity.common.java.util.ObjectMapper @@ -178,75 +176,5 @@ class WebAppsUtil { fun hasSameSchemeAndHost(urlA: String, urlB: String): Boolean { return getSchemeAndHost(urlA) == getSchemeAndHost(urlB) } - - /** - * Parses and validates PoP (Proof of Possession) parameters from a - * [WebAppsGetTokenSubOperationRequest], returning a [WebAppsPopAuthenticationSchemeInternal] - * if the request specifies PoP, or null if Bearer/unspecified. - * - * Top-level fields (`tokenType`, `reqCnf`) take priority over values found in - * `extraParameters`. - * - * @param request The token sub-operation request to parse. - * @return A [WebAppsPopAuthenticationSchemeInternal] if tokenType is "pop" with a valid - * reqCnf, or null if no PoP params are present (Bearer is assumed). - * @throws ClientException if PoP params are partially specified or otherwise invalid. - */ - @JvmStatic - @Throws(ClientException::class) - fun parsePopAuthSchemeFromRequest(request: WebAppsGetTokenSubOperationRequest): WebAppsPopAuthenticationSchemeInternal? { - try { - // Check top-level fields first (higher priority than extraParameters). - // Each field is resolved independently: use top-level if present, else fall back - // to extraParameters. - val tokenType = if (!request.tokenType.isNullOrBlank()) { - request.tokenType - } else { - request.extraParameters?.get(WebAppsGetTokenSubOperationRequest.FIELD_TOKEN_TYPE) - } - - val reqCnf = if (!request.reqCnf.isNullOrBlank()) { - request.reqCnf - } else { - request.extraParameters?.get(WebAppsGetTokenSubOperationRequest.FIELD_REQ_CNF) - } - - // If neither tokenType nor reqCnf is present, no PoP scheme needed. - if (tokenType.isNullOrBlank() && reqCnf.isNullOrBlank()) { - return null - } - - // Validate: tokenType is "pop" but reqCnf is missing or empty. - if ("pop".equals(tokenType, ignoreCase = true) && reqCnf.isNullOrBlank()) { - throw ClientException( - ClientException.MISSING_PARAMETER, - "tokenType is 'pop' but reqCnf is missing or empty." - ) - } - - // Validate: reqCnf is provided but tokenType is missing or empty. - if (!reqCnf.isNullOrBlank() && tokenType.isNullOrBlank()) { - throw ClientException( - ClientException.MISSING_PARAMETER, - "reqCnf is provided but tokenType is missing or empty." - ) - } - - // If tokenType is "pop" with a valid reqCnf, create and return the scheme. - if ("pop".equals(tokenType, ignoreCase = true)) { - return WebAppsPopAuthenticationSchemeInternal(reqCnf!!) - } - - // tokenType is present but not "pop" (e.g., "bearer"); no PoP scheme needed. - return null - } catch (e: Exception) { - if (e is ClientException) throw e - throw ClientException( - ErrorStrings.UNKNOWN_ERROR, - "Failed to parse PoP parameters from request: ${e.message}", - e - ) - } - } } } diff --git a/common/src/test/java/com/microsoft/identity/common/internal/util/WebAppsUtilPopParseTest.kt b/common/src/test/java/com/microsoft/identity/common/internal/util/WebAppsUtilPopParseTest.kt deleted file mode 100644 index 8f3e5d859b..0000000000 --- a/common/src/test/java/com/microsoft/identity/common/internal/util/WebAppsUtilPopParseTest.kt +++ /dev/null @@ -1,189 +0,0 @@ -// 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.internal.util - -import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal -import com.microsoft.identity.common.java.commands.webapps.WebAppsGetTokenSubOperationRequest -import com.microsoft.identity.common.java.exception.ClientException -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class WebAppsUtilPopParseTest { - - private fun buildRequest( - tokenType: String? = null, - reqCnf: String? = null, - extraParameters: Map? = null - ): WebAppsGetTokenSubOperationRequest { - return WebAppsGetTokenSubOperationRequest( - homeAccountId = null, - clientId = "clientId", - authority = "https://login.microsoftonline.com/common", - scopes = "User.Read", - redirectUri = "https://app.example.com/", - tokenType = tokenType, - reqCnf = reqCnf, - extraParameters = extraParameters - ) - } - - @Test - fun testParsePopAuthScheme_noPopParams_returnsNull() { - val request = buildRequest() - val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) - assertNull(result) - } - - @Test - fun testParsePopAuthScheme_bearerTokenType_returnsNull() { - val request = buildRequest(tokenType = "bearer") - val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) - assertNull(result) - } - - @Test - fun testParsePopAuthScheme_bearerTokenTypeCaseInsensitive_returnsNull() { - val request = buildRequest(tokenType = "BEARER") - val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) - assertNull(result) - } - - @Test - fun testParsePopAuthScheme_popWithReqCnf_returnsScheme() { - val reqCnfValue = "test-req-cnf-value" - val request = buildRequest(tokenType = "pop", reqCnf = reqCnfValue) - val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) - assertNotNull(result) - assertEquals(reqCnfValue, result!!.requestConfirmation) - assertEquals(WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED, result.name) - } - - @Test - fun testParsePopAuthScheme_popTokenTypeCaseInsensitive_returnsScheme() { - val reqCnfValue = "test-req-cnf-value" - val request = buildRequest(tokenType = "POP", reqCnf = reqCnfValue) - val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) - assertNotNull(result) - assertEquals(reqCnfValue, result!!.requestConfirmation) - } - - @Test - fun testParsePopAuthScheme_popTokenTypeMixedCase_returnsScheme() { - val reqCnfValue = "test-req-cnf-value" - val request = buildRequest(tokenType = "Pop", reqCnf = reqCnfValue) - val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) - assertNotNull(result) - assertEquals(reqCnfValue, result!!.requestConfirmation) - } - - @Test(expected = ClientException::class) - fun testParsePopAuthScheme_popWithoutReqCnf_throwsException() { - val request = buildRequest(tokenType = "pop") - WebAppsUtil.parsePopAuthSchemeFromRequest(request) - } - - @Test(expected = ClientException::class) - fun testParsePopAuthScheme_reqCnfWithoutTokenType_throwsException() { - val request = buildRequest(reqCnf = "some-cnf-value") - WebAppsUtil.parsePopAuthSchemeFromRequest(request) - } - - @Test - fun testParsePopAuthScheme_popInExtraParameters_returnsScheme() { - val reqCnfValue = "extra-param-cnf" - val request = buildRequest( - extraParameters = mapOf( - WebAppsGetTokenSubOperationRequest.FIELD_TOKEN_TYPE to "pop", - WebAppsGetTokenSubOperationRequest.FIELD_REQ_CNF to reqCnfValue - ) - ) - val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) - assertNotNull(result) - assertEquals(reqCnfValue, result!!.requestConfirmation) - } - - @Test - fun testParsePopAuthScheme_topLevelTakesPriorityOverExtraParams() { - val topLevelReqCnf = "top-level-cnf" - val extraParamReqCnf = "extra-param-cnf" - val request = buildRequest( - tokenType = "pop", - reqCnf = topLevelReqCnf, - extraParameters = mapOf( - WebAppsGetTokenSubOperationRequest.FIELD_TOKEN_TYPE to "pop", - WebAppsGetTokenSubOperationRequest.FIELD_REQ_CNF to extraParamReqCnf - ) - ) - val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) - assertNotNull(result) - // Top-level reqCnf should take priority - assertEquals(topLevelReqCnf, result!!.requestConfirmation) - } - - @Test(expected = ClientException::class) - fun testParsePopAuthScheme_popInExtraParamsWithoutReqCnf_throwsException() { - val request = buildRequest( - extraParameters = mapOf( - WebAppsGetTokenSubOperationRequest.FIELD_TOKEN_TYPE to "pop" - ) - ) - WebAppsUtil.parsePopAuthSchemeFromRequest(request) - } - - @Test(expected = ClientException::class) - fun testParsePopAuthScheme_reqCnfInExtraParamsWithoutTokenType_throwsException() { - val request = buildRequest( - extraParameters = mapOf( - WebAppsGetTokenSubOperationRequest.FIELD_REQ_CNF to "some-cnf" - ) - ) - WebAppsUtil.parsePopAuthSchemeFromRequest(request) - } - - @Test - fun testParsePopAuthScheme_tokenTypeTopLevel_reqCnfInExtraParams_returnsScheme() { - val reqCnfValue = "extra-param-cnf" - val request = buildRequest( - tokenType = "pop", - extraParameters = mapOf( - WebAppsGetTokenSubOperationRequest.FIELD_REQ_CNF to reqCnfValue - ) - ) - val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) - assertNotNull(result) - assertEquals(reqCnfValue, result!!.requestConfirmation) - } - - @Test - fun testParsePopAuthScheme_emptyTokenType_returnsNull() { - val request = buildRequest(tokenType = "") - val result = WebAppsUtil.parsePopAuthSchemeFromRequest(request) - assertNull(result) - } -} From b5f2b3cf0ec7731f15a61bddb851d51320fe0a74 Mon Sep 17 00:00:00 2001 From: Melissa Ahn Date: Fri, 6 Mar 2026 16:32:14 -0800 Subject: [PATCH 4/7] update changelog --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 5de0541dfd..1da2713187 100644 --- a/changelog.txt +++ b/changelog.txt @@ -2,7 +2,7 @@ vNext ---------- - [MINOR] Remove LruCache from SharedPreferencesFileManager (#2910) - [MINOR] Edge TB: Claims (#2925) -- [MINOR] Edge TB: PoP support for WebApps (Edge Token Broker) +- [MINOR] Edge TB: PoP support (#3006) Version 24.0.1 ---------- From 728d2b2d9b6574fd09f7857b01a9f6031464d8af Mon Sep 17 00:00:00 2001 From: Melissa Ahn Date: Mon, 9 Mar 2026 12:41:19 -0700 Subject: [PATCH 5/7] suggestions --- .../AuthenticationSchemeTypeAdapterTests.java | 22 +++++++++++++++++++ ...ebAppsPopAuthenticationSchemeInternal.java | 10 +++++++++ .../MicrosoftStsAccountCredentialAdapter.java | 1 + 3 files changed, 33 insertions(+) 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..638800be03 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\":\"test_req_cnf\",\"name\":\"PoP_Pregenerated\"}"; + final WebAppsPopAuthenticationSchemeInternal scheme = + new WebAppsPopAuthenticationSchemeInternal("test_req_cnf"); + + 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\":\"test_req_cnf\",\"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/WebAppsPopAuthenticationSchemeInternal.java b/common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java index 86044f192d..4944fbeb47 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java +++ b/common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java @@ -22,6 +22,8 @@ // THE SOFTWARE. package com.microsoft.identity.common.java.authscheme; +import static com.microsoft.identity.common.java.util.StringUtil.isNullOrEmpty; + import com.google.gson.annotations.SerializedName; import lombok.EqualsAndHashCode; @@ -69,9 +71,17 @@ public static class SerializedNames { * 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; } 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 bc2366bde6..37f94efd56 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 @@ -359,6 +359,7 @@ public AccessTokenRecord createAccessTokenRecord( } else if (authenticationScheme instanceof WebAppsPopAuthenticationSchemeInternal) { // For WebApps PoP, we use a pre-generated req_cnf; no device key/kid is associated. accessTokenRecord.setCredentialType(CredentialType.AccessToken_With_AuthScheme.name()); + accessTokenRecord.setKid(((WebAppsPopAuthenticationSchemeInternal) authenticationScheme).getRequestConfirmation()); } else { accessTokenRecord.setCredentialType(CredentialType.AccessToken.name()); } From 1cfc7899d6a6b2c2ceaf7c7329bff54ee086c4c0 Mon Sep 17 00:00:00 2001 From: Melissa Ahn Date: Tue, 17 Mar 2026 13:30:31 -0700 Subject: [PATCH 6/7] changes --- .../AuthenticationSchemeTypeAdapterTests.java | 6 +- ...ebAppsPopAuthenticationSchemeInternal.java | 19 +- .../identity/common/java/base64/Base64Util.kt | 25 +++ .../cache/AbstractAccountCredentialCache.java | 10 +- .../MicrosoftStsAccountCredentialAdapter.java | 5 +- ...psPopAuthenticationSchemeInternalTest.java | 171 ++++++++++++++++++ 6 files changed, 227 insertions(+), 9 deletions(-) create mode 100644 common4j/src/test/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternalTest.java 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 638800be03..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 @@ -80,9 +80,9 @@ public void testSerialize_PopAuthenticationSchemeWithClientKeyInternal() throws @Test public void testSerialize_WebAppsPopAuthenticationSchemeInternal() throws MalformedURLException, IllegalArgumentException { final String expectedJson = - "{\"req_cnf\":\"test_req_cnf\",\"name\":\"PoP_Pregenerated\"}"; + "{\"req_cnf\":\"eyJraWQiOiJteS1rZXktaWQifQ\",\"kid\":\"my-key-id\",\"name\":\"PoP_Pregenerated\"}"; final WebAppsPopAuthenticationSchemeInternal scheme = - new WebAppsPopAuthenticationSchemeInternal("test_req_cnf"); + new WebAppsPopAuthenticationSchemeInternal("eyJraWQiOiJteS1rZXktaWQifQ"); final String json = AuthenticationSchemeTypeAdapter.getGsonInstance().toJson(scheme); Assert.assertEquals(expectedJson, json); @@ -119,7 +119,7 @@ public void testDeserialize_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\":\"test_req_cnf\",\"name\":\"PoP_Pregenerated\"}"; + "{\"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); 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 index 4944fbeb47..ebee5a5e85 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java +++ b/common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java @@ -22,9 +22,14 @@ // 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; @@ -42,7 +47,7 @@ public class WebAppsPopAuthenticationSchemeInternal extends TokenAuthenticationScheme implements ITokenAuthenticationSchemeInternal { - + private static final String TAG = WebAppsPopAuthenticationSchemeInternal.class.getSimpleName(); private static final long serialVersionUID = 1L; /** @@ -57,6 +62,9 @@ public static class SerializedNames { @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. @@ -83,6 +91,15 @@ public WebAppsPopAuthenticationSchemeInternal(@NonNull final String requestConfi } 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()); + } } /** 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 de0a162fdb..7faf2f50f3 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 @@ -239,9 +239,13 @@ 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(WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED))) { + if (authScheme.equalsIgnoreCase(WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED)) { + final String credKid = ((AccessTokenRecord)credential).getKid(); + if (!(credKid != null && credKid.equals(kid))) { + continue; + } + } else if (!(authScheme.equalsIgnoreCase(PopAuthenticationSchemeWithClientKeyInternal.SCHEME_POP_WITH_CLIENT_KEY) + || authScheme.equalsIgnoreCase(PopAuthenticationSchemeInternal.SCHEME_POP))) { 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 37f94efd56..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,6 +24,8 @@ 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; @@ -357,9 +359,8 @@ public AccessTokenRecord createAccessTokenRecord( accessTokenRecord.setCredentialType(CredentialType.AccessToken_With_AuthScheme.name()); accessTokenRecord.setKid(((PopAuthenticationSchemeWithClientKeyInternal) authenticationScheme).getKid()); } else if (authenticationScheme instanceof WebAppsPopAuthenticationSchemeInternal) { - // For WebApps PoP, we use a pre-generated req_cnf; no device key/kid is associated. accessTokenRecord.setCredentialType(CredentialType.AccessToken_With_AuthScheme.name()); - accessTokenRecord.setKid(((WebAppsPopAuthenticationSchemeInternal) authenticationScheme).getRequestConfirmation()); + accessTokenRecord.setKid(((WebAppsPopAuthenticationSchemeInternal) authenticationScheme).getKid()); } else { accessTokenRecord.setCredentialType(CredentialType.AccessToken.name()); } 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()); + } +} From ea94912a802c7aa1e01eabb3dd6cf3a3143b68cb Mon Sep 17 00:00:00 2001 From: Melissa Ahn Date: Tue, 17 Mar 2026 14:28:00 -0700 Subject: [PATCH 7/7] removing redundant addition --- .../java/cache/AbstractAccountCredentialCache.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 7faf2f50f3..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 @@ -239,13 +239,9 @@ protected List getCredentialsFilteredByInternal(@NonNull final List< } if (TokenRequest.TokenType.POP.equalsIgnoreCase(atType)) { - if (authScheme.equalsIgnoreCase(WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED)) { - final String credKid = ((AccessTokenRecord)credential).getKid(); - if (!(credKid != null && credKid.equals(kid))) { - continue; - } - } else if (!(authScheme.equalsIgnoreCase(PopAuthenticationSchemeWithClientKeyInternal.SCHEME_POP_WITH_CLIENT_KEY) - || authScheme.equalsIgnoreCase(PopAuthenticationSchemeInternal.SCHEME_POP))) { + if (!(authScheme.equalsIgnoreCase(PopAuthenticationSchemeWithClientKeyInternal.SCHEME_POP_WITH_CLIENT_KEY) + || authScheme.equalsIgnoreCase(PopAuthenticationSchemeInternal.SCHEME_POP) + || authScheme.equalsIgnoreCase(WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED))) { continue; } } else if (!authScheme.equalsIgnoreCase(atType)) continue;