diff --git a/Modules/MSCloudLoginAssistant/ConnectionProfile.psm1 b/Modules/MSCloudLoginAssistant/ConnectionProfile.psm1 index d91717d..8e26139 100644 --- a/Modules/MSCloudLoginAssistant/ConnectionProfile.psm1 +++ b/Modules/MSCloudLoginAssistant/ConnectionProfile.psm1 @@ -34,6 +34,7 @@ class MSCloudLoginConnectionProfile # Workloads Object Creation $this.ExchangeOnline = New-Object ExchangeOnline $this.MicrosoftGraph = New-Object MicrosoftGraph + $this.MSCommerce = New-Object MSCommerce $this.PnP = New-Object PnP $this.PowerPlatform = New-Object PowerPlatform $this.SecurityComplianceCenter = New-Object SecurityComplianceCenter @@ -333,6 +334,27 @@ class MicrosoftGraph:Workload } } +class MSCommerce:Workload +{ + [string] + $Scope = 'aeb86249-8ea3-49e2-900b-54cc8e308f85/.default' + + MSCommerce() + { + } + + [void] Connect() + { + ([Workload]$this).Setup() + + if ($null -ne $this.Credentials -and [System.String]::IsNullOrEmpty($this.TenantId)) + { + $this.TenantId = $this.Credentials.Username.Split('@')[1] + } + Connect-MSCloudLoginMSCommerce + } +} + class PnP:Workload { [string] diff --git a/Modules/MSCloudLoginAssistant/MSCloudLoginAssistant.psd1 b/Modules/MSCloudLoginAssistant/MSCloudLoginAssistant.psd1 index 6f8d04b..717d9a3 100644 --- a/Modules/MSCloudLoginAssistant/MSCloudLoginAssistant.psd1 +++ b/Modules/MSCloudLoginAssistant/MSCloudLoginAssistant.psd1 @@ -12,7 +12,7 @@ RootModule = 'MSCloudLoginAssistant.psm1' # Version number of this module. - ModuleVersion = '1.1.16' + ModuleVersion = '1.1.17.1' # Supported PSEditions # CompatiblePSEditions = @() @@ -70,6 +70,7 @@ 'ConnectionProfile.psm1', 'Workloads\ExchangeOnline.psm1', 'Workloads\MicrosoftGraph.psm1', + 'Workloads\MSCommerce.psm1', 'Workloads\Teams.psm1', 'Workloads\PnP.psm1', 'Workloads\PowerPlatform.psm1', diff --git a/Modules/MSCloudLoginAssistant/MSCloudLoginAssistant.psm1 b/Modules/MSCloudLoginAssistant/MSCloudLoginAssistant.psm1 index 64451f6..f7cf3d1 100644 --- a/Modules/MSCloudLoginAssistant/MSCloudLoginAssistant.psm1 +++ b/Modules/MSCloudLoginAssistant/MSCloudLoginAssistant.psm1 @@ -124,6 +124,16 @@ function Connect-M365Tenant $Global:MSCloudLoginConnectionProfile.Teams.Identity = $Identity $Global:MSCloudLoginConnectionProfile.Teams.Connect() } + 'MSCommerce' + { + $Global:MSCloudLoginConnectionProfile.MSCommerce.Credentials = $Credential + $Global:MSCloudLoginConnectionProfile.MSCommerce.ApplicationId = $ApplicationId + $Global:MSCloudLoginConnectionProfile.MSCommerce.TenantId = $TenantId + $Global:MSCloudLoginConnectionProfile.MSCommerce.CertificateThumbprint = $CertificateThumbprint + $Global:MSCloudLoginConnectionProfile.MSCommerce.ApplicationSecret = $ApplicationSecret + $Global:MSCloudLoginConnectionProfile.MSCommerce.AccessTokens = $AccessTokens + $Global:MSCloudLoginConnectionProfile.MSCommerce.Connect() + } 'PnP' { $Global:MSCloudLoginConnectionProfile.PnP.Credentials = $Credential @@ -974,3 +984,36 @@ function Assert-IsNonInteractiveShell return $true } + +function Get-JWTPayload +{ + param( + [Parameter()] + [System.String] + $AccessToken + ) + + # kindly nicked from https://www.michev.info/blog/post/2140/decode-jwt-access-and-id-tokens-via-powershell + + #Validate as per https://tools.ietf.org/html/rfc7519 + #Access and ID tokens are fine, Refresh tokens will not work + if (-not $AccessToken.Contains(".") -or -not $AccessToken.StartsWith("eyJ")) + { + throw "Invalid token '$AccessToken' - it looks like a Refresh token" + } + + #Payload + $tokenPayload = $AccessToken.Split(".")[1].Replace('-', '+').Replace('_', '/') + #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0 + while ($tokenPayload.Length % 4) { Write-Verbose "Invalid length for a Base-64 char array or string, adding ="; $tokenPayload += "=" } + #Convert to Byte array + $tokenByteArray = [System.Convert]::FromBase64String($tokenPayload) + #Convert to string array + $tokenArray = [System.Text.Encoding]::ASCII.GetString($tokenByteArray) + Write-Verbose "Decoded array in JSON format:" + Write-Verbose $tokenArray + #Convert from JSON to PSObject + $tokobj = $tokenArray | ConvertFrom-Json + + return $tokobj +} diff --git a/Modules/MSCloudLoginAssistant/Workloads/MSCommerce.psm1 b/Modules/MSCloudLoginAssistant/Workloads/MSCommerce.psm1 new file mode 100644 index 0000000..b69e389 --- /dev/null +++ b/Modules/MSCloudLoginAssistant/Workloads/MSCommerce.psm1 @@ -0,0 +1,210 @@ +function Connect-MSCloudLoginMSCommerce +{ + [CmdletBinding()] + param() + + $ProgressPreference = 'SilentlyContinue' + $WarningPreference = 'SilentlyContinue' + $VerbosePreference = 'SilentlyContinue' + + # If the current profile is not the same we expect, make the switch. + if ($Global:MSCloudLoginConnectionProfile.MSCommerce.Connected) + { + if (($Global:MSCloudLoginConnectionProfile.MSCommerce.AuthenticationType -eq 'ServicePrincipalWithSecret' ` + -or $Global:MSCloudLoginConnectionProfile.MSCommerce.AuthenticationType -eq 'Identity') ` + -and (Get-Date -Date $Global:MSCloudLoginConnectionProfile.MSCommerce.ConnectedDateTime) -lt [System.DateTime]::Now.AddMinutes(-50)) + { + Write-Verbose -Message 'Token is about to expire, renewing' + + $Global:MSCloudLoginConnectionProfile.MSCommerce.Connected = $false + } + elseif ($null -eq (Get-MgContext)) + { + $Global:MSCloudLoginConnectionProfile.MSCommerce.Connected = $false + } + else + { + return + } + } + + $Global:MSCloudLoginConnectionProfile.MSCommerce.AccessTokens = @() + $azureCloudInstanceArg = @{} + if ($this.EnvironmentName) + { + $azureCloudInstanceArg.AzureCloudInstance = $this.EnvironmentName + } + + Import-Module MSCommerce -Global + #Connect-MSCommerce not used, it provides token-acquisition as below but with next to no options. + # it is required to call the other MSCommerce-cmdlets/functions with an explicit token: + # $Global:MSCloudLoginConnectionProfile.MSCommerce.AccessTokens[0] + + if ($Global:MSCloudLoginConnectionProfile.MSCommerce.AuthenticationType -eq 'CredentialsWithApplicationId' -or + $Global:MSCloudLoginConnectionProfile.MSCommerce.AuthenticationType -eq 'Credentials') + { + Write-Verbose -Message 'Will try connecting with user credentials' + Connect-MSCloudLoginMSCommerceWithUser + } + elseif ($Global:MSCloudLoginConnectionProfile.MSCommerce.AuthenticationType -eq 'CredentialsWithTenantId') + { + Write-Verbose -Message 'Will try connecting with user credentials and Tenant Id' + Connect-MSCloudLoginMSCommerceWithUser -TenantId $Global:MSCloudLoginConnectionProfile.MSCommerce.TenantId + } + elseif ($Global:MSCloudLoginConnectionProfile.MSCommerce.AuthenticationType -eq 'Identity') + { + Write-Verbose 'Connecting with managed identity' + + $resourceEndpoint = ($Global:MSCloudLoginConnectionProfile.MSCommerce.ResourceUrl -split '/')[2] + if ('AzureAutomation/' -eq $env:AZUREPS_HOST_ENVIRONMENT) + { + $url = $env:IDENTITY_ENDPOINT + $headers = New-Object 'System.Collections.Generic.Dictionary[[String],[String]]' + $headers.Add('X-IDENTITY-HEADER', $env:IDENTITY_HEADER) + $headers.Add('Metadata', 'True') + $body = @{resource = "https://$resourceEndPoint/" } + $oauth2 = Invoke-RestMethod $url -Method 'POST' -Headers $headers -ContentType 'application/x-www-form-urlencoded' -Body $body + $accessToken = $oauth2.access_token + } + elseif('http://localhost:40342' -eq $env:IMDS_ENDPOINT) + { + #Get endpoint for Azure Arc Connected Device + $apiVersion = "2020-06-01" + $resource = "https://$resourceEndpoint" + $endpoint = "{0}?resource={1}&api-version={2}" -f $env:IDENTITY_ENDPOINT,$resource,$apiVersion + $secretFile = "" + try + { + Invoke-WebRequest -Method GET -Uri $endpoint -Headers @{Metadata='True'} -UseBasicParsing + } + catch + { + $wwwAuthHeader = $_.Exception.Response.Headers["WWW-Authenticate"] + if ($wwwAuthHeader -match "Basic realm=.+") + { + $secretFile = ($wwwAuthHeader -split "Basic realm=")[1] + } + } + $secret = Get-Content -Raw $secretFile + $response = Invoke-WebRequest -Method GET -Uri $endpoint -Headers @{Metadata='True'; Authorization="Basic $secret"} -UseBasicParsing + if ($response) + { + $accessToken = (ConvertFrom-Json -InputObject $response.Content).access_token + } + } + else + { + # Get correct endopint for AzureVM + $oauth2 = Invoke-RestMethod -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2F$($resourceEndpoint)%2F" -Headers @{Metadata = 'true' } + $accessToken = $oauth2.access_token + } + + #$accessToken = $accessToken | ConvertTo-SecureString -AsPlainText -Force + + $Global:MSCloudLoginConnectionProfile.MSCommerce.ConnectedDateTime = [System.DateTime]::Now.ToString() + $Global:MSCloudLoginConnectionProfile.MSCommerce.MultiFactorAuthentication = $false + $Global:MSCloudLoginConnectionProfile.MSCommerce.Connected = $true + $Global:MSCloudLoginConnectionProfile.MSCommerce.AccessTokens += $accessToken + } + else + { + try + { + if ($Global:MSCloudLoginConnectionProfile.MSCommerce.AuthenticationType -eq 'ServicePrincipalWithThumbprint') + { + # Get certificate from CurrentUser or Localmachine + $cert = Get-ChildItem -Path "Cert:\*$($Global:MSCloudLoginConnectionProfile.MSCommerce.CertificateThumbprint)" -Recurse + $token = Get-MsalToken -ClientId $Global:MSCloudLoginConnectionProfile.ApplicationId ` + -TenantId $Global:MSCloudLoginConnectionProfile.MSCommerce.TenantId ` + -Certificate $cert ` + -Scopes $Global:MSCloudLoginConnectionProfile.MSCommerce.Scope ` + @azureCloudInstanceArg + $Global:MSCloudLoginConnectionProfile.MSCommerce.ConnectedDateTime = [System.DateTime]::Now.ToString() + $Global:MSCloudLoginConnectionProfile.MSCommerce.MultiFactorAuthentication = $false + $Global:MSCloudLoginConnectionProfile.MSCommerce.Connected = $true + $Global:MSCloudLoginConnectionProfile.MSCommerce.AccessTokens += $token.AccessToken + } + elseif($Global:MSCloudLoginConnectionProfile.MSCommerce.AuthenticationType -eq 'ServicePrincipalWithSecret') + { + Write-Verbose -Message 'Connecting to MSCommerce with ApplicationSecret' + $secStringPassword = ConvertTo-SecureString -String $Global:MSCloudLoginConnectionProfile.MSCommerce.ApplicationSecret -AsPlainText -Force + #$userName = $Global:MSCloudLoginConnectionProfile.MSCommerce.ApplicationId + #[pscredential]$credObject = New-Object System.Management.Automation.PSCredential ($userName, $secStringPassword) + $token = Get-MsalToken -ClientId $Global:MSCloudLoginConnectionProfile.ApplicationId ยด + -TenantId $Global:MSCloudLoginConnectionProfile.MSCommerce.TenantId ` + -ClientSecret $secStringPassword + -Scopes $Global:MSCloudLoginConnectionProfile.MSCommerce.Scope ` + @azureCloudInstanceArg + $Global:MSCloudLoginConnectionProfile.MSCommerce.ConnectedDateTime = [System.DateTime]::Now.ToString() + $Global:MSCloudLoginConnectionProfile.MSCommerce.MultiFactorAuthentication = $false + $Global:MSCloudLoginConnectionProfile.MSCommerce.Connected = $true + $Global:MSCloudLoginConnectionProfile.MSCommerce.AccessTokens += $token.AccessToken + } + elseif($Global:MSCloudLoginConnectionProfile.MSCommerce.AuthenticationType -eq 'AccessToken') + { + Write-Verbose -Message 'Connecting to MSCommerce with AccessToken' + $Global:MSCloudLoginConnectionProfile.MSCommerce.ConnectedDateTime = [System.DateTime]::Now.ToString() + $Global:MSCloudLoginConnectionProfile.MSCommerce.MultiFactorAuthentication = $false + $Global:MSCloudLoginConnectionProfile.MSCommerce.Connected = $true + $Global:MSCloudLoginConnectionProfile.MSCommerce.TenantId = (Get-JWTPayload -AccessToken $Global:MSCloudLoginConnectionProfile.MSCommerce.AccessTokens[0]).tid + } + Write-Verbose -Message 'Connected' + } + catch + { + Write-Verbose -Message $_ + throw $_ + } + } +} + +function Connect-MSCloudLoginMSCommerceWithUser +{ + [CmdletBinding()] + + <# initial attempt. Doesn't fly + $sessionState = $PSCmdlet.SessionState + $msCommerceToken = $sessionState.PSVariable.GetValue('token') + if ($null -ne $msCommerceToken) + { + # decode JWT to enable identifying authenticated user + $tokenPayload = Get-JWTPayload -AccessToken $msCommerceToken + } + if ($null -ne $msCommerceToken -and $Global:MSCloudLoginConnectionProfile.MSCommerce.Credentials.UserName -ne $tokenPayLoad.upn) + { + Write-Verbose -Message "The current account that is connected doesn't match the one we're trying to authenticate with." + } + #> + + try + { + #Connect-MSCommerce # won't work - DSC-resource expects token to be stored in $Global:MSCloudLoginConnectionProfile.MSCommerce.AccessTokens[0] + + $token = Get-MsalToken -ClientId '3d5cffa9-04da-4657-8cab-c7f074657cad' ` + -UserCredential $Global:MSCloudLoginConnectionProfile.MSCommerce.Credentials ` + -RedirectUri 'http://localhost/m365/commerce' ` + -Scopes $Global:MSCloudLoginConnectionProfile.MSCommerce.Scope ` + @azureCloudInstanceArg + + $Global:MSCloudLoginConnectionProfile.MSCommerce.ConnectedDateTime = [System.DateTime]::Now.ToString() + $Global:MSCloudLoginConnectionProfile.MSCommerce.MultiFactorAuthentication = $true + $Global:MSCloudLoginConnectionProfile.MSCommerce.Connected = $true + $Global:MSCloudLoginConnectionProfile.MSCommerce.AccessTokens += $token.AccessToken + } + catch + { + if ($_.Exception -like 'System.Net.WebException: The remote server returned an error: (400) Bad Request.*' -and ` + (Assert-IsNonInteractiveShell) -eq $true) + { + $warningPref = $WarningPreference + $WarningPreference = 'Continue' + Write-Warning -Message "Unable to retrieve AccessToken. Have you registered the 'M365 License Manager' application already? Please run 'Connect-MSCommerce' and logon using '$($Global:MSCloudLoginConnectionProfile.MSCommerce.Credentials.Username)'" + $WarningPreference = $warningPref + return + } + else + { + throw "Terminating error connecting to MSCommerce: $($_.Eception.Message)" + } + } +} diff --git a/er b/er new file mode 100644 index 0000000..3d6e843 --- /dev/null +++ b/er @@ -0,0 +1,45 @@ +error: wrong number of arguments, should be from 1 to 2 +usage: git config [] + +Config file location + --global use global config file + --system use system config file + --local use repository config file + --worktree use per-worktree config file + -f, --file use given config file + --blob read config from given blob object + +Action + --get get value: name [value-pattern] + --get-all get all values: key [value-pattern] + --get-regexp get values for regexp: name-regex [value-pattern] + --get-urlmatch get value specific for the URL: section[.var] URL + --replace-all replace all matching variables: name value [value-pattern] + --add add a new variable: name value + --unset remove a variable: name [value-pattern] + --unset-all remove all matches: name [value-pattern] + --rename-section rename section: old-name new-name + --remove-section remove a section: name + -l, --list list all + --fixed-value use string equality when comparing values to 'value-pattern' + -e, --edit open an editor + --get-color find the color configured: slot [default] + --get-colorbool find the color setting: slot [stdout-is-tty] + +Type + -t, --type <> value is given this type + --bool value is "true" or "false" + --int value is decimal number + --bool-or-int value is --bool or --int + --bool-or-str value is --bool or string + --path value is a path (file or directory name) + --expiry-date value is an expiry date + +Other + -z, --null terminate values with NUL byte + --name-only show variable names only + --includes respect include directives on lookup + --show-origin show origin of config (file, standard input, blob, command line) + --show-scope show scope of config (worktree, local, global, system, command) + --default with --get, use default value when missing entry +