functions/client_auth_portal.ps1
function Invoke-CecPortalAuthentication { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'Password', Justification = 'Obsolete')] param( $Email, [String] $Password, $OrganizationId = "org_Xe6mj0fXtjGPVzFs", $TenantId = "9e276386-a70c-4cc6-e5e7-08dda91b30c7" ) $ErrorActionPreference = "STOP" $defaultRequestArguments = (Get-Variable -Scope Global -Name ClientDefaultProperties).Value Invoke-WebRequest -SessionVariable "CecLoginSession" -Uri "https://cec.sitecorecloud.io" -Method GET -UseBasicParsing @defaultRequestArguments | Out-Null $response = Invoke-WebRequest -WebSession $CecLoginSession -Uri "https://account.sitecorecloud.io/login/?redirect=https%3A%2F%2Fcec.sitecorecloud.io&scope=%5B%22portal%22%2C%22search-rec%22%2C%22admin%22%2C%22internal%22%2C%22util%22%2C%22discover%22%2C%22event%22%2C%22ingestion%22%5D" -Method GET -UseBasicParsing @defaultRequestArguments # Fetch chuncks Write-Information "Requesting account script bundles to find IDP definitions..." $idpDefinition = ([regex]"`"(/_next/[^`"]+.js)`"").Matches($response.Content) | ForEach-Object { $url = "https://account.sitecorecloud.io$($_.Groups[1].Value)" $content = Invoke-RestMethod -Uri $url @defaultRequestArguments if ($content -match "prod:\s*\{.*?oauth2:\s*(\{sitecoreIdp:\s*\{.*?\}\})") { $Matches[1] } } | ConvertFrom-Json | Select-Object -ExpandProperty sitecoreIdp if($null -eq $idpDefinition) { throw "Could not find IDP definition in the response, please check the login URL or the response content." } else { Write-Information "Found IDP definition for $($idpDefinition.authorizeUrl)" } # Using OIDC PKCE (Proof Key for Code Exchange) to enhance security $pkce = CreatePkceValues $cookie = [System.Net.Cookie]::new('code_verifier', $pkce.Verifier, "/", ".sitecorecloud.io") $CecLoginSession.Cookies.Add($cookie) $url = $idpDefinition.authorizeUrl + "?response_type=code" + "&client_id=" + $idpDefinition.clientId + "&redirect_uri=" + [uri]::EscapeDataString($idpDefinition.redirectUrl) + "&scope=" + [uri]::EscapeDataString($idpDefinition.scope) + "&audience=" + [uri]::EscapeDataString($idpDefinition.audience) + "&code_challenge=" + $pkce.Challenge + "&code_challenge_method=S256" + "&product_codes=Search%2CDiscover" if ("${organizationId}" -ne "") { $url += "&organization_id=${OrganizationId}" } if ("${tenantId}" -ne "") { $url += "&tenant_id=${TenantId}" } $response = Invoke-WebRequest -MaximumRedirection 0 -SkipHttpErrorCheck -ErrorAction SilentlyContinue -WebSession $CecLoginSession -Uri $url -Method GET -UseBasicParsing @defaultRequestArguments $url = ([uri]$idpDefinition.authorizeUrl).GetLeftPart('Authority') + $response.Headers.Location $response = Invoke-WebRequest -WebSession $CecLoginSession -Uri $url -Method GET -UseBasicParsing @defaultRequestArguments do { $formData = New-FormResponseData -Response $response -Values @{ username = $Email password = $Password } Write-Verbose "Submitting login form to $url" $response = Invoke-WebRequest -WebSession $CecLoginSession -Uri $url -Method POST -UseBasicParsing -Body $formData -ContentType "application/x-www-form-urlencoded" -MaximumRedirection 0 -SkipHttpErrorCheck -ErrorAction SilentlyContinue @defaultRequestArguments while ($response.StatusCode -eq 302) { $newUrl = $response.Headers.Location | Select-Object -First 1 if($newUrl -match "^\/") { $url = ([uri]$url).GetLeftPart('Authority') + $newUrl Write-Verbose "Got redirect to $newUrl will request $url" } else { $url = $newUrl } Write-Verbose "Requesting '$url'" $response = Invoke-WebRequest -WebSession $CecLoginSession -Uri $url -Method GET -UseBasicParsing -MaximumRedirection 0 -SkipHttpErrorCheck -ErrorAction SilentlyContinue @defaultRequestArguments } } while ($response.StatusCode -eq 200 -and $response.Content -match "name=`"username`"") $code = $url -split "code=" | Select-Object -Last 1 if ("${code}" -eq "") { throw "Could not login, there is no code in url $($url)" } $formData = @{ grant_type = "authorization_code" code = $code code_verifier = $pkce.Verifier client_id = $idpDefinition.clientId redirect_uri = $idpDefinition.redirectUrl } $response = Invoke-WebRequest -WebSession $CecLoginSession -Uri "https://auth.sitecorecloud.io/oauth/token" -Method POST -Body $formData -ContentType "application/x-www-form-urlencoded" -UseBasicParsing @defaultRequestArguments $token = $response.Content | ConvertFrom-Json Set-CecRefreshToken -RefreshToken $token.refresh_token Set-CecAccessToken -AccessToken $token.access_token } function CreatePkceValues { # Using OIDC PKCE (Proof Key for Code Exchange) to enhance security # https://github.com/darrenjrobinson/PKCE/blob/main/PKCE.psm1 param( # Length must be between 43 and 128 characters [int]$Length = 43 ) # Code verifier is just random string characters, in CEC it is using Crypto module random bytes and therefore base64 and then removing the padding and replacing characters # we will just generate a string of valid characters $codeVerifier = -join (((48..57) * 4) + ((65..90) * 4) + ((97..122) * 4) | Get-Random -Count $Length | ForEach-Object { [char]$_ }) # Hash the verifier $hashAlgo = [System.Security.Cryptography.HashAlgorithm]::Create('sha256') $hash = $hashAlgo.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($codeVerifier)) $b64Hash = [System.Convert]::ToBase64String($hash) $code_challenge = $b64Hash.Substring(0, 43) # Encode by replacing characters $code_challenge = $code_challenge.Replace("/","_") $code_challenge = $code_challenge.Replace("+","-") $code_challenge = $code_challenge.Replace("=","") return @{ Verifier = $codeVerifier Challenge = $code_challenge } } function New-FormResponseData { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function')] param( $Response, $Values = @{} ) $formData = @{} $inputFields = ([regex]"<input[^>]+>").Matches($Response.Content) foreach ($f in $inputFields) { $name = ([regex]"name\s*=\s*""([^""]+)""").Match($f.Value).Groups[1].Value $value = ([regex]"value\s*=\s*""([^""]+)""").Match($f.Value).Groups[1].Value if ($Values.ContainsKey($name)) { $value = $Values[$name] } if ($name -and $value) { $formData[$name] = $value } } $formData } |