From 71f0e2e437033943176de90d943e1e424c35a133 Mon Sep 17 00:00:00 2001 From: Kelvin Mok Date: Fri, 14 Dec 2018 13:59:34 -0800 Subject: [PATCH 1/9] Created OKTA Remote Password Changer scripts v1. --- Accounts/Okta/OKTA-Heartbeat.ps1 | 114 +++++++++++++++ Accounts/Okta/OKTA-PassChange.ps1 | 137 ++++++++++++++++++ Accounts/Okta/README.md | 56 +++++++ Accounts/Okta/docs/Okta-Administrators.png | Bin 0 -> 29626 bytes .../TSS-Okta-APIKeyTemplatePermissions.png | Bin 0 -> 113510 bytes .../Okta/docs/TSS-Okta-PasswordChanger.png | Bin 0 -> 47228 bytes ...TSS-Okta-RemotePasswordChangingOptions.png | Bin 0 -> 60182 bytes .../OKTA-APIKeyPrivilegedAccount.xml | Bin 0 -> 5892 bytes .../OKTA-LocalServiceAccountUser.xml | Bin 0 -> 4846 bytes 9 files changed, 307 insertions(+) create mode 100644 Accounts/Okta/OKTA-Heartbeat.ps1 create mode 100644 Accounts/Okta/OKTA-PassChange.ps1 create mode 100644 Accounts/Okta/README.md create mode 100644 Accounts/Okta/docs/Okta-Administrators.png create mode 100644 Accounts/Okta/docs/TSS-Okta-APIKeyTemplatePermissions.png create mode 100644 Accounts/Okta/docs/TSS-Okta-PasswordChanger.png create mode 100644 Accounts/Okta/docs/TSS-Okta-RemotePasswordChangingOptions.png create mode 100644 Accounts/Okta/templates/OKTA-APIKeyPrivilegedAccount.xml create mode 100644 Accounts/Okta/templates/OKTA-LocalServiceAccountUser.xml diff --git a/Accounts/Okta/OKTA-Heartbeat.ps1 b/Accounts/Okta/OKTA-Heartbeat.ps1 new file mode 100644 index 0000000..5887ca5 --- /dev/null +++ b/Accounts/Okta/OKTA-Heartbeat.ps1 @@ -0,0 +1,114 @@ +# Set initial status to fail until proven to succeed. +$exit_status = -1 + +# TODO: Sanitize these inputs! +# Transfer variables from call. +$DOMAIN = $Args[0] +$OKTA_API_KEY = $Args[1] +$USERNAME = $Args[2] +$PASSWORD = $Args[3] +$PORT = '443' + + +#Enter a key manually here if not specifying in the parameters. +#$OKTA_API_KEY = '' + +try { + # Use DNS resolution to ensure a valid domain name was entered, fastest and easiest way to check. + Resolve-DnsName -Name $DOMAIN -DnsOnly +} +catch { + Write-Error "FATAL: Cannot resolve the domain name, please check the domain name parameter. $($PSItem.Execption.GetType())" + $PSItem.Exception | Get-Member | Write-Debug + throw $PSItem +} + +# Set system configuration for secure communications. +# [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $True } # DO NOT Validate SSL Cert to trust store. (For Self Signed certs and testing only) +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; # Require TLS 1.2 + +$cred= @{ + username = "${USERNAME}" + password = "${PASSWORD}" + options = @{ + multiOptionalFactorEnroll = "false" + warnBeforePasswordExpired = "false" + } +} + +$auth = $cred | ConvertTo-Json + +# Compile the URL +$URL = "https://${DOMAIN}:${PORT}/api/v1/authn" + +# Add the API key into the authentication header. +$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" +$headers.Add("Authorization","SSWS ${OKTA_API_KEY}") + +try { + # -UserAgent 'ThycoticSecretServerPowerShell' -Body ${auth} + $output = Invoke-RestMethod -Uri ${URL} -Method Post -Body ${auth} -ContentType 'application/json' -Headers ${headers} +} +catch [System.Net.WebException] { + # Determine why login failed, make it a bit more user friendly and still provide detailed messages. + # Reference: https://stackoverflow.com/questions/38419325/catching-full-exception-message + if ( $PSItem.Exception.Response.StatusCode -match "BadRequest" ) { + throw [System.Net.WebException]::new("Failure: Target account credentials are invalid or locked out.", $PSItem.Exception) + } + elseif ( $PSItem.Exception.Response.StatusCode -match "InternalServerError" ) { + throw [System.Net.WebException]::new("Error: An Internal Server Error has occurred.", $PSItem.Exception) + } + elseif ( $PSItem.Exception.Response.StatusCode -match "Unauthorized" ) { + throw [System.Net.WebException]::new("Failure: Target account or API credentials invalid.", $PSItem.Exception) + } + + # Uncaught and unhandled and unknown exceptions get extra dump treatment. + $PSitem.Exception.Response | Format-List * | Write-Debug + Write-Error "Unable to retrieve session token: $($PSItem.ToString())" + Write-Error "FATAL: Unknown API Exception Encountred $($PSItem.Exception.GetType())" + $innerException = $PSItem.Exception.InnerExceptionMessage + Write-Debug "Inner Exception: $innerException" + $e = $_.Exception + $msg = $e.Message + while ($e.InnerException) { + $e = $e.InnerException + $msg += "`n" + $e.Message + } + Write-Error $msg + $PSItem.Exception | Get-Member | Write-Debug + + throw $PSItem +} +catch { + Write-Error "Double FATAL: Unhandled Exception Type of $($PSItem.Exception.GetType())" + Write-Error $PSItem.ToString() + $PSItem.Exception | Get-Member | Write-Debug + throw $PSItem +} + +$stateToken = ${output}.stateToken +$status = ${output}.status + +if("${status}" -match "PASSWORD_EXPIRED" -Or "${status}" -match "PASSWORD_RESET" -Or "${status}" -match "PASSWORD_WARN") { + $return_status = @{ "Status" = "Password Expired"; "stateToken" = "${stateToken}" } + Write-Output "${return_status}" + $exit_status = 0 +} +elseif("${status}" -match "SUCCESS") { + $return_status = @{ "Status" = "Success"; "stateToken" = "${stateToken}" } + Write-Output "${return_status}" + $exit_status = 0 +} +elseif("${status}" -match "MFA_REQUIRED" -Or "${status}" -match "MFA_ENROLL") { +$return_status = @{ "Status" = "MFA Required"; "stateToken" = "${stateToken}" } + Write-Output "${return_status}" + $exit_status = 0 +} +else { + Write-Output @{ "Status" = "Failure"; "SessionToken" = "" } + # Any other status, count it as soft bad. + # throw [System.ApplicationException]::new("Cannot parse authorization token.",$PSItem) + $exit_status = 1; +} + +exit $exit_status; \ No newline at end of file diff --git a/Accounts/Okta/OKTA-PassChange.ps1 b/Accounts/Okta/OKTA-PassChange.ps1 new file mode 100644 index 0000000..c35fca6 --- /dev/null +++ b/Accounts/Okta/OKTA-PassChange.ps1 @@ -0,0 +1,137 @@ +# Set initial status to fail until proven to succeed. +$exit_status = -1 + +# TODO: Sanitize these inputs! +# Transfer variables from call in Secret Server. +$DOMAIN = $Args[0] +$OKTA_API_KEY = $Args[1] +$USERNAME = $Args[2] +$PASSWORD = $Args[3] +$NEWPASS = $Args[4] +$PORT = '443' + +# Uncomment and enter a key manually here if not specifying in the parameters. +#$OKTA_API_KEY = '' + +# Sanitize Username by encoding it, as this parameter is suseptible to injection otherwise. +try { + $USERNAMEURL = [System.Web.HttpUtility]::UrlEncode("${USERNAME}") +} +catch [System.Management.Automation.RuntimeException] { # Handle this exception on servers that do not have the module loaded on PowerShell. + try { + # Reference: https://stackoverflow.com/questions/38408729/unable-to-find-type-system-web-httputility-in-powershell + Add-Type -AssemblyName System.Web + $USERNAMEURL = [System.Web.HttpUtility]::UrlEncode("${USERNAME}") # Now try again. + } + catch { + Write-Error "FATAL: Cannot load URLEncoding library. Unhandled Exception Type of $($PSItem.Exception.GetType())" + Write-Error $PSItem.ToString() + $PSItem.Exception | Get-Member | Write-Debug + throw $PSItem + } +} +catch { + Write-Error "Double FATAL: Unhandled Exception Type of $($PSItem.Exception.GetType())" + Write-Error $PSItem.ToString() + $PSItem.Exception | Get-Member | Write-Debug + throw $PSItem +} + +try { + # Use DNS resolution to ensure a valid domain name was entered, fastest and easiest way to check. + Resolve-DnsName -Name $DOMAIN -DnsOnly +} +catch { + Write-Error "FATAL: Cannot resolve the domain name, please check the domain name parameter. $($PSItem.Execption.GetType())" + $PSItem.Exception | Get-Member | Write-Debug + throw $PSItem +} + +# Set system configuration for secure communications. +# [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $True } # DO NOT Validate SSL Cert to trust store. (For Self Signed certs and testing only) +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; # Require TLS 1.2 + +# Format hashtable with information to be changed to JSON. +$passes= @{ + oldPassword = @{ + value = "${PASSWORD}" + } + newPassword = @{ + value = "${NEWPASS}" + } +} + +$passchange = ${passes} | ConvertTo-Json + +# Compile the URL call to query for user information. +$USERURL = "https://${DOMAIN}:${PORT}/api/v1/users/${USERNAMEURL}" + +# Add the API key into the authentication header. +$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" +$headers.Add("Authorization","SSWS ${OKTA_API_KEY}") + +try { + # Query for the user ID with the username, then present the user ID to change the password. + $userObject = Invoke-RestMethod -Uri ${USERURL} -Method Get -UserAgent 'ThycoticSecretServerPowerShell' -ContentType 'application/json' -Headers ${headers} + $userID = $userObject.id + $changeURL = "https://${DOMAIN}:${PORT}/api/v1/users/${userID}/credentials/change_password" + $changeOutput = Invoke-RestMethod -Uri ${changeURL} -Method Post -Body ${passchange} -UserAgent 'ThycoticSecretServerPowerShell' -ContentType 'application/json' -Headers ${headers} +} +catch [System.Net.WebException] { + # Determine why login failed, make it a bit more user friendly and still provide detailed messages. + # Reference: https://stackoverflow.com/questions/38419325/catching-full-exception-message + if ( $PSItem.Exception.Response.StatusCode -match "BadRequest" ) { + throw [System.Net.WebException]::new("Failure: API or target account credentials are invalid or locked out.", $PSItem.Exception) + } + elseif ( $PSItem.Exception.Response.StatusCode -match "InternalServerError" ) { + throw [System.Net.WebException]::new("Error: An Internal Server Error has occurred.", $PSItem.Exception) + } + elseif ( $PSItem.Exception.Response.StatusCode -match "Unauthorized" ) { + throw [System.Net.WebException]::new("Failure: API Credentials invalid.", $PSItem.Exception) + } + elseif ( $PSItem.Exception.Response.StatusCode -match "Forbidden" ) { + throw [System.Net.WebException]::new("Failure: Old Password incorrect.", $PSItem.Exception) + } + + # Uncaught and unhandled and unknown exceptions get extra dump treatment. + $PSitem.Exception.Response | Format-List * | Write-Debug + Write-Error "Unable to retrieve session token: $($PSItem.ToString())" + Write-Error "FATAL: Unknown API Exception Encountred $($PSItem.Exception.GetType())" + $innerException = $PSItem.Exception.InnerExceptionMessage + Write-Debug "Inner Exception: $innerException" + $e = $_.Exception + $msg = $e.Message + while ($e.InnerException) { + $e = $e.InnerException + $msg += "`n" + $e.Message + } + Write-Error $msg + $PSItem.Exception | Get-Member | Write-Debug + + throw $PSItem +} +catch { + Write-Error "Double FATAL: Unhandled Exception Type of $($PSItem.Exception.GetType())" + Write-Error $PSItem.ToString() + $PSItem.Exception | Get-Member | Write-Debug + throw $PSItem +} + + +$passwordOutput = ${changeOutput}.password +$provider = ${changeOutput}.provider +$providerName = ${provider}.name + +if("${providerName}" -match "OKTA") { + $return_status = @{ "Status" = "Success"; "stateToken" = "${passwordOutput}" } + Write-Output "${return_status}" + $exit_status = 0 +} +else { + Write-Output @{ "Status" = "Failure"; "SessionToken" = "" } + # Any other status, count it as soft bad. + # throw [System.ApplicationException]::new("Cannot parse authorization token.",$PSItem) + $exit_status = 1; +} + +exit $exit_status; \ No newline at end of file diff --git a/Accounts/Okta/README.md b/Accounts/Okta/README.md new file mode 100644 index 0000000..cfea265 --- /dev/null +++ b/Accounts/Okta/README.md @@ -0,0 +1,56 @@ +# Thycotic Secret Server integration with OKTA local account credentials + +## Prerequisites + +You need to create and obtain an API Key for an account in OKTA with the following Administrative permissions: +* API Access Management (to create/rotate the key for the service account as that service account, can be removed after key is created or rotated) +* Group w/ access to manage all users (to be able to enumerate all users) + + ![alt text](docs/Okta-Administrators.png "OKTA Administrators configuration screen.") + +***DO NOT*** create an API key as a normal user or your personal account! It should be an access neutral service account with MFA enabled that prevents normal login not through an API except to just create the inital API Key. + +## Installation + +1. Add the PS1 mentioned in the **Scripts** section below in to the scripts area on Secret Server (**ADMIN** > **Scripts**) with the appropriate script **Category** selected for each script. +2. Create a new **Password Changer** in Secret Server at **ADMIN** > **Remote Password Changing Configuration** > **Configure Password Changers** > **+ New**. +3. Define the script arguments according to the scripts parameters or as shown in the image below. + + ![alt text](docs/TSS-Okta-PasswordChanger.png "Secret Server OKTA password changer arguments configuration screen.") +4. Import the Secret Templates mentioned in the **Templates** section below, with the appropriate fields security set. +5. Configure the password changing for both templates to use the password changer you set in step 2 above, you can also set the **Default Privileged Account** to the privileged API key later. +6. For each secret, including the API secret, set the **Remote Password Changing** options as follows: + * Auto Change: Checked + * Run PowerShell Using: Privileged Account Credentials + * Privileged Account: `