DSInternals.Passkeys.Entra.psm1

# Needed for [Microsoft.Graph.PowerShell.Models.MicrosoftGraphFido2AuthenticationMethod] type
Import-Module -Name Microsoft.Graph.Identity.SignIns -ErrorAction Stop

<#
.SYNOPSIS
Retrieves creation options required to generate and register a Microsoft Entra ID compatible passkey.
 
.DESCRIPTION
Retrieves a server-issued challenge and the associated WebAuthn parameters needed to register (attest) a new passkey for the specified Microsoft Entra ID user. The returned object can be piped to New-Passkey to drive the local authenticator and then to Register-EntraPasskey to complete enrollment.
 
Requires an active Microsoft Graph connection (Connect-MgGraph) with the UserAuthenticationMethod.ReadWrite.All scope. The caller must be an administrator; self-service is not supported.
 
.PARAMETER UserId
The unique identifier of the user. Either the object id (GUID) or UPN.
 
.PARAMETER ChallengeTimeout
Overrides the timeout of the server-generated challenge returned in the request. The default value is 5 minutes, with the accepted range being between 5 minutes and 30 days.
 
.EXAMPLE
Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All'
Get-EntraPasskeyRegistrationOptions -UserId 'AdeleV@contoso.com'
 
Fetches default creation options for the specified user.
 
.EXAMPLE
Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All'
Get-EntraPasskeyRegistrationOptions -UserId 'AdeleV@contoso.com' -ChallengeTimeout (New-TimeSpan -Minutes 10)
 
Fetches creation options with an extended 10-minute challenge timeout, useful when the user needs more time to complete the authenticator ceremony.
 
.EXAMPLE
Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All'
Get-EntraPasskeyRegistrationOptions -UserId 'AdeleV@contoso.com' | New-Passkey | Register-EntraPasskey -UserId 'AdeleV@contoso.com' -DisplayName 'YubiKey 5 Nano'
 
Performs end-to-end passkey registration in Microsoft Entra ID in a single pipeline.
 
.NOTES
Self-service operations aren't supported for Entra ID.
 
.LINK
Register-EntraPasskey
 
.LINK
New-Passkey
 
.LINK
https://learn.microsoft.com/en-us/graph/api/fido2authenticationmethod-creationoptions
 
#>

function Get-EntraPasskeyRegistrationOptions
{
    [Alias('Get-PasskeyRegistrationOptions')]
    [OutputType([DSInternals.Win32.WebAuthn.PublicKeyCredentialCreationOptions])]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({
            # Microsoft Graph accepts either a UPN (email-like) or an object ID (GUID) as the user identifier.
            # The regex follows RFC 5322's local-part character set, then '@', then a dot-separated domain of LDH labels.
            return $PSItem -match "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" -or $true -eq [guid]::TryParse($PSItem, $([ref][guid]::Empty))
        })]
        [Alias('User')]
        [string] $UserId,

        [Parameter(Mandatory = $false)]
        [ValidateScript({
            if ($PSItem -is [TimeSpan]) {
                [timespan] $min = New-TimeSpan -Minutes 5
                [timespan] $max = New-TimeSpan -Days 30
                return $PSItem -ge $min -and $PSItem -le $max
            }
            else {
                throw "Parameter must be a TimeSpan object."
            }
        })]
        [Alias('Timeout')]
        [timespan] $ChallengeTimeout = (New-TimeSpan -Minutes 5)
    )
    try {
        # Generate the user-specific URL, e.g., https://graph.microsoft.com/beta/users/af4cf208-16e0-429d-b574-2a09c5f30dea/authentication/fido2Methods/creationOptions
        [string] $credentialOptionsUrl = '/beta/users/{0}/authentication/fido2Methods/creationOptions' -f [uri]::EscapeDataString($UserId)

        [string] $response = Invoke-MgGraphRequest `
            -Method GET `
            -Uri $credentialOptionsUrl `
            -Body @{ challengeTimeoutInMinutes = $ChallengeTimeout.TotalMinutes } `
            -OutputType Json

        # Parse JSON response and return the inner WebAuthn public-key options
        return [DSInternals.Win32.WebAuthn.Entra.MicrosoftGraphWebauthnCredentialCreationOptions]::Create($response).PublicKeyOptions
    }
    catch {
        throw $PSItem
    }
}

<#
.SYNOPSIS
Registers a new passkey in Microsoft Entra ID.
 
.DESCRIPTION
Registers a new passkey for the specified user in Microsoft Entra ID.
 
When called without the -Passkey parameter, this cmdlet performs the full registration flow: it requests a challenge from Entra ID, drives the local authenticator (which prompts the system passkey UI), and submits the attestation to complete enrollment.
When called with -Passkey, it submits a previously produced attestation, which is useful when the credential ceremony was run separately (e.g. via New-Passkey in a pipeline).
 
Requires an active Microsoft Graph connection (Connect-MgGraph) with the UserAuthenticationMethod.ReadWrite.All scope. The caller must be an administrator; self-service is not supported.
 
.PARAMETER UserId
The unique identifier of the user. Either the object id (GUID) or UPN.
 
.PARAMETER ChallengeTimeout
Overrides the timeout of the server-generated challenge returned in the request. The default value is 5 minutes, with the accepted range being between 5 minutes and 30 days.
 
.PARAMETER Passkey
The attestation credential produced by the local WebAuthn authenticator (e.g. via New-Passkey). Wrapped into a Microsoft Graph attestation response before being submitted.
 
.PARAMETER DisplayName
Custom name given to the registered passkey.
 
.EXAMPLE
Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All'
Register-EntraPasskey -UserId 'AdeleV@contoso.com' -DisplayName 'YubiKey 5 Nano'
 
Performs the full registration ceremony in one step: fetches creation options, prompts the local authenticator, and submits the attestation to Entra ID with the given display name.
 
.EXAMPLE
Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All'
Register-EntraPasskey -UserId 'AdeleV@contoso.com' -DisplayName 'YubiKey 5 Nano' -ChallengeTimeout (New-TimeSpan -Minutes 10)
 
Registers a passkey using an extended 10-minute challenge timeout, giving the user more time to complete the authenticator ceremony.
 
.EXAMPLE
Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All'
Get-EntraPasskeyRegistrationOptions -UserId 'AdeleV@contoso.com' | New-Passkey | Register-EntraPasskey -UserId 'AdeleV@contoso.com' -DisplayName 'YubiKey 5 Nano'
 
Splits the registration into explicit pipeline stages: fetch options, create the credential locally, and submit the attestation. Equivalent to the single-step form but lets the caller inspect intermediate values.
 
.NOTES
Self-service operations aren't supported for Entra ID.
 
.LINK
Get-EntraPasskeyRegistrationOptions
 
.LINK
New-Passkey
 
.LINK
https://learn.microsoft.com/en-us/graph/api/authentication-post-fido2methods
 
#>

function Register-EntraPasskey
{
    [CmdletBinding(DefaultParameterSetName = 'New')]
    [Alias('Register-Passkey', 'Register-MgUserAuthenticationFido2Method')]
    [OutputType([Microsoft.Graph.PowerShell.Models.MicrosoftGraphFido2AuthenticationMethod])]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'New')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Existing')]
        [ValidateScript({
            # Microsoft Graph accepts either a UPN (email-like) or an object ID (GUID) as the user identifier.
            # The regex follows RFC 5322's local-part character set, then '@', then a dot-separated domain of LDH labels.
            return $PSItem -match "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" -or $true -eq [guid]::TryParse($PSItem, $([ref][guid]::Empty))
        })]
        [Alias('User')]
        [string] $UserId,

        [Parameter(Mandatory = $true, ParameterSetName = 'Existing', ValueFromPipeline = $true)]
        [Alias('Attestation')]
        [DSInternals.Win32.WebAuthn.AttestationPublicKeyCredential]
        $Passkey,

        [Parameter(Mandatory = $true, ParameterSetName = 'New')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Existing')]
        [ValidateLength(1, 30)]
        [string] $DisplayName,

        [Parameter(Mandatory = $false, ParameterSetName = 'New')]
        [ValidateScript({
            if ($PSItem -is [TimeSpan]) {
                [timespan] $min = New-TimeSpan -Minutes 5
                [timespan] $max = New-TimeSpan -Minutes 43200
                return $PSItem -ge $min -and $PSItem -le $max
            }
            else {
                throw "Parameter must be a TimeSpan object."
            }
        })]
        [Alias('Timeout')]
        [TimeSpan] $ChallengeTimeout = (New-TimeSpan -Minutes 5)
    )

    process {
        try
        {
            if ($PSCmdlet.ParameterSetName -eq 'New')
            {
                [DSInternals.Win32.WebAuthn.PublicKeyCredentialCreationOptions] $options =
                    Get-EntraPasskeyRegistrationOptions -UserId $UserId -ChallengeTimeout $ChallengeTimeout -ErrorAction Stop

                [DSInternals.Win32.WebAuthn.WebAuthnApi] $api = [DSInternals.Win32.WebAuthn.WebAuthnApi]::new()
                $Passkey = $api.AuthenticatorMakeCredential($options)
            }

            [DSInternals.Win32.WebAuthn.Entra.MicrosoftGraphWebauthnAttestationResponse] $attestationResponse =
                [DSInternals.Win32.WebAuthn.Entra.MicrosoftGraphWebauthnAttestationResponse]::new($Passkey, $DisplayName)

            # Generate the user-specific URL, e.g., https://graph.microsoft.com/beta/users/af4cf208-16e0-429d-b574-2a09c5f30dea/authentication/fido2Methods
            [string] $registrationUrl = '/beta/users/{0}/authentication/fido2Methods' -f [uri]::EscapeDataString($UserId)

            [string] $response = Invoke-MgGraphRequest `
                                    -Method POST `
                                    -Uri $registrationUrl `
                                    -OutputType Json `
                                    -ContentType 'application/json' `
                                    -Body $attestationResponse.ToString()

            return [Microsoft.Graph.PowerShell.Models.MicrosoftGraphFido2AuthenticationMethod]::FromJsonString($response)
        }
        catch
        {
            throw $PSItem
        }
    }
}

# Functions and aliases are filtered by FunctionsToExport / AliasesToExport in the parent manifest.