Hello everyone, my name is Liju and I am a Cloud Solutions Architect helping customers secure their cloud and hybrid identities. With this post, I would like to show how FIDO2 security key authentication for Windows sign‑in can be audited on client devices.
Recently, a customer of mine asked how they could:
- Audit each use of a FIDO2 security key on a Windows client device
- Track all PIN verification attempts on the security key, including both successful and unsuccessful attempts
- Determine which user successfully authenticated to a Windows device using a FIDO2 security key
While standard Windows logon events such as 4624 and 4625 report the user and logon type, they do not indicate whether a FIDO2 security key was used. We can find this information in the Microsoft‑Windows‑WebAuthN/Operational event log, although interpreting these events requires additional decoding and correlation.
Table of Contents
But first, let us see how Entra ID stores the information when a user registers a FIDO2 security ley as an authentication method.
Entra ID
For each user that has registered a FIDO2 security key, the keys are represented as a fido2AuthenticationMethod resource on the user object. The identifier for the key is stored with a Base64URL encoding.
In the example below the value is 7ebzDmVTSreLsJkrjm1mNA2
When a FIDO2 key is registered, an audit event is generated in Entra ID. The KeyIdentifier is stored using standard Base64 encoding.
In the example below the value is 7ebzDmVTSreLsJkrjm1mNA==
If diagnostic logging is enabled for Entra ID and if the AuditLogs are sent to a Log Analytics Workspace, this information can be queried using KQL.
AuditLogs
| where Category == "UserManagement"
| where OperationName == "Add Passkey (device-bound)"
| extend UserUPN = tostring(TargetResources[0].userPrincipalName)
| extend FIDOkeyId = tostring(TargetResources[0].displayName)
FIDO2 Security Key authentication in Windows
When a user signs in with a FIDO2 security key, Windows is trying to answer one question:
Can this authenticator (security key) prove possession of the private key associated with a registered credential for this user?
This proof is provided in the form of a WebAuthN assertion, which is a cryptographic response generated by the authenticator.
Authentication flow (high-level)
- Challenge generation
During FIDO2 authentication for a Microsoft Entra user, a challenge is generated by the relying party (for example, login.microsoft.com) and provided to the client (Windows).
- Request construction
Windows initiates a WebAuthN GetAssertion request, which is encoded using CBOR (Concise Binary Object Representation), a compact binary format used by the FIDO2 protocol.
The request contains the clientDataHash which is a hashed JSON object containing the challenge sent by Entra (along with other parameters).
- Authenticator processing
The request is sent to the authenticator using CTAP (Client to Authenticator Protocol).
The authenticator then locates a matching credential for the relying party, performs user verification if required (for example, PIN or biometric) and constructs the authenticatorData, which includes the hash of the relying party ID (rpIdHash)
The authenticator finally generates the assertion by signing (authenticatorData + clientDataHash) using the credential’s private key
- Response processing
The authenticator returns the assertion (encoded in CBOR) to Windows.
Windows then decodes the CBOR response, extracts the assertion components (credential ID, authenticatorData, signature), evaluates the result and completes the WebAuthN operation.
Mapping the steps to WebAuthN events
Before we take a look at the WebAuthN events on the Windows client, let us see how the logon process maps directly to the Event Log task categories.
|
Step |
Details |
Event entry |
|
Challenge generation |
Windows initiates authentication using a FIDO2 credential A TransactionId is created that ties all related events together. |
WebAuthN Ctap GetAssertion started (Event ID 1003) |
|
Request construction |
Windows builds the CTAP2 request to send to the key Encoded in the request are the rpId and clientDataHash. For Entra ID, the rpId is login.microsoft.com |
Cbor encode GetAssertion request (Event ID 1103) |
|
Authenticator processing |
Windows transitions from WebAuthN to the CTAP layer, and authenticator interaction begins |
Ctap GetAssertion started (Event ID 2100) |
|
Windows exchanges CTAP commands with the key This includes:
|
Ctap Usb Send Receive (Event ID 2225) | |
|
Response processing |
Authenticator returns result to Windows |
Ctap GetAssertion completed (Event ID 2102 / 2103) |
|
Windows interpret the authenticator’s response |
Cbor decode GetAssertion response (Event ID 1104) | |
|
Windows completes WebAuthN operation |
WebAuthN Ctap GetAssertion completed (Event ID 1004 / 1005) |
WebAuthN Events
Challenge generation
The WebAuthN Ctap GetAssertion started event (Event ID 1003) indicates that Windows has initiated a WebAuthN authentication operation and is beginning the process of requesting an assertion from an authenticator. This marks the start of the FIDO2 authentication flow but does not yet involve communication with the security key or indicate whether authentication will succeed.
Request construction
The Cbor encode GetAssertion request event (Event ID 1103) shows Windows encoding a targeted WebAuthN GetAssertion request.
When the Request begins with 0x02, it indicates that this is a authenticatorGetAssertion CTAP command.
Note that whether or not a credential ID is present in this event depends on the scenario. When AllowCredentialCount is greater than zero, the request includes one or more specific credential IDs (making it a “targeted” WebAuthN GetAssertion request). When it is zero, the authenticator is performing a credential discovery.
The description may be parsed to get the credential Id and will match the key identifier from Entra ID when Base64 encoded.
| The Cbor encode GetAssertion request event (Event ID 1103) is generally the most useful event for auditing each authentication attempts of a FIDO2 security key on a Windows client device. |
How to parse the CBOR-encoded request
Let us take the Cbor Encode GetAssertion Request event (ID 1103) and parse the CBOR-encoded data in its description
TransactionId: {3443b0f7-a6a2-4b1c-9026-aea3ab93f662}
RpId: login.microsoft.com
ClientDataHashAlgId: S256
ClientDataLength: 176
ClientDataHash: 0x0E3A6FC2C6941563481563AFADF439276A2280A9F59E85197478BB748E625DF3
AllowCredentialCount: 1Request: 0x02A401736C6F67696E2E6D6963726F736F66742E636F6D0258200E3A6FC2C6941563481563AFADF439276A2280A9F59E85197478BB748E625DF30381A262696450EDE6F30E65534AB78BB0992B8E6D663464747970656A7075626C69632D6B657905A1627570F5
- The first byte gives us the CTAP command.
- In this case it is 0x02 which means authenticatorGetAssertion (Client to Authenticator Protocol (CTAP))
- Everything after that first byte is the CBOR payload (A401736C6F67696E2E6D6963726F736F66742E636F6D0258200E3A6FC2C6941563481563AFADF439276A2280A9F59E85197478BB748E625DF30381A262696450EDE6F30E65534AB78BB0992B8E6D663464747970656A7075626C69632D6B657905A1627570F5).
- Note that The CBOR payload starts with A4. This means that the CBOR body is a map with 4 entries or named fields.
- If you do not want to decode the bytes by hand, a simple way to inspect the payload is to paste it into an online CBOR decoder such as CBOR Playground. The site accepts hex input and can parse it into a readable CBOR structure.
- Paste the CBOR payload into the input area. Make sure the input mode is Hex, then use Parse.
- For this example, the decoded result is:
{
1: "login.microsoft.com",
2: h'0E3A6FC2C6941563481563AFADF439276A2280A9F59E85197478BB748E625DF3',
3: [
{
"id": h'EDE6F30E65534AB78BB0992B8E6D6634',
"type": "public-key"
}
],
5: {
"up": true
}
}
- This output is still using the CTAP numeric field keys (1-5), so the next step is to translate those numbers into the field names used by the GetAssertion request based on the table at Client to Authenticator Protocol (CTAP):
- 1 = rpId
- 2 = clientDataHash
- 3 = allowList
- 5 = options
- So in plain English, the payload says:
{
"rpId": "login.microsoft.com",
"clientDataHash": "0E3A6FC2C6941563481563AFADF439276A2280A9F59E85197478BB748E625DF3",
"allowList": [
{
"id": "EDE6F30E65534AB78BB0992B8E6D6634",
"type": "public-key"
}
],
"options": {
"up": true
}
}
- For a bit more detail about each field:
- Key 1 contains the relying party ID
- Key 2 contains the 32-byte clientDataHash
- Key 3 contains an allowList array with one credential descriptor
- Key 5 contains an options map
- Inside options, up: true means user presence was requested
For details on how to decode the CBOR payload yourself see RFC 8949: Concise Binary Object Representation (CBOR)
Translating Entra key identifier to WebAuthN Credential Id
The key identifier from Entra ID when Base64 decoded will match the CredentialId in the event.
A sample PowerShell function that does this is given below:
function Convert-Base64UrlToBytes {
param(
[Parameter(Mandatory=$true)]
[string]$Base64Url
)
# Convert Base64url to normal Base64
$b64 = $Base64Url.Replace('-', '+').Replace('_', '/')
# Add padding if required
switch ($b64.Length % 4) {
2 { $b64 += "==" }
3 { $b64 += "=" }
0 { } # already aligned
1 { throw "Invalid Base64url string length" }
}
# Decode Base64 → byte array
return [Convert]::FromBase64String($b64)
}
cls
# Conversion from Base64URL encoded identifier (user object)
$bytes = Convert-Base64UrlToBytes "7ebzDmVTSreLsJkrjm1mNA2"
($bytes | ForEach-Object { $_.ToString("X2") }) -join ""
# Conversion from Base64 encoded identifier (audit log)
$bytes = Convert-Base64UrlToBytes "7ebzDmVTSreLsJkrjm1mNA=="
($bytes | ForEach-Object { $_.ToString("X2") }) -join ""
Authenticator processing
The Ctap GetAssertion started event (Event ID 2100) shows Windows starting a CTAP GetAssertion operation against a specific FIDO2 key.
Authenticator PIN Validation
All PIN attempts generate a Ctap Usb Send Receive event (Event ID 2225) where the Request starts with 0x06A401010205
- 06 means this is a PIN-related command
- If the request starts with 06A401010205 this denotes a getPinToken flag, meaning a PIN verification attempt.
If the Response starts 0x00, it indicates a Success.
Other possible values for the response field are:
- 0x31 - Incorrect PIN
- 0x33 - PIN Auth Invalid
- 0x34 - PIN Required
| Therefore, Ctap Usb Send Receive events (Event ID 2225) where the Request starts with 0x06A401010205 will report all security key PIN attempts, both successful and unsuccessful, on the client device. |
How to parse the CBOR-encoded request
Let us try and parse the CBOR-encoded data in the event’s description once again.
TransactionId: {3443b0f7-a6a2-4b1c-9026-aea3ab93f662}
Request Command: 0x90
Response Command: 0x90Request: 0x06A40101020503A5010203381820012158206F67957900E6BBEC39838F015E3F5D6918F5A8D76E401828363E51F6C256541222582027E8B11854CE667B8EE19DF3A3DEE4A3F0DB43809811C3A93F2E9C0293E466AA0650746BE172CD2402CFFCC94734BC98D16A
Response: 0x00A1025093B2EE5307CC81EA08684FEBE22D536D
- The first byte in the request (0x06) gives us the authenticatorClientPIN CTAP command (Client to Authenticator Protocol (CTAP))
- Everything after that first byte is the CBOR payload (A40101020503A5010203381820012158206F67957900E6BBEC39838F015E3F5D6918F5A8D76E401828363E51F6C256541222582027E8B11854CE667B8EE19DF3A3DEE4A3F0DB43809811C3A93F2E9C0293E466AA0650746BE172CD2402CFFCC94734BC98D16A).
- As before A4 means that the CBOR body is a map with 4 entries or named fields.
- Parsing this payload using CBOR Playground we get:
{
1: 1,
2: 5,
3: {
1: 2,
3: -25,
-1: h'6F67957900E6BBEC39838F015E3F5D6918F5A8D76E401828363E51F6C2565412',
-2: h'27E8B11854CE667B8EE19DF3A3DEE4A3F0DB43809811C3A93F2E9C0293E466AA'
},
6: h'746BE172CD2402CFFCC94734BC98D16A'
}
- Using the table at Client to Authenticator Protocol (CTAP) to translate the numeric keys we have:
- key 1 = pinUvAuthProtocol
- key 2 = subCommand
- key 3 = keyAgreement
- key 6 = pinHashEnc
- After the translation, the payload says:
{
"pinUvAuthProtocol": 1,
"subCommand": 5,
"keyAgreement": {
1: 2,
3: -25,
-1: h'6F67957900E6BBEC39838F015E3F5D6918F5A8D76E401828363E51F6C2565412',
-2: h'27E8B11854CE667B8EE19DF3A3DEE4A3F0DB43809811C3A93F2E9C0293E466AA'
},
"pinHashEnc": h'746BE172CD2402CFFCC94734BC98D16A'
}
- The information most useful for us here is "subCommand": 5, which as you can see from the second table in Client to Authenticator Protocol (CTAP) tells is a getPinToken subcommand.
In summary, when the request begins with 06 A4 01 01 02 05, it can be identified as a PIN verification attempt. The leading byte 0x06 indicates the CTAP authenticatorClientPIN command. The next byte A4 shows that the CBOR payload is a map with four fields. Within that map, the sequence 01 01 corresponds to pinUvAuthProtocol = 1, and 02 05 corresponds to subCommand = 5. In the Client PIN command set, subcommand 5 represents getPinToken, which is used during PIN verification. Together, this byte pattern reliably indicates that the operation is a PIN-based authentication step rather than a standard assertion request.
Turn on Annotate if you want the CBOR Playground site to show how each byte is interpreted.
Authenticator GetAssertion operation
Ctap Usb Send Receive events (Event ID 2225) where the Request begins with 0x02 indicate an authenticatorGetAssertion CTAP2 Operation. The encoded payload includes the RpId and ClientDataHash.
If the Response begins with 0x00 it was successful. Included in the CBOR payload is the id (Credential ID) and the rpIdHash
Response processing
The Ctap GetAssertion completed event (Event ID 2102) tells us that the authenticator successfully completed a GetAssertion operation and returned a valid signed assertion to Windows.
Included in the response payload are security key device information, status of the operation (6673746174757300 stands for status = 0), the credential used and authenticator data.
The Cbor decode GetAssertion response event (Event ID 1104) is logged when the authenticator successfully returns a WebAuthN assertion for the relying party using a particular credential.
This is one of the best events to track successful authentication because the important fields are already parsed out.
- The RpIdHash of 356C9ED4A09321B9695F1EAF918203F1B55F689DA61FBC96184C157DDA680C81 is the SHA-256 hash of “login.microsoft.com”
- A Flags value of 0x85 means 0x80 + 0x04 + 0x01
- 0x01: UP (the user was present and interacted with the key)
- 0x04: UV (user verification succeeded, which in this scenario means PIN was successfully validated)
- 0x80: ED (extension data was included in the assertion)
- The CredentialId of EDE6F30E65534AB78BB0992B8E6D6634 when Base64 encoded, will match the key identifier in Entra.
| Therefore, Cbor decode GetAssertion response events (Event ID 1104) will tell you which users successfully authenticated to the Windows device using a FIDO2 security key. |
Finally, the WebAuthN Ctap GetAssertion completed event (Event ID 1004) tells us that WebAuthN GetAssertion operation completed successfully for this TransactionId.
Tying it all together
I started out by outlining what my customer’s monitoring goals were; the table below summarizes the events recommended for monitoring:
|
What to Monitor |
Event |
Notes |
|
Each use of a FIDO2 security key on a Windows client device.
|
Cbor encode GetAssertion request (Event ID 1103) |
Filter for events where:
Parse Request for credential Id. Base64 encode credential Id to match key identifier and user in Entra ID.
|
|
All attempts, both successful and unsuccessful, when a PIN was tried to unlock a credential on the FIDO2 security key on the device.
|
Ctap Usb Send Receive (Event ID 2225) |
Filter for events where Request starts with 0x06A401010205. The Response property indicates result. |
|
Which user successfully authenticated to the Windows device using their FIDO2 security key.
|
Cbor decode GetAssertion response (Event ID 1104) |
Base64 encode credential Id to match key identifier and user in Entra ID. |
The techniques outlined in this document show how to identify individual FIDO2 credentials, track PIN verification attempts, and conclusively determine which user authenticated to a Windows device using a security key. With this approach, passwordless authentication becomes not only more secure, but also more observable and supportable in enterprise environments.