Merge pull request #3061 from koen-lee/make-1password-integration-more-tolerant

Make 1password integration more tolerant
This commit is contained in:
Dimitrij
2025-12-29 09:57:47 +00:00
committed by GitHub
2 changed files with 107 additions and 35 deletions

View File

@@ -13,8 +13,18 @@ public class OnePasswordCliException(string message, string arguments) : Excepti
public class OnePasswordCli
{
private const string OnePasswordCliExecutable = "op.exe";
// Username / password purpose metadata is used on Login category item fields
private const string UserNamePurpose = "USERNAME";
private const string PasswordPurpose = "PASSWORD";
// Server category items (and perhaps others) do have a built-in username/password field but don't have the `purpose` set
// and because it's a built-in field this can't be set afterwards.
// We use the label for as fallback because that can be user-modified to fit this convention in all cases.
private const string UserNameLabel = "username";
private const string PasswordLabel = "password";
private const string StringType = "STRING";
private const string SshKeyType = "SSHKEY";
private const string DomainLabel = "domain";
@@ -41,45 +51,59 @@ public class OnePasswordCli
}
private static void ItemGet(string item, string? vault, string? account, out string username, out string password, out string domain, out string privateKey)
{
var args = new List<string> { "item", "get", item };
{
var args = new List<string> { "item", "get", item };
if (!string.IsNullOrEmpty(account))
if (!string.IsNullOrEmpty(account))
{
args.Add("--account");
args.Add(account);
}
if (!string.IsNullOrEmpty(vault))
{
args.Add("--vault");
args.Add(vault);
}
args.Add("--format");
args.Add("json");
string commandLine = OnePasswordCliExecutable + " " + string.Join(' ', args);
var exitCode = RunCommand(OnePasswordCliExecutable, args, out var output, out var error);
if (exitCode != 0)
{
username = string.Empty;
password = string.Empty;
privateKey = string.Empty;
domain = string.Empty;
throw new OnePasswordCliException($"Error running op item get: {error}",
commandLine);
}
var items = JsonSerializer.Deserialize<VaultItem>(output, JsonSerializerOptions) ??
throw new OnePasswordCliException("1Password returned null",
commandLine);
username = FindField(items, UserNamePurpose, UserNameLabel);
password = FindField(items, PasswordPurpose, PasswordLabel);
privateKey = items.Fields?.FirstOrDefault(x => x.Type == SshKeyType)?.Value ?? string.Empty;
domain = items.Fields?.FirstOrDefault(x => x.Type == StringType && x.Label == DomainLabel)?.Value ?? string.Empty;
if(string.IsNullOrEmpty(password) && string.IsNullOrEmpty(privateKey))
{
args.Add("--account");
args.Add(account);
throw new OnePasswordCliException("No secret found in 1Password. At least fields with labels username/password or a SshKey are expected.", commandLine);
}
}
if (!string.IsNullOrEmpty(vault))
{
args.Add("--vault");
args.Add(vault);
}
private static string FindField(VaultItem items, string purpose, string fallbackLabel)
{
return items.Fields?.FirstOrDefault(x => x.Purpose == purpose)?.Value ??
items.Fields?.FirstOrDefault(x => x.Type == StringType && string.Equals(x.Id, fallbackLabel, StringComparison.InvariantCultureIgnoreCase))?.Value ??
items.Fields?.FirstOrDefault(x => x.Type == StringType && string.Equals(x.Label, fallbackLabel, StringComparison.InvariantCultureIgnoreCase))?.Value ??
string.Empty;
}
args.Add("--format");
args.Add("json");
var exitCode = RunCommand(OnePasswordCliExecutable, args, out var output, out var error);
if (exitCode != 0)
{
username = string.Empty;
password = string.Empty;
privateKey = string.Empty;
domain = string.Empty;
throw new OnePasswordCliException($"Error running op item get: {error}",
OnePasswordCliExecutable + " " + string.Join(' ', args));
}
var items = JsonSerializer.Deserialize<VaultItem>(output, JsonSerializerOptions) ??
throw new OnePasswordCliException("1Password returned null",
OnePasswordCliExecutable + " " + string.Join(' ', args));
username = items.Fields?.FirstOrDefault(x => x.Purpose == UserNamePurpose)?.Value ?? string.Empty;
password = items.Fields?.FirstOrDefault(x => x.Purpose == PasswordPurpose)?.Value ?? string.Empty;
privateKey = items.Fields?.FirstOrDefault(x => x.Type == SshKeyType)?.Value ?? string.Empty;
domain = items.Fields?.FirstOrDefault(x => x.Type == StringType && x.Label == DomainLabel)?.Value ?? string.Empty;
}
private static int RunCommand(string command, IReadOnlyCollection<string> arguments, out string output,
private static int RunCommand(string command, IReadOnlyCollection<string> arguments, out string output,
out string error)
{
var processStartInfo = new ProcessStartInfo

View File

@@ -3,10 +3,11 @@ Credential Vault Connector
**************************
mRemote supports fetching credentials from external credential vaults. This allows providing credentials to the connection without storing sensitive information in the config file, which has numerous benefits (security, auditing, rotating passwords, etc).
Two password vaults are currently supported:
Three password vaults are currently supported:
- Delinea Secret Server
- Clickstudios Passwordstate
- 1Password
The feature is implemented for RDP, RDP Gateway and SSH connections.
@@ -42,3 +43,50 @@ Authentication works with WinAuth/SSO and list-based API-Keys. MFA via OTP is su
- There is currently no support for token authentication, so if your API has MFA enabled, you need to specify a fresh OTP code quite frequently
- If you are using list-based API keys to access the vault, only one API key can currently be specified in the connector configuration
1Password
---------
The secret reference uses the 1Password URL format. Specify the secret in the mRemote ``UserViaAPI`` field using this format:
``op://vault-name/item-name``
``op:///item-name``
Or with an optional account parameter:
``op://vault-name/item-name?account=account-name``
Where:
- ``vault-name`` is the name or id of your 1Password vault
- ``item-name`` is the name or id of the item containing the credentials
- ``account-name`` (optional) specifies which 1Password account to use if you have multiple accounts
Field Mapping
~~~~~~~~~~~~~
The 1Password integration retrieves the following fields from your 1Password item:
- **Username**: Fields with purpose "USERNAME" or label "username"
- **Password**: Fields with purpose "PASSWORD" or label "password"
- **Domain**: String fields with label "domain"
- **SSH Private Key**: Fields of type "SSHKEY"
At least a password or SSH private key must be present in the item.
Prerequisites
~~~~~~~~~~~~~
The 1Password CLI (``op.exe``) must be installed and available in your system PATH. You can download it from https://1password.com/downloads/command-line/
The CLI requires the GUI application to run, because the GUI provides the authentication prompt. You can verify authentication by running:
``op signin``
Configuration Notes
~~~~~~~~~~~~~~~~~~~
- Server category items and some other item types may not have the ``purpose`` metadata set on their username/password fields. In these cases, the integration will fall back to matching by field label ("username" and "password").
- You can modify field labels in 1Password to match the expected conventions if needed.
- The domain field is optional and should be a string field with the label "domain".