Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2749af0
Add NativeAuthRequestInterceptor for custom per-request headers
spetrescu84 May 13, 2026
f04517b
Merge origin/dev and resolve changelog conflict
Copilot May 13, 2026
f450877
Address PR review feedback for interceptor wiring and docs
Copilot May 13, 2026
d415f28
Add docs and interceptor type guard per review feedback
Copilot May 13, 2026
1611df9
Add comprehensive interceptor tests for all interactors
spetrescu84 May 13, 2026
d98d3e1
Build: publish libraries directly to NewAndroid feed
fadidurah May 14, 2026
ce8c4e7
Address PR review comments: improve warn message, document overwrite …
spetrescu84 May 14, 2026
4dd8406
Consolidate duplicated test logic into RequestInterceptorHeaderUtilsTest
spetrescu84 May 14, 2026
baae279
common4j: don't tie sourcesJar/javadocJar to assemble
fadidurah May 14, 2026
46c3b31
Add gradle configure-on-demand for build perf
fadidurah May 15, 2026
4343317
Merge branch 'dev' into spetrescu/custom_headers
spetrescu84 May 15, 2026
bf80e0b
Updated message
spetrescu84 May 15, 2026
521100e
Updated comment
spetrescu84 May 15, 2026
d91d6bf
Add symmetric null-interceptor tests for all JIT methods
spetrescu84 May 15, 2026
89f5860
Use realistic per-method endpoint URLs in interceptor tests
spetrescu84 May 15, 2026
3371b7e
Merge branch 'dev' into fadi/publish-libraries-to-newandroid-feed
fadidurah May 18, 2026
0d1e6fb
Merge branch 'dev' into spetrescu/custom_headers
spetrescu84 May 20, 2026
d313d38
Fix POM: write current version for project deps to avoid 'unspecified'
fadidurah May 21, 2026
28adbb7
Merge branch 'dev' of https://github.com/AzureAD/microsoft-aut status
fadidurah May 21, 2026
c8cd68b
fix token endpoint parsing
fadidurah May 22, 2026
62b6e05
Move NativeAuthRequestInterceptor to MSAL module
spetrescu84 May 26, 2026
2dc5782
Merge branch 'dev' into spetrescu/custom_headers
spetrescu84 May 26, 2026
7a812bf
Merge branch 'dev' into fadi/publish-libraries-to-newandroid-feed
fadidurah May 27, 2026
f005147
Revert "Build: publish libraries directly to NewAndroid feed"
fadidurah May 27, 2026
7a4b22b
Add Native Auth to installed apps
spetrescu84 May 27, 2026
62520d6
Merge branch 'dev' into spetrescu/custom_headers
spetrescu84 May 27, 2026
ba9f604
Merge branch 'dev' into spetrescu/custom_headers
spetrescu84 Jun 3, 2026
a6bf2df
Merge commit f005147 (fadi/publish-libraries-to-newandroid-feed rever…
spetrescu84 Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ static final class UserType {
public static final String ONPREM = "onprem";
public static final String GUEST = "guest";
public static final String B2C = "b2c";
public static final String CIAM = "ciam";
}

static final class UserRole {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public enum UserType {
CLOUD(LabConstants.UserType.CLOUD),
B2C(LabConstants.UserType.B2C),
GUEST(LabConstants.UserType.GUEST),
ONPREM(LabConstants.UserType.ONPREM);
ONPREM(LabConstants.UserType.ONPREM),
CIAM(LabConstants.UserType.CIAM);

final String value;

Expand Down
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ vNext
- [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)
- [MINOR] Add onboarding telemetry recorder, field keys, and session persistence for mobile onboarding flow (#3088)
- [PATCH] Move Multiple Listening apps check to the authorization layer (#3070)
- [MINOR] Add NativeAuthRequestInterceptor for custom per-request headers in native auth flows (#3112)
- [PATCH] Edge TB: Fix lookup mode (#3108)

Version 24.2.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2710,6 +2710,7 @@ class NativeAuthMsalController : BaseNativeAuthController() {
.platformComponents(parameters.platformComponents)
.challengeTypes(parameters.challengeType)
.capabilities(parameters.capabilities)
.requestInterceptor(parameters.requestInterceptor)
.build()

return parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthConstan
import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthOAuth2Configuration
import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthOAuth2Strategy
import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthOAuth2StrategyFactory
import com.microsoft.identity.common.java.providers.oauth2.OAuth2RequestInterceptor
import com.microsoft.identity.common.java.providers.oauth2.OAuth2StrategyParameters

/**
Expand Down Expand Up @@ -76,7 +77,7 @@ class NativeAuthCIAMAuthority (
mAuthorityUrlString = authorityUrl
}

private fun createNativeAuthOAuth2Configuration(challengeTypes: List<String>?, capabilities: List<String>?): NativeAuthOAuth2Configuration {
private fun createNativeAuthOAuth2Configuration(challengeTypes: List<String>?, capabilities: List<String>?, requestInterceptor: OAuth2RequestInterceptor?): NativeAuthOAuth2Configuration {
LogSession.logMethodCall(
tag = TAG,
correlationId = null,
Expand All @@ -86,7 +87,8 @@ class NativeAuthCIAMAuthority (
authorityUrl = this.authorityURL,
clientId = this.clientId,
challengeType = getChallengeTypesWithDefault(challengeTypes),
capabilities = getCapabilities(capabilities)
capabilities = getCapabilities(capabilities),
requestInterceptor = requestInterceptor
)
}

Expand Down Expand Up @@ -123,7 +125,11 @@ class NativeAuthCIAMAuthority (

@Throws(ClientException::class)
override fun createOAuth2Strategy(parameters: OAuth2StrategyParameters): NativeAuthOAuth2Strategy {
val config = createNativeAuthOAuth2Configuration(parameters.mChallengeTypes, parameters.mCapabilities)
val config = createNativeAuthOAuth2Configuration(
parameters.mChallengeTypes,
parameters.mCapabilities,
parameters.mRequestInterceptor
)

// CIAM Authorities can fetch endpoints from open id configuration. We disable this option.
parameters.setUsingOpenIdConfiguration(NATIVE_AUTH_USE_OPENID_CONFIGURATION)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.microsoft.identity.common.java.commands.parameters.CommandParameters;
import com.microsoft.identity.common.java.logging.Logger;
import com.microsoft.identity.common.java.nativeauth.authorities.NativeAuthCIAMAuthority;
import com.microsoft.identity.common.java.providers.oauth2.OAuth2RequestInterceptor;
import com.microsoft.identity.common.java.nativeauth.util.ILoggable;

import java.util.List;
Expand Down Expand Up @@ -62,6 +63,13 @@ public abstract class BaseNativeAuthCommandParameters extends CommandParameters
@Nullable
public final List<String> capabilities;

/**
* An optional interceptor for injecting custom HTTP headers into native auth requests.
*/
@Nullable
@EqualsAndHashCode.Exclude
public final transient OAuth2RequestInterceptor requestInterceptor;

@Override
public void logParameters(String tag, String correlationId) {
Logger.infoWithObject(tag, null, correlationId, this);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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.nativeauth.providers

import com.microsoft.identity.common.java.logging.Logger

/**
* Validates custom headers provided by an [OAuth2RequestInterceptor].
* Enforces that header names start with "x-" and do not use reserved prefixes.
*/
object NativeAuthHeaderValidator {

private val TAG = NativeAuthHeaderValidator::class.java.simpleName

private val RESERVED_PREFIXES = listOf("x-ms-", "x-client-", "x-broker-", "x-app-")

/**
* Filters a map of headers, returning only those that are valid per the interceptor contract.
* Invalid headers are logged as warnings and excluded from the result.
*
* @param headers The raw headers provided by the interceptor.
* @return A map containing only valid headers using lowercase field names, or an empty map if none are valid.
*/
fun filterValidHeaders(headers: Map<String, String>): Map<String, String> {
val validHeaders = mutableMapOf<String, String>()

for ((field, value) in headers) {
val lowerField = field.lowercase()

if (!lowerField.startsWith("x-")) {
Logger.warn(
TAG,
"Additional header field \"$field\" must start with the \"x-\" prefix. Ignoring."
)
continue
}

var isReserved = false
for (reserved in RESERVED_PREFIXES) {
if (lowerField.startsWith(reserved)) {
Logger.warn(
TAG,
"Additional header field \"$field\" uses reserved prefix \"$reserved\". Ignoring."
)
isReserved = true
break
}
}

if (!isReserved) {
validHeaders[lowerField] = value
}
}

return validHeaders
}
Comment thread
spetrescu84 marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ package com.microsoft.identity.common.java.nativeauth.providers
import com.microsoft.identity.common.java.nativeauth.BuildValues
import com.microsoft.identity.common.java.logging.Logger
import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsOAuth2Configuration
import com.microsoft.identity.common.java.providers.oauth2.OAuth2RequestInterceptor
import com.microsoft.identity.common.java.util.UrlUtil
import java.net.MalformedURLException
import java.net.URISyntaxException
Expand All @@ -41,6 +42,7 @@ class NativeAuthOAuth2Configuration(
val clientId: String,
val challengeType: String,
val capabilities: String?,
val requestInterceptor: OAuth2RequestInterceptor? = null,
// Need this to decide whether or not to return mock api authority or actual authority supplied in configuration
// Turn this on if you plan to use web auth and/or open id configuration
val useMockApiForNativeAuth: Boolean = BuildValues.shouldUseMockApiForNativeAuth(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,33 @@ class NativeAuthOAuth2StrategyFactory {
config: NativeAuthOAuth2Configuration,
strategyParameters: OAuth2StrategyParameters,
): NativeAuthOAuth2Strategy {
val requestInterceptor = config.requestInterceptor
return NativeAuthOAuth2Strategy(
strategyParameters = strategyParameters,
config = config,
signInInteractor = SignInInteractor(
httpClient = UrlConnectionHttpClient.getDefaultInstance(),
nativeAuthRequestProvider = NativeAuthRequestProvider(config = config),
nativeAuthResponseHandler = NativeAuthResponseHandler()
nativeAuthResponseHandler = NativeAuthResponseHandler(),
requestInterceptor = requestInterceptor
),
signUpInteractor = SignUpInteractor(
httpClient = UrlConnectionHttpClient.getDefaultInstance(),
nativeAuthRequestProvider = NativeAuthRequestProvider(config = config),
nativeAuthResponseHandler = NativeAuthResponseHandler()
nativeAuthResponseHandler = NativeAuthResponseHandler(),
requestInterceptor = requestInterceptor
),
resetPasswordInteractor = ResetPasswordInteractor(
httpClient = UrlConnectionHttpClient.getDefaultInstance(),
nativeAuthRequestProvider = NativeAuthRequestProvider(config = config),
nativeAuthResponseHandler = NativeAuthResponseHandler()
nativeAuthResponseHandler = NativeAuthResponseHandler(),
requestInterceptor = requestInterceptor
),
jitInteractor = JITInteractor(
httpClient = UrlConnectionHttpClient.getDefaultInstance(),
nativeAuthRequestProvider = NativeAuthRequestProvider(config = config),
nativeAuthResponseHandler = NativeAuthResponseHandler()
nativeAuthResponseHandler = NativeAuthResponseHandler(),
requestInterceptor = requestInterceptor
)
Comment thread
spetrescu84 marked this conversation as resolved.
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.microsoft.identity.common.java.logging.Logger
import com.microsoft.identity.common.java.nativeauth.commands.parameters.JITChallengeAuthMethodCommandParameters
import com.microsoft.identity.common.java.nativeauth.commands.parameters.JITIntrospectCommandParameters
import com.microsoft.identity.common.java.nativeauth.commands.parameters.JITContinueCommandParameters
import com.microsoft.identity.common.java.providers.oauth2.OAuth2RequestInterceptor
import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthRequestProvider
import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthResponseHandler
import com.microsoft.identity.common.java.nativeauth.providers.requests.jit.JITChallengeRequest
Expand All @@ -51,7 +52,8 @@ import com.microsoft.identity.common.java.util.ObjectMapper
class JITInteractor(
private val httpClient: UrlConnectionHttpClient,
private val nativeAuthRequestProvider: NativeAuthRequestProvider,
private val nativeAuthResponseHandler: NativeAuthResponseHandler
private val nativeAuthResponseHandler: NativeAuthResponseHandler,
private val requestInterceptor: OAuth2RequestInterceptor? = null
) {
private val TAG: String = this::class.java.simpleName

Expand Down Expand Up @@ -94,7 +96,7 @@ class JITInteractor(
)
val encodedRequest: String =
ObjectMapper.serializeObjectToFormUrlEncoded(request.parameters)
val headers = request.headers
val headers = applyInterceptorHeaders(request.requestUrl, request.headers, requestInterceptor)
val requestUrl = request.requestUrl

val response = httpClient.post(
Expand Down Expand Up @@ -169,7 +171,7 @@ class JITInteractor(
)
val encodedRequest: String =
ObjectMapper.serializeObjectToFormUrlEncoded(request.parameters)
val headers = request.headers
val headers = applyInterceptorHeaders(request.requestUrl, request.headers, requestInterceptor)
val requestUrl = request.requestUrl

val response = httpClient.post(
Expand Down Expand Up @@ -243,7 +245,7 @@ class JITInteractor(
)
val encodedRequest: String =
ObjectMapper.serializeObjectToFormUrlEncoded(request.parameters)
val headers = request.headers
val headers = applyInterceptorHeaders(request.requestUrl, request.headers, requestInterceptor)
val requestUrl = request.requestUrl

val response = httpClient.post(
Expand Down Expand Up @@ -275,4 +277,4 @@ class JITInteractor(
return result
}
//endregion
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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.nativeauth.providers.interactors

import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthHeaderValidator
import com.microsoft.identity.common.java.providers.oauth2.OAuth2RequestInterceptor
import java.net.URL

/**
* Applies additional interceptor headers to the base request headers for native auth interactors.
*
* Interceptor headers replace matching base headers regardless of casing. Interceptor headers are
* first validated and normalized to lowercase by [NativeAuthHeaderValidator], which filters out
* any non-`x-` prefixed headers and reserved prefixes (`x-ms-`, `x-client-`, `x-broker-`, `x-app-`).
* This ensures that mandatory SDK headers (e.g., `Content-Type`, `x-client-SKU`) cannot be
* overwritten by the interceptor, since they either lack the `x-` prefix or use a reserved prefix.
*
* @param requestUrl The outbound request URL.
* @param headers The base request headers.
* @param requestInterceptor Optional interceptor providing additional headers.
* @return The merged headers map with interceptor values taking precedence for valid custom headers.
*/
internal fun applyInterceptorHeaders(
requestUrl: URL,
headers: Map<String, String?>,
requestInterceptor: OAuth2RequestInterceptor?
): Map<String, String?> {
if (requestInterceptor == null) return headers

val additionalHeaders = requestInterceptor.additionalHeaders(requestUrl) ?: return headers
// For case-insensitive merge, the headers in RESERVED_PREFIXES are filtered out
val validHeaders = NativeAuthHeaderValidator.filterValidHeaders(additionalHeaders)
if (validHeaders.isEmpty()) return headers

val mergedHeaders = headers.toMutableMap()
for ((field, value) in validHeaders) {
val existingHeader = mergedHeaders.keys.firstOrNull { it.equals(field, ignoreCase = true) }
if (existingHeader != null) {
mergedHeaders.remove(existingHeader)
Comment thread
mustafamizrak marked this conversation as resolved.
}
mergedHeaders[field] = value
Comment thread
mustafamizrak marked this conversation as resolved.
}

return mergedHeaders
}
Loading
Loading