Private/Get-EntraIDInteractiveUserAccessToken.ps1
function Get-EntraIDInteractiveUserAccessToken { [CmdletBinding(DefaultParameterSetName = "default")] Param( [Parameter(Mandatory = $true)] $AccessTokenProfile ) process { # If we already have a refresh token, use that to get a new access token if ($AccessTokenProfile["RefreshToken"]) { Write-Verbose "We have a refresh token, using it to get a new access token" $tokenUrl = "https://login.microsoftonline.com/$($AccessTokenProfile.TenantId)/oauth2/v2.0/token" $body = @{ grant_type = "refresh_token" client_id = $AccessTokenProfile.ClientId refresh_token = $AccessTokenProfile["RefreshToken"] scope = $AccessTokenProfile.Scope } $response = Invoke-RestMethod -Method Post -Uri $tokenUrl -Body $body -ContentType "application/x-www-form-urlencoded" -Headers @{ Origin = "{0}://localhost:{1}/" -f ($AccessTokenProfile.Https ? "https" : "http"), $AccessTokenProfile.LocalhostPort } if ($response.refresh_token) { Write-Debug "Received new refresh token, storing it for later use" $AccessTokenProfile["RefreshToken"] = $response.refresh_token } if ($response.access_token) { $response return } else { Write-Error "Failed to obtain access token, going into interactive" } } # Create listener $listener = $null if($AccessTokenProfile["Listener"]) { Write-Verbose "Using existing listener" $listener = $AccessTokenProfile["Listener"] } elseif ($AccessTokenProfile.Https) { Write-Verbose "Creating HTTPS listener on port $($AccessTokenProfile.LocalhostPort)" $listener = New-Object System.Net.HttpListener $listener.Prefixes.Add("https://localhost:$($AccessTokenProfile.LocalhostPort)/") } else { Write-Verbose "Creating HTTP listener on port $($AccessTokenProfile.LocalhostPort)" $listener = New-Object System.Net.HttpListener $listener.Prefixes.Add("http://localhost:$($AccessTokenProfile.LocalhostPort)/") } Write-Verbose "Starting listener" $listener.Start() # Calculate the url to send the user to - Thanks to https://github.com/darrenjrobinson/PKCE/blob/main/PKCE.psm1 $codeVerifier = -join (((48..57) * 4) + ((65..90) * 4) + ((97..122) * 4) | Get-Random -Count 43 | ForEach-Object { [char]$_ }) Write-Debug "Created code verifier: $($codeVerifier)" $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) $code_challenge = $code_challenge.Replace("/", "_") $code_challenge = $code_challenge.Replace("+", "-") $code_challenge = $code_challenge.Replace("=", "") $authorizeUrl = "https://login.microsoftonline.com/$($AccessTokenProfile.TenantId)/oauth2/v2.0/authorize?client_id=$($AccessTokenProfile.ClientId)&response_type=code&redirect_uri=http://localhost:$($AccessTokenProfile.LocalhostPort)/&scope=$($AccessTokenProfile.Scope)&code_challenge=$($code_challenge)&code_challenge_method=S256" # Display message or launch browser if ($AccessTokenProfile.LaunchBrowser) { Write-Verbose "Launching browser to url $($authorizeUrl)" Start-Process $authorizeUrl } else { Write-Host "Please navigate to the following URL in your browser: $($PSStyle.Foreground.BrightYellow)$($authorizeUrl)$($PSStyle.Reset)" } # Wait for incoming requests Write-Verbose "Wait for incoming request" $contextTask = $listener.GetContextAsync() $counter = 0 while (-not $contextTask.AsyncWaitHandle.WaitOne(100)) { $counter += 1 if($counter % 30 -eq 0) { Write-Verbose "Waiting for authorization response..." } } $context = $contextTask.GetAwaiter().GetResult() $request = $context.Request $code = $request.QueryString["code"] $errorShort = $request.QueryString["error"] $errorDescription = $request.QueryString["error_description"] if ($code) { Write-Verbose "Writing success message back to http stream" $context.Response.OutputStream.Write([System.Text.Encoding]::UTF8.GetBytes("<html><body>Authorization code received. You can close this window now.</body></html>")) } else { $errorHtml = "<html><body>ERROR: Authorization code not received. Go back to powershell and retry.</body></html>" if ($errorShort -or $errorDescription) { $errorHtml = "<html><body><p><b>ERROR:</b> $($errorShort)</p><p>$($errorDescription)</p><p>Go back to powershell and retry.</p></body></html>" } Write-Verbose "Writing error message back to http stream" $context.Response.OutputStream.Write([System.Text.Encoding]::UTF8.GetBytes($errorHtml)) } $context.Response.OutputStream.Close() # $listener.Stop() if (!$code) { Write-Error "Unable to get authorization code" return } # Exchange the code for an access token Write-Verbose "Exchanging authorization code for access token" $tokenUrl = "https://login.microsoftonline.com/$($AccessTokenProfile.TenantId)/oauth2/v2.0/token" $body = @{ client_id = $AccessTokenProfile.ClientId scope = $AccessTokenProfile.Scope code = $code redirect_uri = "{0}://localhost:{1}/" -f ($AccessTokenProfile.Https ? "https" : "http"), $AccessTokenProfile.LocalhostPort grant_type = "authorization_code" code_verifier = $codeVerifier } $headers = @{ Origin = "{0}://localhost:{1}/" -f ($AccessTokenProfile.Https ? "https" : "http"), $AccessTokenProfile.LocalhostPort } Write-Debug "Preparing body for token request:" $body.Keys | ForEach-Object { Write-Debug " - $($_) = $($body[$_])" } Write-Debug "Preparing headers for token request: $($headers | ConvertTo-Json)" $response = Invoke-RestMethod -Method Post -Uri $tokenUrl -Body $body -ContentType "application/x-www-form-urlencoded" -Headers $headers if ($response.refresh_token) { Write-Debug "Received refresh token, storing it for later use" $AccessTokenProfile["RefreshToken"] = $response.refresh_token } if ($response.access_token) { Write-Verbose "Successfully retrieved access token" $response } else { Write-Error "Unable to retrieve access token" } } } |