Skip to content

Commit

Permalink
Make userHandle response field optional (#2560)
Browse files Browse the repository at this point in the history
### Summary
The Windows app team found a bug while testing cross-device passkey
auth: https://microsoft.visualstudio.com/OS/_workitems/edit/55529057

In the logs, I see that the first attempt fails due to the userHandle
attribute being missing in the response from CredMan. We currently have
this attribute set as required, since the server side had mentioned that
this attribute was required for them, but looking at the official
WebAuthN spec, userHandle is only required if allowCredentials is NOT
provided. If it is (like in the first attempt) then userHandle is
optional:
https://w3c.github.io/webauthn/#iface-authenticatorassertionresponse

I've confirmed with Authenticator that they always do send userHandle,
and our current thinking is that some middle layer (since this is
cross-device auth) might be removing that value from the final response.
I'm not sure why this might be the case, but this is perfectly fine
according to the WebAuthN spec. Therefore, we will follow the spec as
well and will not block the response on userHandle.
  • Loading branch information
melissaahn authored Jan 2, 2025
1 parent e29fcf1 commit 9954c92
Show file tree
Hide file tree
Showing 3 changed files with 21 additions and 5 deletions.
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ vNext
- [MINOR] Add Child Spans for Interactive Span (#2516)
- [MINOR] For MSAL CPP flows, match exact claims when deleting AT with intersecting scopes (#2548)
- [MINOR] Replace Deprecated Keystore API for Android 28+ (#2558)
- [PATCH] Make userHandle response field optional (#2560)

Version 18.2.2
----------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@ import com.microsoft.identity.common.java.constants.FidoConstants.Companion.WEBA
import com.microsoft.identity.common.java.constants.FidoConstants.Companion.WEBAUTHN_RESPONSE_ID_JSON_KEY
import com.microsoft.identity.common.java.constants.FidoConstants.Companion.WEBAUTHN_RESPONSE_SIGNATURE_JSON_KEY
import com.microsoft.identity.common.java.constants.FidoConstants.Companion.WEBAUTHN_RESPONSE_USER_HANDLE_JSON_KEY
import com.microsoft.identity.common.logging.Logger
import org.json.JSONException
import org.json.JSONObject

/**
* A utility class to help convert to and from strings in WebAuthn json format.
*/
class WebAuthnJsonUtil {
companion object {
companion object {

private val TAG = WebAuthnJsonUtil::class.simpleName.toString()

/**
* Takes applicable parameters and creates a string representation of
* PublicKeyCredentialRequestOptionsJSON (https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson)
Expand Down Expand Up @@ -74,6 +79,7 @@ class WebAuthnJsonUtil {
* @throws JSONException if a value is not present that should be.
*/
fun extractAuthenticatorAssertionResponseJson(fullResponseJson : String): String {
val methodTag = "$TAG:extractAuthenticatorAssertionResponseJson"
val fullResponseJsonObject = JSONObject(fullResponseJson);
val authResponseJsonObject = fullResponseJsonObject
.getJSONObject(FidoConstants.WEBAUTHN_AUTHENTICATION_ASSERTION_RESPONSE_JSON_KEY)
Expand All @@ -87,8 +93,17 @@ class WebAuthnJsonUtil {
WEBAUTHN_RESPONSE_CLIENT_DATA_JSON_KEY))
assertionResult.put(WEBAUTHN_RESPONSE_SIGNATURE_JSON_KEY, authResponseJsonObject.get(
WEBAUTHN_RESPONSE_SIGNATURE_JSON_KEY))
assertionResult.put(WEBAUTHN_RESPONSE_USER_HANDLE_JSON_KEY, authResponseJsonObject.get(
WEBAUTHN_RESPONSE_USER_HANDLE_JSON_KEY))
// UserHandle is optional if allowCredentials was provided in the request (username flow).
if (authResponseJsonObject.isNull(WEBAUTHN_RESPONSE_USER_HANDLE_JSON_KEY)) {
Logger.info(methodTag, "UserHandle not found in assertion response.")
} else {
Logger.info(methodTag, "UserHandle was included in assertion response.")
assertionResult.put(
WEBAUTHN_RESPONSE_USER_HANDLE_JSON_KEY, authResponseJsonObject.get(
WEBAUTHN_RESPONSE_USER_HANDLE_JSON_KEY
)
)
}
return assertionResult.toString()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ class WebAuthnJsonUtilTest {
val attestationObject = "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUj5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGRdAAAAAAAAAAAAAAAAAAAAAAAAAAAAEChA3rcWXFH4p4VYumWuZ2WlAQIDJiABIVgg4RqZaJyaC24Pf4tT-8ONIZ5_Elddf3dNotGOx81jj3siWCAWXS6Lz70hvC2g8hwoLllOwlsbYatNkO2uYFO-eJID6A"

val expectedAuthenticationAssertionResponseJsonAllFieldsFilled = """{"authenticatorData":"$authenticatorData","clientDataJSON":"$clientDataJSON","id":"$idAssertionResponse","signature":"$signature","userHandle":"$userHandle"}"""
val expectedAuthenticationAssertionResponseOnlyRequiredFields = """{"authenticatorData":"$authenticatorData","clientDataJSON":"$clientDataJSON","id":"$idAssertionResponse","signature":"$signature","userHandle":"$userHandle"}"""
val expectedAuthenticationAssertionResponseOnlyRequiredFields = """{"authenticatorData":"$authenticatorData","clientDataJSON":"$clientDataJSON","id":"$idAssertionResponse","signature":"$signature"}"""

val demoAuthenticationResponseJsonAllFieldsFilled = """{"authenticatorAttachment":"$authenticatorAttachment","clientExtensionResults":{},"id":"KEDetxZcUfinhVi6Za5nZQ","rawId":"KEDetxZcUfinhVi6Za5nZQ","response":{"attestationObject":"$attestationObject","authenticatorData":"$authenticatorData","clientDataJSON":"$clientDataJSON","id":"$idAssertionResponse","signature":"$signature","userHandle":"$userHandle"},"type":"public-key"}"""
val demoAuthenticationResponseJsonOnlyRequiredFields = """{"clientExtensionResults":{},"id":"KEDetxZcUfinhVi6Za5nZQ","rawId":"KEDetxZcUfinhVi6Za5nZQ","response":{"attestationObject":null,"authenticatorData":"$authenticatorData","clientDataJSON":"$clientDataJSON","id":"$idAssertionResponse","signature":"$signature","userHandle":"$userHandle"},"type":"public-key"}"""
val demoAuthenticationResponseJsonOnlyRequiredFields = """{"clientExtensionResults":{},"id":"KEDetxZcUfinhVi6Za5nZQ","rawId":"KEDetxZcUfinhVi6Za5nZQ","response":{"attestationObject":null,"authenticatorData":"$authenticatorData","clientDataJSON":"$clientDataJSON","id":"$idAssertionResponse","signature":"$signature"},"type":"public-key"}"""
val demoAuthenticationResponseJsonMissingSignature = """{"clientExtensionResults":{},"id":"KEDetxZcUfinhVi6Za5nZQ","rawId":"KEDetxZcUfinhVi6Za5nZQ","response":{"attestationObject":null,"authenticatorData":"$authenticatorData","clientDataJSON":"$clientDataJSON","id":"$idAssertionResponse","userHandle":"$userHandle"},"type":"public-key"}"""

// Demo JWT from https://jwt.io/
Expand Down

0 comments on commit 9954c92

Please sign in to comment.