Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add check for unset correlation ID when configuring request headers #2435

Merged
merged 27 commits into from
Jul 29, 2024

Conversation

rmccahill
Copy link
Contributor

@rmccahill rmccahill commented Jun 20, 2024

If a correlation ID hasn't been configured for a request, the 'UNSET' value will be sent to the server, and the server will then return that 'UNSET' value from the response. To prevent this, the correlation ID should only be appended if it is a valid value.

Additionally, tests have been added to ensure that an 'UNSET' value isn't added to the request headers, and valid correlation IDs are being added to the request headers on a valid request.

Also, there are a few tweaks made to NativeAuthRequestHandlerTest - there are some tests where a request is configured and created, but there are no assertions made on those requests. I've added a few checks to tests that were missing validations, and some other small tweaks.

Update:
“If a correlation ID hasn't been configured for a request, the 'UNSET' value will be sent to the server, and the server will then return that 'UNSET' value from the response.” -> After experiment, this assumption is wrong. The header used on the SDK side is client-request-id not ms-client-request-id, these two values are different if they both attached in the response header. The interesting thing is that, if client sends invalid correlation id like "UNSET", the server would not return the client-request-id to the client. On the contrary, if the correlation id is valid and client sent it in the client-request-id header, the server will return it as the same value in the client-request-id header. SDK made the assumption that if no client-request-id header is sent to the server from client, the server will generate one for the client seems untrue. The correlation id is dependant on client-request-id x-ms-request-id is the request specific, it targets the Service Request Id not the CorrelationId, while x-ms-request-id is returned from the server in all times.

@rmccahill rmccahill requested a review from a team as a code owner June 20, 2024 14:29
@rmccahill rmccahill requested a review from a team as a code owner June 20, 2024 14:33
@@ -310,7 +311,11 @@ class NativeAuthRequestProvider(private val config: NativeAuthOAuth2Configuratio
//region helpers
private fun getRequestHeaders(correlationId: String): Map<String, String?> {
val headers: MutableMap<String, String?> = TreeMap()
headers[AuthenticationConstants.AAD.CLIENT_REQUEST_ID] = correlationId

if (correlationId != "UNSET") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string 'UNSET' should be used from a constant file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a look at the rest of the codebase and "UNSET" seems to be used in place instead of being used as a constant value. I'd rather stick with the convention here, given that it's a straightforward name and something that doesn't have a specific meaning or is used outside of the context of the SDK.

@@ -148,6 +151,22 @@ class NativeAuthRequestHandlerTest {
)
}

@Test
fun testSignUpStartWithUnsetCorrelationIdShouldHaveNilHeader() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testSignUpStartWithUnsetCorrelationIdShouldHaveNilHeader -> 'testSignUpStartWithUnsetCorrelationIdShouldNotHaveHeader'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The title is accurate as it is - the test passes a "UNSET" value, and in this case the header should be nil.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rmccahill can you elaborate? Why should we be sending an empty header (i.e. the REQUEST_ID header is set, but it doesn't contain a value), rather than not set the header at all?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, what you're saying seems to contradict line 316 of NativeAuthRequestProvider.kt. Or maybe we're mis-communicating :D

The way I see it there's a difference between "no header" vs. "header is set and value is null". Saurabh and I mean the first (and so I would agree that the test name should be "should not have header").

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see what you mean - in my mind 'nil header' refers to the header not being present. But I can see how that's confusing. I've updated the naming on these tests to reflect the suggested name.

@SammyO
Copy link
Contributor

SammyO commented Jun 24, 2024

@rmccahill could you remove the conditional setting of the correlation ID in AccountState and see if the multiple tokens tests still pass?

Context:

  • this issue came up when I was writing the multiple token tests, and added that statement was a quick fix to make the tests pass
  • why? in the context of tests the threading naming mechanism in DiagnosticContext doesn't work correctly, and so the correlation ID for API calls was set to UNSET by DiagnosticContext. The API returned this, and in subsequent refresh token logic an UUID was attempted to be composed based on the UNSET String. This caused an exception.
  • with the changes of this PR, this crash shouldn't be happening anymore.

@Yuki-YuXin
Copy link
Contributor

The removal and fix of the conditional setting of the correlation ID in AccountState is here AzureAD/microsoft-authentication-library-for-android#2135

@Yuki-YuXin Yuki-YuXin added the Skip-Consumers-Check Only include this if making a breaking change purposefully, and there is an MSAL/ADAL/Broker PR label Jul 3, 2024
# Conflicts:
#	common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/NativeAuthRequestProvider.kt
…nse(requestCorrelationId = requestCorrelationId),the SDK use the local correlation id rather than the header from the http response. Thus,the filer in the getHeader won't work for this issue.
if (correlationId == null) {
correlationId = UNSET;
if (correlationId == null || correlationId.equals(UNSET)) {
correlationId = UUID.randomUUID().toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also use correlationId = ""; and let CommandDispatcher.initializeDiagnosticContext
final String correlationId = StringUtil.isNullOrEmpty(requestCorrelationId) ? UUID.randomUUID().toString() : requestCorrelationId;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Yuki-YuXin if what you mention is also a solution, then why did you add this code change here? to rephrase: I agree that what CommandDispatcher.initializeDiagnosticContext does should be sufficient. So why is your code change needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the current code snippet will set the correlationId to UNSET. If the correlationId passed into CommandDispatcher.initializeDiagnosticContext is UNSET, then after correlationId = StringUtil.isNullOrEmpty(requestCorrelationId) ? UUID.randomUUID().toString() : requestCorrelationId, the correlationId would keep as "UNSET"

@@ -310,8 +311,8 @@ class NativeAuthRequestProvider(private val config: NativeAuthOAuth2Configuratio
private fun getRequestHeaders(correlationId: String): Map<String, String?> {
val headers: MutableMap<String, String?> = TreeMap()
headers[AuthenticationConstants.AAD.CLIENT_REQUEST_ID] = correlationId
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert it back because we are using the local parameters.getCorrelationId() in the http/SDK response rather than the response header CLIENT_REQUEST_ID, which means even though we filter out the UNSET in the header, it will still exist in the response.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following this. This method isn't used in responses; it's used to create requests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the reason we don't want UNSET to be sent in the http request header is because we don't want the server to response "UNSET" to us in the http response header. However, the SDK didn't use the http response header to construct the SDK response. It used the local correlation passed in or recorded in DiagnosticContext. Since we don't use the correlation id in the http response header then nothing sent in the request will affect the correlation ID of the SDK response

…r.createSignUpChallengeRequest(correlationId = "UNSET")
if (correlationId == null) {
correlationId = UNSET;
if (correlationId == null || correlationId.equals(UNSET)) {
correlationId = UUID.randomUUID().toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I right in saying:
it's okay to create a UUID here, because:

  1. we don't need to use/set the thread ID, because that's not what's being used in the request headers (i.e. when setting the request headers, we use map key DiagnosticContext.CORRELATION_ID, not DiagnosticContext.THREAD_ID
  2. CommandDispatcher.initializeDiagnosticContext() does the same; generate a UUID.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct.

@@ -310,8 +311,8 @@ class NativeAuthRequestProvider(private val config: NativeAuthOAuth2Configuratio
private fun getRequestHeaders(correlationId: String): Map<String, String?> {
val headers: MutableMap<String, String?> = TreeMap()
headers[AuthenticationConstants.AAD.CLIENT_REQUEST_ID] = correlationId
headers[AuthenticationConstants.SdkPlatformFields.PRODUCT] = LibraryInfoHelper.getLibraryName()
headers[AuthenticationConstants.SdkPlatformFields.VERSION] = LibraryInfoHelper.getLibraryVersion()
headers[AuthenticationConstants.SdkPlatformFields.PRODUCT] = DiagnosticContext.INSTANCE.requestContext[AuthenticationConstants.SdkPlatformFields.PRODUCT]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code change is wrong. And the one on the line below.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea why the changes here. If that's wrong, I will revert it back.

@@ -103,7 +103,7 @@ class SignInInteractor(
)

val rawApiResponse = nativeAuthResponseHandler.getSignInInitiateResultFromHttpResponse(
requestCorrelationId = requestCorrelationId,
requestCorrelationId = requestCorrelationId, // Question: should we use the CLIENT_REQUEST_ID header from response here?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're misunderstanding what's happening here (as did I when we talked about it yesterday).

  1. when the request is created (nativeAuthRequestProvider.createSignInInitiateRequest) getRequestHeaders() is used to compose the correlation ID, which in turn takes the correlation ID from the command parameters.
  2. when the response object is created (by converting the raw API response into a local data model), we pass the original correlation ID to the request provider. We do this because the API might not return a correlation ID, in which case we set it manually using requestCorrelationId.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, my bad.
After checking inside getSignInInitiateResultFromHttpResponse

// If the API doesn't return a correlation ID header value, use the correlation ID of the original API request
        val correlationId: String = response.getHeaderValue(AuthenticationConstants.AAD.CLIENT_REQUEST_ID, 0).let {responseCorrelationId ->

Then we definitely should have the try catch mechanism in the header.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may explain why SDK will get UNSET even when the header filter is applied,
image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the client request id only exists when client request id is valid. The client request id is different from x-ms-request-id
image

@@ -310,8 +311,8 @@ class NativeAuthRequestProvider(private val config: NativeAuthOAuth2Configuratio
private fun getRequestHeaders(correlationId: String): Map<String, String?> {
val headers: MutableMap<String, String?> = TreeMap()
headers[AuthenticationConstants.AAD.CLIENT_REQUEST_ID] = correlationId
Copy link
Contributor

@SammyO SammyO Jul 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going back to the original goal of this ticket: I still think we need a catch-all fallback here that catches any "UNSET" values and ensures we don't send it to the API (because if we do, the API will return "UNSET" back to us, which we don't want). There are 2 ways I can think of:

  1. catch "UNSET" and replace with UUID
  2. catch "UNSET" and not set correlation ID at all. This is what MSAL does. The expectation is that the API should then create a correlation ID and send it back to us.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, I keep the changes to apply the second option: "catch "UNSET" and not set correlation ID at all". The expectation could not be achieved for the reason in the PR summary. I don't think we should make changes to the NativeAuthResponseHandler part about the header in this PR. If the pipeline passes, maybe we can have a discussion later on what would be best.

@Yuki-YuXin
Copy link
Contributor

Yuki-YuXin commented Jul 19, 2024

@SammyO After talking to IOS, the UNSET correlation id seems to be only an issue on the Android side.

"In brief what native auth iOS SDK operates around the correlationID:

  • every initial actions on the public SDK interface (like signIN, getAccessToken, signUp, resetPassword) accept an optional correlationId
  • this correlation has UUID as type, so this means that when the user specifies a correlation Id, it has a valid format
    when the user doesn't not specify a correlationID, the SDK create a new one internally (just a random UUID). This is the same behaviour of MSAL Objective-C
  • this means that native auth iOS SDK always send a valid UUID string as correlation ID to eSTS."

@SammyO
Copy link
Contributor

SammyO commented Jul 22, 2024

@SammyO After talking to IOS, the UNSET correlation id seems to be only an issue on the Android side.

"In brief what native auth iOS SDK operates around the correlationID:

  • every initial actions on the public SDK interface (like signIN, getAccessToken, signUp, resetPassword) accept an optional correlationId
  • this correlation has UUID as type, so this means that when the user specifies a correlation Id, it has a valid format
    when the user doesn't not specify a correlationID, the SDK create a new one internally (just a random UUID). This is the same behaviour of MSAL Objective-C
  • this means that native auth iOS SDK always send a valid UUID string as correlation ID to eSTS."

@Yuki-YuXin I don't think that's exactly the question we want to ask the iOS team. iOS' intended behaviour that you describe is also the intended behaviour on Android. However, we need to account for situations where that intended design has a bug. The question is: if the SDK (iOS or Android) doesn't send a correlation ID to the API, does the API generate one and send it back through headers? Our assumption is yes, but our findings from Friday state the opposite.

@Yuki-YuXin Yuki-YuXin merged commit c831849 into dev Jul 29, 2024
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Skip-Consumers-Check Only include this if making a breaking change purposefully, and there is an MSAL/ADAL/Broker PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants