Private/Invoke-DuneApiAuthSocial.ps1
|
<#
.SYNOPSIS Authenticate to the Dune API using interactive browser-based social login. .DESCRIPTION Opens the Dune login page in the default browser and starts a local HTTP listener to receive the authentication callback. Exchanges the returned temporary token for JWT tokens and stores them in the script-scoped DuneSession. The session is also persisted to disk. .PARAMETER DuneInstance The target Dune instance. Valid values: Prod, Dev, Test, Local. .PARAMETER Tenant The tenant name to authenticate against. .EXAMPLE PS> Invoke-DuneApiAuthSocial -DuneInstance Prod -Tenant "yendico" Opens a browser for interactive login and stores the resulting session. #> function Invoke-DuneApiAuthSocial { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet("Prod", "Dev","Test","Local")] [string]$DuneInstance, [Parameter(Mandatory)] [string]$Tenant ) begin { $DuneApiUrl = Get-DuneApiUrl -DuneInstance $DuneInstance $DuneAuthUrl = Get-DuneAuthUrl -DuneInstance $DuneInstance } process { # Function to get an available port function Get-FreePort { $socket = New-Object System.Net.Sockets.Socket([System.Net.Sockets.AddressFamily]::InterNetwork, [System.Net.Sockets.SocketType]::Stream, [System.Net.Sockets.ProtocolType]::Tcp) $socket.Bind((New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0))) $freePort = $socket.LocalEndPoint.Port $socket.Close() # Release it so HttpListener can grab it return $freePort } # Get an available port with retry logic $Port = Get-FreePort $CallbackUrl = "http://localhost:$Port/" Write-Debug "Found available port: $Port" # starting a local http listener $Listener = $null try { $Listener = New-Object System.Net.HttpListener $Listener.Prefixes.Add($CallbackUrl) $Listener.Start() Write-Debug "Started HTTP Listener: $CallbackUrl" } catch { throw "Could not start HTTP Listener: $_" } Write-Host "Please login to browser..." try { # open browser with dynamic callback URL Start-Process "${DuneAuthUrl}?tenantname=${Tenant}&continue=${CallbackUrl}" # waiting for Redirect-Request # asynchronous wait allows to be stopped instead of GetContext() $Task = $Listener.GetContextAsync() # allows to be stopped while (-not $Task.IsCompleted) { Start-Sleep -Milliseconds 200 } $Context = $Task.Result $Request = $Context.Request Write-Debug "Received request from: $($Request.Url)" $Session = if ($Request.Url.Query -match 's=(?<s>[^&]+)') { $Matches.s } # not sure if needed $TempToken = if ($Request.Url.Query -match 'tmptok=(?<tmptok>[^&]+)') { $Matches.tmptok } $ErrorCode = if ($Request.Url.Query -match 'f=(?<f>[^&]+)') { $Matches.f } $ErrorMessage = if ($Request.Url.Query -match 'm=(?<m>[^&]+)') { $Matches.m.Replace('+',' ') } if ($ErrorCode) { $ResponseTitle = 'Login failed' $ResponseDetail = '{0} ({1})' -f $ErrorCode, $ErrorMessage } else { $ResponseTitle = 'Login successful' $ResponseDetail = 'You can close this window.' } # response to browser - send response BEFORE closing $ResponseString = @" <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Dune Login</title> <style> body { margin: 0; height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #1f1f1f; color: #e3e3e3; font-family: "Inter", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; text-align: center; } .lockup { display: flex; align-items: center; justify-content: center; gap: 0.6rem; } .logo { width: 48px; height: 48px; } .brand { font-size: 2rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: #e0c388; } h1 { margin: 1.25rem 0 0.5rem; font-size: 1.6rem; font-weight: 600; letter-spacing: 0.02em; } p { margin: 0; font-size: 1.05rem; color: #9a9a9a; font-family: "Cascadia Code", Consolas, "Courier New", monospace; } </style> </head> <body> <main> <div class="lockup"> <svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#e0c388"><path d="m40-240 240-320 190 253.33h83.67l-152.34-202L560-720l360 480H40Z"/></svg> <span class="brand">Dune</span> </div> <h1>$ResponseTitle</h1> <p>$ResponseDetail</p> </main> </body> </html> "@ $Buffer = [System.Text.Encoding]::UTF8.GetBytes($ResponseString) try { $Context.Response.ContentLength64 = $Buffer.Length $Context.Response.OutputStream.Write($Buffer, 0, $Buffer.Length) $Context.Response.OutputStream.Flush() } catch { Write-Warning "Error writing response: $_" } finally { $Context.Response.Close() } if ($TempToken) { Write-Debug "TempToken: $TempToken" $ReturnedTokens = Get-DuneJWTTokens -DuneInstance $DuneInstance -Tenant $Tenant -TemporaryToken $TempToken $Token = $ReturnedTokens.accesstoken $RefreshToken = $ReturnedTokens.refreshToken if ($Token) { $ParsedToken = Parse-JwtToken -Token $Token # Use local time: the expiry is compared against Get-Date (local) in Assert-DuneSession. $TokenExpiryDate = [System.DateTimeOffset]::FromUnixTimeSeconds($ParsedToken.exp).LocalDateTime # save auth info $Script:DuneSession = [PSCustomObject]@{ Type = 'SocialLogin' DuneApiUrl = $DuneApiUrl Token = ($Token | ConvertTo-SecureString -AsPlainText -Force) ExpiryDate = $TokenExpiryDate RefreshToken = ($RefreshToken | ConvertTo-SecureString -AsPlainText -Force) Tenant = $Tenant } Write-Verbose "Login successfull" Save-DuneSession } else { throw "Token not set" } } else { throw "Login failed: $($ErrorCode) ($($ErrorMessage))" } } catch { throw $_ } finally { # closing the HTTP Listener if ($Listener) { try { $Listener.Stop() $Listener.Close() } catch { Write-Debug "Error closing listener: $_" } } } } end {} } |