Public/Get-YubiKeys.ps1

<#
.SYNOPSIS
Retrieves and reports on Yubico device-bound passkey authenticators (YubiKeys) by user in Microsoft Entra ID.
 
.DESCRIPTION
This Cmdlet connects to Microsoft Entra ID and generates a report of registered Yubico device-bound passkey
authenticators (YubiKeys) for specified users or all accessible users in the tenant. For each listed user,
the report includes firmware version(s) and authenticator nickname(s) of each associated YubiKey.
 
.EXAMPLE
Get-YubiKeys -User "bob@contoso.com"
Get YubiKey information for a single user
 
.EXAMPLE
Get-YubiKeys -User "bob@contoso.com", "alice@contoso.com"
Get YubiKey information for multiple users
 
.EXAMPLE
Get-YubiKeys -All
Get YubiKey information for all users you have access to in the tenant
 
.NOTES
- Requires the Microsoft.Graph PowerShell module
- Requires appropriate permissions in Entra ID (User.Read.All, Device.Read.All)
- Will prompt for authentication if not already connected to Microsoft Graph
 
.LINK
https://github.com/JMarkstrom/entraYK
 
.LINK
https://yubi.co/aaguids
#>


# Function with parameters
function Get-YubiKeys {
    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName = "SpecificUsers", HelpMessage = "Specify one or more users by their UPN.")]
        [string[]] $User,

        [Parameter(ParameterSetName = "AllUsers",
                  HelpMessage = "Get YubiKey information for all users you have access to in the tenant.")]
        [switch]
        $All
    )

    begin {

        # Call the function to check and install the required module(s)
        Resolve-ModuleDependencies -ModuleName "Microsoft.Graph.Authentication"

        # Define required scopes
        $requiredScopes = @("User.Read.All", "Device.Read.All","UserAuthenticationMethod.Read.All")

    }

    process {

        # Check if already connected with correct permissions
        $context = Get-MgContext
        $needsAuth = $false
        $needsBrowserAuth = $false

        if ($null -eq $context) {
            $needsAuth = $true
            $needsBrowserAuth = $true
        } else {
            # Check if all required scopes are present (case-insensitive comparison)
            $missingScopes = $requiredScopes | Where-Object { $context.Scopes -notcontains $_ }
            if ($missingScopes.Count -gt 0) {
                $needsAuth = $true
                Write-Host "Missing required scopes: $($missingScopes -join ', ')" -ForegroundColor Yellow
            }
        }

        # Handle authentication
        if ($needsAuth) {
            # Show prompt before any authentication attempts
            Clear-Host
            Write-Host "NOTE: Authenticate in the browser to obtain the required permissions (press any key to continue)" -ForegroundColor Yellow
            [System.Console]::ReadKey() > $null
            Clear-Host

            Write-Debug "Attempting to refresh existing token"
            try {
                # First try silent token refresh
                Connect-MgGraph -Scopes $requiredScopes -NoWelcome -ErrorAction Stop
                
                # Verify connection was successful
                $context = Get-MgContext
                if ($null -eq $context) {
                    $needsBrowserAuth = $true
                }
            } catch {
                Write-Debug "Silent token refresh failed, will attempt browser authentication"
                $needsBrowserAuth = $true
            }

            if ($needsBrowserAuth) {
                try {
                    Connect-MgGraph -Scopes $requiredScopes -NoWelcome # -UseDeviceAuthentication
                    
                    # Verify final connection status
                    $context = Get-MgContext
                    if ($null -eq $context) {
                        throw "Authentication failed! Please ensure you approve all requested permissions."
                    }
                } catch {
                    Write-Error "Failed to authenticate: $_"
                    throw
                }
            }
        } else {
            Write-Debug "Already authenticated with the required permissions."
        }

        # Get information about YubiKeys from helper function
        $YubiKeyInfo = Get-YubiKeyInfo
        
        Clear-Host
        if ($PSCmdlet.ParameterSetName -eq "SpecificUsers") {
            # Ensure proper validation of each user
            $users = foreach ($userUpn in $User) {
                if ($userUpn -and $userUpn.Trim() -ne "") {
                    try {
                        Write-Verbose "Retrieving user: $userUpn"
                        Get-MgUser -Filter "userPrincipalName eq '$userUpn'"
                    } catch {
                        Write-Warning "Failed to retrieve user: $userUpn. Error: $_"
                    }
                }
            }
                    
            if (-not $users) {
                Write-Warning "No specified users were found."
                return
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq "AllUsers") {
            
            # Warn the user when using -All parameter
            Write-Warning "You are about to retrieve YubiKey information for ALL accessible users in the tenant.`n"
            
            $proceed = $false
            do {
                $ans = Read-Host "Proceed? (Y/n)"
                switch ($ans.ToLower()) {
                    {$_ -eq 'y' -or $_ -eq ''} {
                        Write-Debug "`nProceeding with retrieval of selected users..."
                        $proceed = $true
                        break
                    }
                    'n' {
                        Clear-Host
                        Write-Host "Operation cancelled by user." -ForegroundColor Red
                        return
                    }
                    default {
                        Write-Output "Invalid input. Please enter 'y' or 'n'."
                    }
                }
            } while (-not $proceed)

            $users = Get-MgUser -All:$true -PageSize 100
            if (-not $users) {
                Clear-Host
                Write-Warning "No users found in tenant."
                return
            }
        } else {
            Write-Warning "Either -User or -All parameter must be specified."
            return
        }
        
        $totalUsers = $users.Count
        
        # Initialize an array to store the report data
        $report = @()
        $counter = 0

        Clear-Host

        # Loop through each user in the tenant
        foreach ($currentUser in $users) {
            $counter++
            $percentComplete = [math]::Round(($counter / $totalUsers) * 100)
            Write-Progress -Activity "Now processing" -Status "user ($counter of $totalUsers)" -PercentComplete $percentComplete

            try {
                $authMethods = Get-MgUserAuthenticationMethod -UserId $currentUser.Id
                $hasFido2 = $false
                
                if ($authMethods) {
                    foreach ($method in $authMethods) {
                        $odataType = $method.AdditionalProperties['@odata.type']

                        if ($odataType -eq "#microsoft.graph.fido2AuthenticationMethod") {
                            $hasFido2 = $true
                            $aaguid = $method.AdditionalProperties['aaGuid']
                            $nickname = $method.AdditionalProperties['displayName']
                            # Get only the first matching firmware for this AAGUID
                            $info = $YubiKeyInfo | Where-Object { $_.'AAGUID' -eq $aaguid } | Select-Object -First 1
                            $firmware = $info.Firmware
                            $certification = $info.Certification

                            $report += [pscustomobject]@{
                                UPN      = $currentUser.UserPrincipalName
                                Nickname = $nickname
                                Firmware = $firmware
                                Certification = $certification
                            }
                        }
                    }
                }
                
                # Add user to report with empty strings if they don't have any FIDO2 methods
                if (-not $hasFido2) {
                    $report += [pscustomobject]@{
                        UPN      = $currentUser.UserPrincipalName
                        Nickname = ""
                        Firmware = ""
                        Certification = ""
                    }
                }
            } catch {
                Write-Error "Failed processing user $($currentUser.UserPrincipalName): $_"
            }
        }

        # Clear screen and display summary
        Clear-Host
        Write-Host "*************************************************************************************************" -ForegroundColor Yellow
        Write-Host "YUBIKEY ASSIGNMENTS REPORT" -ForegroundColor Yellow
        Write-Host "*************************************************************************************************" -ForegroundColor Yellow
        Write-Host "ℹ️ A user with multiple assignments will appear multiple times!" -ForegroundColor Yellow
        # Return the report
        $report | Format-Table -AutoSize
        Write-Host ""

        # Disconnect from Microsoft Graph
        try {
            Write-Debug "Disconnecting from Microsoft Graph..."
            Disconnect-MgGraph | Out-Null  # Suppress output
            Write-Debug "Disconnected from Microsoft Graph"
        } catch {
            Write-Warning "Failed to disconnect from Microsoft Graph: $_"
        }
    }
}