Functions/Public/Authentication.ps1
|
# Authentication, connection, and token management functions Function Connect-NectarCloud { <# .SYNOPSIS Connects to Nectar DXP cloud and store the credentials for later use. .DESCRIPTION Connects to Nectar DXP cloud and store the credentials for later use. .PARAMETER CloudFQDN The FQDN of the Nectar DXP cloud. .PARAMETER TenantName The name of a Nectar DXP cloud tenant to connect to and use for subsequent commands. Only useful for multi-tenant deployments .PARAMETER Credential The credentials used to access the Nectar DXP UI. Normally in username@domain.com format .PARAMETER CredSecret Use stored credentials saved via Set-Secret. Requires prior installation of Microsoft.PowerShell.SecretManagement PS module and an appropriate secret vault, such as Microsoft.PowerShell.SecretStore. Locally, the Microsoft.PowerShell.SecretStore can be used to store secrets securely on the local machine. This is the minimum requirement for using this feature. Install the modules by running: Install-Module Microsoft.PowerShell.SecretManagement Install-Module Microsoft.PowerShell.SecretStore Register a credential secret by doing something like: Set-Secret -Name NectarCreds -Vault SecretStore -Secret (Get-Credential) .PARAMETER EnvFromFile Use a CSV file called N10EnvList.csv located in the user's default Documents folder to show a list of environments to select from. Run [Environment]::GetFolderPath("MyDocuments") to find your default document folder. This parameter is only available if N10EnvList.csv is found in the user's default Documents folder (ie: C:\Users\username\Documents) Also sets the default credentials to use for the selected environment. This feature uses the Microsoft.PowerShell.SecretManagement PS module, which must be installed and configured with a secret store prior to using this option. N10EnvList.csv must have a header with three columns defined as "Environment, DefaultTenant, Secret". Each environment and Secret (if used) should be on their own separate lines .PARAMETER UseToken Use a JWT (JSON web token) to connect to Nectar DXP instead of using credentials. This feature uses the Microsoft.PowerShell.SecretManagement PS module, which must be installed and configured with a secret store prior to using this option. The PS SecretManagement module can use any number of 3rd party secret stores that provide access to centralized secret management tools, such as Keeper and AWS Secrets. Locally, the Microsoft.PowerShell.SecretStore can be used to store secrets securely on the local machine. This is the minimum requirement for using this feature. Install the modules by running: Install-Module Microsoft.PowerShell.SecretManagement Install-Module Microsoft.PowerShell.SecretStore When -UseToken is selected, the function will check for a secret called <envname>-accesstoken (ie contoso.nectar.services-accesstoken). The secret must contain two fields called AccessToken and RefreshToken and must be writable. The token itself can be generated in the Nectar DXP UI or via New-NectarToken (when logged in with a local account). The New-NectarTokenRegistration can be used to generate a token using the default secret store (if supported by the secret store). If using the default Microsoft SecretStore, you can generate a token and save it as a secret on the local machine by running: New-NectarToken -TokenName <tokenname> | New-NectarTokenRegistration -CloudFQDN <NectarDXPFQDN> ie. New-NectarToken -TokenName laptop | New-NectarTokenRegistration -CloudFQDN contoso.nectar.services .PARAMETER TokenIdentifier An optional unique identifier (such as script name or username) to use when retreiving secrets from a secret store shared by multiple parties/scripts .EXAMPLE $Cred = Get-Credential Connect-NectarCloud -Credential $cred -CloudFQDN contoso.nectar.services Connects to the contoso.nectar.services Nectar DXP cloud using the credentials supplied to the Get-Credential command .EXAMPLE Connect-NectarCloud -CloudFQDN contoso.nectar.services -CredSecret MyNectarCreds Connects to contoso.nectar.services Nectar DXP cloud using previously stored secret called MyNectarCreds .EXAMPLE Connect-NectarCloud -CloudFQDN contoso.nectar.services -UseToken Connects to contoso.nectar.services Nectar DXP cloud using previously stored token stored in a Microsoft Secret Vault called contoso.nectar.services-accesstoken .NOTES Version 2.0 #> [Alias("cnc")] Param ( [Parameter(ValueFromPipeline, Mandatory=$False)] [ValidateScript ({ If ($_ -Match "^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$") { $True } Else { Throw "ERROR: Nectar DXP cloud name must be in FQDN format." } })] [string]$CloudFQDN, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$TenantName, [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.Credential()] [PSCredential]$Credential, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$CredSecret, [Parameter(Mandatory=$False)] [switch]$UseToken, [Parameter(Mandatory=$False)] [string]$TokenIdentifier ) DynamicParam { $DefaultDocPath = [Environment]::GetFolderPath("MyDocuments") $EnvPath = "$DefaultDocPath\N10EnvList.csv" If (Test-Path $EnvPath -PathType Leaf) { # Set the dynamic parameters' name $ParameterName = 'EnvFromFile' # Create the dictionary $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary # Create the collection of attributes $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] # Create and set the parameters' attributes $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute $ParameterAttribute.Mandatory = $False $ParameterAttribute.Position = 1 # Add the attributes to the attributes collection $AttributeCollection.Add($ParameterAttribute) # Generate and set the ValidateSet $EnvSet = Import-Csv -Path $EnvPath $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($EnvSet.Environment) # Add the ValidateSet to the attributes collection $AttributeCollection.Add($ValidateSetAttribute) # Create and return the dynamic parameter $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection) $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter) Return $RuntimeParameterDictionary } } Begin { # Bind the dynamic parameter to a friendly variable If (Test-Path $EnvPath -PathType Leaf) { If ($PsBoundParameters[$ParameterName]) { $CloudFQDN = $PsBoundParameters[$ParameterName] Write-Verbose "CloudFQDN: $CloudFQDN" # Get the array position of the selected environment $EnvPos = $EnvSet.Environment.IndexOf($CloudFQDN) # Check for default tenant in N10EnvList.csv and use if available, but don't override if user explicitly set the TenantName If (!$PsBoundParameters['TenantName']) { $TenantName = $EnvSet[$EnvPos].DefaultTenant Write-Verbose "DefaultTenant: $TenantName" } # Check for secret in N10EnvList.csv and use if available $CredSecret = $EnvSet[$EnvPos].CredSecret Write-Verbose "Secret: $CredSecret" } } } Process { # Need to force TLS 1.2, if not already set If ([Net.ServicePointManager]::SecurityProtocol -ne 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } # Ask for the tenant name if global Nectar tenant variable not available and not entered on command line If ((-not $Global:NectarCloud) -And (-not $CloudFQDN)) { $CloudFQDN = Read-Host "Enter the Nectar DXP cloud FQDN" $RegEx = "^(?:http(s)?:\/\/)?([\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:?#[\]@!\$&'\(\)\*\+,;=.]+)" $FQDNMatch = Select-String -Pattern $Regex -InputObject $CloudFQDN $CloudFQDN = $FQDNMatch.Matches.Groups[2].Value } ElseIf (($Global:NectarCloud) -And (-not $CloudFQDN)) { $CloudFQDN = $Global:NectarCloud } # Ask for credentials if global Nectar creds aren't available If (((-not $Global:NectarCred) -And (-not $Credential)) -Or (($Global:NectarCloud -ne $CloudFQDN) -And (-Not $Credential)) -And (-Not $CredSecret) -And (-Not $UseToken) -And (-Not $Global:NectarSecretName)) { $Credential = Get-Credential } ElseIf ($Global:NectarCred -And (-not $Credential)) { $Credential = $Global:NectarCred } # Set the token name based on the inputs If ($TokenIdentifier) { $NectarSecretName = "$($CloudFQDN)-$($TokenIdentifier)-accesstoken" } ElseIf ($UseToken) { $NectarSecretName = "$($CloudFQDN)-accesstoken" } # Check secret store for stored token If ($Global:NectarSecretName -ne $NectarSecretName -And $UseToken) { Write-Verbose "Attempting to retrieve $NectarSecretName from secret store" Try { $Global:NectarDefaultVault = (Get-SecretVault | Where-Object {$_.IsDefault -eq $True}).Name $Global:NectarToken = Get-Secret -Name $NectarSecretName -AsPlainText -Vault $Global:NectarDefaultVault -ErrorAction SilentlyContinue #Stop # If no secret name was found, try to escape the dots with a \. Explicitly required for Keeper vaults, but may be used by others If (-Not $Global:NectarToken) { $NectarSecretName = $NectarSecretName.Replace('.','\.') $Global:NectarToken = Get-Secret -Name $NectarSecretName -AsPlainText -Vault $Global:NectarDefaultVault -ErrorAction Stop } $Global:NectarSecretName = $NectarSecretName } Catch { Throw "Could not find access token for $CloudFQDN. Run New-NectarTokenRegistration to create one." Return } } # Pull credentials from secret if specified If ($CredSecret) { Try { $Credential = Get-Secret $CredSecret } Catch { Throw "Cannot find secret: $CredSecret" } } # Only run on first execution of Connect-NectarCloud or if the CloudFQDN has changed If ((-Not $Global:NectarCred -And -Not $Global:NectarSecretName) -Or (-Not $Global:NectarCloud) -Or ($Global:NectarCloud -ne $CloudFQDN)) { # Check and notify if updated Nectar PS module available [string]$InstalledNectarPSVer = (Get-InstalledModule -Name Nectar10 -ErrorAction SilentlyContinue).Version If ($InstalledNectarPSVer -gt 0) { [string]$LatestNectarPSVer = (Find-Module Nectar10).Version If ($LatestNectarPSVer -gt $InstalledNectarPSVer) { Write-Host "=============== Nectar PowerShell module version $LatestNectarPSVer available ===============" -ForegroundColor Yellow Write-Host "You are running version $InstalledNectarPSVer. Type " -ForegroundColor Yellow -NoNewLine Write-Host 'Update-Module Nectar10' -ForegroundColor Green -NoNewLine Write-Host ' to update. NOTE: Close and reopen the PowerShell window for any module update to take effect.' -ForegroundColor Yellow } } # Create authorization header If ($UseToken) { # Refresh token and create auth header Try { $RefreshHeaders = @{ 'x-refresh-token' = $Global:NectarToken.RefreshToken 'authorization' = "Bearer $($Global:NectarToken.AccessToken)" 'x-domain-name' = $Global:NectarToken.DomainName } Write-Verbose "x-refresh-token = $($Global:NectarToken.RefreshToken)" Write-Verbose "authorization = Bearer $($Global:NectarToken.AccessToken)" Write-Verbose "x-domain-name = $($Global:NectarToken.DomainName)" Write-Verbose "Renewing token via https://$CloudFQDN/aapi/jwt/token/renew" $WebRequest = Invoke-WebRequest -Uri "https://$CloudFQDN/aapi/jwt/token/renew" -Method POST -Headers $RefreshHeaders -UseBasicParsing Write-Verbose $WebRequest.Content $WebResponse = ($WebRequest.Content | ConvertFrom-JSON) $Global:NectarToken.AccessToken = $WebResponse.AccessToken $Global:NectarToken.RefreshToken = $WebResponse.RefreshToken $Global:NectarTokenRefreshTime = Get-Date If ($Global:NectarDefaultVault -eq 'Keeper') { Set-Secret -Name "$($Global:NectarSecretName).AccessToken" -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken.AccessToken Set-Secret -Name "$($Global:NectarSecretName).RefreshToken" -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken.RefreshToken } Else { Set-Secret -Name $Global:NectarSecretName -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken } $Headers = @{ 'Authorization' = "Bearer $($Global:NectarToken.AccessToken)" 'x-domain-name' = $Global:NectarToken.DomainName } $ConnectionString = $NectarSecretName.Replace('\.','.') # Remove credential global variables Remove-Variable NectarCred -Scope Global -ErrorAction:SilentlyContinue } Catch { Throw $_ } } Else { # Create basic auth header $UserName = $Credential.UserName $Password = $Credential.GetNetworkCredential().Password $Base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($UserName):$($Password)")) $Headers = @{Authorization = "Basic $Base64AuthInfo"} $ConnectionString = $Credential.UserName } # Attempt connection to tenant $URI = "https://$CloudFQDN/dapi/info/network/types" Write-Verbose $URI $WebRequest = Invoke-WebRequest -Uri $URI -Method GET -Headers $Headers -UseBasicParsing -SessionVariable NectarSession If ($WebRequest.StatusCode -ne 200) { Throw "Could not connect to $CloudFQDN using $ConnectionString" } Else { Write-Host -ForegroundColor Green "Successful connection to " -NoNewLine Write-Host -ForegroundColor Yellow "https://$CloudFQDN" -NoNewLine Write-Host -ForegroundColor Green " using " -NoNewLine Write-Host -ForegroundColor Yellow $ConnectionString $Global:NectarCloud = $CloudFQDN $Global:NectarCred = $Credential $Global:NectarAuthHeader = $Headers $Global:NectarTimeZone = (Get-NectarUserAccountSettings).Preferences.timeZone $UserInfo = Invoke-RestMethod -Uri "https://$Global:NectarCloud/adminapi/user" -Method GET -Headers $Global:NectarAuthHeader -WebSession $NectarSession # Extract any available service provider names $SPNames = $UserInfo.serviceProviderClients If ($SPNames) { $SPNames = $SPNames | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name } # Create tenant list, combining tenant and service provider names. The double @() is because things get weird if there is only one tenant $Global:NectarTenantList = @(@($UserInfo.tenants.tenant) + $SPNames) | Sort-Object # If there is only one available tenant, assign that to the NectarTenantName global variable If ($Global:NectarTenantList.Count -eq 1) { $Global:NectarTenantName = $Global:NectarTenantList } } } # If token was used, check the last refresh time and update it if its more than 90 minutes old If ($Global:NectarToken -And (New-TimeSpan -Start $Global:NectarTokenRefreshTime -End (Get-Date)).TotalMinutes -gt 90) { $RefreshHeaders = @{ 'x-refresh-token' = $Global:NectarToken.RefreshToken 'authorization' = "Bearer $($Global:NectarToken.AccessToken)" } If ($Global:NectarToken.DomainName -ne '') { $RefreshHeaders.Add('x-domain-name', $Global:NectarToken.DomainName) } Write-Verbose 'Refreshing token' $WebRequest = Invoke-WebRequest -Uri "https://$($Global:NectarCloud)/aapi/jwt/token/renew" -Method POST -Headers $RefreshHeaders -UseBasicParsing -SessionVariable NectarSession $Global:NectarToken.AccessToken = ($WebRequest.Content | ConvertFrom-JSON).AccessToken $Global:NectarTokenRefreshTime = Get-Date If ($Global:NectarDefaultVault -eq 'Keeper') { Set-Secret -Name "$($Global:NectarSecretName).AccessToken" -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken.AccessToken } Else { Set-Secret -Name $Global:NectarSecretName -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken } $Headers = @{ 'authorization' = "Bearer $($Global:NectarToken.AccessToken)" } $Global:NectarAuthHeader = $Headers } # Check to see if tenant name was entered and set global variable, if valid. If ($TenantName) { Try { If ($Global:NectarTenantList -Contains $TenantName) { $Global:NectarTenantName = $TenantName.ToLower() # Get the large tenant mode for the tenant $Global:NectarLargeTenantMode = [bool]([int]::Parse((Get-NectarTenantSettings -Parameter large_tenant -TenantName $TenantName).value)) Write-Host -ForegroundColor Green "Successsfully set the tenant name to " -NoNewLine Write-Host -ForegroundColor Yellow "$TenantName" -NoNewLine Write-Host -ForegroundColor Green ". This tenant name will be used in all subsequent commands." } Else { $TList = $Global:NectarTenantList -join ', ' Write-Error "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)" } } Catch { # Just set the tenant name if we are unable to validate the tenant name. $Global:NectarTenantName = $TenantName Write-Host -ForegroundColor Green "Set the tenant name to " -NoNewLine Write-Host -ForegroundColor Yellow "$TenantName" -NoNewLine Write-Host -ForegroundColor Green " but unable to verify if the tenant exists. This tenantname will be used in all subsequent commands." } } ElseIf ($PSBoundParameters.ContainsKey('TenantName')) { # Remove the NectarTenantName global variable only if TenantName is explicitly set to NULL Remove-Variable NectarTenantName -Scope Global -ErrorAction:SilentlyContinue } } } Function Disconnect-NectarCloud { <# .SYNOPSIS Disconnects from any active Nectar DXP connection .DESCRIPTION Essentially deletes any stored credentials and FQDN from global variables .EXAMPLE Disconnect-NectarCloud Disconnects from all active connections to Nectar DXP tenants .NOTES Version 1.1 #> [Alias("dnc")] [cmdletbinding()] param () Remove-Variable NectarCred -Scope Global -ErrorAction:SilentlyContinue Remove-Variable NectarCloud -Scope Global -ErrorAction:SilentlyContinue Remove-Variable NectarTenantName -Scope Global -ErrorAction:SilentlyContinue Remove-Variable NectarTenantList -Scope Global -ErrorAction:SilentlyContinue Remove-Variable NectarAuthHeader -Scope Global -ErrorAction:SilentlyContinue Remove-Variable NectarSecretName -Scope Global -ErrorAction:SilentlyContinue Remove-Variable NectarToken -Scope Global -ErrorAction:SilentlyContinue Remove-Variable NectarTokenRefreshTime -Scope Global -ErrorAction:SilentlyContinue Remove-Variable NectarDefaultVault -Scope Global -ErrorAction:SilentlyContinue Remove-Variable NectarTimeZone -Scope Global -ErrorAction:SilentlyContinue Remove-Variable NectarLargeTenantMode -Scope Global -ErrorAction:SilentlyContinue } Function Get-NectarToken { <# .SYNOPSIS Returns a list of all JWT tokens assigned to the logged in user .DESCRIPTION Returns a list of all JWT tokens assigned to the logged in user .EXAMPLE Get-NectarToken .NOTES Version 1.0 #> [cmdletbinding()] param () Begin { Connect-NectarCloud } Process { Try { $URI = "https://$Global:NectarCloud/aapi/jwt/token" Write-Verbose $URI $JSON = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader Return $JSON } Catch { } } } Function New-NectarToken { <# .SYNOPSIS Creates a new JSON web token (JWT) to be used in scripts .DESCRIPTION Creates a new JSON web token (JWT) to be used in scripts .PARAMETER TokenName A descriptive name for the token .EXAMPLE New-NectarToken -TokenName 'ScriptToken' .NOTES Version 1.0 #> [cmdletbinding()] param ( [Parameter(Mandatory=$True)] [string]$TokenName ) Begin { Connect-NectarCloud } Process { If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName } Try { $URI = "https://$Global:NectarCloud/aapi/jwt/token?tokenName=$TokenName&validityDays=$ValidityDuration&tenant=$TenantName" Write-Verbose $URI $JSON = Invoke-RestMethod -Method POST -URI $URI -Headers $Global:NectarAuthHeader Return $JSON } Catch { Write-Error "Could not create access token. $($_.Exception.Message)" } } } Function Update-NectarToken { <# .SYNOPSIS Refreshes an expired Nectar token .DESCRIPTION Refreshes the currently used Nectar token .EXAMPLE Update-NectarToken .NOTES Version 1.0 #> $RefreshHeaders = @{ 'x-refresh-token' = $Global:NectarToken.RefreshToken 'authorization' = "Bearer $($Global:NectarToken.AccessToken)" 'x-domain-name' = $Global:NectarToken.DomainName } Try { $Global:NectarToken.AccessToken = Invoke-RestMethod -Uri "https://$Global:NectarCloud/aapi/jwt/token/renew" -Method POST -Headers $RefreshHeaders $Headers = @{ 'authorization' = "Bearer $($Global:NectarToken.AccessToken)" } $Global:NectarAuthHeader = $Headers Set-Secret -Name "$($Global:NectarSecretName).AccessToken" -Secret $Global:NectarToken.AccessToken Write-Host -ForegroundColor Green "Successfully updated " -NoNewLine Write-Host -ForegroundColor Yellow $Global:NectarSecretName } Catch { Throw "Error refreshing $($Global:NectarSecretName)" } } Function Remove-NectarToken { <# .SYNOPSIS Remove a Nectar token .DESCRIPTION Remove a Nectar token .PARAMETER RefreshToken The GUID of a refresh token to remove. .EXAMPLE Remove-NectarToken -AccessToken fd173c75-891c-4357-b5a3-0855c2a56299 .EXAMPLE Get-NectarToken | Where-Object {$_.Name -eq 'Testing'} | Remove-NectarToken .NOTES Version 1.0 #> [cmdletbinding()] param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)] [string]$RefreshToken ) Begin { Connect-NectarCloud } Process { $RefreshHeaders = $Global:NectarAuthHeader $RefreshHeaders['x-refresh-token'] = $RefreshToken Try { $URI = "https://$Global:NectarCloud/aapi/jwt/token/revoke" Write-Verbose $URI $JSON = Invoke-RestMethod -Method DELETE -URI $URI -Headers $RefreshHeaders Return "Total number of tokens removed: $JSON" } Catch { Write-Error "Could not delete access token with refresh token $($RefreshToken). $($_.Exception.Message)" } } } Function New-NectarTokenRegistration { <# .SYNOPSIS Registers a Nectar DXP token for connecting to Nectar DXP using JWT .DESCRIPTION Registers a Nectar DXP token for connecting to Nectar DXP using JWT. One-time task required before attempting to access Nectar DXP APIs. Stored using Microsoft SecretManagement PS module. If SecretManagement PS module is is not installed, install via: Install-Module Microsoft.PowerShell.SecretManagement There are several PS modules that connect to different secret providers. Install the one appropriate for your situation prior to running this command. For example, the PS SecretStore stores secrets on the local machine and can be installed via: Install-Module Microsoft.PowerShell.SecretStore .PARAMETER CloudFQDN The FQDN of the Nectar DXP cloud. .PARAMETER Identifier An optional unique identifier (such as script name or username) to use when saving secrets to a secret store shared by multiple parties/scripts .PARAMETER AccessToken The access token to use for connecting to Nectar DXP .PARAMETER RefreshToken The refresh token used to refresh the Nectar DXP access token every 2 hours .PARAMETER DomainName The name of the email domain to use for the token. Should be the same one used for the user's login .PARAMETER SecretVault The name of the secret vault to install the secret. Use if installing to non-default secret vault .EXAMPLE Connect-NectarCloud -Credential $cred -CloudFQDN contoso.nectar.services Connects to the contoso.nectar.services Nectar DXP cloud using the credentials supplied to the Get-Credential command .NOTES Version 1.0 #> Param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] # [ValidateScript ({ # If ($_ -Match "^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$") { # $True # } # Else { # Throw "ERROR: Nectar DXP cloud name must be in FQDN format." # } # })] [string]$CloudFQDN, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$Identifier, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)] [string]$AccessToken, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)] [string]$RefreshToken, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$DomainName, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$SecretVault ) Begin { # Verify the SecretManagement module is installed If (!(Get-InstalledModule -Name 'Microsoft.PowerShell.SecretManagement' -ErrorAction SilentlyContinue)) { Throw "SecretManagement module not installed. Please install using 'Install-Module Microsoft.PowerShell.SecretManagement'" } } Process { # Need to force TLS 1.2, if not already set If ([Net.ServicePointManager]::SecurityProtocol -ne 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } If (($Global:NectarCloud) -And (-not $CloudFQDN)) { $CloudFQDN = $Global:NectarCloud } # Make sure CloudFQDN does not have extraneous characters and just includes the FQDN $RegEx = "^(?:http(s)?:\/\/)?([\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:?#[\]@!\$&'\(\)\*\+,;=.]+)" $FQDNMatch = Select-String -Pattern $Regex -InputObject $CloudFQDN $CloudFQDN = $FQDNMatch.Matches.Groups[2].Value # Create hash table with token information $TokenData = @{ 'AccessToken' = $AccessToken 'RefreshToken' = $RefreshToken 'DomainName' = $DomainName } If ($Identifier) { $SecretName = "$($CloudFQDN)-$($Identifier)-accesstoken" } Else { $SecretName = "$($CloudFQDN)-accesstoken" } $Params = @{ 'Name' = $SecretName 'Secret' = $TokenData } If ($SecretVault) {$Params.Add('Vault',$SecretVault)} Set-Secret @Params Write-Host "Successfully created $SecretName" } } Function Get-NectarConnectionInfo { <# .SYNOPSIS Shows information about the active Nectar DXP connection .DESCRIPTION Shows information about the active Nectar DXP connection .EXAMPLE Get-NectarConnectionInfo .NOTES Version 1.1 #> [Alias("gnci")] [cmdletbinding()] param () $CloudInfo = "" | Select-Object -Property CloudFQDN, Credential $CloudInfo.CloudFQDN = $Global:NectarCloud $CloudInfo.Credential = ($Global:NectarCred).UserName $CloudInfo | Add-Member -TypeName 'Nectar.CloudInfo' Try { $TenantCount = Get-NectarTenantNames If ($TenantCount.Count -gt 1) { If ($Global:NectarTenantName) { $CloudInfo | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $Global:NectarTenantName } Else { $CloudInfo | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue '<Not Set>' } } } Catch { } Return $CloudInfo } Function Get-MSGraphAccessToken { <# .SYNOPSIS Get a Microsoft Graph access token for a given MS tenant. Needed to run other Graph API queries. .DESCRIPTION Get a Microsoft Graph access token for a given MS tenant. Needed to run other Graph API queries. .PARAMETER MSClientID The MS client ID for the application granted access to Azure AD. .PARAMETER MSClientSecret The MS client secret for the application granted access to Azure AD. .PARAMETER MSTenantID The MS tenant ID for the O365 customer granted access to Azure AD. .PARAMETER CertFriendlyName The friendly name of an installed certificate to be used for certificate authentication. Can be used instead of MSClientSecret .PARAMETER CertThumbprint The thumbprint of an installed certificate to be used for certificate authentication. Can be used instead of MSClientSecret .PARAMETER CertPath The path to a PFX certificate to be used for certificate authentication. Can be used instead of MSClientSecret .PARAMETER CertStore The certificate store to be used for certificate authentication. Select either LocalMachine or CurrentUser. Used in conjunction with CertThumbprint or CertFriendlyName Can be used instead of MSClientSecret. .PARAMETER Scope The scope for the associated access token. Select from GraphAPI or Teams. Defaults to GraphAPI .EXAMPLE $AuthToken = Get-MSGraphAccessToken -MSClientID 41a228ad-db6c-4e4e-4184-6d8a1175a35f -MSClientSecret 43Rk5Xl3K349w-pFf0i_Rt45Qd~ArqkE32. -MSTenantID 17e1e614-8119-48ab-8ba1-6ff1d94a6930 Obtains an authtoken for the given tenant using secret-based auth and saves the results for use in other commands in a variable called $AuthToken .EXAMPLE $AuthToken = Get-MSGraphAccessToken -MSClientID 029834092-234234-234234-23442343 -MSTenantID 234234234-234234-234-23-42342342 -CertFriendlyName 'CertAuth' -CertStore LocalMachine Obtains an authtoken for the given tenant using certificate auth and saves the results for use in other commands in a variable called $AuthToken .NOTES Version 1.1 #> Param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)] [string]$MSClientID, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$MSClientSecret, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)] [string]$MSTenantID, [Parameter(Mandatory=$False)] [switch]$N10Cert, [Parameter(Mandatory=$False)] [string]$CertFriendlyName, [Parameter(Mandatory=$False)] [string]$CertThumbprint, [Parameter(Mandatory=$False)] [string]$CertPath, [Parameter(Mandatory=$False)] [ValidateSet('LocalMachine','CurrentUser', IgnoreCase=$True)] [string]$CertStore = 'CurrentUser', [Parameter(Mandatory=$False)] [ValidateSet('GraphAPI','TeamsPS','TeamsAPI','Test', IgnoreCase=$True)] [string]$Scope = 'GraphAPI', [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$TenantName ) Begin { Switch ($Scope) { 'GraphAPI' { $AppliedScope = 'https://graph.microsoft.com/.default' } 'TeamsPS' { $AppliedScope = '48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default' } # Can be used to connect to TeamsPS module along with Graph API token: Connect-MicrosoftTeams -AccessTokens @("$graphToken", "$teamsToken") 'TeamsAPI' { $AppliedScope = 'https://ring0.api.interfaces.records.teams.microsoft.com/.default' } # Requires Azure app with permission 'Skype and Teams Tenant Admin API/application_access' 'Test' { $AppliedScope = 'https://api.interfaces.records.teams.microsoft.com/user_impersonation/.default' } } } Process { If ($MSClientSecret) { # Get the Azure Graph API auth token $AuthBody = @{ grant_type = 'client_credentials' client_id = $MSClientID client_secret = $MSClientSecret scope = $AppliedScope } $URI = "https://login.microsoftonline.com/$MSTenantID/oauth2/v2.0/token" Write-Verbose $URI $JSON_Auth = Invoke-RestMethod -Method POST -URI $URI -Body $AuthBody $AuthToken = $JSON_Auth.access_token Return $AuthToken } Else { <# Needs access to the full certificate stored in Nectar DXP, and can be exported to PEM via Get-NectarMSTeamsSubscription (only if you have global admin privs) Get-NectarMSTeamsSubscription -ExportCertificate Need to create a certificate from the resulting PEM files using the following command: openssl pkcs12 -export -in TeamsCert.pem -inkey TeamsPriv.key -CSP "Microsoft Enhanced RSA and AES Cryptographic Provider" -out FullCert.pfx Requires that OpenSSL is installed To convert a PFX to a Kubernetes secret in Base64 format, run the following: $fileContentBytes = get-content FullCert.pfx -AsByteStream [System.Convert]::ToBase64String($fileContentBytes) | Out-File pfx-encoded-bytes.txt Then use the resulting certificate to obtain an access token: $GraphToken = Get-MSGraphAccessToken -MSTenantID <tenantID> -MSClientID <Client/AppID> -CertPath .\FullCert.pfx Then use the resulting token in other commands like: Test-MSTeamsConnectivity -AuthToken $GraphToken #> # First try to get the certificate automatically from the user's certificate store If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { $TenantName = $Global:NectarTenantName } ElseIf ($TenantName) { If ($TenantName -NotIn $Global:NectarTenantList) { $TList = $Global:NectarTenantList -join ', ' Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)" } } $Certificate = Get-ChildItem Cert:\CurrentUser\My -ErrorAction SilentlyContinue | Where-Object {$_.FriendlyName -eq $TenantName} # Get the certificate information via one of several methods If ($CertThumbprint) { $Certificate = Get-Item Cert:\$CertStore\My\$CertThumbprint } If ($CertFriendlyName) { $Certificate = Get-ChildItem Cert:\$CertStore\My | Where-Object {$_.FriendlyName -eq $CertFriendlyName} } If ($CertPath) { $Certificate = Get-PfxCertificate -FilePath $CertPath } If ($N10Cert) { # Get certificate BASE64 encoding from N10 $CertBlob = (Get-NectarMSTeamsSubscription).msClientCertificateDto.certificate $CertRaw = $CertBlob -replace "-----BEGIN CERTIFICATE-----", $NULL -replace "-----END CERTIFICATE-----", $NULL $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $Certificate.Import([Convert]::FromBase64String($CertRaw)) } If ($Certificate) { # Adapted from https://adamtheautomator.com/microsoft-graph-api-powershell/ # Create base64 hash of certificate $CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash()) # Create JWT timestamp for expiration $StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime() $JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds $JWTExpiration = [math]::Round($JWTExpirationTimeSpan,0) # Create JWT validity start timestamp $NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0) # Create JWT header $JWTHeader = @{ alg = "RS256" typ = "JWT" # Use the CertificateBase64Hash and replace/strip to match web encoding of base64 x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '=' } # Create JWT payload $JWTPayload = @{ # What endpoint is allowed to use this JWT aud = "https://login.microsoftonline.com/$MSTenantID/oauth2/token" # Expiration timestamp exp = $JWTExpiration # Issuer = your application iss = $MSClientID # JWT ID: random guid jti = [guid]::NewGuid() # Not to be used before nbf = $NotBefore # JWT Subject sub = $MSClientID } # Convert header and payload to base64 $JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json)) $EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte) $JWTPayloadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json)) $EncodedPayload = [System.Convert]::ToBase64String($JWTPayloadToByte) # Join header and Payload with "." to create a valid (unsigned) JWT $JWT = $EncodedHeader + "." + $EncodedPayload # Get the private key object of your certificate $PrivateKey = $Certificate.PrivateKey # Define RSA signature and hashing algorithm $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1 $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256 # Create a signature of the JWT ##### Breaks down here. Only works with full cert downloaded/installed ##### $Signature = [Convert]::ToBase64String($PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)) -replace '\+','-' -replace '/','_' -replace '=' # Join the signature to the JWT with "." $JWT = $JWT + "." + $Signature # Create a hash with body parameters $Body = @{ client_id = $MSClientID client_assertion = $JWT client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' scope = $AppliedScope grant_type = 'client_credentials' } $Uri = "https://login.microsoftonline.com/$MSTenantID/oauth2/v2.0/token" Write-Verbose $URI # Use the self-generated JWT as Authorization $Header = @{ Authorization = "Bearer $JWT" } # Splat the parameters for Invoke-Restmethod for cleaner code $PostSplat = @{ ContentType = 'application/x-www-form-urlencoded' Method = 'POST' Body = $Body Uri = $Uri Headers = $Header } $Request = Invoke-RestMethod @PostSplat $AuthToken = $Request.access_token Return $AuthToken } Else { Write-Error "Could not find certificate in $CertStore. $($_.Exception.Message)" } } } } Function Test-MSTeamsConnectivity { <# .SYNOPSIS Tests if we are able to retrieve Teams call data and Azure AD information from a O365 tenant. .DESCRIPTION Tests if we are able to retrieve Teams call data and Azure AD information from a O365 tenant. .PARAMETER MSClientID The MS client ID for the application granted access to Azure AD. .PARAMETER MSClientSecret The MS client secret for the application granted access to Azure AD. .PARAMETER MSTenantID The MS tenant ID for the O365 customer granted access to Azure AD. .PARAMETER SkipUserCount Skips the user count .PARAMETER TenantName The name of the Nectar DXP tenant. Used in multi-tenant configurations. .PARAMETER AuthToken The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken .EXAMPLE Get-NectarMSTeamsSubscription -TenantName contoso | Test-MSAzureADAccess .NOTES Version 1.1 #> Param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$MSClientID, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$MSClientSecret, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$MSTenantID, [Parameter(Mandatory=$False)] [switch]$HideOutput, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$TenantName, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$AuthToken ) Begin { # Use globally set tenant name, if one was set and not explicitly included in the command If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { $TenantName = $Global:NectarTenantName } ElseIf ($TenantName) { If ($TenantName -NotIn $Global:NectarTenantList) { $TList = $Global:NectarTenantList -join ', ' Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)" } } } Process { If ($MSTenantID) { $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID } ElseIf (!$AuthToken) { $AuthToken = Get-NectarMSTeamsConfig -TenantName $TenantName | Get-MSGraphAccessToken } If (!$AuthToken) { Throw "Could not obtain auth token for tenant $TenantName. Does the tenant exist and have a valid MSTeams config?" } $Headers = @{ Authorization = "Bearer $AuthToken" } $AccessResults = [pscustomobject][ordered]@{ [string]'TeamsCallRecords' = 'FAIL' [string]'TeamsDevices' = 'FAIL' [string]'AzureUsers' = 'FAIL' [string]'AzureGroups' = 'FAIL' } # Test MS Teams call record access Try { $FromDateTime = (Get-Date -Format 'yyyy-MM-ddT00:00:00Z') $URI = "https://graph.microsoft.com/v1.0/communications/callRecords?`$filter=startDateTime ge $FromDateTime" Write-Verbose $URI If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - " -NoNewLine } If (!$HideOutput) { Write-Host 'Teams CR Status: ' -NoNewLine } $NULL = Invoke-RestMethod -Method GET -URI $URI -Headers $Headers If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green } $AccessResults.TeamsCallRecords = 'PASS' } Catch { If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red } } # Test MS Teams device access Try { $URI = 'https://graph.microsoft.com/beta/teamwork/devices' If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - " -NoNewLine } If (!$HideOutput) { Write-Host 'Teams Device Status: ' -NoNewLine } $NULL = Invoke-RestMethod -Method GET -URI $URI -Headers $Headers If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green } $AccessResults.TeamsDevices = 'PASS' } Catch { If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red } } # Test Azure AD user access Try { $URI = 'https://graph.microsoft.com/v1.0/users' If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - " -NoNewLine } If (!$HideOutput) { Write-Host 'Azure AD User Status: ' -NoNewLine } $NULL = Invoke-RestMethod -Method GET -URI $URI -Headers $Headers If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green } $AccessResults.AzureUsers = 'PASS' } Catch { If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red } Get-JSONErrorStream -JSONResponse $_ $SkipUserCount = $True } # Test Azure AD group access Try { $URI = 'https://graph.microsoft.com/v1.0/groups' If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - " -NoNewLine } If (!$HideOutput) { Write-Host 'Azure AD Group Status: ' -NoNewLine } $NULL = Invoke-RestMethod -Method GET -URI $URI -Headers $Headers If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green } $AccessResults.AzureGroups = 'PASS' } Catch { If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red } } If ($AccessResults.AzureUsers -eq 'PASS') { $UserCount = Get-MSTeamsUserLicense -AuthToken $AuthToken -TenantName $TenantName } Clear-Variable AuthToken $Results = @{ AccessResults = $AccessResults UserCount = $UserCount } Return $Results } } Function Get-NectarDefaultTenantName { <# .SYNOPSIS Set the default tenant name for use with other commands. .DESCRIPTION Set the default tenant name for use with other commands. .NOTES Version 1.0 #> [Alias("stne")] Param () If ($Global:NectarTenantName) { # Use globally set tenant name, if one was set and not explicitly included in the command Return $Global:NectarTenantName } ElseIf (!$TenantName -And !$Global:NectarTenantName) { # If a tenant name wasn't set (normal for most connections, set the TenantName variable if only one available $URI = "https://$Global:NectarCloud/aapi/tenant" Write-Verbose $URI $TenantList = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader If ($TenantList.Count -eq 1) { Return $TenantList } Else { $TenantList | ForEach-Object { $TList += ($(If($TList){", "}) + $_) } Write-Error "TenantName was not specified. Select one of $TList" $PSCmdlet.ThrowTerminatingError() Return } } } |