-
Notifications
You must be signed in to change notification settings - Fork 26
Description
When running Connect-Keeper, I want to be able to automatically answer the DeviceAuth and TwoFactor steps in the AuthFlow. I've tried to answer them by piping the answer into another process, which partially works but is quite fragile.
What I want to do
Consider the TwoFactor step:
Keeper Username: $username
Available Commands
channel=<authenticator> to change channel.
expire=<now | 30_days | never> to set 2fa expiration.
<code> to send a 2fa code.
<Enter> to resume
2FA channel(authenticator) expire[now]:
I want to automatically answer the OTP here.
The same for DeviceAuth, but I want to first send "channel=2fa" and then the OTP.
What I have implemented
My current solution patches Connect-Keeper as defined in AuthCommands.ps1
with two new parameters and two if statements (see commented areas)
function Connect-Keeper {
<#
.Synopsis
Login to Keeper
.Parameter Username
User email
.Parameter NewLogin
Do not use Last Login information
.Parameter SsoPassword
Use Master Password for SSO account
.Parameter Server
Change default keeper server
# New Parameters
.Parameter TwoFactorChannel
Which channel to use when authenticating with 2FA
.Parameter TwoFactorAction
The action to take when authenticating with 2FA (i.e. push or <code>)
.Parameter DeviceAuthChannel
Which channel to use when authenticating a device
.Parameter DeviceAuthAction
The action to take when authenticating a device (i.e. push or <code>)
#>
[CmdletBinding(DefaultParameterSetName = 'regular')]
Param(
[Parameter(Position = 0)][string] $Username,
[Parameter()] [SecureString]$Password,
[Parameter()][switch] $NewLogin,
[Parameter(ParameterSetName = 'sso_password')][switch] $SsoPassword,
[Parameter(ParameterSetName = 'sso_provider')][switch] $SsoProvider,
[Parameter()][string] $Server,
# New parameters
[Parameter()][securestring[]] $TwoFactorActions,
[Parameter()][SecureString[]] $DeviceAuthActions # Can be sensitive
)
Disconnect-Keeper -Resume | Out-Null
$storage = New-Object KeeperSecurity.Configuration.JsonConfigurationStorage
if (-not $Server) {
$Server = $storage.LastServer
if ($Server) {
Write-Information -MessageData "`nUsing Keeper Server: $Server`n"
}
else {
Write-Information -MessageData "`nUsing Default Keeper Server: $([KeeperSecurity.Authentication.KeeperEndpoint]::DefaultKeeperServer)`n"
}
}
$endpoint = New-Object KeeperSecurity.Authentication.KeeperEndpoint($Server, $storage.Servers)
$endpoint.DeviceName = 'PowerShell Commander'
$endpoint.ClientVersion = 'c16.1.0'
$authFlow = New-Object KeeperSecurity.Authentication.Sync.AuthSync($storage, $endpoint)
$authFlow.ResumeSession = $true
$authFlow.AlternatePassword = $SsoPassword.IsPresent
if (-not $NewLogin.IsPresent -and -not $SsoProvider.IsPresent) {
if (-not $Username) {
$Username = $storage.LastLogin
}
}
$namePrompt = 'Keeper Username'
if ($SsoProvider.IsPresent) {
$namePrompt = 'Enterprise Domain'
}
if ($Username) {
Write-Output "$(($namePrompt + ': ').PadLeft(21, ' ')) $Username"
}
else {
while (-not $Username) {
$Username = Read-Host -Prompt $namePrompt.PadLeft(20, ' ')
}
}
if ($SsoProvider.IsPresent) {
$authFlow.LoginSso($Username).GetAwaiter().GetResult() | Out-Null
}
else {
$passwords = @()
if ($Password) {
if ($Password -is [SecureString]) {
$passwords += [Net.NetworkCredential]::new('', $Password).Password
}
elseif ($Password -is [String]) {
$passwords += $Password
}
}
$authFlow.Login($Username, $passwords).GetAwaiter().GetResult() | Out-Null
}
Write-Output ""
while (-not $authFlow.IsCompleted) {
if ($lastStep -ne $authFlow.Step.State) {
printStepHelp $authFlow
$lastStep = $authFlow.Step.State
}
$prompt = getStepPrompt $authFlow
#### Start My Changes ####
# If we're on the DeviceApproval step and the user has specified $DeviceAuthActions, do those actions in order
if ($DeviceAuthActions -and $authFlow.Step -is [KeeperSecurity.Authentication.Sync.DeviceApprovalStep]) {
executeStepAction $authFlow ($DeviceAuthActions[0] | ConvertFrom-SecureString -AsPlainText)
$DeviceAuthActions = $DeviceAuthActions[1..$DeviceAuthActions.Length] # Just do one thing per while loop
}
# If we're on the TwoFactor step and the user has specified $TwoFactorActions, do those actions in order
elseif ($TwoFactorActions -and $authFlow.Step -is [KeeperSecurity.Authentication.Sync.TwoFactorStep]) {
executeStepAction $authFlow ($TwoFactorActions[0] | ConvertFrom-SecureString -AsPlainText)
$TwoFactorActions = $TwoFactorActions[1..$TwoFactorActions.Length] # Just do one thing per while loop
}
# Otherwise, do everything like normal
elseif ($action) {
# move this here to avoid the "Read-Host" prompt
if ($authFlow.Step -is [KeeperSecurity.Authentication.Sync.PasswordStep]) {
$securedPassword = Read-Host -Prompt $prompt -AsSecureString
if ($securedPassword.Length -gt 0) {
$action = [Net.NetworkCredential]::new('', $securedPassword).Password
}
else {
$action = ''
}
}
else {
$action = Read-Host -Prompt $prompt
}
if ($action -eq '?') {
}
else {
executeStepAction $authFlow $action
}
}
#### End My Changes ####
}
if ($authFlow.Step.State -ne [KeeperSecurity.Authentication.Sync.AuthState]::Connected) {
if ($authFlow.Step -is [KeeperSecurity.Authentication.Sync.ErrorStep]) {
Write-Warning $authFlow.Step.Message
}
return
}
$auth = $authFlow
if ([KeeperSecurity.Authentication.AuthExtensions]::IsAuthenticated($auth)) {
Write-Debug -Message "Connected to Keeper as $Username"
$vault = New-Object KeeperSecurity.Vault.VaultOnline($auth)
$task = $vault.SyncDown()
Write-Information -MessageData 'Syncing ...'
$task.GetAwaiter().GetResult() | Out-Null
$vault.AutoSync = $true
$Script:Context.Auth = $auth
$Script:Context.Vault = $vault
[KeeperSecurity.Vault.VaultData]$vaultData = $vault
Write-Information -MessageData "Decrypted $($vaultData.RecordCount) record(s)"
Set-KeeperLocation -Path '\' | Out-Null
}
}
I can then run this to do all the steps I'd like:
$otp = Get-Otp $KeeperTotpSecret | ConvertTo-SecureString -AsPlainText
$params = @{
Username = $KeeperUser
Password = $KeeperPassword
SsoPassword = $true
TwoFactorActions = @(("channel=authenticator" | ConvertTo-SecureString -AsPlainText), $otp)
DeviceAuthActions = @(("channel=2fa" | ConvertTo-SecureString -AsPlainText), $otp)
ErrorAction = 'Stop'
}
Connect-Keeper @params
(I don't know if the actions should be secure string or not, from a security perspective)
Limitations with my method
This works pretty well for my purposes, but I'd like for something like this in the source so I don't have to verify that it works after every new release.
One drawback is that it still unnecessarily prints out
Keeper Username: $username
Available Commands
channel=<authenticator> to change channel.
expire=<now | 30_days | never> to set 2fa expiration.
<code> to send a 2fa code.
<Enter> to resume