Skip to content
This repository has been archived by the owner on Jan 4, 2021. It is now read-only.

Created OKTA Remote Password Changer scripts v1. #24

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
161 changes: 161 additions & 0 deletions Accounts/Okta/OKTA-Heartbeat.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Set initial status to fail until proven to succeed.
$exit_status = -1

# 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 = ''

# Check input validity.
if(-not ($OKTA_API_KEY -imatch '^[a-z0-9,._-]+$')) {
throw [System.ArgumentException]::new("Invalid OKTA API Token provided.",'OKTA_API_KEY')
}

if(-not ($PORT -gt 0 -and $PORT -le 65535)) {
throw [System.ArgumentOutOfRangeException]::new("Port number not in valid range between 1 and 65535 inclusive.",'PORT')
}

# 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.
$DNSOutput = Resolve-DnsName -Name ${DOMAIN} -DnsOnly
if(${DNSOutput}.Name.GetType().FullName -match "System.String") {
$ResolvedName = ${DNSOutput}.Name
}
else {
$ResolvedName = ${DNSOutput}.Name[0]
}
}
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.
# Uncomment below line to NOT Validate SSL Cert to trust store. (For Self Signed certs and testing only)
# [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $True }

# Require TLS 1.2. See: https://help.okta.com/en/prod/Content/Topics/Miscellaneous/okta-ends-browser-support-for-TLS-1.1.htm
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
}
catch {
# In case administrator has disabled TLS 1.2 in SCHANNEL for some reason.
throw [System.Net.ProtocolViolationException]::new("OKTA requires TLS 1.2 to be enabled. Unable to set SecurityProtocol to 'Tls12', please check SCHANNEL configuration.")
}

# Format hashtable to be converted to JSON.
$cred= @{
username = "${USERNAME}"
password = "${PASSWORD}"
options = @{
multiOptionalFactorEnroll = "false"
warnBeforePasswordExpired = "false"
}
}

$auth = $cred | ConvertTo-Json

# Compile the URL
$URL = "https://${ResolvedName}:${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;
160 changes: 160 additions & 0 deletions Accounts/Okta/OKTA-PassChange.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Set initial status to fail until proven to succeed.
$exit_status = -1

# 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 = ''

# Check input validity.
if(-not ($OKTA_API_KEY -imatch '^[a-z0-9,._-]+$')) {
throw [System.ArgumentException]::new("Invalid OKTA API Token provided.",'OKTA_API_KEY');
}

if(-not ($PORT -gt 0 -and $PORT -le 65535)) {
throw [System.ArgumentOutOfRangeException]::new("Port number not in valid range between 1 and 65535 inclusive.",'PORT')
}

# 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.
$DNSOutput = Resolve-DnsName -Name ${DOMAIN} -DnsOnly
if(${DNSOutput}.Name.GetType().FullName -match "System.String") {
$ResolvedName = ${DNSOutput}.Name
}
else {
$ResolvedName = ${DNSOutput}.Name[0]
}
}
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.
# Uncomment below line to NOT Validate SSL Cert to trust store. (For Self Signed certs and testing only)
# [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $True }

# Require TLS 1.2. See: https://help.okta.com/en/prod/Content/Topics/Miscellaneous/okta-ends-browser-support-for-TLS-1.1.htm
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
}
catch {
# In case administrator has disabled TLS 1.2 in SCHANNEL for some reason.
throw [System.Net.ProtocolViolationException]::new("OKTA requires TLS 1.2 to be enabled. Unable to set SecurityProtocol to 'Tls12', please check SCHANNEL configuration.")
}

# 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://${ResolvedName}:${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://${ResolvedName}:${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;
60 changes: 60 additions & 0 deletions Accounts/Okta/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Thycotic Secret Server integration with OKTA local account credentials

## Prerequisites

* Account with PowerShell access on the nodes. (***BUILTIN\Administrators*** or ***BUILTIN\Remote Management Users***)
* Thycotic Secret Server version 10.5.00000 or higher. (May work on any 10.x version, but tested only on 10.5 and up.)
* OKTA local account for Thycotic Secret Server to use to access the OKTA API.
***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.
* Obtain an API Key for the above 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.")

* TLS 1.2 enabled on the nodes running the PowerShell scripts.

## 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: `<Select the appropriate privileged account that can execute PowerShell on the Secret Server>`
* The following Secrets are available to be used in Custom Password Changing Commands and Scripts:
Select the OKTA API Privileged secret using the OKTA-APIKeyPrivilegedAccount.xml template. `OKTA API KEY (TSS Integration)`
![alt text](docs/TSS-Okta-RemotePasswordChangingOptions.png "Secret Server OKTA secret remote password changing options screen.")


## Scripts

* **OKTA-Heartbeat.ps1** - To test the validity of a credential on OKTA.
1. `$DOMAIN` - The OKTA domain that is to be contacted (ex: _example.oktapreview.com_)
2. `$[1]$APIKEY` - The API key generated to be used to access the APIs.
3. `$USERNAME` - The UPN or OKTA login of the user to which the password heartbeat is checked against.
4. `$PASSWORD` - The password to be checked.
* **OKTA-PassChange.ps1** - To change the password of a local OKTA account. Will attempt to change other sources, but may not work depending on how the directory or application integration is configured.
1. `$DOMAIN` - The OKTA domain that is to be contacted (ex: _example.oktapreview.com_)
2. `$[1]$APIKEY` - The API key generated to be used to access the APIs.
3. `$USERNAME` - The UPN or OKTA login of the user to which the password is to be changed.
4. `$PASSWORD` - The OLD password.
5. `$NEWPASSWORD` - The NEW password.

## Templates

* **OKTA-APIKeyPrivilegedAccount.xml** - This is used to create a secret of the API Key with privileged access and the secret using this template is used by all OKTA secrets (including itself) to manage the passwords.
* You must make sure that the OKTA API Key template have the appropriate permissions set and configured to prevent leaking of secrets to other non-administrative users selecting that secret for remote password change.
![alt text](docs/TSS-Okta-APIKeyTemplatePermissions.png "Secret Server OKTA API Secret Template permissions/fields screen.")
* APIKey
* Edit Requires: Not Editable (or Owner)
* Hide on View: Checked
* Password
* Edit Requires: Owner
* Hide on View: Checked
* **OKTA-LocalServiceAccountUser.xml** - This template is used by all other OKTA accounts where the secret needs to be managed by Secret Server.
Binary file added Accounts/Okta/docs/Okta-Administrators.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Accounts/Okta/docs/TSS-Okta-PasswordChanger.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.