Powershell function for Okta API

So…like most people/admins, I search google and find code/scripts that suit my purpose and then modify it. I’ve spent hours on stack overflow and other forums snagging code here/there. Now I want to share/give back for a change. Here is my powershell function that I use to do everything with Okta API.

Basically, I send in the uri that accesses the api I need and get the data back to process it as needed. Rinse, lather, repeat. I used to have it all in Python but a lot of colleagues use powershell…so here you go! Enjoy.

Comments, critiques, criticisms are all welcome.

Function getHTML {
    Param (
        [Parameter(Mandatory=$true)]  [String]$uri, # always need to pass in a URI for the API call
        [Parameter(Mandatory=$false)] [String]$method   = "GET", # default is GET, can also be POST, DELETE etc. etc.
        [Parameter(Mandatory=$false)] [String]$data = $null # default is NULL, passing data to okta is tricky. Here is an example for a profile update: $data = '{"profile": {"firstName": "Delete","lastName": "Me"}}'
    )
          
    Begin {
        # headers needed to be passed in.  Authorization is the API token created in Okta and you're limited to its permissions    
        $headers = @{}
        #$headers["Authorization"] = "SSWS **** INSERT TOKEN HERE ****"  See next line for how it should look
        #$headers["Authorization"] = "SSWS 00dfjaojf84j-jdfo9hjd94h987gfhDHNDdj638hd"
        $headers["Accept"] = "application/json"
        $headers["Content-Type"] = "application/json"		
    }
    
    Process {
        try {
            # to avoid issues we change the passed parameters based on the uri and method.  Here we add on the body (data) field only if we're NOT doing a GET (so a DELETE or a POST)
            # we also use body if we're going to do an 'activate'
            if ($method -ne "GET" -and $uri -inotmatch "activate") {
                $response = Invoke-WebRequest -Method $method -Uri $uri -Headers $headers -Body $data
            # pretty much used for all GET 
            } else {
                $response = Invoke-WebRequest -Method $method -Uri $uri -Headers $headers
            }
			# checks the header for the api limit.  Sleeps the script if we're about to hit limit
			if ([int]$response.Headers['x-rate-limit-remaining'] -lt 2) {
				# minimum delay, just in case
				$apiDelay = 5
				# each header has the rate limit.  If we're about to hit it, we check the reset time and wait that long.
				$apiDelay = [math]::Round(($response.Headers['x-rate-limit-reset']) - (Get-Date -UFormat %s) - 18000) + 1				
				write-host "API Limit reached...pausing for $($apiDelay) seconds"
				start-sleep -seconds $apiDelay
			}
            # count is just used to keep track of how many times we've pestered Okta with an api call.            
            $global:count += 1

            # sometimes queries run long...this spits out a progress report every 100th api call...just so we can watch the paint dry
            if ($global:count % 100 -eq 0) {
                write-host "$global:count queries processed"
            }

            # some API calls return nothing, so we handle those separately so as not to cause a panic
            if ($response.content.length -lt 3) {            
                $htmlCode = $response.RawContent.Split("`n")[0].trim()
                if ($method -eq "DELETE") {
                    $oktaData = "$htmlCode - (Deletion Succesful)"
                } else {
                    # there are a [very] few outliers that are not DELETES and have no response...handling just in case
                    $oktaData = $($htmlCode) + " - (No content expected)"
                }
            # this code block runs for all successful queries (other than the aforementioned deletes).
            } else {

                # we convert the reply from Okta to JSON format.  Normally it comes back as a string.
                $oktaData = ConvertFrom-Json $response.content

                # Check to see if there is more data/we hit a return limit.  If the results are too long, Okta paginates it so we have to get the next chunk
                if ($response.Headers.link) {
                    # ok...admittingly this is ugly/overly complicated but here goes.  If there is pagination, the headers will contain a link value.
                    # The link value has 2 parts: 'self' (the url  was called) and 'next' which is what we need. So we split the link value, find the part that has 'next' in it and
                    # run the results through a regex that pulls out just the url we need.  Phew!  Horn tooting complete.  proceed
                    $after = ([Regex]::Matches($response.Headers.link.Split(",") -match 'rel="next"', '(?<=<)(.*?)(?=>)')).Value

                    # if we've found pagination and successfully parsed the link, we call the function again.  Engage Recursive Mode!
                    if ($after) {
                        # since there is recursion, we append the new results to the existing JSON and move to the next bit
                        $oktaData = $oktaData + (getHTML -uri $after -method $method)
                    }
                }
            }
            # we return the now JSON formatted results back to you
            return $oktaData
        }

        # ruh roh...we screwed up.
        catch [System.Net.WebException] {
            
            $resp = $_.Exception.Response

            # well..we didn't screw up here, but we did overload Okta and hit an API limit.  Technically there is code above here that should prevent this...but just in case...
            if ([int]$resp.StatusCode -eq 429) {

                # increase the delay seconds
                $global:delay += 5

                # welp...we've run up a 2 minute delay...clearly we broke Okta so let's quit while we're behind
                if ($global:delay -eq 120) {
                    write-host "Delay has reached 2 minutes.  Cancelling operations"
                    exit
                }

                # executes the current delay
                write-host "API Limit reached...pausing for $($global:delay) seconds"
                Start-Sleep -Seconds $global:delay
                
                # clear the error for fun
                $error.Clear()

                # reruns the current query and hopefully resume normal operations
                $oktaData = $oktaData + (getHTML -uri $uri -method $method)
            }

            # no error message?
            if ($resp -eq $null) {
                Write-host $_.Exception
            }

            # prints out a nice error message for us
            else {
                $reqstream = $resp.GetResponseStream()
                $sr = New-Object System.IO.StreamReader $reqstream
                $body = $sr.ReadToEnd()

                # Okta is nice enough to print out an error code and an explanation.  Go here for more/same info: https://developer.okta.com/docs/reference/error-codes/
                write-host "Status: $([int]$resp.StatusCode) - $($resp.StatusCode)"
                Write-host ($body.Split(",") -match "errorSummary")
            }                    

        # who the hell knows what we did wrong this time....meh
        } catch {            
            Write-host $_.Exception
        }
    }
}


<# EXAMPLES

Here we are populating the $data variable with profile info: firstName and lastName.
We then call the getHTML function with a POST and the $data passed in.
POST on a user + data = update the user's profile

$data = '{"profile": {"firstName": "Delete","lastName": "Menow"}}'
getHTML -Uri "https://kars4kids.okta.com/api/v1/users/00u1k10wnz3gf32NPnF1d8" -method 'POST' -data $data

------

This is a simple get on a specific user, id is hardcoded 
getHTML -Uri "https://kars4kids.okta.com/api/v1/users/00u1k10wn344zggNPnF1d8"

------

This searches for all users who have a last name starting in y
getHTML -uri 'https://kars4kids.okta.com/api/v1/users?search=profile.lastName sw "y"'

------

This returns all Okta mastered groups
getHTML -uri 'https://kars4kids.okta.com/api/v1/groups?filter=type+eq+"OKTA_GROUP"'
------

This resets a user's MFA
getHTML -Uri "https://kars4kids.okta.com/api/v1/users/00u1k10F44fa2NPnF1d8/lifecycle/reset_factors" -method Post

------

OK - so enough with all the examples.  What do we do now?  Whatever you want....
Here is a real world example: Get user info on all people assigned to an application.
0oag3455g3dsdsD01d8 (Some AWS Random App.  I usually grab the IDs i'm targeting directly from the URL in my browser: https://kars4kids-admin.okta.com/admin/app/amazon_aws/instance/0oag3455g3dsdsD01d8/)
1) /api/v1/apps/0oag3455g3dsdsD01d8/users = gets all the users assigned to the app
2) loop through the users and get a link to their profile (using the ._links.user.href value)
3) get the user's info
4) print out what you want.

$users = getHTML -Uri "https://kars4kids.okta.com/api/v1/apps/0oag3455g3dsdsD01d8/users"
foreach ($user in $users) {
    $userInfo = getHTML -uri $user._links.user.href
    "{0},{1},{2},{3}" -f $userInfo.profile.firstName,$userInfo.profile.lastName,$userInfo.profile.email,$user._links.group.name
}

This yields results like:
Jane,Doe,jane.doe@kars4kids.com,Random_AWS_App_Group1
John,Doe,john.doe@kars4kids.com,Random_AWS_App_Group1
#>

@medic459 I don’t have idea on power shell. Could you please let me know how to user it - Function GetHtml need to be saved as separate file and then invoke and use the below code ?
$users = getHTML -Uri “https://kars4kids.okta.com/api/v1/apps/0oag3455g3dsdsD01d8/users
foreach ($user in $users) {
$userInfo = getHTML -uri $user._links.user.href
“{0},{1},{2},{3}” -f $userInfo.profile.firstName,$userInfo.profile.lastName,$userInfo.profile.email,$user._links.group.name
}

I have a requirement to get list of users assigned to specific app ( users assigned to the app are about 130000 users ) and export the file to FTP server or send the report to user’s email ?

Any known issue with above getHtml function ?
I ensured that authorization header is uncommented and provided api token from my environment and saved the file get-html.ps1 .

Tried to ran the script in several ways, don’t see any output and don’t see any errors also .

  1. Below didn;t give any results. User record is available in our org and provided okta id is correct
    powershell.exe -File ./get-html.ps1 -uri ‘https://*****.oktapreview.com/api/v1/users/****************’ -method GET

  2. Below didn’t give any results. Serveral okta managed groups are available in our org.

powershell.exe -File ./get-html.ps1 -uri -uri ‘https://*********.oktapreview.com/api/v1/groups?filter=type+eq+“OKTA_GROUP”’

  1. Below didn’t give any results. Same as 2, however tried to send the output to a file

powershell.exe -File ./get-html.ps1 -uri ‘https://cadence.oktapreview.com/api/v1/groups?filter=type+eq+“OKTA_GROUP”’ | Out-File userout.csv

@medic459
thank you so much for the response .
I tried placing the below in .ps1 file and when I run ps1 file (
powershell.exe -File ./get-html_new.ps1 ( OR ) ./get-html_new.ps1 ) getting errors .

Kindly give me the code as an exact script file, so will try running it .
Also could you please suggest how to enable TLS 1.2 via script file

Here is the ps1 file ( file name : get-html-new.ps1) script

#Function getHTML {

#[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

Param (
    [Parameter(Mandatory=$true)]  [String]$uri, # always need to pass in a URI for the API call
    [Parameter(Mandatory=$false)] [String]$data = $null # default is NULL, passing data to okta is tricky. Here is an example for a profile update: $data = '{"profile": {"firstName": "Delete","lastName": "Me"}}'
)
      
Begin {
    # headers needed to be passed in.  Authorization is the API token created in Okta and you're limited to its permissions    
    $headers = @{}
    #$headers["Authorization"] = "SSWS **** INSERT TOKEN HERE ****"  See next line for how it should look
    $headers["Authorization"] = "SSWS 00MazdyvaVfm0XC6p99jqKjrXOJ8X5C-hZWiKt4vrH"
    $headers["Accept"] = "application/json"
    $headers["Content-Type"] = "application/json"		
}

Process {
    try {
        if ($method -ne "GET" -and $uri -inotmatch "activate") {
            $response = Invoke-WebRequest -Method $method -Uri $uri -Headers $headers -Body $data
        
        } else {
            $response = Invoke-WebRequest -Method $method -Uri $uri -Headers $headers
        }
		
		if ([int]$response.Headers['x-rate-limit-remaining'] -lt 2) {
			# minimum delay, just in case
			$apiDelay = 5
		
			$apiDelay = [math]::Round(($response.Headers['x-rate-limit-reset']) - (Get-Date -UFormat %s) - 18000) + 1				
			write-host "API Limit reached...pausing for $($apiDelay) seconds"
			start-sleep -seconds $apiDelay
		}
        $global:count += 1

        
        if ($global:count % 100 -eq 0) {
            write-host "$global:count queries processed"
        }

        
        if ($response.content.length -lt 3) {            
            $htmlCode = $response.RawContent.Split("`n")[0].trim()
            if ($method -eq "DELETE") {
                $oktaData = "$htmlCode - (Deletion Succesful)"
            } else {
        
                $oktaData = $($htmlCode) + " - (No content expected)"
            }
        
        } else {

        
            $oktaData = ConvertFrom-Json $response.content

        
            if ($response.Headers.link) {
                $after = ([Regex]::Matches($response.Headers.link.Split(",") -match 'rel="next"', '(?<=<)(.*?)(?=>)')).Value

        
                if ($after) {
        
                    $oktaData = $oktaData + (getHTML -uri $after -method $method)
                }
            }
        }

        return $oktaData
    }


    catch [System.Net.WebException] {
        
        $resp = $_.Exception.Response


        if ([int]$resp.StatusCode -eq 429) {


            $global:delay += 5


            if ($global:delay -eq 120) {
                write-host "Delay has reached 2 minutes.  Cancelling operations"
                exit
            }


            write-host "API Limit reached...pausing for $($global:delay) seconds"
            Start-Sleep -Seconds $global:delay
            

            $error.Clear()


            $oktaData = $oktaData + (getHTML -uri $uri -method $method)
        }


        if ($resp -eq $null) {
            Write-host $_.Exception
        }


        else {
            $reqstream = $resp.GetResponseStream()
            $sr = New-Object System.IO.StreamReader $reqstream
            $body = $sr.ReadToEnd()


            write-host "Status: $([int]$resp.StatusCode) - $($resp.StatusCode)"
            Write-host ($body.Split(",") -match "errorSummary")
        }                    

    } catch {            
        Write-host $_.Exception
    }
}

$users = get-html_new -Uri “https://******.oktapreview.com/api/v1/apps/0oag378945judsdsD01d8/users”
foreach ($user in $users) {
$userInfo = getHTML -uri $user._links.user.href
“{0},{1},{2},{3}” -f $userInfo.profile.firstName,$userInfo.profile.lastName,$userInfo.profile.email,$user._links.group.name
}
#}

Getting below errors

==================

OK - A few things.
The powershell script I left is a function, getHTML. You can simply run the code and it now adds that cmdlet to your current powershell instance or, you can run it as a script with some minor edits.
It looks like you combined the 2.

I left this exact use case as a comment/example in the script:

Here is a real world example: Get user info on all people assigned to an application.
0oag3455g3dsdsD01d8 (Some AWS Random App. I usually grab the IDs i’m targeting directly from the URL in my browser: Sign In)

  1. /api/v1/apps/0oag3455g3dsdsD01d8/users = gets all the users assigned to the app
  2. loop through the users and get a link to their profile (using the ._links.user.href value)
  3. get the user’s info
  4. print out what you want.

So to get this to work as a script, copy the example code and paste it after the trailing #>. Find the app’s guid that you’re trying to report on and put it in the ‘INSERTGUIDHERE’.
Update/replace the ‘$headers[“Authorization”]’ code with a valid token.
The bottom of the script should look like this:

$users = getHTML -Uri “https://cadence.oktapreview.com/api/v1/apps/INSERTGUIDHERE/users
foreach ($user in $users) {
$userInfo = getHTML -uri $user._links.user.href
“{0},{1},{2},{3}” -f $userInfo.profile.firstName,$userInfo.profile.lastName,$userInfo.profile.email,$user._links.group.name
}

It does not look like you copied the function declaration from my initial post…the very first line…
Function getHTML {

WRT TLS 1.2…
Have you tried google?

@medic459 ok, so copy function as is and append the below as well (GUID will be placed) and then save it as .ps1 file and run it. Tried it with below and it didn;t work .

Function getHTML {

Param (
    [Parameter(Mandatory=$true)]  [String]$uri, # always need to pass in a URI for the API call
    [Parameter(Mandatory=$false)] [String]$method   = "GET", # default is GET, can also be POST, DELETE etc. etc.
    [Parameter(Mandatory=$false)] [String]$data = $null # default is NULL, passing data to okta is tricky. Here is an example for a profile update: $data = '{"profile": {"firstName": "Delete","lastName": "Me"}}'
)
      
Begin {
    
    $headers = @{}
    #$headers["Authorization"] = "SSWS **** INSERT TOKEN HERE ****"  See next line for how it should look
    $headers["Authorization"] = "SSWS *********************************************"
    $headers["Accept"] = "application/json"
    $headers["Content-Type"] = "application/json"		
}

Process {
    try {
        if ($method -ne "GET" -and $uri -inotmatch "activate") {
            $response = Invoke-WebRequest -Method $method -Uri $uri -Headers $headers -Body $data
    
        } else {
            $response = Invoke-WebRequest -Method $method -Uri $uri -Headers $headers
        }
	
		if ([int]$response.Headers['x-rate-limit-remaining'] -lt 2) {
	
			$apiDelay = 5
	
			$apiDelay = [math]::Round(($response.Headers['x-rate-limit-reset']) - (Get-Date -UFormat %s) - 18000) + 1				
			write-host "API Limit reached...pausing for $($apiDelay) seconds"
			start-sleep -seconds $apiDelay
		}
    
        $global:count += 1

    
        if ($global:count % 100 -eq 0) {
            write-host "$global:count queries processed"
        }

    
        if ($response.content.length -lt 3) {            
            $htmlCode = $response.RawContent.Split("`n")[0].trim()
            if ($method -eq "DELETE") {
                $oktaData = "$htmlCode - (Deletion Succesful)"
            } else {
    
                $oktaData = $($htmlCode) + " - (No content expected)"
            }
    
        } else {

    
            $oktaData = ConvertFrom-Json $response.content

    
            if ($response.Headers.link) {
                $after = ([Regex]::Matches($response.Headers.link.Split(",") -match 'rel="next"', '(?<=<)(.*?)(?=>)')).Value

    
                if ($after) {
    
                    $oktaData = $oktaData + (getHTML -uri $after -method $method)
                }
            }
        }
    
        return $oktaData
    }

    
    catch [System.Net.WebException] {
        
        $resp = $_.Exception.Response

    
        if ([int]$resp.StatusCode -eq 429) {

    
            $global:delay += 5

    
            if ($global:delay -eq 120) {
                write-host "Delay has reached 2 minutes.  Cancelling operations"
                exit
            }

    
            write-host "API Limit reached...pausing for $($global:delay) seconds"
            Start-Sleep -Seconds $global:delay
            
    
            $error.Clear()

    
            $oktaData = $oktaData + (getHTML -uri $uri -method $method)
        }

    
        if ($resp -eq $null) {
            Write-host $_.Exception
        }

    
        else {
            $reqstream = $resp.GetResponseStream()
            $sr = New-Object System.IO.StreamReader $reqstream
            $body = $sr.ReadToEnd()

    
            write-host "Status: $([int]$resp.StatusCode) - $($resp.StatusCode)"
            Write-host ($body.Split(",") -match "errorSummary")
        }                    

    } catch {            
        Write-host $_.Exception
    }
}

}

$users = getHTML -Uri “https://*******.oktapreview.com/api/v1/apps/0oag3455g3dsdsD01d8/users”
foreach ($user in $users) {
$userInfo = getHTML -uri $user._links.user.href
“{0},{1},{2},{3}” -f $userInfo.profile.firstName,$userInfo.profile.lastName,$userInfo.profile.email,$user._links.group.name
}

I can’t everything for you…and you’ve included no error messages/output screenshots.

I use this code every day in my environment. If it’s not working for you, i’m sorry!

Perhaps you can/should try a different method to accomplish your task?

@medic459 It worked fine . Sorry to bother you.
Now I am getting into rate limits issue, my app has about 140k users and it is taking too much time to generate the report . Can you please suggest if any improvement can be done .
I am not looping through the results, just getting the data to $users and getting rate limits error and execution is getting paused for long time.

$users = getHTML -Uri “https://cadence.oktapreview.com/api/v1/apps/0oaexki4kbrlHM0rJ0h7/users”

Rate limits are imposed by Okta…sorry.

Also…on the ‘assignments’ pane for every app, there should be a ‘reports’ box on the right side:

REPORTS
Current Assignments
Recent Unassignments

You can have the current assignments emailed to you…

@medic459 I can get that on demand via okta reports, however wanted to schedule the daily report and hence trying via the custom script