Connect-Keeper: Automatically perform actions on DeviceAuth and TwoFactor
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
If a Keeper client is used in a hosted environment (there is no user interaction) we suggest to prepare a configuration file config.json so the Device Approval and Two Factor steps are already completed.
You probably do not need to automate Device Approval and Two Factor steps.
But if you really need it then I would suggest to have a separate cmdlet that prepares a new Keeper configuration file that has Device Approval and Two Factor steps done.
Thank you! That seems a bit easier.
But I can't seem to get it to work.
Connect-Keeper identifies the config.json file, but asks for both the 2fa code and password every time.
I generate the config file like this:
$otp = Get-Otp $KeeperTotpSecret
$KeeperConfig = @'
{{
"server":"https://keepersecurity.com/api/v2/",
"user":"{0}",
"password":"{1}",
"mfa_type":"device_token",
"mfa_token":"{2}",
"debug":false,
"commands":[]
}}
'@
[string]::format($KeeperConfig, $KeeperUser, (ConvertFrom-SecureString -SecureString $KeeperPassword -AsPlainText), $otp) | Out-File -FilePath config.json
Run Connect-Keeper once, enter the 2FA token and then try to connect again:
Connect-Keeper -Username $KeeperUser
... entering 2FA and masterpassword
Connect-Keeper
... prompted for 2FA and masterpassword again
If I specify the password and username on the CLI, I only get prompted for the 2FA:
Connect-Keeper -Username $KeeperUser -Password $KeeperPassword -SsoPassword
... prompted for the 2FA
I can see that you use a different format for the config file. That is the Python's Commander config file. .Net SDK config file is different.
{
"server": "keepersecurity.com",
"clone_code": "...",
"user": "[email protected]",
"devices": [
{
"device_token": "...",
"private_key": "...",
"server_info": [
{
"server": "keepersecurity.com",
"clone_code": "..."
}
]
}
],
"last_login": "[email protected]",
"last_server": "keepersecurity.com",
"servers": [
{
"server": "keepersecurity.com",
"server_key_id": 3
}
],
"users": [
{
"last_device": {
"device_token": "..."
},
"server": "keepersecurity.com",
"user": "[email protected]"
}
]
}
If device approval step appears every login it means the library creates a new (so called) "device" and the backend enforces a full login flow.
I see. I got it to work now after I deleted the entire config file and let powercommander make it from scratch. I still have to pass the password, but that's not a problem.
I've been able to use the config file on a headless system, so it works for my purposes.
Thank you very much for your help!
@bror-lauritz Hey sorry to ping you here but do you happen to have an example of how you got unnatended auth to work with the powershell module? I think I've correctly constructed the config.json but Connect-Keeper just keeps asking for device approval and is not giving any feedback on if its detecting the json or not.
@rvdwegen If Connect-Keeper keeps asking you for device approval it means there is an issue with config.json file.
This file is either cannot be found or misformatted.
You can use the .Net Commander to create and prepare config.json file for use by a service.
Please note that the config file format is different for Python and .Net Commanders.
https://github.com/Keeper-Security/keeper-sdk-dotnet/releases/tag/v1.1.0-beta01
The default file location is the path returned by Environment.GetFolderPath(Environment.SpecialFolder.Personal) for your environment.
https://github.com/Keeper-Security/keeper-sdk-dotnet/blob/87ca142c756580451a3017360d7c10122d559913/KeeperSdk/auth/JsonConfiguration.cs#L499
Windows: %USER_HOME%\Documents.keeper
Posix: $USER_HOME/.keeper
The config file can be setup for persistent login using this-device Commander's command.
In this case, the powershell module requires the config.json should persist between sessions if used in hosted environment
Hi, your support replied with similar instructions which I'll try out tomorrow. But does that mean the powershell module itself can't be used to generate the config.json?
Powershell module generates config.json file if it does not exists using default settings.
This module does not expose methods for customizing it.
Hi @rvdwegen! In my experience this works quite well (I'm on Linux, so YMMV):
New-Item -Type File -Name config.json
$USERNAME = Read-Host -Prompt "Keeper username"
$PASSWORD = Read-Host -AsSecureString -Prompt "Keeper password"
Connect-Keeper -Username $USERNAME -Password $PASSWORD -SsoPassword
(Some of the flags might not be necessary in your case)
You probably want to set expire=never on the first prompt.
It's based on that Keeper finds the config.json file either in the current directory or $HOME/.keeper.
If you create an empty file in the local directory it'll be populated when you run Connect-Keeper. You can then use it as you please.
NOTE: I think you still need to supply the password whenever you run Connect-Keeper, but you don't have to use the 2FA code or authenticate the device.
Unfortunately that still seems to result in a demand to input the password? I'm looking to use the PowerShell module (not .dotnet KeeperCommander) fully unnatended in either a function app or azure automation account.
The dotnet PowerCommander is correctly logging in completely unnatended now as soon as I start Keeper so the JSON is working now.
I can see that you use a different format for the config file. That is the Python's Commander config file. .Net SDK config file is different.
{ "server": "keepersecurity.com", "clone_code": "...", "user": "[email protected]", "devices": [ { "device_token": "...", "private_key": "...", "server_info": [ { "server": "keepersecurity.com", "clone_code": "..." } ] } ], "last_login": "[email protected]", "last_server": "keepersecurity.com", "servers": [ { "server": "keepersecurity.com", "server_key_id": 3 } ], "users": [ { "last_device": { "device_token": "..." }, "server": "keepersecurity.com", "user": "[email protected]" } ] }
The config file dotnet PowerCommander generated looks nothing like the above example though.
{ "user": "svc_keeper", "server": "keepersecurity.eu", "device_token": "", "private_key": "", "clone_code": "" }
If your setup is like our organization, you have to specify the -SsoPassword flag for it to not ask the password. Otherwise, I don't have any other ways to make it work.
Same result unfortunately.
But you can see that it picks up something from the JSON,
I did, I've tried every combination that made sense.
Hey i'd like to follow up on the masterpassword prompt.
either i go through: $keeperUsername = "myemailadress" $keeperPassword = ConvertTo-SecureString "mypassword" -AsPlainText -Force Connect-Keeper -Username $keeperUsername -Password $keeperPassword or: $keeperConfigPath = "C:\pathtomyconfigfile\config.json" Connect-Keeper -Config $keeperConfigPath
it always ask me to enter the master password, but i want to get it done automatically somehow it does not get the password from the configfile is something wrong?
{ "devices": [ { "device_token": "", "private_key": "", "server_info": [ { "server": "keepersecurity.com" }, { "clone_code": "", "server": "keepersecurity.eu" } ] } ], "last_login": "", "last_server": "", "servers": [ { "server": "keepersecurity.com", "server_key_id": 3 }, { "server": "keepersecurity.eu", "server_key_id": 3 } ], "users": [ { "last_device": { "device_token": "" }, "server": "keepersecurity.eu", "user": "", "password": "" } ] }
PowerCommander ignored "password" property of the configuration file. This issue has been fixed in the latest PowerCommander release.
Passing user's password in the command line should work.
Probably your password contains powershell special characters like $ or `
PS> $password = Read-Host -AsSecureString -Prompt 'Enter Password'
PowerCommander ignored "password" property of the configuration file. This issue has been fixed in the latest PowerCommander release.
Yep, works here too now.