Skip to content

Commit 242da8c

Browse files
spetrescu84CopilotCopilotfadidurah
authored
Add NativeAuthRequestInterceptor for custom per-request headers and UI Automation, Fixes AB#3600652 (#3112)
Implement custom HTTP headers request interceptor for native auth CIAM requests - Add NativeAuthHeaderValidator to enforce header naming rules - Pass interceptor through the full config propagation chain: OAuth2StrategyParameters → NativeAuthCIAMAuthority → NativeAuthOAuth2Configuration → NativeAuthOAuth2StrategyFactory → all 4 interactors (SignIn, SignUp, ResetPassword, JIT) - Add interceptor field to BaseNativeAuthCommandParameters - Wire interceptor in NativeAuthMsalController.createOAuth2Strategy() - Add unit tests for header validation - Add integration tests for interceptor in all interactors ### UI Automation Test Support - **`NativeAuthSampleApp.java`** — App model class for UI automation (package name, APK filename constants, no-op `handleFirstRun()`/`initialiseAppImpl()`) - **`CopyFileRule.java`** — Added `NativeAuthSampleApp.NATIVE_AUTH_SAMPLE_APK` to the list of APKs copied from `/sdcard/` to `/data/local/tmp/` at test runtime - **`LabConstants.java`** — Added `CIAM` user type constant - **`UserType.java`** — Added `CIAM` enum value for lab queries ### API Change: NativeAuthRequestInterceptor moved to MSAL The `NativeAuthRequestInterceptor` interface has been moved to the MSAL module (`com.microsoft.identity.nativeauth` package) as it is a public-facing type that customers implement. Common now uses the base `OAuth2RequestInterceptor` type internally, which `NativeAuthRequestInterceptor` extends. This follows the established pattern where public nativeauth types live in MSAL. [AB#3600652](https://identitydivision.visualstudio.com/Engineering/_workitems/edit/3600652) --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: fadidurah <fadidurah@microsoft.com> Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com>
1 parent ea91c62 commit 242da8c

25 files changed

Lines changed: 1872 additions & 30 deletions

File tree

LabApiUtilities/src/main/com/microsoft/identity/labapi/utilities/constants/LabConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ static final class UserType {
6262
public static final String ONPREM = "onprem";
6363
public static final String GUEST = "guest";
6464
public static final String B2C = "b2c";
65+
public static final String CIAM = "ciam";
6566
}
6667

6768
static final class UserRole {

LabApiUtilities/src/main/com/microsoft/identity/labapi/utilities/constants/UserType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ public enum UserType {
5252
CLOUD(LabConstants.UserType.CLOUD),
5353
B2C(LabConstants.UserType.B2C),
5454
GUEST(LabConstants.UserType.GUEST),
55-
ONPREM(LabConstants.UserType.ONPREM);
55+
ONPREM(LabConstants.UserType.ONPREM),
56+
CIAM(LabConstants.UserType.CIAM);
5657

5758
final String value;
5859

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ vNext
1515
- [PATCH] Extend filter-then-clone optimization to load() and getIdTokensForAccountRecord() in MsalOAuth2TokenCache: when ENABLE_FILTER_THEN_CLONE_IN_MEMORY_CACHE flight is enabled, skip clone-all preload and call direct flight-gated overloads that clone only matching credentials; add new getCredentialsFilteredBy overload with kid support (#3100)
1616
- [MINOR] Add onboarding telemetry recorder, field keys, and session persistence for mobile onboarding flow (#3088)
1717
- [PATCH] Move Multiple Listening apps check to the authorization layer (#3070)
18+
- [MINOR] Add NativeAuthRequestInterceptor for custom per-request headers in native auth flows (#3112)
1819
- [PATCH] Edge TB: Fix lookup mode (#3108)
1920

2021
Version 24.2.0

common/src/main/java/com/microsoft/identity/common/nativeauth/internal/controllers/NativeAuthMsalController.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2710,6 +2710,7 @@ class NativeAuthMsalController : BaseNativeAuthController() {
27102710
.platformComponents(parameters.platformComponents)
27112711
.challengeTypes(parameters.challengeType)
27122712
.capabilities(parameters.capabilities)
2713+
.requestInterceptor(parameters.requestInterceptor)
27132714
.build()
27142715

27152716
return parameters

common4j/src/main/com/microsoft/identity/common/java/nativeauth/authorities/NativeAuthCIAMAuthority.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthConstan
3232
import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthOAuth2Configuration
3333
import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthOAuth2Strategy
3434
import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthOAuth2StrategyFactory
35+
import com.microsoft.identity.common.java.providers.oauth2.OAuth2RequestInterceptor
3536
import com.microsoft.identity.common.java.providers.oauth2.OAuth2StrategyParameters
3637

3738
/**
@@ -76,7 +77,7 @@ class NativeAuthCIAMAuthority (
7677
mAuthorityUrlString = authorityUrl
7778
}
7879

79-
private fun createNativeAuthOAuth2Configuration(challengeTypes: List<String>?, capabilities: List<String>?): NativeAuthOAuth2Configuration {
80+
private fun createNativeAuthOAuth2Configuration(challengeTypes: List<String>?, capabilities: List<String>?, requestInterceptor: OAuth2RequestInterceptor?): NativeAuthOAuth2Configuration {
8081
LogSession.logMethodCall(
8182
tag = TAG,
8283
correlationId = null,
@@ -86,7 +87,8 @@ class NativeAuthCIAMAuthority (
8687
authorityUrl = this.authorityURL,
8788
clientId = this.clientId,
8889
challengeType = getChallengeTypesWithDefault(challengeTypes),
89-
capabilities = getCapabilities(capabilities)
90+
capabilities = getCapabilities(capabilities),
91+
requestInterceptor = requestInterceptor
9092
)
9193
}
9294

@@ -123,7 +125,11 @@ class NativeAuthCIAMAuthority (
123125

124126
@Throws(ClientException::class)
125127
override fun createOAuth2Strategy(parameters: OAuth2StrategyParameters): NativeAuthOAuth2Strategy {
126-
val config = createNativeAuthOAuth2Configuration(parameters.mChallengeTypes, parameters.mCapabilities)
128+
val config = createNativeAuthOAuth2Configuration(
129+
parameters.mChallengeTypes,
130+
parameters.mCapabilities,
131+
parameters.mRequestInterceptor
132+
)
127133

128134
// CIAM Authorities can fetch endpoints from open id configuration. We disable this option.
129135
parameters.setUsingOpenIdConfiguration(NATIVE_AUTH_USE_OPENID_CONFIGURATION)

common4j/src/main/com/microsoft/identity/common/java/nativeauth/commands/parameters/BaseNativeAuthCommandParameters.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.microsoft.identity.common.java.commands.parameters.CommandParameters;
2727
import com.microsoft.identity.common.java.logging.Logger;
2828
import com.microsoft.identity.common.java.nativeauth.authorities.NativeAuthCIAMAuthority;
29+
import com.microsoft.identity.common.java.providers.oauth2.OAuth2RequestInterceptor;
2930
import com.microsoft.identity.common.java.nativeauth.util.ILoggable;
3031

3132
import java.util.List;
@@ -62,6 +63,13 @@ public abstract class BaseNativeAuthCommandParameters extends CommandParameters
6263
@Nullable
6364
public final List<String> capabilities;
6465

66+
/**
67+
* An optional interceptor for injecting custom HTTP headers into native auth requests.
68+
*/
69+
@Nullable
70+
@EqualsAndHashCode.Exclude
71+
public final transient OAuth2RequestInterceptor requestInterceptor;
72+
6573
@Override
6674
public void logParameters(String tag, String correlationId) {
6775
Logger.infoWithObject(tag, null, correlationId, this);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
package com.microsoft.identity.common.java.nativeauth.providers
24+
25+
import com.microsoft.identity.common.java.logging.Logger
26+
27+
/**
28+
* Validates custom headers provided by an [OAuth2RequestInterceptor].
29+
* Enforces that header names start with "x-" and do not use reserved prefixes.
30+
*/
31+
object NativeAuthHeaderValidator {
32+
33+
private val TAG = NativeAuthHeaderValidator::class.java.simpleName
34+
35+
private val RESERVED_PREFIXES = listOf("x-ms-", "x-client-", "x-broker-", "x-app-")
36+
37+
/**
38+
* Filters a map of headers, returning only those that are valid per the interceptor contract.
39+
* Invalid headers are logged as warnings and excluded from the result.
40+
*
41+
* @param headers The raw headers provided by the interceptor.
42+
* @return A map containing only valid headers using lowercase field names, or an empty map if none are valid.
43+
*/
44+
fun filterValidHeaders(headers: Map<String, String>): Map<String, String> {
45+
val validHeaders = mutableMapOf<String, String>()
46+
47+
for ((field, value) in headers) {
48+
val lowerField = field.lowercase()
49+
50+
if (!lowerField.startsWith("x-")) {
51+
Logger.warn(
52+
TAG,
53+
"Additional header field \"$field\" must start with the \"x-\" prefix. Ignoring."
54+
)
55+
continue
56+
}
57+
58+
var isReserved = false
59+
for (reserved in RESERVED_PREFIXES) {
60+
if (lowerField.startsWith(reserved)) {
61+
Logger.warn(
62+
TAG,
63+
"Additional header field \"$field\" uses reserved prefix \"$reserved\". Ignoring."
64+
)
65+
isReserved = true
66+
break
67+
}
68+
}
69+
70+
if (!isReserved) {
71+
validHeaders[lowerField] = value
72+
}
73+
}
74+
75+
return validHeaders
76+
}
77+
}

common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/NativeAuthOAuth2Configuration.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ package com.microsoft.identity.common.java.nativeauth.providers
2626
import com.microsoft.identity.common.java.nativeauth.BuildValues
2727
import com.microsoft.identity.common.java.logging.Logger
2828
import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsOAuth2Configuration
29+
import com.microsoft.identity.common.java.providers.oauth2.OAuth2RequestInterceptor
2930
import com.microsoft.identity.common.java.util.UrlUtil
3031
import java.net.MalformedURLException
3132
import java.net.URISyntaxException
@@ -41,6 +42,7 @@ class NativeAuthOAuth2Configuration(
4142
val clientId: String,
4243
val challengeType: String,
4344
val capabilities: String?,
45+
val requestInterceptor: OAuth2RequestInterceptor? = null,
4446
// Need this to decide whether or not to return mock api authority or actual authority supplied in configuration
4547
// Turn this on if you plan to use web auth and/or open id configuration
4648
val useMockApiForNativeAuth: Boolean = BuildValues.shouldUseMockApiForNativeAuth(),

common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/NativeAuthOAuth2StrategyFactory.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,28 +39,33 @@ class NativeAuthOAuth2StrategyFactory {
3939
config: NativeAuthOAuth2Configuration,
4040
strategyParameters: OAuth2StrategyParameters,
4141
): NativeAuthOAuth2Strategy {
42+
val requestInterceptor = config.requestInterceptor
4243
return NativeAuthOAuth2Strategy(
4344
strategyParameters = strategyParameters,
4445
config = config,
4546
signInInteractor = SignInInteractor(
4647
httpClient = UrlConnectionHttpClient.getDefaultInstance(),
4748
nativeAuthRequestProvider = NativeAuthRequestProvider(config = config),
48-
nativeAuthResponseHandler = NativeAuthResponseHandler()
49+
nativeAuthResponseHandler = NativeAuthResponseHandler(),
50+
requestInterceptor = requestInterceptor
4951
),
5052
signUpInteractor = SignUpInteractor(
5153
httpClient = UrlConnectionHttpClient.getDefaultInstance(),
5254
nativeAuthRequestProvider = NativeAuthRequestProvider(config = config),
53-
nativeAuthResponseHandler = NativeAuthResponseHandler()
55+
nativeAuthResponseHandler = NativeAuthResponseHandler(),
56+
requestInterceptor = requestInterceptor
5457
),
5558
resetPasswordInteractor = ResetPasswordInteractor(
5659
httpClient = UrlConnectionHttpClient.getDefaultInstance(),
5760
nativeAuthRequestProvider = NativeAuthRequestProvider(config = config),
58-
nativeAuthResponseHandler = NativeAuthResponseHandler()
61+
nativeAuthResponseHandler = NativeAuthResponseHandler(),
62+
requestInterceptor = requestInterceptor
5963
),
6064
jitInteractor = JITInteractor(
6165
httpClient = UrlConnectionHttpClient.getDefaultInstance(),
6266
nativeAuthRequestProvider = NativeAuthRequestProvider(config = config),
63-
nativeAuthResponseHandler = NativeAuthResponseHandler()
67+
nativeAuthResponseHandler = NativeAuthResponseHandler(),
68+
requestInterceptor = requestInterceptor
6469
)
6570
)
6671
}

common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/interactors/JITInteractor.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.microsoft.identity.common.java.logging.Logger
2727
import com.microsoft.identity.common.java.nativeauth.commands.parameters.JITChallengeAuthMethodCommandParameters
2828
import com.microsoft.identity.common.java.nativeauth.commands.parameters.JITIntrospectCommandParameters
2929
import com.microsoft.identity.common.java.nativeauth.commands.parameters.JITContinueCommandParameters
30+
import com.microsoft.identity.common.java.providers.oauth2.OAuth2RequestInterceptor
3031
import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthRequestProvider
3132
import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthResponseHandler
3233
import com.microsoft.identity.common.java.nativeauth.providers.requests.jit.JITChallengeRequest
@@ -51,7 +52,8 @@ import com.microsoft.identity.common.java.util.ObjectMapper
5152
class JITInteractor(
5253
private val httpClient: UrlConnectionHttpClient,
5354
private val nativeAuthRequestProvider: NativeAuthRequestProvider,
54-
private val nativeAuthResponseHandler: NativeAuthResponseHandler
55+
private val nativeAuthResponseHandler: NativeAuthResponseHandler,
56+
private val requestInterceptor: OAuth2RequestInterceptor? = null
5557
) {
5658
private val TAG: String = this::class.java.simpleName
5759

@@ -94,7 +96,7 @@ class JITInteractor(
9496
)
9597
val encodedRequest: String =
9698
ObjectMapper.serializeObjectToFormUrlEncoded(request.parameters)
97-
val headers = request.headers
99+
val headers = applyInterceptorHeaders(request.requestUrl, request.headers, requestInterceptor)
98100
val requestUrl = request.requestUrl
99101

100102
val response = httpClient.post(
@@ -169,7 +171,7 @@ class JITInteractor(
169171
)
170172
val encodedRequest: String =
171173
ObjectMapper.serializeObjectToFormUrlEncoded(request.parameters)
172-
val headers = request.headers
174+
val headers = applyInterceptorHeaders(request.requestUrl, request.headers, requestInterceptor)
173175
val requestUrl = request.requestUrl
174176

175177
val response = httpClient.post(
@@ -243,7 +245,7 @@ class JITInteractor(
243245
)
244246
val encodedRequest: String =
245247
ObjectMapper.serializeObjectToFormUrlEncoded(request.parameters)
246-
val headers = request.headers
248+
val headers = applyInterceptorHeaders(request.requestUrl, request.headers, requestInterceptor)
247249
val requestUrl = request.requestUrl
248250

249251
val response = httpClient.post(
@@ -275,4 +277,4 @@ class JITInteractor(
275277
return result
276278
}
277279
//endregion
278-
}
280+
}

0 commit comments

Comments
 (0)