public/oauth2.ps1

function Request-FalconToken {
<#
.SYNOPSIS
Request an OAuth2 access token
.DESCRIPTION
If successful, your credentials ('ClientId', 'ClientSecret', 'MemberCid' and 'Cloud'/'CustomUrl'/'Hostname') and
token are cached for re-use.

If an active OAuth2 access token is due to expire in less than 60 seconds, a new token will automatically be
requested using your cached credentials.

The 'Collector' parameter allows for the submission of a [System.Collections.Hashtable] object containing the
parameters included with a 'Register-FalconEventCollector' command ('Path', 'Token' and 'Enable') in order to
log an initial OAuth2 access token request.
.PARAMETER ClientId
OAuth2 client identifier
.PARAMETER ClientSecret
OAuth2 client secret
.PARAMETER Cloud
CrowdStrike cloud [default: 'us-1']
.PARAMETER CustomUrl
Custom API URL for module troubleshooting
.PARAMETER Hostname
CrowdStrike API hostname [default: 'https://api.crowdstrike.com']
.PARAMETER MemberCid
Member CID, used when authenticating within a multi-CID environment ('Falcon Flight Control')
.PARAMETER Collector
A hashtable containing 'Path', 'Token' and 'Enable' properties for 'Register-FalconEventCollector'
.LINK
https://github.com/crowdstrike/psfalcon/wiki/Request-FalconToken
#>

  [CmdletBinding(DefaultParameterSetName='/oauth2/token:post',SupportsShouldProcess)]
  [OutputType([void])]
  param(
    [Parameter(ParameterSetName='Cloud',ValueFromPipelineByPropertyName,Position=1)]
    [Parameter(ParameterSetName='Custom',ValueFromPipelineByPropertyName,Position=1)]
    [Parameter(ParameterSetName='/oauth2/token:post',ValueFromPipelineByPropertyName,Position=1)]
    [Alias('client_id')]
    [ValidatePattern('^[a-fA-F0-9]{32}$')]
    [string]$ClientId,
    [Parameter(ParameterSetName='Cloud',ValueFromPipelineByPropertyName,Position=2)]
    [Parameter(ParameterSetName='Custom',ValueFromPipelineByPropertyName,Position=2)]
    [Parameter(ParameterSetName='/oauth2/token:post',ValueFromPipelineByPropertyName,Position=2)]
    [Alias('client_secret')]
    [ValidatePattern('^\w{40}$')]
    [string]$ClientSecret,
    [Parameter(ParameterSetName='Cloud',ValueFromPipelineByPropertyName,Position=3)]
    [ValidateSet('eu-1','us-1','us-2','us-gov-1','us-gov-2',IgnoreCase=$false)]
    [string]$Cloud,
    [Parameter(ParameterSetName='Custom',ValueFromPipelineByPropertyName,Position=3)]
    [string]$CustomUrl,
    [Parameter(ParameterSetName='/oauth2/token:post',ValueFromPipelineByPropertyName,Position=3)]
    [ValidateSet('https://api.crowdstrike.com','https://api.us-2.crowdstrike.com',
      'https://api.laggar.gcw.crowdstrike.com','https://api.us-gov-2.crowdstrike.mil',
      'https://api.eu-1.crowdstrike.com',IgnoreCase=$false)]
    [string]$Hostname,
    [Parameter(ParameterSetName='Cloud',ValueFromPipelineByPropertyName,Position=4)]
    [Parameter(ParameterSetName='Custom',ValueFromPipelineByPropertyName,Position=4)]
    [Parameter(ParameterSetName='/oauth2/token:post',ValueFromPipelineByPropertyName,Position=4)]
    [Alias('cid','member_cid')]
    [ValidatePattern('^[a-fA-F0-9]{32}(-\w{2})?$')]
    [string]$MemberCid,
    [Parameter(ParameterSetName='Cloud',ValueFromPipelineByPropertyName,Position=5)]
    [Parameter(ParameterSetName='Custom',ValueFromPipelineByPropertyName,Position=5)]
    [Parameter(ParameterSetName='/oauth2/token:post',ValueFromPipelineByPropertyName,Position=5)]
    [ValidateScript({
      @($_.Keys).foreach{
        if ($_ -notmatch '^(Enable|Token|Uri)$') { throw "Unexpected key in 'Collector' object. ['$_']" }
      }
      foreach ($Key in @('Token','Uri')) {
        if ($_.Keys -notcontains $Key) { throw "'Collector' requires '$Key'." } else { $true }
      }
    })]
    [System.Collections.Hashtable]$Collector
  )
  begin {
    function Get-ApiCredential ($UserInput) {
      $Output = @{}
      @('ClientId','ClientSecret','Hostname','MemberCid').foreach{
        # Use input before existing ApiClient value
        $Value = if ($UserInput.$_) { $UserInput.$_ } elseif ($null -ne $Script:Falcon.$_) { $Script:Falcon.$_ }
        if (!$Value -and $_ -match '^(ClientId|ClientSecret)$') {
          # Prompt for ClientId/ClientSecret and validate input
          $Value = Read-Host $_
          $BaseError = 'Cannot validate argument on parameter "{0}". The argument "{1}" does not match the "{2}"' +
            ' pattern. Supply an argument that matches "{2}" and try the command again.'
          $ValidPattern = if ($_ -eq 'ClientId') { '^[a-fA-F0-9]{32}$' } else { '^\w{40}$' }
          if ($Value -notmatch $ValidPattern) {
            $InvalidValue = $BaseError -f $_,$Value,$ValidPattern
            throw $InvalidValue
          }
        } elseif (!$Value -and !$UserInput.CustomUrl -and $_ -eq 'Hostname') {
          # Default to 'us-1' cloud
          $Value = 'https://api.crowdstrike.com'
        }
        if ($Value) { $Output.Add($_,$Value) }
      }
      return $Output
    }
  }
  process {
    if ($PSBoundParameters.MemberCid) {
      # Validate MemberCid value
      $PSBoundParameters.MemberCid = Confirm-CidValue $PSBoundParameters.MemberCid
    }
    if ($PSBoundParameters.CustomUrl) {
      # Override Hostname with CustomUrl
      $PSBoundParameters['Hostname'] = $PSBoundParameters.CustomUrl
      [void]$PSBoundParameters.Remove('CustomUrl')
    } elseif ($PSBoundParameters.Cloud) {
      # Convert 'Cloud' to 'Hostname'
      $Value = switch ($PSBoundParameters.Cloud) {
        'eu-1' { 'https://api.eu-1.crowdstrike.com' }
        'us-1' { 'https://api.crowdstrike.com' }
        'us-2' { 'https://api.us-2.crowdstrike.com' }
        'us-gov-1' { 'https://api.laggar.gcw.crowdstrike.com' }
        'us-gov-2' { 'https://api.us-gov-2.crowdstrike.mil' }
      }
      $PSBoundParameters['Hostname'] = $Value
      [void]$PSBoundParameters.Remove('Cloud')
    }
    if (!$Script:Falcon) {
      try {
        # Initiate ApiClient, set SslProtocol and UserAgent
        $Script:Falcon = Get-ApiCredential $PSBoundParameters
        $Script:Falcon.Add('Api',[ApiClient]::New())
        if ($Script:Falcon.Api) {
          [string]$String = try {
            # Set TLS 1.2 for [System.Net.Http.HttpClientHandler]
            $Script:Falcon.Api.Handler.SslProtocols = 'Tls12'
            '[System.Net.Http.HttpClientHandler]'
          } catch {
            if ([Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12') {
              # Set TLS 1.2 for PowerShell session
              [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
              '[Net.ServicePointManager]'
            }
          }
          if ($String) { Write-Log 'Request-FalconToken' "Set TLS 1.2 via $String." }
          $Script:Falcon.Api.Handler.AutomaticDecompression = [System.Net.DecompressionMethods]::Gzip,
            [System.Net.DecompressionMethods]::Deflate
          $Script:Falcon.Api.Client.DefaultRequestHeaders.UserAgent.ParseAdd($Script:Falcon.Api.UserAgent)
        } else {
          $PSCmdlet.WriteError(
            [System.Management.Automation.ErrorRecord]::New(
              [Exception]::New('Failed to initialize [ApiClient] object.'),
              'failed_to_initialize_api_client_object',
              [System.Management.Automation.ErrorCategory]::ObjectNotFound,
              $null
            )
          )
        }
        [string]$FormatPath = Join-Path (Show-FalconModule).ModulePath (Join-Path format format.json)
        if ((Test-Path $FormatPath) -eq $false) { throw "Unable to find 'format.json'." }
        $Script:Falcon.Add('Format',((Get-Content $FormatPath | ConvertFrom-Json)))
        if ($Script:Falcon.Format) { Write-Log 'Request-FalconToken' "Loaded 'format.json'." }
      } catch {
        throw $_
      }
    } else {
      (Get-ApiCredential $PSBoundParameters).GetEnumerator().foreach{
        # Update existing ApiClient with new input
        if ($Script:Falcon.($_.Key) -ne $_.Value) { $Script:Falcon.($_.Key) = $_.Value }
      }
    }
    if ($PSBoundParameters.Collector) {
      $Collector = $PSBoundParameters.Collector
      Register-FalconEventCollector @Collector
    }
    if ($Script:Falcon.ClientId -and $Script:Falcon.ClientSecret) {
      try {
        $Param = @{
          Path = $Script:Falcon.Hostname,'oauth2/token' -join '/'
          Method = 'post'
          Headers = @{ Accept = 'application/json'; ContentType = 'application/x-www-form-urlencoded' }
          Body = "client_id=$($Script:Falcon.ClientId)&client_secret=$($Script:Falcon.ClientSecret)"
        }
        if ($Script:Falcon.MemberCid) { $Param.Body += "&member_cid=$($Script:Falcon.MemberCid)" }
        $Request = $Script:Falcon.Api.Invoke($Param)
        if ($Request.Result) {
          $Region = $Request.Result.Headers.GetEnumerator().Where({$_.Key -eq 'X-Cs-Region'}).Value
          $Redirect = switch ($Region) {
            # Update ApiClient hostname if redirected
            'eu-1' { 'https://api.eu-1.crowdstrike.com' }
            'us-1' { 'https://api.crowdstrike.com' }
            'us-2' { 'https://api.us-2.crowdstrike.com' }
            'us-gov-1' { 'https://api.laggar.gcw.crowdstrike.com' }
            'us-gov-2' { 'https://api.us-gov-2.crowdstrike.mil' }
          }
          if ($Redirect -and $Script:Falcon.Hostname -ne $Redirect) {
            Write-Log 'Request-FalconToken' "Redirected to '$Region'."
            $Script:Falcon.Hostname = $Redirect
          }
          if (@(308,429) -contains $Request.Result.StatusCode.GetHashCode()) {
            # Retry token request when rate limited or unable to automatically follow redirection
            & $MyInvocation.MyCommand.Name
          } else {
            $Result = Write-Result (ConvertFrom-Json (
              $Request.Result.Content).ReadAsStringAsync().Result)
            if ($Result.token_type -and $Result.access_token -and $Result.expires_in) {
              # Cache access token in ApiClient
              [string]$Token = $Result.token_type,$Result.access_token -join ' '
              if (!$Script:Falcon.Api.Client.DefaultRequestHeaders.Authorization) {
                $Script:Falcon.Api.Client.DefaultRequestHeaders.Add('Authorization',$Token)
              } else {
                $Script:Falcon.Api.Client.DefaultRequestHeaders.Authorization = $Token
              }
              $Script:Falcon.Expiration = (Get-Date).AddSeconds($Result.expires_in)
              Write-Log 'Request-FalconToken' "Authorized until: $($Script:Falcon.Expiration)"
            }
          }
        }
      } catch {
        @('ClientId','ClientSecret','MemberCid').foreach{ [void]$Script:Falcon.Remove($_) }
        [void]$Script:Falcon.Api.Client.DefaultRequestHeaders.Remove('Authorization')
        throw $_
      }
    } else {
      throw 'Missing required credentials.'
    }
  }
}
function Revoke-FalconToken {
<#
.SYNOPSIS
Revoke your active OAuth2 access token
.DESCRIPTION
Revokes your active OAuth2 access token and clears cached credential information ('ClientId', 'ClientSecret',
'MemberCid', 'Cloud'/'Hostname') from the module.
.LINK
https://github.com/crowdstrike/psfalcon/wiki/Revoke-FalconToken
#>

  [CmdletBinding(DefaultParameterSetName='/oauth2/revoke:post',SupportsShouldProcess)]
  param()
  process {
    if ($Script:Falcon.Api.Client.DefaultRequestHeaders.Authorization.Parameter -and
    $Script:Falcon.ClientId -and $Script:Falcon.ClientSecret) {
      # Revoke OAuth2 access token
      $Param = @{
        Path = $Script:Falcon.Hostname,'oauth2/revoke' -join '/'
        Method = 'post'
        Headers = @{
          Accept = 'application/json'
          ContentType = 'application/x-www-form-urlencoded'
          Authorization = "basic $([System.Convert]::ToBase64String(
            [System.Text.Encoding]::ASCII.GetBytes(($Script:Falcon.ClientId,
              $Script:Falcon.ClientSecret -join ':'))))"

        }
        Body = 'token',$Script:Falcon.Api.Client.DefaultRequestHeaders.Authorization.Parameter -join '='
      }
      $Request = $Script:Falcon.Api.Invoke($Param)
      Write-Result $Request
      [void]$Script:Falcon.Api.Client.DefaultRequestHeaders.Remove('Authorization')
    }
    @('ClientId','ClientSecret','MemberCid').foreach{
      if ($Script:Falcon.$_) { [void]$Script:Falcon.Remove($_) }
    }
  }
}
function Test-FalconToken {
<#
.SYNOPSIS
Display OAuth2 access token status
.DESCRIPTION
Displays a [PSCustomObject] containing token status ('Token') along with cached 'Hostname', 'ClientId' and
'MemberCid' values.
.LINK
https://github.com/crowdstrike/psfalcon/wiki/Test-FalconToken
#>

  [CmdletBinding()]
  param()
  process {
    if ($Script:Falcon) {
      [PSCustomObject]@{
        Token = if ($Script:Falcon.Api.Client.DefaultRequestHeaders.Authorization -and
          ($Script:Falcon.Expiration -gt (Get-Date).AddSeconds(240))) { $true } else { $false }
        Hostname = $Script:Falcon.Hostname
        ClientId = $Script:Falcon.ClientId
        MemberCid = $Script:Falcon.MemberCid
      }
    } else {
      $PSCmdlet.WriteError(
        [System.Management.Automation.ErrorRecord]::New(
          [Exception]::New('No authorization token available. Try "Request-FalconToken".'),
          'no_authorization_request_made',
          [System.Management.Automation.ErrorCategory]::ResourceUnavailable,
          $null
        )
      )
    }
  }
}