Skip to content

Instantly share code, notes, and snippets.

@infamousjoeg
Created March 27, 2025 19:06
Show Gist options
  • Save infamousjoeg/6e03e3e6037b7cff0df81f7e6f2f3e91 to your computer and use it in GitHub Desktop.
Save infamousjoeg/6e03e3e6037b7cff0df81f7e6f2f3e91 to your computer and use it in GitHub Desktop.
CyberArk PAM (Self-Hosted) REST API Authentication via PingFederate SAML

CyberArk PAM REST API Authentication via PingFederate SAML

This guide demonstrates how to authenticate to CyberArk's Self-Hosted Privileged Access Management (PAM) REST API using PingFederate SAML authentication with PowerShell.

Overview

The script implements a complete SAML authentication flow that:

  1. Initiates SAML authentication with CyberArk
  2. Redirects to PingFederate for authentication
  3. Processes the SAML response
  4. Exchanges the SAML assertion for a CyberArk session token

PowerShell Implementation

<#
.SYNOPSIS
    Authenticates to CyberArk's Self-Hosted PAM REST API using PingFederate SAML.

.DESCRIPTION
    This script performs SAML-based authentication through PingFederate to CyberArk's
    PAM REST API using the /saml/logon endpoint. It handles the full authentication 
    flow and returns a session token that can be used for subsequent API calls.

.PARAMETER CyberArkUrl
    Base URL for CyberArk PAM (e.g., 'https://pam.example.com')

.PARAMETER Username
    Username for PingFederate authentication

.PARAMETER Password
    Password for PingFederate authentication

.PARAMETER SkipCertificateCheck
    Switch to skip SSL certificate validation (for testing only)

.EXAMPLE
    .\CyberArk-PingFederate-Auth.ps1 -CyberArkUrl "https://pam.example.com" -Username "[email protected]" -Password "Password123"

.NOTES
    Author: CyberArk DevOps Solutions Engineer
    Date: March 27, 2025
    Requirements: PowerShell 5.1 or higher
#>

[CmdletBinding()]
param (
    [Parameter(Mandatory = $true)]
    [string]$CyberArkUrl,

    [Parameter(Mandatory = $true)]
    [string]$Username,

    [Parameter(Mandatory = $true)]
    [string]$Password,

    [Parameter(Mandatory = $false)]
    [switch]$SkipCertificateCheck
)

# Set error action preference
$ErrorActionPreference = "Stop"

# Create function to handle HTML parsing
function Get-HtmlElement {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Html,
        
        [Parameter(Mandatory = $true)]
        [string]$TagName,
        
        [Parameter(Mandatory = $false)]
        [hashtable]$Attributes
    )
    
    # Load HTML Agility Pack if available, otherwise use regex
    try {
        # Try to load HTML Agility Pack if available
        if (-not ([System.Management.Automation.PSTypeName]'HtmlAgilityPack.HtmlDocument').Type) {
            Add-Type -Path (Join-Path $PSScriptRoot "HtmlAgilityPack.dll") -ErrorAction SilentlyContinue
        }
        
        if (([System.Management.Automation.PSTypeName]'HtmlAgilityPack.HtmlDocument').Type) {
            $doc = New-Object HtmlAgilityPack.HtmlDocument
            $doc.LoadHtml($Html)
            
            $xpath = "//$TagName"
            if ($Attributes) {
                $conditions = @()
                foreach ($key in $Attributes.Keys) {
                    $conditions += "@$key='$($Attributes[$key])'"
                }
                $xpath += "[" + ($conditions -join " and ") + "]"
            }
            
            return $doc.DocumentNode.SelectNodes($xpath)
        }
    }
    catch {
        Write-Verbose "HTML Agility Pack not available, falling back to regex"
    }
    
    # Fallback to simple regex-based parsing (less reliable)
    $pattern = "<$TagName"
    if ($Attributes) {
        foreach ($key in $Attributes.Keys) {
            $pattern += "(?:.*?)$key=`"$($Attributes[$key])`""
        }
    }
    $pattern += "(?:.*?)>(.*?)</$TagName>"
    
    $matches = [regex]::Matches($Html, $pattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
    return $matches
}

function Get-FormInputs {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Html,
        
        [Parameter(Mandatory = $false)]
        [string]$FormId
    )
    
    $inputs = @{}
    
    # Try to use HTML Agility Pack first
    try {
        if (([System.Management.Automation.PSTypeName]'HtmlAgilityPack.HtmlDocument').Type) {
            $doc = New-Object HtmlAgilityPack.HtmlDocument
            $doc.LoadHtml($Html)
            
            $formNode = $null
            if ($FormId) {
                $formNode = $doc.DocumentNode.SelectSingleNode("//form[@id='$FormId']")
            }
            else {
                $formNode = $doc.DocumentNode.SelectSingleNode("//form")
            }
            
            if ($formNode) {
                $inputNodes = $formNode.SelectNodes(".//input")
                foreach ($input in $inputNodes) {
                    $name = $input.GetAttributeValue("name", $null)
                    $value = $input.GetAttributeValue("value", "")
                    if ($name) {
                        $inputs[$name] = $value
                    }
                }
                
                # Get form action
                $action = $formNode.GetAttributeValue("action", $null)
                if ($action) {
                    $inputs["__form_action"] = $action
                }
            }
            
            return $inputs
        }
    }
    catch {
        Write-Verbose "Error using HTML Agility Pack: $_"
    }
    
    # Fallback to regex
    # Extract form if form ID is specified
    if ($FormId) {
        $formPattern = "<form(?:.*?)id=`"$FormId`"(?:.*?)>(.*?)</form>"
        $formMatch = [regex]::Match($Html, $formPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
        if ($formMatch.Success) {
            $Html = $formMatch.Groups[1].Value
        }
    }
    
    # Get form action
    $actionPattern = "<form(?:.*?)action=`"([^`"]*)`""
    $actionMatch = [regex]::Match($Html, $actionPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
    if ($actionMatch.Success) {
        $inputs["__form_action"] = $actionMatch.Groups[1].Value
    }
    
    # Get input elements
    $inputPattern = "<input(?:.*?)name=`"([^`"]*)`"(?:.*?)(?:value=`"([^`"]*)`")?(?:.*?)/?>"
    $inputMatches = [regex]::Matches($Html, $inputPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
    
    foreach ($match in $inputMatches) {
        $name = $match.Groups[1].Value
        $value = $match.Groups[2].Value
        if ($name) {
            $inputs[$name] = $value
        }
    }
    
    return $inputs
}

function Extract-SAMLResponse {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Html
    )
    
    $inputs = Get-FormInputs -Html $Html
    return $inputs["SAMLResponse"]
}

# Skip certificate validation if requested (for testing only)
if ($SkipCertificateCheck) {
    Write-Warning "SSL certificate validation has been disabled. This should only be used for testing."
    
    # For PowerShell 5.1
    if (-not ([System.Management.Automation.PSTypeName]'ServerCertificateValidationCallback').Type) {
        $certCallback = @"
using System;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
public class ServerCertificateValidationCallback
{
    public static void Ignore()
    {
        if(ServicePointManager.ServerCertificateValidationCallback == null)
        {
            ServicePointManager.ServerCertificateValidationCallback += 
                delegate
                (
                    Object obj, 
                    X509Certificate certificate, 
                    X509Chain chain, 
                    SslPolicyErrors errors
                )
                {
                    return true;
                };
        }
    }
}
"@
        Add-Type $certCallback -ErrorAction SilentlyContinue
        [ServerCertificateValidationCallback]::Ignore()
    }
    
    # For PowerShell Core
    if ($PSVersionTable.PSEdition -eq 'Core') {
        $PSDefaultParameterValues.Add('Invoke-RestMethod:SkipCertificateCheck', $true)
        $PSDefaultParameterValues.Add('Invoke-WebRequest:SkipCertificateCheck', $true)
    }
}

# Ensure CyberArkUrl doesn't end with a trailing slash
$CyberArkUrl = $CyberArkUrl.TrimEnd('/')

# Create a session to maintain cookies
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession

Write-Host "Step 1: Initiating SAML authentication with CyberArk..." -ForegroundColor Cyan

try {
    # Initial request to CyberArk SAML logon endpoint
    $initialResponse = Invoke-WebRequest -Uri "$CyberArkUrl/PasswordVault/API/auth/saml/logon" -WebSession $session -UseBasicParsing
    
    # Extract form data from response
    $formData = Get-FormInputs -Html $initialResponse.Content
    
    if (-not $formData -or -not $formData["__form_action"]) {
        throw "Failed to extract SAML request form from CyberArk response"
    }
    
    $pingFedUrl = $formData["__form_action"]
    $formData.Remove("__form_action")
    
    Write-Verbose "PingFederate URL: $pingFedUrl"
    Write-Host "Step 2: Submitting SAML request to PingFederate..." -ForegroundColor Cyan
    
    # Build form data for PingFederate request
    $pingFedFormData = @{}
    foreach ($key in $formData.Keys) {
        $pingFedFormData[$key] = $formData[$key]
    }
    
    # Submit SAML request to PingFederate
    $pingFedResponse = Invoke-WebRequest -Uri $pingFedUrl -Method POST -Body $pingFedFormData -WebSession $session -UseBasicParsing
    
    # Extract login form from PingFederate response
    $loginFormData = Get-FormInputs -Html $pingFedResponse.Content -FormId "loginForm"
    
    if (-not $loginFormData -or -not $loginFormData["__form_action"]) {
        # Check if we're already logged in and have a SAML response
        $samlResponse = Extract-SAMLResponse -Html $pingFedResponse.Content
        
        if ($samlResponse) {
            Write-Host "Already authenticated with PingFederate, proceeding with SAML response..." -ForegroundColor Green
        }
        else {
            throw "Failed to extract login form from PingFederate response"
        }
    }
    else {
        $loginUrl = $loginFormData["__form_action"]
        $loginFormData.Remove("__form_action")
        
        # Add username and password to form data
        $loginFormData["username"] = $Username
        $loginFormData["password"] = $Password
        
        Write-Host "Step 3: Authenticating with PingFederate..." -ForegroundColor Cyan
        
        # Submit credentials to PingFederate
        $authResponse = Invoke-WebRequest -Uri $loginUrl -Method POST -Body $loginFormData -WebSession $session -UseBasicParsing
        
        # Check for additional authentication pages (e.g., MFA)
        # This is a simplified implementation - you may need to add handling for MFA challenges
        
        # Extract SAML response from authentication response
        $samlResponse = Extract-SAMLResponse -Html $authResponse.Content
        
        if (-not $samlResponse) {
            throw "Failed to obtain SAML response from PingFederate"
        }
    }
    
    Write-Host "Step 4: Submitting SAML response to CyberArk..." -ForegroundColor Cyan
    
    # Create payload for CyberArk authentication
    $cyberArkPayload = @{
        SAMLResponse = $samlResponse
    } | ConvertTo-Json
    
    # Submit SAML response to CyberArk
    $headers = @{
        "Content-Type" = "application/json"
    }
    
    $cyberArkAuthResponse = Invoke-RestMethod -Uri "$CyberArkUrl/PasswordVault/API/auth/saml/logon" -Method POST -Body $cyberArkPayload -Headers $headers -WebSession $session
    
    # Extract session token
    if ($cyberArkAuthResponse.CyberArkLogonResult) {
        Write-Host "Authentication successful!" -ForegroundColor Green
        
        # Output results
        $result = @{
            Success      = $true
            SessionToken = $cyberArkAuthResponse.CyberArkLogonResult
            Username     = $cyberArkAuthResponse.UserName
            Source       = $cyberArkAuthResponse.Source
            SessionId    = $cyberArkAuthResponse.SessionID
        }
        
        # Return the session token and other details
        Write-Output $result
        
        # Example of how to use the session token
        Write-Host "`nExample API call using the session token:" -ForegroundColor Yellow
        Write-Host "Invoke-RestMethod -Uri '$CyberArkUrl/PasswordVault/API/Accounts' -Method GET -Headers @{`"Authorization`" = `"$($cyberArkAuthResponse.CyberArkLogonResult)`"}"
        
        return $result
    }
    else {
        throw "Authentication failed. CyberArk did not return a session token."
    }
}
catch {
    Write-Host "Error during authentication: $_" -ForegroundColor Red
    
    $errorDetails = @{
        Success = $false
        Error   = $_.Exception.Message
    }
    
    if ($_.Exception.Response) {
        try {
            $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
            $errorDetails.ResponseContent = $reader.ReadToEnd()
            $reader.Close()
        }
        catch {
            $errorDetails.ResponseContent = "Could not read response content"
        }
    }
    
    return $errorDetails
}

Key Implementation Details

HTML Parsing

Since PowerShell doesn't have built-in HTML parsing capabilities, the script includes three custom functions:

  1. Get-HtmlElement - Extracts specific HTML elements:

    • Tries to use HTML Agility Pack if available
    • Falls back to regex-based parsing if necessary
  2. Get-FormInputs - Extracts form inputs and action URLs:

    • Parses all input elements within a form
    • Returns a hashtable with input names/values
    • Includes the form's action URL as a special "__form_action" key
  3. Extract-SAMLResponse - Specifically extracts SAML responses:

    • Uses Get-FormInputs to find the SAMLResponse input
    • Simplifies handling this critical authentication component

SSL Certificate Handling

For development and testing environments, the script includes certificate validation bypass:

if ($SkipCertificateCheck) {
    # Different approaches for PowerShell 5.1 vs PowerShell Core
    # Creates certificate validation callback that always returns true
}

This is implemented differently for PowerShell 5.1 (using a custom C# callback class) and PowerShell Core (using built-in parameters).

Authentication Flow

The authentication flow follows these steps:

  1. Initiate SAML Authentication with CyberArk

    $initialResponse = Invoke-WebRequest -Uri "$CyberArkUrl/PasswordVault/API/auth/saml/logon" -WebSession $session

    This request to CyberArk's SAML logon endpoint returns an HTML form with a SAML request.

  2. Extract and Submit SAML Request to PingFederate

    $formData = Get-FormInputs -Html $initialResponse.Content
    $pingFedUrl = $formData["__form_action"]
    $pingFedResponse = Invoke-WebRequest -Uri $pingFedUrl -Method POST -Body $pingFedFormData -WebSession $session

    The SAML request is extracted and submitted to PingFederate.

  3. Submit Credentials to PingFederate

    $loginFormData["username"] = $Username
    $loginFormData["password"] = $Password
    $authResponse = Invoke-WebRequest -Uri $loginUrl -Method POST -Body $loginFormData -WebSession $session

    Username and password are submitted to PingFederate's login form.

  4. Extract SAML Response

    $samlResponse = Extract-SAMLResponse -Html $authResponse.Content

    After successful authentication, PingFederate returns a SAML response.

  5. Submit SAML Response to CyberArk

    $cyberArkPayload = @{
        SAMLResponse = $samlResponse
    } | ConvertTo-Json
    
    $cyberArkAuthResponse = Invoke-RestMethod -Uri "$CyberArkUrl/PasswordVault/API/auth/saml/logon" -Method POST -Body $cyberArkPayload

    The SAML response is submitted to CyberArk to obtain a session token.

Session Management

The script uses PowerShell's WebRequestSession to maintain cookies across requests:

$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession

This ensures cookies set during the authentication flow are preserved for subsequent requests.

Error Handling

Comprehensive error handling provides detailed information about failures:

catch {
    $errorDetails = @{
        Success = $false
        Error   = $_.Exception.Message
    }
    
    if ($_.Exception.Response) {
        # Extract response content from failed web requests
    }
}

The script attempts to extract response content from failed web requests, which can be invaluable for troubleshooting authentication issues.

MFA Considerations

The script includes a placeholder for Multi-Factor Authentication (MFA):

# Check for additional authentication pages (e.g., MFA)
# This is a simplified implementation - you may need to add handling for MFA challenges

To support MFA, additional logic would need to be added to detect and respond to MFA challenges from PingFederate.

Usage Examples

Basic Usage

.\CyberArk-PingFederate-Auth.ps1 -CyberArkUrl "https://pam.example.com" -Username "[email protected]" -Password "Password123"

For Testing Environments

.\CyberArk-PingFederate-Auth.ps1 -CyberArkUrl "https://pam.example.com" -Username "[email protected]" -Password "Password123" -SkipCertificateCheck

Using the Session Token

After successful authentication, the script provides an example of how to use the obtained session token:

Invoke-RestMethod -Uri "https://pam.example.com/PasswordVault/API/Accounts" -Method GET -Headers @{"Authorization" = "your-session-token"}

Conclusion

This script provides a complete implementation for authenticating to CyberArk's Self-Hosted PAM REST API using PingFederate SAML. It handles the entire authentication flow, from initiating the SAML request to exchanging the SAML assertion for a CyberArk session token.

The modular design with custom HTML parsing functions makes it adaptable to different PingFederate configurations and can be extended to support additional authentication factors if required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment