├── .github └── workflows │ └── sign-and-release.yaml ├── LICENSE ├── README.md └── src ├── EntraIDPasskeyHelper.psd1 ├── EntraIDPasskeyHelper.psm1 └── public ├── Get-PasskeyDeviceBoundAAGUID.ps1 └── Set-PasskeyAuthenticationMethodsPolicy.ps1 /.github/workflows/sign-and-release.yaml: -------------------------------------------------------------------------------- 1 | name: Create PowerShell module release artifacts and publish them to GitHub and PowerShell Gallery 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | CreateRelease: 8 | runs-on: windows-latest 9 | permissions: 10 | id-token: write 11 | contents: write 12 | 13 | steps: 14 | - name: Azure CLI login 15 | uses: azure/login@v1 16 | with: 17 | tenant-id: ${{ vars.TENTANT_ID }} 18 | client-id: ${{ vars.CLIENT_ID }} 19 | allow-no-subscriptions: true 20 | 21 | - name: Azure CLI get token 22 | run: | 23 | $kv_token=$(az account get-access-token --scope https://vault.azure.net/.default --query accessToken --output tsv) 24 | echo "::add-mask::$kv_token" 25 | echo "CODE_SIGN_AKV_ACCESS_TOKEN=$kv_token" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 26 | 27 | - uses: actions/checkout@v3 28 | 29 | - name: Install AzureSignTool 30 | run: dotnet tool install --no-cache --global AzureSignTool 31 | 32 | - name: Get PowerShell module version 33 | id: moduleversion 34 | shell: pwsh 35 | run: | 36 | $ModuleRoot = "$($env:GITHUB_WORKSPACE)/src" 37 | $ManfifestPath = "$($ModuleRoot)/EntraIDPasskeyHelper.psd1" 38 | if ( -not (Test-Path $ManfifestPath )) { 39 | Write-Error "Could not find PowerShell module manifest ($ManfifestPath)" 40 | throw 41 | } else { 42 | $CurrentVersion = Import-PowerShellDataFile $ManfifestPath | Select-Object -ExpandProperty ModuleVersion 43 | Add-Content -Path $env:GITHUB_OUTPUT -Value "tag=$CurrentVersion" 44 | } 45 | 46 | - name: Sign PowerShell module 47 | id: module-signing 48 | shell: pwsh 49 | run: | 50 | Get-ChildItem ${{ github.workspace }}/src -Recurse -Force -Filter *.ps* | Select-Object -ExpandProperty FullName | Out-File -FilePath ./signfiles.txt 51 | azuresigntool.exe sign --verbose ` 52 | --azure-key-vault-url "${{ secrets.CODE_SIGN_KEYVAULT }}" ` 53 | --azure-key-vault-accesstoken ${{ env.CODE_SIGN_AKV_ACCESS_TOKEN }} ` 54 | --azure-key-vault-certificate "${{ vars.CODE_SIGN_CERTIFICATENAME }}" ` 55 | --timestamp-rfc3161 "http://timestamp.digicert.com" ` 56 | --input-file-list ./signfiles.txt 57 | Copy-Item ${{ github.workspace }}/src -Recurse -Destination ${{ github.workspace }}/EntraIDPasskeyHelper/ -Force 58 | 59 | - name: Update PowerShell Module to PowerShell Gallery 60 | id: publish-to-gallery 61 | shell: pwsh 62 | run: | 63 | Publish-Module -Path ${{ github.workspace }}/EntraIDPasskeyHelper -NuGetApiKey ${{ secrets.PS_GALLERY_KEY }} 64 | 65 | - name: Build PowerShell module for GitHub 66 | id: module-creation 67 | shell: pwsh 68 | run: | 69 | Compress-Archive -Path ${{ github.workspace }}/EntraIDPasskeyHelper/* -DestinationPath ${{ github.workspace }}/EntraIDPasskeyHelper.zip 70 | 71 | - name: Create release 72 | uses: ncipollo/release-action@v1 73 | with: 74 | artifacts: "EntraIDPasskeyHelper.zip" 75 | replacesArtifacts: true 76 | allowUpdates: true 77 | generateReleaseNotes: true 78 | makeLatest: legacy 79 | prerelease: false 80 | tag: ${{ steps.moduleversion.outputs.tag }} 81 | commit: ${{ github.sha }} 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Visorian GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Entra ID device-bound passkey preview 2 | 3 | This module helps you to enable the Entra ID device-bound passkey preview feature for your organization. 4 | 5 | The Entra ID device-bound passkey preview feature allows you to use the Entra ID device-bound passkey as an authentication method for your organization. 6 | 7 | ## Installation 8 | 9 | To install the module from the [PowerShell Gallery](https://www.powershellgallery.com/packages/EntraIdPasskeyHelper), you can use the following command: 10 | 11 | ```powershell 12 | Install-Module -Name EntraIDPasskeyHelper 13 | ``` 14 | 15 | You find the module in the [PowerShell Gallery](https://www.powershellgallery.com/packages/EntraIdPasskeyHelper). 16 | 17 | [![PSGallery Version](https://img.shields.io/powershellgallery/v/EntraIDPasskeyHelper.svg?style=flat&logo=powershell&label=PSGallery%20Version)](https://www.powershellgallery.com/packages/EntraIDPasskeyHelper) [![PSGallery Downloads](https://img.shields.io/powershellgallery/dt/EntraIDPasskeyHelper.svg?style=flat&logo=powershell&label=PSGallery%20Downloads)](https://www.powershellgallery.com/packages/EntraIDPasskeyHelper) 18 | 19 | ## Usage 20 | 21 | ### Example #1 22 | 23 | This example enables the Entra ID device-bound passkey preview feature for all new Microsoft AAGUIDs while maintaining all existing AAGUIDs. 24 | It first queries all passkey device-bound AAGUIDs already present in current tenant and then sets the authentication method policy for passkeys/FIDO2. 25 | Since the parameter `-MicrosoftAAGUIDsAllowed` is set to All, all Microsoft AAGUIDs are allowed as passkeys. 26 | 27 | ```powershell 28 | # Connect to Microsoft Graph 29 | Connect-MgGraph -Scopes "AuditLog.Read.All", "Policy.ReadWrite.AuthenticationMethod", "User.Read.All", "UserAuthenticationMethod.Read.All" 30 | # Enable the Entra ID device-bound passkey preview feature for all new Microsoft AAGUIDs while maintaining all existing AAGUIDs 31 | Get-PasskeyDeviceBoundAAGUID | Set-PasskeyAuthenticationMethodsPolicy -MicrosoftAAGUIDsAllowed All 32 | ``` 33 | 34 | ### Example #2 35 | 36 | In this example, the Entra ID device-bound passkey preview feature is enabled, while maintaining all existing AAGUIDs but only allowing a subset of the new Microsoft AAGUIDs. 37 | 38 | ```powershell 39 | # Connect to Microsoft Graph 40 | Connect-MgGraph -Scopes "AuditLog.Read.All", "Policy.ReadWrite.AuthenticationMethod", "User.Read.All", "UserAuthenticationMethod.Read.All" 41 | # Enable the Entra ID device-bound passkey preview feature for Android AAGUIDs while maintaining all existing AAGUIDs 42 | Get-PasskeyDeviceBoundAAGUID | Set-PasskeyAuthenticationMethodsPolicy -MicrosoftAAGUIDsAllowed 'Android' -OverwriteExistingAAGUIDs 43 | ``` 44 | 45 | ### Example #3 46 | 47 | If you would like to configure the authentication policy method yourself, you can use the following example to gather information about all currently registered FIDO2 security keys. 48 | 49 | ```powershell 50 | # Connect to Microsoft Graph 51 | Connect-MgGraph -Scopes "AuditLog.Read.All", "User.Read.All", "UserAuthenticationMethod.Read.All" -DeviceCode -NoWelcome 52 | # Gather information about all currently registered FIDO2 security keys 53 | Get-PasskeyDeviceBoundAAGUID 54 | ``` 55 | 56 | ## Known limitations 57 | 58 | If the tenant is not licensed with Entra ID P1 or P2 the Microsoft Graph endpoint 'reports/authenticationMethods/userRegistrationDetails' is not available. 59 | In this case all users are enumerated and check for authentication methods. 60 | 61 | This can be a very slow process if you have thousands of users. 62 | -------------------------------------------------------------------------------- /src/EntraIDPasskeyHelper.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'EntraIDPasskeyHelper' 3 | # 4 | # Generated by: Fabian Bader 5 | # 6 | # Generated on: 2/18/2023 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'EntraIDPasskeyHelper.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.0.3' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '2cbdc418-77fb-46aa-986c-7c59fa934b75' 22 | 23 | # Author of this module 24 | Author = 'Fabian Bader' 25 | 26 | # Company or vendor of this module 27 | CompanyName = '' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) Fabian Bader. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Helper module manage the Entra ID device-bound passkey preview feature for your organization.' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | # PowerShellVersion = '' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # ClrVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = @( 73 | 'Get-PasskeyDeviceBoundAAGUID', 74 | 'Set-PasskeyAuthenticationMethodsPolicy' 75 | ) 76 | 77 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 78 | CmdletsToExport = '*' 79 | 80 | # Variables to export from this module 81 | VariablesToExport = '*' 82 | 83 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 84 | AliasesToExport = '*' 85 | 86 | # DSC resources to export from this module 87 | # DscResourcesToExport = @() 88 | 89 | # List of all modules packaged with this module 90 | # ModuleList = @() 91 | 92 | # List of all files packaged with this module 93 | # FileList = @() 94 | 95 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 96 | PrivateData = @{ 97 | 98 | PSData = @{ 99 | 100 | # Tags applied to this module. These help with module discovery in online galleries. 101 | Tags = @("EntraID", "Passkey", "FIDO2", "Authentication", "Microsoft365", "AzureAD") 102 | 103 | # A URL to the license for this module. 104 | # LicenseUri = '' 105 | 106 | # A URL to the main website for this project. 107 | ProjectUri = 'https://github.com/f-bader/EntraIDPasskeyHelper' 108 | 109 | # A URL to an icon representing this module. 110 | # IconUri = '' 111 | 112 | # ReleaseNotes of this module 113 | # ReleaseNotes = '' 114 | 115 | # Prerelease string of this module 116 | # Prerelease = '' 117 | 118 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 119 | # RequireLicenseAcceptance = $false 120 | 121 | # External dependent modules of this module 122 | # ExternalModuleDependencies = @() 123 | 124 | } # End of PSData hashtable 125 | 126 | } # End of PrivateData hashtable 127 | 128 | # HelpInfo URI of this module 129 | # HelpInfoURI = '' 130 | 131 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 132 | # DefaultCommandPrefix = '' 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/EntraIDPasskeyHelper.psm1: -------------------------------------------------------------------------------- 1 | # Import private and public scripts and expose the public ones 2 | $privateScripts = @(Get-ChildItem -Path "$PSScriptRoot\private" -Recurse -Filter "*.ps1" | Sort-Object Name ) 3 | $publicScripts = @(Get-ChildItem -Path "$PSScriptRoot\public" -Recurse -Filter "*.ps1" | Sort-Object Name ) 4 | 5 | foreach ($script in @($privateScripts + $publicScripts)) { 6 | Write-Verbose $script 7 | try { 8 | . $script.FullName 9 | Write-Verbose -Message ("Imported function {0}" -f $script) 10 | } catch { 11 | Write-Error -Message ("Failed to import function {0}: {1}" -f $script, $_) 12 | } 13 | } 14 | 15 | Export-ModuleMember -Function $publicScripts.BaseName 16 | -------------------------------------------------------------------------------- /src/public/Get-PasskeyDeviceBoundAAGUID.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Get the AAGUIDs of all passkeys that are registered in the tenant. 4 | 5 | .DESCRIPTION 6 | Get the AAGUIDs of all passkeys that are registered in the tenant. 7 | 8 | .EXAMPLE 9 | # Connect-MgGraph -Scopes "AuditLog.Read.All", "Policy.ReadWrite.AuthenticationMethod", "User.Read.All", "UserAuthenticationMethod.Read.All" 10 | # Get-PasskeyDeviceBoundAAGUID 11 | 12 | This example gets the AAGUIDs of all passkeys that are registered in the tenant. 13 | 14 | .NOTES 15 | Read more about the Entra ID passkey preview at https://cloudbrothers.info/passkeyPreview 16 | #> 17 | function Get-PasskeyDeviceBoundAAGUID { 18 | [CmdletBinding()] 19 | param () 20 | 21 | $ReturnValue = [System.Collections.Generic.List[System.Object]]::new() 22 | 23 | Write-Verbose "Getting AAGUIDs of all passkeys that are registered in the tenant..." 24 | 25 | $NextUri = "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?`$filter=methodsRegistered/any(x:x eq 'passKeyDeviceBound')&`$select=id" 26 | try { 27 | do { 28 | Write-Progress "Enumerating all user that have passkeys registered" -PercentComplete -1 29 | $Result = Invoke-MgGraphRequest -Uri $NextUri -Verbose:$false 30 | $NextUri = $Result['@odata.nextLink'] 31 | $Result['value'] | ForEach-Object { 32 | $ReturnValue.Add($_) | Out-Null 33 | } 34 | } while (-not [string]::IsNullOrWhiteSpace($NextUri) ) 35 | } catch { 36 | if ($_ -match "Authentication_RequestFromNonPremiumTenantOrB2CTenant") { 37 | Write-Warning "The Microsoft Graph API endpoint 'reports/authenticationMethods/userRegistrationDetails' requires an Entra ID Premium P1 or P2 license." 38 | Write-Warning "Fallback to get a list of all users in the tenant and enumerate their FIDO2 methods instead. This may be very slow." 39 | $UseFullEnumeration = $true 40 | } else { 41 | throw "Failed to get current list of passkey device-bound users. Error: $_" 42 | } 43 | } 44 | 45 | if ($UseFullEnumeration) { 46 | Write-Verbose "Fallback to get a list of all users in the tenant and enumerate their FIDO2 methods instead. This may be very slow." 47 | $NextUri = "https://graph.microsoft.com/beta/users?`$filter=accountEnabled+eq+true&`$select=id&`$top=999" 48 | try { 49 | do { 50 | Write-Progress "Enumerating all users" -PercentComplete -1 51 | $Result = Invoke-MgGraphRequest -Uri $NextUri -Verbose:$false 52 | $NextUri = $Result['@odata.nextLink'] 53 | $Result['value'] | ForEach-Object { 54 | $ReturnValue.Add($_) 55 | } 56 | } while (-not [string]::IsNullOrWhiteSpace($NextUri) ) 57 | } catch { 58 | throw "Failed to get current list of passkey device-bound users. Error: $_" 59 | } 60 | } 61 | 62 | Write-Verbose "Found $($ReturnValue.Count) passkey device-bound users" 63 | 64 | try { 65 | if ($PSVersionTable.PSEdition -eq "Core") { 66 | [array]$PassKeyDeviceBoundUsers = $ReturnValue | Select-Object -ExpandProperty id 67 | } else { 68 | [array]$PassKeyDeviceBoundUsers = $ReturnValue | ForEach-Object { $_.id } 69 | } 70 | $PassKeyDeviceBoundAAGUIDs = [System.Collections.Generic.List[System.Object]]::new() 71 | 72 | $MgBatchSize = 20 73 | for ($i = 0; $i -lt $PassKeyDeviceBoundUsers.Count; $i += $MgBatchSize) { 74 | Write-Progress "Getting AAGUIDs of all enumerated users" -PercentComplete ($i / $PassKeyDeviceBoundUsers.Count * 100) 75 | $LastItem = $i + $MgBatchSize - 1 76 | if ($LastItem -ge $PassKeyDeviceBoundUsers.Count) { $LastItem = $PassKeyDeviceBoundUsers.Length } 77 | 78 | # Create a batch request for all users in the current batch 79 | $id = 0 80 | $Requests = $PassKeyDeviceBoundUsers[$i..($LastItem)] | ForEach-Object { 81 | [PSCustomObject]@{ 82 | 'Id' = ++$id 83 | 'Method' = 'GET' 84 | 'Url' = "/users/$($_)/authentication/fido2Methods" 85 | } 86 | } 87 | 88 | # Send the batch request 89 | $requestParams = @{ 90 | 'Method' = 'Post' 91 | 'Uri' = 'https://graph.microsoft.com/v1.0/$batch' 92 | 'ContentType' = 'application/json' 93 | 'Body' = @{ 94 | 'requests' = @($requests) 95 | } | ConvertTo-Json 96 | } 97 | $Result = Invoke-MgGraphRequest @requestParams 98 | # Invoke-MgGraphRequest deserializes request to a hashtable 99 | if ($PSVersionTable.PSEdition -eq "Core") { 100 | $Result.responses | Where-Object status -EQ 200 | Select-Object -ExpandProperty body | Select-Object -ExpandProperty value | ForEach-Object { 101 | $PassKeyDeviceBoundAAGUIDs.Add([pscustomobject]$_) 102 | } 103 | } else { 104 | $Result.responses | Where-Object status -EQ 200 | ForEach-Object { $_.body.value } | ForEach-Object { 105 | $PassKeyDeviceBoundAAGUIDs.Add([pscustomobject]$_) 106 | } 107 | } 108 | } 109 | } catch { 110 | throw "Failed to get current list of passkey device-bound users. Error: $_" 111 | } 112 | 113 | Write-Verbose "Found $($PassKeyDeviceBoundAAGUIDs | Select-Object AAGuid -Unique | Measure-Object | Select-Object -ExpandProperty Count ) unique AAGUIDs" 114 | 115 | $PassKeyDeviceBoundAAGUIDs | Select-Object aaGuid, Model -Unique | Sort-Object Model 116 | } 117 | -------------------------------------------------------------------------------- /src/public/Set-PasskeyAuthenticationMethodsPolicy.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Set the authentication method policy for passkeys/FIDO2 and enable the Entra ID passkey preview feature. 4 | 5 | .DESCRIPTION 6 | This function sets the authentication method policy for passkeys/FIDO2 and enables the Entra ID passkey preview feature for your organization. 7 | The Entra ID passkey preview feature allows you to use passkey as an authentication method for your organization. 8 | 9 | Read more about the Entra ID passkey preview at https://cloudbrothers.info/passkeyPreview 10 | 11 | .PARAMETER EnforceAttestation 12 | Enforce attestation for FIDO2 authenticators. 13 | This means that the authenticator must be verified by the manufacturer before it can be used for authentication. 14 | In the public preview, this setting is not supported and must be set to $false. 15 | 16 | Default value is $false. 17 | 18 | .PARAMETER AAGUIDsAllowed 19 | List of AAGUIDs that are allowed to be used as passkeys. AAGUIDs are unique identifiers for FIDO2 authenticators. 20 | 21 | Default value is an empty array. 22 | 23 | .PARAMETER MicrosoftAAGUIDsAllowed 24 | All = All passkeys from Microsoft are allowed. This includes all Microsoft Authenticator device-bound passkeys. 25 | iOS = 90a3ccdf-635c-4729-a248-9b709135078f - iOS Microsoft Authenticator device bound passkey 26 | Android = de1e552d-db1d-4423-a619-566b625cdc84 - Android Microsoft Authenticator device bound passkey 27 | 28 | Default value is "All". 29 | 30 | .PARAMETER OverwriteExistingAAGUIDs 31 | Overwrite existing AAGUIDs with the ones provided in the AAGUIDsAllowed parameter. 32 | If this switch is not used, the AAGUIDs provided in the AAGUIDsAllowed parameter will be added to the existing list of allowed AAGUIDs. 33 | 34 | Default value is $false. 35 | 36 | .EXAMPLE 37 | # Connect-MgGraph -Scopes "UserAuthenticationMethod.Read.All" 38 | # Set-PasskeyAuthenticationMethodsPolicy -EnforceAttestation $false -AAGUIDsAllowed "77010bd7-212a-4fc9-b236-d2ca5e9d4084" -MicrosoftAAGUIDsAllowed "All" 39 | 40 | This example sets the authentication method policy for passkeys/FIDO2 to allow all Microsoft AAGUIDs and the custom AAGUID 77010bd7-212a-4fc9-b236-d2ca5e9d4084 (Feitian BioPass FIDO2 Authenticator). 41 | 42 | .NOTES 43 | Read more about the Entra ID passkey preview at https://cloudbrothers.info/passkeyPreview 44 | #> 45 | function Set-PasskeyAuthenticationMethodsPolicy { 46 | [CmdletBinding(SupportsShouldProcess = $True)] 47 | param ( 48 | [Parameter()] 49 | [bool]$EnforceAttestation = $false, 50 | 51 | [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] 52 | [ValidateScript( 53 | { $_ | ForEach-Object { $_ -match "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" } } 54 | )] 55 | [Alias("aaGuid")] 56 | [string[]]$AAGUIDsAllowed, 57 | 58 | [Parameter()] 59 | [ValidateSet("All", "iOS", "Android")] 60 | [string[]]$MicrosoftAAGUIDsAllowed = "All", 61 | 62 | [Parameter()] 63 | [switch]$OverwriteExistingAAGUIDs 64 | ) 65 | 66 | begin { 67 | # Initialize the array list 68 | $AAGUIDsAllowedFromPipeline = [System.Collections.ArrayList]::new() 69 | } 70 | 71 | process { 72 | # Add the AAGUIDs from the pipeline to the array list 73 | $AAGUIDsAllowed | ForEach-Object { 74 | $AAGUIDsAllowedFromPipeline.Add($_) | Out-Null 75 | } 76 | } 77 | 78 | end { 79 | 80 | if ($AAGUIDsAllowedFromPipeline -ne $null) { 81 | $TmpAAGUIDsAllowed = $AAGUIDsAllowedFromPipeline 82 | } else { 83 | $TmpAAGUIDsAllowed = $AAGUIDsAllowed 84 | } 85 | 86 | $AvailablePasskeyAAGUIDs = @{ 87 | "iOS" = "90a3ccdf-635c-4729-a248-9b709135078f" 88 | "Android" = "de1e552d-db1d-4423-a619-566b625cdc84" 89 | } 90 | 91 | $SelectedAAGUIDs = [System.Collections.ArrayList]::new() 92 | if ( $MicrosoftAAGUIDsAllowed -contains "All" ) { 93 | Write-Verbose "All Microsoft AAGUIDs are allowed" 94 | $SelectedAAGUIDs.AddRange($AvailablePasskeyAAGUIDs.Values) | Out-Null 95 | } else { 96 | foreach ( $AAGUID in $MicrosoftAAGUIDsAllowed ) { 97 | Write-Verbose "Adding Microsoft AAGUID $AAGUID to the allowed list" 98 | $SelectedAAGUIDs.Add($AvailablePasskeyAAGUIDs[$AAGUID]) | Out-Null 99 | } 100 | } 101 | 102 | foreach ( $AAGUID in $TmpAAGUIDsAllowed ) { 103 | Write-Verbose "Adding custom AAGUID $AAGUID to the allowed list" 104 | $SelectedAAGUIDs.Add($AAGUID) | Out-Null 105 | } 106 | 107 | if ($EnforceAttestation -eq $false) { 108 | Write-Warning "EnforceAttestation is set to $false because it's required for the public preview. Please make sure if this is the desired setting for your organization." 109 | } 110 | 111 | try { 112 | $CurrentConfiguration = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/Fido2" -Method Get -Verbose:$false 113 | } catch { 114 | Write-Error "Failed to get current configuration. Error: $_" 115 | throw 116 | } 117 | 118 | if ($CurrentConfiguration.state -eq "disabled") { 119 | Write-Verbose "Authentication method policy for passkeys/FIDO2 is currently disabled." 120 | } else { 121 | Write-Verbose "Authentication method policy for passkeys/FIDO2 is currently enabled." 122 | } 123 | 124 | Write-Verbose "Attestation enforcement is $($CurrentConfiguration.isAttestationEnforced)" 125 | 126 | if ( $CurrentConfiguration.keyRestrictions.isEnforced ) { 127 | Write-Verbose "Enforcement type is $($CurrentConfiguration.keyRestrictions.enforcementType)" 128 | if ($CurrentConfiguration.keyRestrictions.enforcementType -eq "allow") { 129 | Write-Verbose "Currently allowed AAGUIDs are $($CurrentConfiguration.keyRestrictions.aaGuids -join ', ')" 130 | } else { 131 | Write-Verbose "Currently disallowed AAGUIDs are $($CurrentConfiguration.keyRestrictions.aaGuids -join ', ')" 132 | } 133 | } 134 | 135 | if ( $OverwriteExistingAAGUIDs ) { 136 | Write-Verbose "Overwriting existing AAGUIDs" 137 | $SelectedAAGUIDs = $SelectedAAGUIDs | Sort-Object -Unique 138 | } else { 139 | Write-Verbose "Adding new AAGUIDs to existing list" 140 | $SelectedAAGUIDs.AddRange($CurrentConfiguration.keyRestrictions.aaGuids) | Out-Null 141 | $SelectedAAGUIDs = $SelectedAAGUIDs | Sort-Object -Unique 142 | } 143 | 144 | $NewConfiguration = @{ 145 | "@odata.type" = "#microsoft.graph.fido2AuthenticationMethodConfiguration" 146 | "isAttestationEnforced" = $EnforceAttestation 147 | "isSelfServiceRegistrationAllowed" = $true 148 | "keyRestrictions" = @{ 149 | "aaGuids" = $SelectedAAGUIDs 150 | "enforcementType" = "allow" 151 | "isEnforced" = $true 152 | } 153 | "state" = "enabled" 154 | } 155 | 156 | $NewConfiguration | ConvertTo-Json -Depth 10 | Write-Verbose 157 | 158 | try { 159 | Write-Output "Setting new authentication method policy for passkeys/FIDO2" 160 | if ($PSCmdlet.ShouldProcess("https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/Fido2")) { 161 | Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/Fido2" -Method Patch -Body ($NewConfiguration | ConvertTo-Json -Depth 10) -Verbose:$false | Out-Null 162 | } 163 | Write-Output "Successfully set new configuration. Have a nice day!" 164 | } catch { 165 | Write-Error "Failed to set new configuration. Error: $_" 166 | throw 167 | } 168 | } 169 | } --------------------------------------------------------------------------------