Private/Auth.psm1

#!/usr/bin/env pwsh
using namespace System
using namespace System.Text
using namespace System.Collections.Generic

#Requires -Modules clihelper.xconvert

class AuthStrategy {
  AuthStrategy() {}
  [string] GetAuthString() {
    return $null
  }
}

class BasicAuthStrategy : AuthStrategy {
  [pscredential]$credentials

  BasicAuthStrategy([string]$username, [securestring]$password) {
    $this.credentials = [pscredential]::new($username, $($password | xconvert ToSecurestring))
  }

  [string] GetAuthString() {
    $raw = "{0}:{1}" -f $this.credentials.UserName, $this.credentials.GetNetworkCredential().Password
    $bytes = [Text.Encoding]::UTF8.GetBytes($raw)
    return 'Basic ' + [Convert]::ToBase64String($bytes)
  }
}

class TokenAuthStrategy : AuthStrategy {
  [string] $Token

  TokenAuthStrategy([string]$token) {
    $this.Token = $token
  }

  [string] GetAuthString() {
    return 'Bearer ' + $this.Token
  }
}

class Base64Url {
  static [string] Encode([byte[]]$bytes) {
    if ($null -eq $bytes) { return '' }
    return [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
  }

  static [byte[]] Decode([string]$value) {
    if ([string]::IsNullOrEmpty($value)) { return @() }

    $s = $value.Replace('-', '+').Replace('_', '/')
    switch ($s.Length % 4) {
      2 { $s += '==' ; break }
      3 { $s += '='; break }
    }

    return [Convert]::FromBase64String($s)
  }
}

#region JWT



class BaseJwt {
  [string] $Secret
  [hashtable] $Headers
  [hashtable] $Payload

  BaseJwt([string]$secret) {
    $this.Secret = $secret
    $this.Headers = @{ alg = 'HS256'; typ = 'JWT' }
    $this.Payload = @{}
  }

  hidden [string] ToBase64Url([object]$obj) {
    $json = ConvertTo-Json $obj -Compress -Depth 20
    return [Base64Url]::Encode([Encoding]::UTF8.GetBytes($json))
  }

  [string] ToJwt() {
    $headerSegment = $this.ToBase64Url($this.Headers)
    $payloadSegment = $this.ToBase64Url($this.Payload)
    $signingInput = "$headerSegment.$payloadSegment"

    $hmac = [System.Security.Cryptography.HMACSHA256]::new([Encoding]::UTF8.GetBytes($this.Secret))
    try {
      $sigBytes = $hmac.ComputeHash([Encoding]::UTF8.GetBytes($signingInput))
      $signature = [Base64Url]::Encode($sigBytes)
      return "$signingInput.$signature"
    } finally { $hmac.Dispose() }
  }
}

class AccessToken : BaseJwt {
  [string] $AccountSid
  [string] $ApiKeySid
  [string] $Identity
  [int] $Ttl = 3600
  [List[hashtable]] $Grants

  AccessToken([string]$accountSid, [string]$apiKeySid, [string]$apiKeySecret) : base($apiKeySecret) {
    $this.AccountSid = $accountSid
    $this.ApiKeySid = $apiKeySid
    $this.Grants = [List[hashtable]]::new()
  }

  [void] AddGrant([hashtable]$grant) {
    if ($null -ne $grant) { $this.Grants.Add($grant) }
  }

  [string] ToJwt() {
    $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
    $grantPayload = @{}
    foreach ($g in $this.Grants) {
      foreach ($k in $g.Keys) { $grantPayload[$k] = $g[$k] }
    }

    if (![string]::IsNullOrWhiteSpace($this.Identity)) {
      $grantPayload['identity'] = $this.Identity
    }

    $this.Payload = @{
      jti    = "$($this.ApiKeySid)-$now"
      iss    = $this.ApiKeySid
      sub    = $this.AccountSid
      iat    = $now
      exp    = $now + $this.Ttl
      grants = $grantPayload
    }

    return ([BaseJwt]$this).ToJwt()
  }
}

class ChatGrant {
  static [hashtable] Create([string]$serviceSid) {
    return @{ chat = @{ service_sid = $serviceSid } }
  }
}

class VoiceGrant {
  static [hashtable] Create([string]$outgoingApplicationSid) {
    return @{ voice = @{ outgoing = @{ application_sid = $outgoingApplicationSid } } }
  }
}

class VideoGrant {
  static [hashtable] Create([string]$room) {
    return @{ video = @{ room = $room } }
  }
}

class PlaybackGrant {
  static [hashtable] Create() {
    return @{ playback = $true }
  }
}


#endregion JWT