module.psm1

#requires -Modules 'Microsoft.PowerShell.Utility' -Assembly System.Security -Version 3.0
<#
    Functions for the Authorization against Azure Active Directory
    Copyright Chris Speers, Avanade 2016
    No warranty implied or expressed, side effects include insomnia, runny nose, vomiting
#>


#Winforms Sync Context
$Script:FormSyncContext=[hashtable]::Synchronized(@{})
#Discovery Key Cache
$Script:DiscoveryKeyCache=@{}


#Default Native Client Redirect Uri
$Script:DefaultNativeRedirectUri="urn:ietf:wg:oauth:2.0:oob"

$Script:DefaultAuthUrl='https://login.microsoftonline.com'
$Script:DefaultTokenApiVersion="2.1"
$Script:WSFedUserRealmApiVersion="1.0"
#Fungible resource id for ASM and ARM
$Script:DefaultAzureManagementUri='https://management.core.windows.net/'
$Script:DefaultARMUri='https://management.azure.com/'
$Script:DefaultAzurePortalUri='https://portal.azure.com/'
$Script:DefaultAzureVaultUri='https://vault.azure.net'
#Fungible resource id for Legacy and Modern Graph API
$Script:DefaultAzureADGraphUri='https://graph.windows.net/'
$Script:DefaultMicrosoftGraphUri='https://graph.microsoft.com/'
#Native client id for ASM,ARM,graph
$Script:DefaultPowershellClientId='1950a258-227b-4e31-a9cf-717495945fc2'
$Script:DefaultAzurePortalClientId='c44b4083-3bb0-49c1-b47d-974e53cbdf3c'
$Script:DefaultAzureActiveDirectoryClientId="00000002-0000-0000-c000-000000000000"
$Script:DefaultVisualStudioClientId="872cd9fa-d31f-45e0-9eab-6e460a02d1f1"
$Script:DefaultAzureCLIClientId="04b07795-8ddb-461a-bbee-02f9e1bf7b46"

$Global:Azure_ActiveDirectory_WellKnownResourceIds=@{
    ARM=$Script:DefaultARMUri;
    ASM=$Script:DefaultAzureManagementUri;
    Portal=$Script:DefaultAzurePortalUri;
    Vault=$Script:DefaultAzureVaultUri;
    Graph=$Script:DefaultAzureADGraphUri;
    MicrosoftGraph=$Script:DefaultMicrosoftGraphUri;
}
$Global:Azure_ActiveDirectory_WellKnownClientIds=@{
    PowershellARM=$Script:DefaultPowershellClientId;
    Portal=$Script:DefaultAzurePortalClientId;
}
$Global:Azure_ActiveDirectory_WellKnownConnections=New-Object psobject -Property @{
    ARM=@{Resource=$Global:Azure_ActiveDirectory_WellKnownResourceIds['ASM'];ClientId=$Global:Azure_ActiveDirectory_WellKnownClientIds['PowershellARM']}
    Portal=@{Resource=$Global:Azure_ActiveDirectory_WellKnownResourceIds['Portal'];ClientId=$Global:Azure_ActiveDirectory_WellKnownClientIds['Portal']}
    Vault=@{Resource=$Global:Azure_ActiveDirectory_WellKnownResourceIds['Vault'];ClientId=$Global:Azure_ActiveDirectory_WellKnownClientIds['PowershellARM']}
    Graph=@{Resource=$Global:Azure_ActiveDirectory_WellKnownResourceIds['Graph'];ClientId=$Global:Azure_ActiveDirectory_WellKnownClientIds['PowershellARM']}
}

$Global:Azure_ActiveDirectory_Defaults=New-Object psobject -Property @{
    DefaultUri=$Script:DefaultAuthUrl;
    DefaultRedirectUri=$Script:DefaultNativeRedirectUri;
    Connections=$Global:Azure_ActiveDirectory_WellKnownConnections;
}
#endregion

$Script:OauthClientAssertionType='urn:ietf:params:oauth:client-assertion-type:jwt-bearer'

#region SAML Constants
$Script:Saml1AssertionType="urn:oasis:names:tc:SAML:1.0:assertion"
$Script:Saml2AssertionType="urn:oasis:names:tc:SAML:2.0:assertion"
$Script:SamlBearer11TokenType="urn:ietf:params:oauth:grant-type:saml1_1-bearer"
$Script:SamlBearer20TokenType = "urn:ietf:params:oauth:grant-type:saml2-bearer";
#TODO:OAuth OnBehalfOf
$Script:JwtBearerTokenType = "urn:ietf:params:oauth:grant-type:jwt-bearer";

#region STS Envelope
$Script:WSTrustSoapEnvelopeTemplate=@"
    <s:Envelope xmlns:s='http://www.w3.org/2003/05/soap-envelope'
                xmlns:a='http://www.w3.org/2005/08/addressing'
                xmlns:u='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'>
        <s:Header>
        <a:Action s:mustUnderstand='1'>http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue</a:Action>
        <a:messageID>urn:uuid:{2}</a:messageID>
        <a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo>
        <a:To s:mustUnderstand='1'>{3}</a:To>
        <o:Security s:mustUnderstand='1' xmlns:o='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'>
            <u:Timestamp u:Id='_0'>
                <u:Created>{4}</u:Created>
                <u:Expires>{5}</u:Expires>
            </u:Timestamp>
            <o:UsernameToken u:Id='uuid-{2}'>
                <o:Username>{0}</o:Username>
                <o:Password>{1}</o:Password>
            </o:UsernameToken>
        </o:Security>
        </s:Header>
        <s:Body>
        <trust:RequestSecurityToken xmlns:trust='http://docs.oasis-open.org/ws-sx/ws-trust/200512'>
        <wsp:AppliesTo xmlns:wsp='http://schemas.xmlsoap.org/ws/2004/09/policy'>
        <a:EndpointReference>
        <a:Address>urn:federation:MicrosoftOnline</a:Address>
        </a:EndpointReference>
        </wsp:AppliesTo>
        <trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType>
        <trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
        </trust:RequestSecurityToken>
        </s:Body>
    </s:Envelope>
"@

#endregion

#region Helper methods

<#
    .SYNOPSIS
        Converts a Unix Timestamp to DateTime
    .PARAMETER UnixTime
        The Unix Timestamp to be converted
#>

Function ConvertFromUnixTime
{
    [OutputType([System.DateTime])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,ValueFromPipeline=$true)]
        [double[]]$UnixTime
    )
    foreach ($item in $UnixTime) {
        $epoch = New-Object System.DateTime(1970, 1, 1, 0, 0, 0, 0)
        $normaltime=$epoch.AddSeconds($item)
        Write-Output $normaltime        
    }
}

<#
    .SYNOPSIS
        Converts a DateTime to a Unix Timestamp
    .PARAMETER DateTime
        The DateTime to be converted
#>

Function ConvertToUnixTime
{
    [OutputType([System.Double])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,ValueFromPipeline=$true)]
        [datetime[]]$DateTime
    )
    PROCESS
    {
        foreach ($item in $DateTime) {
            $epoch = New-Object System.DateTime(1970, 1, 1, 0, 0, 0, 0);
            $delta = $item - $epoch;
            $unixtime=[Math]::Floor($delta.TotalSeconds)
            Write-Output $unixtime        
        }        
    }
}

<#
    .SYNOPSIS
        Removes Base64 Url Padding from a string
    .PARAMETER Data
        The Input String
#>

Function RemoveBase64UrlPaddingFromString
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [String[]]$Data
    )
    PROCESS
    {
        foreach ($item in $Data)
        {
            $UnpaddedData=$item.Replace('-', '+').Replace('_', '/')
            switch ($item.Length % 4)
            {
                0 { break }
                2 { $UnpaddedData += '==' }
                3 { $UnpaddedData += '=' }
                default { throw New-Object ArgumentException('data') }
            }
            Write-Output $UnpaddedData
        }
    }
}

<#
    .SYNOPSIS
        Adds Base64 Url Padding to a string
    .PARAMETER Data
        The Input String
#>

Function AddBase64UrlPaddingToString
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [String[]]$Data
    )
    PROCESS
    {
        foreach ($item in $Data)
        {
            $CleanedInput=$item.Split('=')|Select-Object -First 1
            #$CleanedInput=$CleanedInput.Replace('-','+').Replace('_','/')
            $CleanedInput=$CleanedInput.Replace('+','-').Replace('/','_')
            Write-Output $CleanedInput
        }
    }
}

Function GetRsaCryptoProvider
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true)]
        [System.Security.Cryptography.RSACryptoServiceProvider]
        $RsaProvider
    )

    if($RsaProvider.CspKeyContainerInfo.ProviderType -in 1,12)
    {
        $csp=New-Object System.Security.Cryptography.CspParameters
        $csp.KeyNumber=$RsaProvider.CspKeyContainerInfo.KeyNumber
        $csp.KeyContainerName=$RsaProvider.CspKeyContainerInfo.KeyContainerName
        if($RsaProvider.CspKeyContainerInfo.MachineKeyStore)
        {
            $csp.Flags=[System.Security.Cryptography.CspProviderFlags]::UseMachineKeyStore
        }
        $csp.Flags=$csp.Flags -bor [System.Security.Cryptography.CspProviderFlags]::UseExistingKey
        $csp.ProviderType=24
        $NewRsaProvider=New-Object System.Security.Cryptography.RSACryptoServiceProvider($csp)
        Write-Output $NewRsaProvider
    }
    else
    {
        Write-Output $RsaProvider
    }
}

Function GetCertificateHash
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2[]]
        $Certificate
    )
    BEGIN
    {

    }
    PROCESS
    {
        foreach ($Cert in $Certificate)
        {
            $Signature=[System.Convert]::ToBase64String($Cert.GetCertHash())
            Write-Output $Signature
        }
    }
    END
    {

    }
}

Function NewClientAssertion
{
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$Audience,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [String]$ClientId,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [String]$JwtId=([Guid]::NewGuid().ToString()),
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [datetime]$Expires=($NotBefore.AddMinutes(60)),
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [DateTime]$NotBefore=([DateTime]::UtcNow)
    )

    $JwtHeaders=[ordered]@{
        "alg"="RS256";
        "x5t"=($Certificate|GetCertificateHash|AddBase64UrlPaddingToString)
    }
    $JwtPayload=[ordered]@{
        "aud"=$Audience.AbsoluteUri;
        "exp"= (ConvertToUnixTime -DateTime $Expires);
        "iss"=$ClientId;
        "jti"=$JwtId;
        "nbf"= (ConvertToUnixTime -DateTime $NotBefore);
        "sub"=$ClientId;
    }

    $HeaderJson=$JwtHeaders|ConvertTo-Json -Compress
    $PayloadJson=$JwtPayload|ConvertTo-Json -Compress

    $HeaderBytes=[System.Text.Encoding]::UTF8.GetBytes($HeaderJson)
    $HeaderString=[Convert]::ToBase64String($HeaderBytes)|AddBase64UrlPaddingToString
    $PayloadBytes=[System.Text.Encoding]::UTF8.GetBytes($PayloadJson)
    $PayloadString=[Convert]::ToBase64String($PayloadBytes)|AddBase64UrlPaddingToString

    $EncodedAssertion="$HeaderString.$PayloadString"
    Write-Output $EncodedAssertion
}

<#
    .SYNOPSIS
        Creates a new WinForm hosting a WebBrowser control to navigate to a URI
    .PARAMETER NavigateUri
        The Uri for the WebBrowser to navigate upon form load
    .PARAMETER FormTitle
        The default title on the form
    .PARAMETER FormWidth
        The form width
    .PARAMETER FormHeight
        The form height
#>

Function CreateWebForm
{
    [OutputType([System.Windows.Forms.Form])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$NavigateUri,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [string]$FormTitle,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [int]$FormWidth,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [int]$FormHeight,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Windows.Forms.FormStartPosition]$StartupPosition=[System.Windows.Forms.FormStartPosition]::CenterParent
    )

    $Script:FormSyncContext=[hashtable]::Synchronized(@{})
    #New WinForm
    $FormSize = New-Object System.Drawing.Size($FormWidth,$FormHeight)
    $objForm = New-Object System.Windows.Forms.Form

    #Navigate on load
    $OnFormLoad={
        param
        (
            [Parameter()]
            [object]
            $sender,
            [Parameter()]
            [System.EventArgs]
            $e
        )
        Write-Verbose "Loaded! Navigating to $($Script:FormSyncContext.NavigateUri)"
        $Script:FormSyncContext.Browser.Navigate($Script:FormSyncContext.NavigateUri,$false)
    }
    $objForm.add_Load($OnFormLoad)

    #Add a web browser control
    $webBrowser=New-Object System.Windows.Forms.WebBrowser
    $webBrowser.Location=(New-Object System.Drawing.Point(0,0))
    $webBrowser.MinimumSize=(New-Object System.Drawing.Size(20, 20))
    $webBrowser.Dock=[System.Windows.Forms.DockStyle]::Fill
    $webBrowser.Name="WebBrowser"

    #$objForm.StartPosition = "CenterScreen"
    $objForm.AutoScaleMode=[System.Windows.Forms.AutoScaleMode]::Font
    $objForm.AutoScaleDimensions=New-Object System.Drawing.SizeF(6.0,13.0)
    $objForm.ClientSize=$FormSize
    $objForm.Controls.Add($webBrowser)
    $objForm.Text = $FormTitle

    [System.Windows.Forms.Application]::EnableVisualStyles()

    #Put these on the sync context
    $Script:FormSyncContext.Form=$objForm
    $Script:FormSyncContext.Browser=$webBrowser
    $Script:FormSyncContext.NavigateUri=$NavigateUri.AbsoluteUri
    Write-Output $objForm
}

<#
    .SYNOPSIS
        Evaluates the WSFederation Metadata for the IntegratedAuth and UsernamePassword endpoints
    .PARAMETER MexDocument
        The WSFederation Metadata Document
#>

Function GetMexPolicies
{
    [CmdletBinding(ConfirmImpact='None')]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [System.Xml.XmlDocument]$MexDocument
    )

    $MexPolicies=@{}

    foreach ($policy in $MexDocument.definitions.Policy)
    {
        if($policy.ExactlyOne -eq $null)
        {
            continue
        }
        else
        {
            Write-Verbose "[GetMexPolicies] Examining Policy $($policy.Id)"
            $AllElement=$policy.ExactlyOne.All
            $NegElem=$AllElement.NegotiateAuthentication

            if($NegElem -ne $null)
            {
                Write-Verbose "[GetMexPolicies] IntegratedAuth policy $($policy.Id) Added."
                $MexPolicies.Add("#$($policy.Id)",(New-Object PSObject -Property @{Id=$policy.Id;AuthType=0}))
            }

            $SupTokenElem=$AllElement.SignedEncryptedSupportingTokens
            if($SupTokenElem -eq $null)
            {
                continue
            }

            $SupTokenPolicyElem=$SupTokenElem.Policy
            if($SupTokenPolicyElem -eq $null)
            {
                continue
            }

            $UserNameElem=$SupTokenPolicyElem.UsernameToken
            if($UserNameElem -eq $null -or $UserNameElem.Policy -eq $null -or $UserNameElem.Policy.WssUsernameToken10 -eq $null)
            {
                continue
            }
            $MexPolicies.Add("#$($policy.Id)",(New-Object PSObject -Property @{Id=$policy.Id;AuthType=1}))
            Write-Verbose "[GetMexPolicies] Username/Password Policy $($policy.Id) Added."
        }
    }
    Write-Output $MexPolicies
}

<#
    .SYNOPSIS
        Retrieves the bindings from the WSFederation metadata
    .PARAMETER MexDocument
        The WSFederation Metadata Document
#>

Function GetMexBindings
{
    [CmdletBinding(ConfirmImpact='None')]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [System.Xml.XmlDocument]$MexDocument
    )

    $MexBindings=@{}

    $MexPolicies=GetMexPolicies -MexDocument $MexDocument
    Write-Verbose "[GetMexBindings] Found $($MexPolicies.Count) Policies"

    foreach ($item in $MexDocument.definitions.binding)
    {
        Write-Verbose "[GetMexBindings] Examining Binding $($item.name)"
        $ItemName=$item.name
        if([String]::IsNullOrEmpty($ItemName))
        {
            continue
        }
        $PolicyRefNode=$item.PolicyReference

        if($PolicyRefNode -eq $null)
        {
            continue
        }
        $ItemUri=$PolicyRefNode.URI
        Write-Verbose "[GetMexBindings] Examining Policy Reference $ItemUri"
        if([String]::IsNullOrEmpty($ItemUri) -or $MexBindings.ContainsKey($ItemUri))
        {
            continue
        }

        $OperationNode=$item.operation
        if($OperationNode -eq $null)
        {
            continue
        }

        $OperationSubNode=$OperationNode.operation
        if($OperationSubNode -eq $null)
        {
            continue
        }

        if([String]::IsNullOrEmpty($OperationSubNode.soapAction))
        {
            continue
        }

        $BindingNode=$item.binding
        if($BindingNode -eq $null)
        {
            continue
        }

        if([String]::IsNullOrEmpty($BindingNode.transport))
        {
            continue
        }

        $MexPolicy=$MexPolicies[$ItemUri]
        if($MexPolicy -eq $null)
        {
            continue
        }

        $MexBindings.Add($ItemName,$MexPolicy)
        Write-Verbose "[GetMexBindings] Binding $ItemUri - $ItemName Added."
    }

    Write-Output $MexBindings
}

<#
    .SYNOPSIS
        Evaluates the response envelope for SAML assertion tokens
    .PARAMETER StsResponse
        The SOAP Envelope from the STS
#>

Function GetSecurityTokensFromEnvelope
{
    [OutputType([PSObject[]])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Xml.XmlDocument[]]$StsResponse
    )
    PROCESS
    {
        foreach ($item in $StsResponse)
        {
            $Tokens=@()
            $EnvelopeBody=$StsResponse.Envelope.Body
            $TokenResponseCollection=$EnvelopeBody.RequestSecurityTokenResponseCollection
            if($TokenResponseCollection -ne $null)
            {
                foreach ($TokenResponse in $TokenResponseCollection.RequestSecurityTokenResponse)
                {
                    $TokenTypeId=$TokenResponse.TokenType
                    $RequestedToken=$TokenResponse.RequestedSecurityToken
                    $TokenAssertion=$RequestedToken.Assertion.OuterXml
                    if($TokenTypeId -eq $Script:Saml1AssertionType)
                    {
                        $AssertionType=$Script:SamlBearer11TokenType
                    }
                    elseif($TokenTypeId -eq $Script:Saml2AssertionType)
                    {
                        $AssertionType=$Script:SamlBearer20TokenType
                    }
                    #We will default to 2.0 like
                    else
                    {
                        $AssertionType=$Script:SamlBearer20TokenType
                    }
                    $Token=New-Object psobject -Property @{
                        AssertionType=$AssertionType;
                        TokenType=$TokenTypeId;
                        Token=$TokenAssertion;
                    }
                    $Tokens+=$Token
                }
            }
            Write-Output $Tokens            
        }        
    }
}

Function GetAzureADUserRealm
{
    [OutputType([System.Management.Automation.PSCustomObject])]
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [System.String[]]$UserPrincipalName,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationEndpoint=$Script:DefaultAuthUrl
    )
    PROCESS
    {
        foreach ($item in $UserPrincipalName) {
            $RealmUriBuilder=New-Object System.UriBuilder($AuthorizationEndpoint)
            $RealmUriBuilder.Path="/common/UserRealm"
            $RealmUriBuilder.Query="api-version=2.1&user=$item"
            $RealmDetails=Invoke-RestMethod -Uri $RealmUriBuilder.Uri -ContentType "application/json" -ErrorAction Stop
            Write-Output $RealmDetails
        }        
    }
}

Function GetWSFedBindings
{
    [OutputType([System.Collections.Hashtable])]
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Xml.XmlDocument[]]$MexDocument
    )
    PROCESS
    {
        foreach ($item in $MexDocument)
        {
            $MexPolicyBindings=GetMexBindings -MexDocument $item
            Write-Verbose "[GetWSFedBindings] Found $($MexPolicyBindings.Count) binding(s)"
            foreach($port in $item.definitions.service.port)
            {
                $BindingName=$port.binding
                if([String]::IsNullOrEmpty($BindingName))
                {
                    continue
                }
                $uri=$BindingName.Split(':',2)|Select-Object -Last 1
                Write-Debug "[GetWSFedBindings] Examining Port:$uri"
                if($MexPolicyBindings[$uri] -eq $null)
                {
                    continue
                }
                $EndpointNode=$port.EndpointReference
                if($EndpointNode -eq $null)
                {
                    continue
                }
                $AddressNode=$EndpointNode.Address
                if($AddressNode -eq $null)
                {
                    continue
                }
                Write-Verbose "[GetWSFedBindings] Adding Url:$AddressNode for item $uri"
                $EndpointUri=New-Object System.Uri($AddressNode)
                $MexPolicyBindings[$uri]|Add-Member -MemberType NoteProperty -Name Url -Value $EndpointUri
            }
            Write-Verbose "Found $($MexPolicyBindings.Count) binding(s)."
            Write-Output $MexPolicyBindings            
        }
    }
}

<#
    .SYNOPSIS
        Evaluates a WSFed metadata document for the specified authentication type
    .PARAMETER MexDocument
        The WSFed metadata
    .PARAMETER AuthType
        The desired authentication type
#>

Function GetWSFedEndpoint
{
    [CmdletBinding(ConfirmImpact='None')]
    [OutputType([System.Uri])]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Xml.XmlDocument]$MexDocument,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [ValidateSet("IntegratedAuth","UsernamePassword")]
        [System.String]$AuthType
    )
    $DesiredAuth=0
    if($AuthType -eq "IntegratedAuth")
    {
        $DesiredAuth=0
    }
    else
    {
        $DesiredAuth=1
    }

    $MexBindings=GetWSFedBindings -MexDocument $MexDocument
    Write-Verbose "[GetWSFedEndpoint] Examining Metadata Bindings..."
    foreach ($BindingId in $MexBindings.Keys)
    {
        Write-Debug "[GetWSFedEndpoint] Examining Binding $($BindingId)"
        $Binding=$MexBindings[$BindingId]
        if($Binding.AuthType -eq $DesiredAuth)
        {
            Write-Debug "[GetWSFedEndpoint] Endpoint:$Binding.Url is a match!"
            Write-Output $Binding.Url
        }
    }
}

<#
    .SYNOPSIS
        Issues a request to the security token service and evaluates SAML assertion tokens
    .PARAMETER AuthUri
        The security token service URI
    .PARAMETER Credential
        The credential to use for authentication
#>

Function GetWSTrustResponse
{
    [OutputType([PSObject])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthUri,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [pscredential]$Credential,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [String]$SoapEnvelopeTemplate=$Script:WSTrustSoapEnvelopeTemplate,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [Int32]$LengthInMinutes=10
    )

    $Now=[DateTime]::UtcNow
    $UUID=[Guid]::NewGuid()
    $UserName=$Credential.UserName
    $Password=$Credential.GetNetworkCredential().Password
    Write-Verbose "[GetWSTrustResponse] Executing SOAP Action against $AuthUri"
    $StartTime=$Now.ToString("yyyy'-'MM'-'ddTHH':'mm':'ss'Z'")
    $EndTime=(($Now.AddMinutes($LengthInMinutes)).ToString("yyyy'-'MM'-'ddTHH':'mm':'ss'Z'"))
    $AuthSoapEnvelope=($SoapEnvelopeTemplate -f $UserName,$Password,$UUID,$AuthUri.AbsoluteUri,$StartTime,$EndTime)
    Write-Verbose "[GetWSTrustResponse] Retrieving STS SOAP Response with Validity $StartTime to $EndTime"
    $Headers=@{SOAPAction=''}
    $result=Invoke-RestMethod -Uri $AuthUri -Headers $Headers -Body $AuthSoapEnvelope -Method Post -ContentType "application/soap+xml" -ErrorAction Stop

    Write-Verbose "[GetWSTrustResponse] Evaluating Response Envelope"
    $StsTokens=GetSecurityTokensFromEnvelope -StsResponse $result
    $WSFedResponse=$StsTokens|Where-Object{$_.TokenType -eq $Script:Saml2AssertionType}
    if ($WSFedResponse -eq $null) {
        $WSFedResponse=$StsTokens|Where-Object{$_.TokenType -eq $Script:Saml1AssertionType}
    }
    if ($WSFedResponse -eq $null) {
        throw "Unable to create a User Assertion"
    }
    Write-Output $WSFedResponse
}

<#
    .SYNOPSIS
        Retrieves an OAuth User Assertion token from the specified username password endpoint
    .PARAMETER UsernamePasswordEndpoint
        The WSFed UsernamePassword endpoint
    .PARAMETER Credential
        The Credential to use for authentication
#>

Function GetWSTrustAssertionToken
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$Endpoint,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [pscredential]$Credential
    )
    #TODO:See if we can do integrated auth....
    Write-Verbose "[GetWSTrustAssertionToken] Retrieving SAML Token from $Endpoint"
    $WsResult=GetWSTrustResponse -AuthUri $Endpoint -Credential $Credential
    Write-Verbose "[GetWSTrustAssertionToken] Retrieved SAML Token: $($WsResult.TokenType) Assertion $($WsResult.AssertionType)"
    #Encode the SAML assertion so we can get a token
    $AssertionType=$WsResult.AssertionType
    $TokenBytes=[System.Text.Encoding]::UTF8.GetBytes($WsResult.Token)
    $EncodedAssertion=[System.Convert]::ToBase64String($TokenBytes)
    #Go and get the token
    Write-Verbose "[GetWSTrustAssertionToken] Retrieving Bearer Token For User Assertion $($WsResult.AssertionType) for Resource:$Resource"
    $UriBuilder=New-Object System.UriBuilder($AuthorizationUri)
    $UriBuilder.Path="$TenantId/$TokenEndpoint"
    $UriBuilder.Query="api-version=$TokenApiVersion"
    $RequestBody=[ordered]@{
        'grant_type'=$AssertionType;
        'client_id'=$ClientId;
        'resource'=$Resource.OriginalString;
        'scope'='openid';
        'assertion'=$EncodedAssertion;
    }
    $Response=Invoke-RestMethod -Method Post -Uri $UriBuilder.Uri -Body $RequestBody -ErrorAction Stop
    Write-Output $Response
}

<#
    .SYNOPSIS
        Retreives an OAuth 2 JWT from Azure Active Directory as a fully managed User
    .PARAMETER Resource
        The Resource Uri to obtain a token for
    .PARAMETER ClientId
        The registered Azure Active Directory application id
    .PARAMETER Credential
        The credential to use for authentication
    .PARAMETER TenantId
        The Azure Active Directory tenant id or domain name
    .PARAMETER AuthorizationUri
        The Azure Active Directory Token AuthorizationEndpoint
    .PARAMETER TokenEndpoint
        The Authorization Token Endpoint
    .PARAMETER TokenApiVersion
        The OAuth Token API Version
#>

Function GetAzureADUserToken
{

    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$Resource,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.String]$ClientId,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [pscredential]$Credential,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TenantId,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationUri=$Script:DefaultAuthUrl,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenEndpoint='oauth2/token',
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenApiVersion=$Script:DefaultTokenApiVersion,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenScope="openid"
    )

    $UserName=$Credential.UserName
    $Password=$Credential.GetNetworkCredential().Password

    $UriBuilder=New-Object System.UriBuilder($AuthorizationUri)
    $UriBuilder.Path="$TenantId/$TokenEndpoint"
    $UriBuilder.Query="api-version=$TokenApiVersion"
    Write-Verbose "[GetAzureADUserToken] Requesting User Token for User $UserName from $($UriBuilder.Uri.AbsoluteUri)"
    $Request=[ordered]@{
        'grant_type'='password';
        'resource'=$Resource.OriginalString;
        'client_id'=$ClientId;
        'username'=$UserName;
        'password'=$Password;
        'scope'=$TokenScope;
    }
    Write-Verbose "Acquiring Token From $($UriBuilder.Uri)"
    $Response=Invoke-RestMethod -Method Post -Uri $UriBuilder.Uri -Body $Request -ErrorAction Stop
    Write-Output $Response
}

<#
    .SYNOPSIS
        Retrieves an Authorization Code for an application interactively using the OAuth Consent Framework
    .PARAMETER AuthorizationUri
        The endpoint to navigate for an OAuth authorization code
 
#>

Function GetAzureADAuthorizationCode
{
    [OutputType([String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationUri
    )

    $ConsentForm=CreateWebForm -NavigateUri $AuthorizationUri -FormTitle "Sign in to Azure Active Directory" -FormWidth 500 -FormHeight 450
    try
    {
        $ConsentBrowser=$ConsentForm.Controls|Select-Object -First 1
        $OnBrowserNavigated={
            param
            (
                [Parameter()]
                [object]
                $sender,
                [Parameter()]
                [System.Windows.Forms.WebBrowserNavigatedEventArgs]
                $e
            )
            $TheForm=$sender.Parent
            Write-Verbose "[GetAzureADAuthorizationCode] Navigated $($e.Url)"
            $uri=New-Object System.Uri($e.Url)
            $QueryParams=$uri.Query.TrimStart('?').Split('&')
            #Make a hashtable of the query
            $Parameters=@{}
            foreach ($item in $QueryParams)
            {
                Write-Verbose "[GetAzureADAuthorizationCode] Parameter:$item"
                $pieces=$item.Split('=')
                $Parameters.Add($pieces[0],[System.Uri]::UnescapeDataString($pieces[1]))
            }
            #Look for the Authorization Code
            if($Parameters.ContainsKey('code'))
            {
                Write-Verbose "[GetAzureADAuthorizationCode] Authorization Code Received!"
                $Script:FormSyncContext.Code=$Parameters['code']
                $TheForm.DialogResult=[System.Windows.Forms.DialogResult]::OK
                $TheForm.Close()
            }
            #Look for an error (cancel)
            elseif($Parameters.ContainsKey('error'))
            {
                $TheForm.DialogResult=[System.Windows.Forms.DialogResult]::Abort
                $TheForm.Close()
                $Script:FormSyncContext.Error="$($Parameters['error']):$($Parameters['error_description'].Replace('+'," "))"
                Write-Verbose "[GetAzureADAuthorizationCode] Error Retrieving Access Code:$($Script:FormSyncContext.Error)"
            }
        }
        $OnDocumentCompleted={
            param
            (
                [object]$sender,
                [System.Windows.Forms.WebBrowserDocumentCompletedEventArgs]$e
            )
            $TheForm=$sender.Parent
            Write-Verbose "[GetAzureADAuthorizationCode] Document Completed! Size:$($sender.Document.Body.ScrollRectangle.Size)"
            $TheForm.Text= $sender.Document.Title
        }
        $ConsentBrowser.add_DocumentCompleted($OnDocumentCompleted)
        $ConsentBrowser.add_Navigated($OnBrowserNavigated)
        $ConsentResult=$ConsentForm.ShowDialog()
        if($ConsentResult -eq [System.Windows.Forms.DialogResult]::OK)
        {
            Write-Output $Script:FormSyncContext.Code
        }
        else
        {
            throw "The Operation Was Cancelled. $($Script:FormSyncContext.Error)"    
        }        
    }
    finally
    {
        $ConsentForm.Dispose()   
    }
}

<#
    .SYNOPSIS
        Retrieves an Access Token for an application interactively using the OAuth Consent Framework.
    .DESCRIPTION
        Retrieves an Access Token for an application interactively using the OAuth Consent Framework.
        Requires the application to allow Implicit Auth
    .PARAMETER AuthorizationUri
        The endpoint to navigate for an OAuth authorization token
 
#>

Function GetAzureADTokenByWebForm
{
    [OutputType([String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationUri
    )

    $AuthForm=CreateWebForm -NavigateUri $AuthorizationUri -FormTitle "Sign in to Azure Active Directory" -FormWidth 500 -FormHeight 450
    $AuthBrowser=$AuthForm.Controls|Select-Object -First 1
    $OnBrowserNavigated={
        param
        (
            [Parameter()]
            [object]
            $sender,
            [Parameter()]
            [System.Windows.Forms.WebBrowserNavigatedEventArgs]
            $e
        )
        $TheForm=$sender.Parent
        Write-Verbose "Navigated $($e.Url)"
        $uri=New-Object System.Uri($e.Url)
        if($uri.AbsoluteUri.Contains("#") -and $uri.AbsoluteUri.Contains("access_token"))
        {
            $QueryVals=$uri.AbsoluteUri.Split('#')|Select-Object -Last 1
        }
        else
        {
              $QueryVals=$uri.Query.TrimStart('?')
        }

        $QueryParams=$QueryVals.Split('&')
        #Make a hashtable of the query
        $Parameters=@{}
        foreach ($item in $QueryParams)
        {
            $pieces=$item.Split('=')
            $Parameters.Add($pieces[0],[System.Uri]::UnescapeDataString($pieces[1]))
        }
        #Look for the access token
        if($Parameters.ContainsKey('access_token'))
        {
            Write-Verbose "Access Token Received!"
            $AuthResult=New-Object PSObject -Property $Parameters
            $Script:FormSyncContext.AuthResult=$AuthResult
            $TheForm.DialogResult=[System.Windows.Forms.DialogResult]::OK
            $TheForm.Close()
        }
        #Look for an error (cancel)
        elseif($Parameters.ContainsKey('error'))
        {
            $TheForm.DialogResult=[System.Windows.Forms.DialogResult]::Abort
            $TheForm.Close()
            $Script:FormSyncContext.Error="$($Parameters['error']):$($Parameters['error_description'].Replace('+'," "))"
            Write-Verbose "Error Retrieving Access Code:$($Script:FormSyncContext.Error)"
        }
    }
    $OnDocumentCompleted={
        param
        (
            [object]$sender,
            [System.Windows.Forms.WebBrowserDocumentCompletedEventArgs]$e
        )
        $TheForm=$sender.Parent
        Write-Verbose "Document Completed! Size:$($sender.Document.Body.ScrollRectangle.Size)"
        $TheForm.Text= $sender.Document.Title
    }
    $AuthBrowser.add_DocumentCompleted($OnDocumentCompleted)
    $AuthBrowser.add_Navigated($OnBrowserNavigated)
    $AuthResult=$AuthForm.ShowDialog()
    if($AuthResult -eq [System.Windows.Forms.DialogResult]::OK)
    {
        Write-Output $Script:FormSyncContext.AuthResult
    }
    else
    {
        "The Operation Was Cancelled. $($Script:FormSyncContext.Error)"
    }

}

#endregion

#region User Realms

<#
    .SYNOPSIS
        Retrieves the WSFederation details for a given user prinicpal name
    .PARAMETER UserPrincipalName
        The user principal name(s) to retrieve details
    .PARAMETER AuthorizationEndpoint
        The OAuth WSFed Endpoint
#>

Function Get-WSTrustUserRealmDetails
{
    [OutputType([pscustomobject])]
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [System.String[]]$UserPrincipalName,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationEndpoint=$Script:DefaultAuthUrl,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [String]$UserRealmApiVersion=$Script:WSFedUserRealmApiVersion
    )
    BEGIN
    {
        $RealmUriBuilder=New-Object System.UriBuilder($AuthorizationEndpoint)
        $RealmUriBuilder.Query="api-version=$UserRealmApiVersion"
    }
    PROCESS
    {
        foreach ($upn in $UserPrincipalName)
        {
            try
            {
                $RealmUriBuilder.Path="/common/UserRealm/$upn"
                Write-Verbose "[Get-WSTrustUserRealmDetails] Retrieving User Realm Detail from $($RealmUriBuilder.Uri.AbsoluteUri) for $upn"
                $RealmDetails=Invoke-RestMethod -Uri $RealmUriBuilder.Uri -ContentType "application/json" -ErrorAction Stop
                if($RealmDetails) {
                    Write-Output $RealmDetails
                }
            }
            catch
            {
                Write-Warning "[Get-WSTrustUserRealmDetails] $upn version:$UserRealmApiVersion $_"
            }
        }
    }
}

<#
    .SYNOPSIS
        Retrieves a set of objects representing User Realm details for a given User Principal NameSpaceType
    .PARAMETER UserPrincipalName
        The user principal name(s) to retrieve details
    .PARAMETER AuthorizationEndpoint
        The OAuth Endpoint
    .PARAMETER FederationEndpoint
        The WSFed Endpoint
    .DESCRIPTION
        Returns object(s) containing user realm details
        Managed Domains:
            RealmDetails
        Federated Domains:
            FederationDoc
            UsernamePasswordEndpoint
            Bindings
            RealmDetails
            WSFedRealmDetails
            AuthorizationUrl
            IntegratedAuthEndpoint
 
#>

Function Get-AzureADUserRealm
{
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [System.String[]]$UserPrincipalName,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationEndpoint=$Script:DefaultAuthUrl,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [String]$UserRealmApiVersion=$Script:WSFedUserRealmApiVersion
    )

    PROCESS
    {
        foreach ($UPN in $UserPrincipalName)
        {

            $RealmDetails=GetAzureADUserRealm -UserPrincipalName $UPN -AuthorizationEndpoint $AuthorizationEndpoint
            Write-Verbose "[Get-AzureADUserRealm] Realm Details $($RealmDetails.DomainName) $($RealmDetails.NamespaceType)"
            if($RealmDetails.NamespaceType -eq "Federated")
            {
                Write-Verbose "[Get-AzureADUserRealm] User is Federated"
                $WsFedRealmDetails=Get-WSTrustUserRealmDetails -UserPrincipalName $UPN -AuthorizationEndpoint $AuthorizationEndpoint
                $MexDataUrl=$WsFedRealmDetails.federation_metadata_url

                Write-Verbose "[Get-AzureADUserRealm] Retrieving Federation Metadata from $MexDataUrl"
                $FedDoc=Invoke-RestMethod -Uri $MexDataUrl -ContentType 'application/soap+xml' -ErrorAction Stop
                $WsTrustBindings=GetWSFedBindings -MexDocument $FedDoc
                $IntegratedEndpoint=GetWSFedEndpoint -MexDocument $FedDoc -AuthType IntegratedAuth -ErrorAction SilentlyContinue
                $UsernameEndpoint=GetWSFedEndpoint -MexDocument $FedDoc -AuthType UsernamePassword -ErrorAction SilentlyContinue
                $UserRealm=New-Object PSObject -Property @{
                    RealmDetails=$RealmDetails;
                    WSFedRealmDetails=$WsFedRealmDetails;
                    FederationDoc=$FedDoc;
                    Bindings=$WsTrustBindings;
                    IntegratedAuthEndpoint=$IntegratedEndpoint;
                    UsernamePasswordEndpoint=$UsernameEndpoint;
                    AuthorizationUrl=$RealmDetails.AuthUrl;
                }
            }
            else
            {
                Write-Verbose "[Get-AzureADUserRealm] User is Managed"
                $UserRealm=New-Object PSObject -Property @{
                    RealmDetails=$RealmDetails;
                }
            }
            Write-Output $UserRealm
        }
    }
}

<#
    .SYNOPSIS
        Retreives the Well known OpenId Connect conifguration for the tenant
    .PARAMETER TenantId
        The tenant to retrieve the details for
    .PARAMETER AuthorizationUri
        The target endpoint
#>

Function Get-AzureADOpenIdConfiguration
{
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [Parameter(Mandatory=$false,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [String[]]$TenantId='common',
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationUri=$Script:DefaultAuthUrl
    )
    PROCESS
    {
        foreach ($id in $TenantId)
        {
            $OpenIdUriBuilder=New-Object System.UriBuilder($AuthorizationUri)
            $OpenIdUriBuilder.Path="$id/.well-known/openid-configuration"
            try {
                $OpenIdConfig=Invoke-RestMethod -Uri $OpenIdUriBuilder.Uri -ContentType "application/json" -ErrorAction Stop
                Write-Output $OpenIdConfig
            }
            catch [System.Exception] {
                Write-Warning "[Get-AzureADOpenIdConfiguration] Tenant $id $_"
            }
        }
    }
}

#endregion

#region JWT Helpers

<#
    .SYNOPSIS
        Converts an encoded JSON Web Token to an object representation
    .PARAMETER RawToken
        The encoded JWT string
    .PARAMETER AsString
        Returns the decoded JWT as a string delimiting sections with a period
#>

Function ConvertFrom-EncodedJWT
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [String[]]$RawToken,
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Switch]$AsString
    )
    PROCESS
    {
        foreach ($JwtString in $RawToken)
        {
            Write-Debug "[ConvertFrom-EncodedJWT] Raw Token $JwtString"
            $TokenSections=$JwtString.Split(".");

            $EncodedHeaders=RemoveBase64UrlPaddingFromString -Data $TokenSections[0]
            $EncodedHeaderBytes=[System.Convert]::FromBase64String($EncodedHeaders)
            $DecodedHeaders=[System.Text.Encoding]::UTF8.GetString($EncodedHeaderBytes)

            $EncodedPayload=RemoveBase64UrlPaddingFromString -Data $TokenSections[1]
            $EncodedPayloadBytes=[System.Convert]::FromBase64String($EncodedPayload)
            $DecodedPayload=[System.Text.Encoding]::UTF8.GetString($EncodedPayloadBytes)

            #$EncodedSignature=RemoveBase64PaddingFromString -Data $TokenSections[2]
            #$EncodedSignatureBytes=[System.Convert]::FromBase64String($EncodedSignature)
            #$DecodedSignature=[System.Text.Encoding]::UTF8.GetString($EncodedSignatureBytes)

            $JwtProperties=@{
                'headers'   = ($DecodedHeaders|ConvertFrom-Json);
                'payload'    = ($DecodedPayload|ConvertFrom-Json);
                #'signature' = ($DecodedSignature|ConvertFrom-Json);
            }
            $DecodedJwt=New-Object PSObject -Property $JwtProperties
            if($AsString.IsPresent)
            {
                #$OutputJwt="$DecodedHeaders`n.$DecodedPayload`n.$DecodedSignature"
                $OutputJwt="$DecodedHeaders`n.$DecodedPayload"
                Write-Output $OutputJwt
            }
            else
            {
                Write-Output $DecodedJwt
            }
        }
    }
}

<#
    .SYNOPSIS
        Test whether the current JWT is expired
    .PARAMETER Token
        The JWT as a string
#>

Function Test-JWTHasExpired
{
    [CmdletBinding()]
    param
    (
       [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
       [String[]]$Token
    )
    PROCESS
    {
        foreach ($item in $Token)
        {
            $DecodedToken=ConvertFrom-EncodedJWT -RawToken $item
            $ExpireTime=ConvertFromUnixTime -UnixTime $DecodedToken.payload.exp
            Write-Debug "[Test-JWTHasExpired] Token Expires: $($ExpireTime)"
            if([System.DateTime]::UtcNow -gt $ExpireTime)
            {
                Write-Output $true
            }
            Write-Output $false
        }
    }
}

<#
    .SYNOPSIS
        Return the current JWT expiry as a DateTime
    .PARAMETER Token
        The JWT as a string
    .PARAMETER AsLocal
        Whether to return the time localized to the current time zone
#>

Function Get-JWTExpiry
{
    [CmdletBinding()]
    param
    (
       [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
       [String[]]$Token,
       [Parameter(ValueFromPipelineByPropertyName=$true)]
       [Switch]$AsLocal
    )
    PROCESS
    {
        foreach ($item in $Token)
        {
            $DecodedToken=ConvertFrom-EncodedJWT -RawToken $item
            $ExpireTime=ConvertFromUnixTime -UnixTime $DecodedToken.payload.exp
            if($AsLocal.IsPresent)
            {
                $ExpireTime=$ExpireTime.ToLocalTime()
            }
            Write-Output $ExpireTime
        }
    }
}

#endregion

#region Token/Code Request

<#
    .SYNOPSIS
        Request an Azure AD OAuth2 authorization code interactively
    .PARAMETER ConnectionDetails
        An object containing all the AAD connection properties
    .PARAMETER Resource
        The Resource Uri to obtain a token for
    .PARAMETER ClientId
        The registered Azure Active Directory application id
    .PARAMETER AuthorizationUri
        The Azure Active Directory Token AuthorizationEndpoint
    .PARAMETER TenantId
        The Azure Active Directory tenant id or domain name
    .PARAMETER RedirectUri
        The approved Redirect URI request for the application
    .PARAMETER AuthEndpoint
        The OAuth2 authorization endpoint
    .PARAMETER TokenApiVersion
        The OAuth Token API Version
    .PARAMETER Consent
        Whether to grant consent during the request
    .PARAMETER AdminConsent
        Whether to grant admin consent during the request
    .PARAMETER Scope
        The oauth scopes to apply to the authorization request
#>

Function Get-AzureADAuthorizationCode
{
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [ValidateNotNullOrEmpty()]
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$Resource,
        [ValidateNotNullOrEmpty()]
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.String]$ClientId,
        [ValidateNotNull()]
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$RedirectUri,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TenantId="common",
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationUri=$Script:DefaultAuthUrl,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$AuthEndpoint='oauth2/authorize',
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenApiVersion=$Script:DefaultTokenApiVersion,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [Switch]$Consent,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [Switch]$AdminConsent,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [String[]]$Scope=@('user_impersonation','openid')
    )

    $TokenUriBuilder=New-Object System.UriBuilder($AuthorizationUri)
    $TokenUriBuilder.Path="$TenantId/$AuthEndpoint"
    #We will assume they got the Uri correct on entry
    $TokenQuery="&redirect_uri=$([Uri]::EscapeDataString($RedirectUri.AbsoluteUri))&resource=$([Uri]::EscapeDataString($Resource.OriginalString))"
    $TokenQuery+="&api-version=$TokenApiVersion&client_id=$($ClientId)&response_type=code"
    if($Consent.IsPresent)
    {
        $TokenQuery+="&prompt=consent"
    }
    elseif($AdminConsent.IsPresent)
    {
        $TokenQuery+="&prompt=admin_consent"
    }
    else
    {
        $TokenQuery+="&prompt=login"
    }
    if($Scope -ne $null)
    {
        $TokenQuery+="&scope=$([String]::Join('+',$Scope))"
    }
    $TokenUriBuilder.Query=$TokenQuery
    $AuthResult=GetAzureADAuthorizationCode -AuthorizationUri $TokenUriBuilder.Uri
    if ($AuthResult) {
        Write-Output $AuthResult
    }
}

<#
    .SYNOPSIS
        Approve an Azure Active Directory Application using the OAuth consent framework
    .PARAMETER ConnectionDetails
        An object containing all the AAD connection properties
    .PARAMETER ClientId
        The registered Azure Active Directory application id
    .PARAMETER AuthorizationCode
        The Authorization Code to exchange
    .PARAMETER AuthorizationUri
        The Azure Active Directory Token AuthorizationEndpoint
    .PARAMETER TenantId
        The Azure Active Directory tenant id or domain name
    .PARAMETER RedirectUri
        The approved Redirect URI request for the application
    .PARAMETER TokenEndpoint
        The Authorization Token Endpoint
    .PARAMETER TokenApiVersion
        The OAuth Token API Version
    .PARAMETER AdminConsent
        Whether to grant admin consent during the request
    .PARAMETER NativeClient
        Uses default native client redirect uri if one is not specified
#>

Function Approve-AzureADApplication
{
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [ValidateNotNullOrEmpty()]
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [string]$ClientId,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$RedirectUri,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$Resource,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TenantId="common",
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationUri=$Script:DefaultAuthUrl,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenEndpoint='oauth2/token',
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$AuthCodeEndpoint='oauth2/authorize',
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        $TokenApiVersion=$Script:DefaultTokenApiVersion,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [Switch]$AdminConsent,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [Switch]$NativeClient
    )

    if($NativeClient.IsPresent -and $RedirectUri -eq $null)
    {
        Write-Verbose "[Approve-AzureADApplication] Using Default Native Redirect URI:$Script:DefaultNativeRedirectUri"
        $RedirectUri=$Script:DefaultNativeRedirectUri
    }

    $ConsentUriBuilder=New-Object System.UriBuilder($AuthorizationUri)
    $ConsentUriBuilder.Path="$TenantId/$AuthCodeEndpoint"
    $QueryStr="api-version=$TokenApiVersion"
    $ConsentType="consent"
    if($AdminConsent.IsPresent)
    {
        Write-Verbose "[Approve-AzureADApplication] Admin Consent Present!"
        $ConsentType="admin_consent"
    }
    $QueryStr+="&client_id=$($ClientId)"
    if ($RedirectUri -ne $null) {
        $QueryStr+="&redirect_uri=$([Uri]::EscapeDataString($RedirectUri.AbsoluteUri))"
    }
    #We will assume they got the Uri correct on entry
    if ($Resource -ne $null) {
        $QueryStr+="&resource=$([Uri]::EscapeDataString($Resource.OriginalString))"
    }
    $QueryStr+="&response_type=code&prompt=$ConsentType"
    $ConsentUriBuilder.Query=$QueryStr
    $AuthCode=GetAzureADAuthorizationCode -AuthorizationUri $ConsentUriBuilder.Uri.AbsoluteUri
    Write-Output $AuthCode
}

<#
    .SYNOPSIS
        Exchanges an Azure Active Directory Authorization Code for a Token
    .PARAMETER ConnectionDetails
        An object containing all the AAD connection properties
    .PARAMETER Resource
        The Resource Uri to obtain a token for
    .PARAMETER ClientId
        The registered Azure Active Directory application id
    .PARAMETER TenantId
        The Azure Active Directory tenant id or domain name
    .PARAMETER AuthorizationCode
        The Authorization Code to exchange
    .PARAMETER AuthorizationUri
        The Azure Active Directory Token AuthorizationEndpoint
    .PARAMETER RedirectUri
        The approved Redirect URI request for the application
    .PARAMETER TokenEndpoint
        The Authorization Token Endpoint
    .PARAMETER TokenApiVersion
        The OAuth Token API Version
#>

Function Get-AzureADAccessTokenFromCode
{
    [OutputType([pscustomobject])]
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$Resource,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$RedirectUri=$Script:DefaultNativeRedirectUri,
        [ValidateNotNullOrEmpty()]
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.String]$ClientId,
        [ValidateNotNullOrEmpty()]
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.String]$AuthorizationCode,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TenantId="common",
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationUri=$Script:DefaultAuthUrl,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenEndpoint='oauth2/token',
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenApiVersion=$Script:DefaultTokenApiVersion
    )

    $TokenUriBuilder=New-Object System.UriBuilder($AuthorizationUri)
    $TokenUriBuilder.Path="$TenantId/$TokenEndpoint"
    $TokenUriBuilder.Query="api-version=$TokenApiVersion"
    $Request=[ordered]@{
        'grant_type'='authorization_code';
        'client_id'=$ClientId;
        'resource'=$Resource.OriginalString;
        'scope'='openid';
        'code'=$AuthorizationCode;
        'redirect_uri'=$RedirectUri.AbsoluteUri;
    }
    $Response=Invoke-RestMethod -Method Post -Uri $TokenUriBuilder.Uri -Body $Request -ErrorAction Stop
    Write-Output $Response
}

<#
    .SYNOPSIS
        Retreives an OAuth 2 JWT from Azure Active Directory as an Application
    .PARAMETER ConnectionDetails
        An object containing all the AAD connection properties
    .PARAMETER Resource
        The Resource Uri to obtain a token for
    .PARAMETER ClientId
        The registered Azure Active Directory application id
    .PARAMETER ClientSecret
        The client secret to use for authentication
    .PARAMETER TenantId
        The Azure Active Directory tenant id or domain name
    .PARAMETER AuthorizationUri
        The Azure Active Directory Token AuthorizationEndpoint
    .PARAMETER TokenEndpoint
        The Authorization Token Endpoint
    .PARAMETER AuthCodeEndpoint
        The Authorization Code Endpoint
    .PARAMETER TokenApiVersion
        The OAuth Token API Version
#>

Function Get-AzureADClientToken
{
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$Resource,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.String]$ClientId,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.String]$ClientSecret,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TenantId="common",
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationUri=$Script:DefaultAuthUrl,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenEndpoint='oauth2/token',
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenApiVersion=$Script:DefaultTokenApiVersion
    )

    $UriBuilder=New-Object System.UriBuilder($AuthorizationUri)
    $UriBuilder.Path="$TenantId/$TokenEndpoint"
    $UriBuilder.Query="api-version=$TokenApiVersion"
    Write-Verbose "[Get-AzureADClientToken] Retrieving token for Client:$ClientId Tenant:$TenantId with Client Secret:[REDACTED] at $($UriBuilder.Uri.AbsolutePath)"
    $Request=[ordered]@{
        'grant_type'='client_credentials';
        'client_id'=$ClientId;
        'client_secret'=$ClientSecret;
        'resource'=$Resource.OriginalString
    }
    $Response=Invoke-RestMethod -Method Post -Uri $UriBuilder.Uri -Body $Request -ErrorAction Stop
    Write-Verbose "[Get-AzureADClientToken] Success!"
    Write-Output $Response
}

<#
    .SYNOPSIS
        Retreives an OAuth 2 JWT from Azure Active Directory as a User
    .PARAMETER ConnectionDetails
        An object containing all the AAD connection properties
    .PARAMETER Resource
        The Resource Uri to obtain a token for
    .PARAMETER ClientId
        The registered Azure Active Directory application id
    .PARAMETER Credential
        The credential to use for authentication
    .PARAMETER TenantId
        The Azure Active Directory tenant id or domain name
    .PARAMETER AuthorizationUri
        The Azure Active Directory Token AuthorizationEndpoint
    .PARAMETER TokenEndpoint
        The Authorization Token Endpoint
    .PARAMETER AuthCodeEndpoint
        The Authorization Code Endpoint
    .PARAMETER TokenApiVersion
        The OAuth Token API Version
    .PARAMETER UseMicrosoftAccount
        Use a microsoft account interactively
#>

Function Get-AzureADUserToken
{
    [OutputType([psobject])]
    [CmdletBinding(ConfirmImpact='None',DefaultParameterSetName='explicit')]
    param
    (
        [Parameter(Mandatory=$false,ParameterSetName='usemsa',ValueFromPipelineByPropertyName=$true)]
        [Parameter(Mandatory=$false,ParameterSetName='explicit',ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$Resource=$Script:DefaultAzureManagementUri,
        [Parameter(Mandatory=$false,ParameterSetName='usemsa',ValueFromPipelineByPropertyName=$true)]
        [Parameter(Mandatory=$false,ParameterSetName='explicit',ValueFromPipelineByPropertyName=$true)]
        [System.String]$ClientId=$Script:DefaultPowershellClientId,
        [Parameter(Mandatory=$true,ParameterSetName='explicit',ValueFromPipelineByPropertyName=$true)]
        [pscredential]$Credential,
        [Parameter(Mandatory=$false,ParameterSetName='explicit',ValueFromPipelineByPropertyName=$true)]
        [Parameter(Mandatory=$false,ParameterSetName='usemsa',ValueFromPipelineByPropertyName=$true)]
        [System.String]$TenantId="common",
        [Parameter(Mandatory=$false,ParameterSetName='usemsa',ValueFromPipelineByPropertyName=$true)]
        [Parameter(Mandatory=$false,ParameterSetName='explicit',ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationUri=$Script:DefaultAuthUrl,
        [Parameter(Mandatory=$false,ParameterSetName='usemsa',ValueFromPipelineByPropertyName=$true)]
        [Parameter(Mandatory=$false,ParameterSetName='explicit',ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenEndpoint='oauth2/token',
        [Parameter(Mandatory=$false,ParameterSetName='usemsa',ValueFromPipelineByPropertyName=$true)]
        [Parameter(Mandatory=$false,ParameterSetName='explicit',ValueFromPipelineByPropertyName=$true)]
        [System.String]$AuthCodeEndpoint='oauth2/authorize',
        [Parameter(Mandatory=$false,ParameterSetName='explicit',ValueFromPipelineByPropertyName=$true)]
        [Parameter(Mandatory=$false,ParameterSetName='usemsa',ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenApiVersion=$Script:DefaultTokenApiVersion,
        [Parameter(Mandatory=$false,ParameterSetName='usemsa',ValueFromPipelineByPropertyName=$true)]
        [Switch]$UseMicrosoftAccount
    )

    Write-Verbose "[Get-AzureADUserToken] Retrieving OAuth Token ClientId:$ClientId Resource:$Resource Tenant:$TenantId as $($Credential.UserName)"
    if($PSCmdlet.ParameterSetName -eq 'usemsa')
    {
        Write-Verbose "[Get-AzureADUserToken] Using Microsoft Account - Requires Interactive Login"
        if($TenantId -ne 'common' -and $TenantId -notmatch '^[{(]?[0-9A-F]{8}[-]?([0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$')
        {
            Write-Verbose "[Get-AzureADUserToken] Retrieving OpenId openid-configuration for $TenantId"
            $OpenIdConfig=Get-AzureADOpenIdConfiguration -TenantId $TenantId
            [Uri]$AuthUri=$OpenIdConfig.authorization_endpoint
            $TenantId=$AuthUri.AbsolutePath.TrimStart('/').Split('/')|Select-Object -First 1
        }
        $AuthCode=Get-AzureADAuthorizationCode -Resource $Resource -ClientId $ClientId `
            -RedirectUri $Script:DefaultNativeRedirectUri -TenantId 'common' -AuthorizationUri $AuthorizationUri `
            -AuthEndpoint $AuthCodeEndpoint -TokenApiVersion $TokenApiVersion
        $AuthToken=Get-AzureADAccessTokenFromCode -Resource $Resource -ClientId $ClientId -RedirectUri $Script:DefaultNativeRedirectUri `
            -AuthorizationCode $AuthCode -TenantId 'common' -AuthorizationUri $AuthorizationUri `
            -TokenEndpoint $TokenEndpoint -TokenApiVersion $TokenApiVersion
        if($TenantId -ne 'common')
        {
            Write-Verbose "[Get-AzureADUserToken] Retrieving Refresh token for $TenantId audience"
            $AuthToken=Get-AzureADRefreshToken -Resource $Resource -RefreshToken $AuthToken.refresh_token `
                -ClientId $ClientId -TenantId $TenantId `
                -AuthorizationUri $AuthorizationUri -TokenEndpoint $TokenEndpoint
        }
        Write-Output $AuthToken
    }
    else
    {
        $UserRealm=Get-AzureADUserRealm -UserPrincipalName $Credential.UserName -AuthorizationEndpoint $AuthorizationUri
        Write-Verbose "[Get-AzureADUserToken] Realm $($UserRealm.RealmDetails.DomainName) NamespaceType:$($UserRealm.RealmDetails.NameSpaceType)"
        if($UserRealm.FederationDoc -eq $null)
        {
            Write-Verbose "[Get-AzureADUserToken] Retrieving OAuth Token for Client:$ClientId as $($Credential.UserName)"
            $UserResult=GetAzureADUserToken -Resource $Resource -ClientId $ClientId -Credential $Credential -TenantId $TenantId
            if ($UserResult -ne $null) {
                Write-Verbose "[Get-AzureADUserToken] Successfully received an OAuth Token!"
                Write-Output $UserResult
            }
            else {
                throw "Failed to receive an OAuth Token!"
            }
        }
        else
        {
            Write-Verbose "[Get-AzureADUserToken] Retrieving WSFed User Assertion Token"
            #Where to we need to authenticate???
            #TODO:See if we can do integrated auth....
            if([String]::IsNullOrEmpty($UserRealm.UsernamePasswordEndpoint) -eq $false)
            {
                $AssertionResult=GetWSTrustAssertionToken -Endpoint $UserRealm.UsernamePasswordEndpoint -Credential $Credential
                if ($AssertionResult -ne $null)
                {
                    Write-Verbose "[Get-AzureADUserToken] Successfully received a WSFed User Assertion Token!"
                }
                else
                {
                    throw "Failed to receive a WSFed User Assertion Token!"
                }
                Write-Output $AssertionResult
            }
            else {
                throw "There is no Username/Password endpoint specified in the Federation Document"
            }
        }
    }
}

<#
    .SYNOPSIS
        Retrieves an OAuth2 JWT using the refresh token framework
    .DESCRIPTION
        Retrieves an OAuth2 JWT using the refresh token framework
    .PARAMETER ConnectionDetails
        An object containing all the AAD connection properties
    .PARAMETER RefreshToken
        The JWT refresh token
    .PARAMETER Resource
        The Resource Uri to obtain a token for
    .PARAMETER ClientId
        The registered Azure Active Directory application id
    .PARAMETER Credential
        The credential to use for authentication
    .PARAMETER TenantId
        The Azure Active Directory tenant id or domain name
    .PARAMETER AuthorizationUri
        The Azure Active Directory Token AuthorizationEndpoint
    .PARAMETER TokenEndpoint
        The Authorization Token Endpoint
    .PARAMETER AuthCodeEndpoint
        The Authorization Code Endpoint
    .PARAMETER TokenApiVersion
        The OAuth Token API Version
 
#>

Function Get-AzureADRefreshToken
{
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$Resource,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [string]$RefreshToken,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [string]$ClientId=$Script:DefaultPowershellClientId,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TenantId="common",
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationUri=$Script:DefaultAuthUrl,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenEndpoint='oauth2/token',
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$AuthCodeEndpoint='oauth2/authorize',
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenApiVersion=$Script:DefaultTokenApiVersion
    )
    Write-Verbose "[Get-AzureADRefreshToken] Retrieving OAuth Refresh Token for ClientId:$ClientId Resource:$Resource Tenant:$TenantId"

    $UriBuilder=New-Object System.UriBuilder($AuthorizationUri)
    $UriBuilder.Path="$TenantId/$TokenEndpoint"
    $UriBuilder.Query="api-version=$TokenApiVersion"

    Write-Verbose "[GetAzureADUserToken] Requesting User Token for User $UserName from $($UriBuilder.Uri.AbsoluteUri)"
    $Request=[ordered]@{
        'grant_type'='refresh_token';
        'resource'=$Resource.OriginalString;
        'client_id'=$ClientId;
        'refresh_token'=$RefreshToken
    }
    Write-Verbose "[Get-AzureADRefreshToken] Acquiring Token From $($UriBuilder.Uri)"
    $Response=Invoke-RestMethod -Method Post -Uri $UriBuilder.Uri -Body $Request -ErrorAction Stop
    Write-Output $Response
}

<#
    .SYNOPSIS
        Retrieves an OAuth access token interactively for an application allowing Implicit Flow
    .DESCRIPTION
        Retrieves an OAuth access token interactively for an application allowing Implicit Flow
    .PARAMETER ConnectionDetails
        An object containing all the AAD connection properties
    .PARAMETER Resource
        The Resource Uri to obtain a token for
    .PARAMETER ClientId
        The registered Azure Active Directory application id
    .PARAMETER AuthorizationUri
        The Azure Active Directory Token AuthorizationEndpoint
    .PARAMETER TenantId
        The Azure Active Directory tenant id or domain name
    .PARAMETER RedirectUri
        The approved Redirect URI request for the application
    .PARAMETER AuthEndpoint
        The OAuth2 authorization endpoint
    .PARAMETER TokenApiVersion
        The OAuth Token API Version
    .PARAMETER Consent
        Whether to grant consent during the request
    .PARAMETER AdminConsent
        Whether to grant admin consent during the request
#>

Function Get-AzureADImplicitFlowToken
{
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$Resource,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.String]$ClientId,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$RedirectUri,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TenantId="common",
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationUri=$Script:DefaultAuthUrl,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$AuthEndpoint='oauth2/authorize',
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenApiVersion=$Script:DefaultTokenApiVersion,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [Switch]$Consent,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [Switch]$AdminConsent
    )

    $TokenUriBuilder=New-Object System.UriBuilder($AuthorizationUri)
    $TokenUriBuilder.Path="$TenantId/$AuthEndpoint"
    $TokenQuery="&redirect_uri=$([Uri]::EscapeDataString($RedirectUri.AbsoluteUri))&resource=$([Uri]::EscapeDataString($Resource.OriginalString))"
    $TokenQuery+="&api-version=$TokenApiVersion&client_id=$($ClientId)&response_type=token"
    if($Consent.IsPresent)
    {
        $TokenQuery+="&prompt=consent"
    }
    elseif($AdminConsent.IsPresent)
    {
        $TokenQuery+="&prompt=admin_consent"
    }
    else
    {
        $TokenQuery+="&prompt=login"
    }
    $TokenUriBuilder.Query=$TokenQuery
    $AuthResult=GetAzureADTokenByWebForm -AuthorizationUri $TokenUriBuilder.Uri
    Write-Output $AuthResult
}

<#
    .SYNOPSIS
        Retrieves an OAuth access token using a certificate
    .DESCRIPTION
        Retrieves an OAuth access token using a certificate
    .PARAMETER ConnectionDetails
        An object containing all the AAD connection properties
    .PARAMETER Resource
        The Resource Uri to obtain a token for
    .PARAMETER Certificate
        The certificate to sign the token request
    .PARAMETER NotBefore
        The start of token validity
    .PARAMETER Expires
        The start of token expiration
    .PARAMETER ClientId
        The registered Azure Active Directory application id
    .PARAMETER AuthorizationUri
        The Azure Active Directory Token AuthorizationEndpoint
    .PARAMETER TenantId
        The Azure Active Directory tenant id or domain name
    .PARAMETER RedirectUri
        The approved Redirect URI request for the application
    .PARAMETER AuthEndpoint
        The OAuth2 authorization endpoint
    .PARAMETER TokenApiVersion
        The OAuth Token API Version
#>

Function Get-AzureADClientAssertionToken
{
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$Resource=$Script:DefaultAzureManagementUri,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.String]$ClientId,
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TenantId="common",
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [DateTime]$NotBefore=([DateTime]::UtcNow),
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [datetime]$Expires=($NotBefore.AddMinutes(60)),
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$AssertionType=$Script:OauthClientAssertionType,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$AuthorizationUri=$Script:DefaultAuthUrl,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenEndpoint='oauth2/token',
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.String]$TokenApiVersion=$Script:DefaultTokenApiVersion
    )
    
    Write-Verbose "[Get-AzureADClientAssertionToken] Retrieving client assertion token using certificate $($Certificate.GetCertHashString())"
    $TokenUriBuilder=New-Object System.UriBuilder($AuthorizationUri)
    $TokenUriBuilder.Path="$TenantId/$TokenEndpoint"
    $Sha=New-Object System.Security.Cryptography.SHA256Cng
    $RsaProvider=GetRsaCryptoProvider -RsaProvider $Certificate.PrivateKey
    try
    {
        #Get the client assertion
        $ClientAssertion=NewClientAssertion -Certificate $Certificate `
            -ClientId $ClientId -Audience $TokenUriBuilder.Uri `
            -Expires $Expires -NotBefore $NotBefore
        #Sign it
        $AssertionBytes=[System.Text.Encoding]::UTF8.GetBytes($ClientAssertion)
        $SignedTokenBytes=$RsaProvider.SignData($AssertionBytes,$Sha)
        $SignedToken=[Convert]::ToBase64String($SignedTokenBytes)|AddBase64UrlPaddingToString
        $EncodedAssertion="$ClientAssertion.$SignedToken"
        #Get the token
        $RequestBody=[ordered]@{
            'grant_type'='client_credentials';
            'client_id'=$ClientId;
            'resource'=$Resource.OriginalString;
            'client_assertion'=$EncodedAssertion;
            'client_assertion_type'=$AssertionType;
        }
        Write-Verbose "[Get-AzureADClientAssertionToken] Retrieving token with gigned assertion $EncodedAssertion"
        $TokenResponse=Invoke-RestMethod -Uri $TokenUriBuilder.Uri -Method Post -Body $RequestBody -ErrorAction Stop
        Write-Output $TokenResponse
    }
    catch
    {
        throw "Error Acquiring Client Assertion Token $_"
    }
    finally
    {
        $Sha.Dispose()   
    }
}

#endregion

<#
    .SYNOPSIS
        Retrieves the Azure AD Token Signing Key
    .DESCRIPTION
        Retrieves the Azure AD Token Signing Key
    .PARAMETER TenantId
        The Azure AD Tenant Id
    .PARAMETER CertficateHash
        The discovery key Hash
    .PARAMETER DiscoveryUri
        The discovery key URI
#>

Function Get-AzureADDiscoveryKey
{
    [CmdletBinding(ConfirmImpact='None')]
    param
    (
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [String]$TenantId="common",
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [String]$CertificateHash,
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [System.Uri]$DiscoveryUri="https://login.windows.net",
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)]
        [String]$KeyPath="discovery/keys"
    )

    $KeyUriBld=New-Object System.UriBuilder($DiscoveryUri)
    $KeyUriBld.Path="$TenantId/$($KeyPath.TrimStart('/'))"
    $KeyResult=Invoke-RestMethod -Uri $KeyUriBld.Uri -Method Get -ContentType 'application/json'
    if ($KeyResult -ne $null) {
        $Output=$KeyResult|Select-Object -ExpandProperty 'keys'
        if([String]::IsNullOrEmpty($CertificateHash) -eq $false)
        {
            $Output=$Output|Where-Object 'x5t' -eq $CertificateHash|Select-Object -First 1
        }
        if($Output -ne $null)
        {
            Write-Output $Output
        }
    }
}

<#
    .SYNOPSIS
        Converts a discovery key object to an x509 Certificate
    .DESCRIPTION
        Converts a discovery key object to an x509 Certificate
    .PARAMETER Key
        The open id discovery key object
#>

Function ConvertFrom-AzureADDiscoveryKey
{
    [CmdletBinding()]
    [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [PSObject[]]$Key
    )
    PROCESS
    {
        foreach ($item in $Key)
        {
            if($Script:DiscoveryKeyCache.ContainsKey($item.x5t))
            {
                Write-Verbose "[ConvertFrom-AzureADDiscoveryKey] Using cached certificate matching hash $($item.x5t)"
                $Cert=$Script:DiscoveryKeyCache[$item.x5t]
            }
            else
            {
                $Cert=New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(([Convert]::FromBase64String($item.x5c[0])),$item.kid)
                $Script:DiscoveryKeyCache.Add($item.x5t,$Cert)
            }
            Write-Output $Cert
        }
    }
}