Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ sl auth login
# Prompts for your API key
```

### 3. 1Password integration (most secure)
### 3. 1Password integration

If you use 1Password, the CLI can retrieve your API key on each request without ever storing it on disk:

Expand All @@ -72,6 +72,16 @@ sl auth login --1password --vault Personal --item "SimpleLogin API Key"

This stores an `op://` reference in the config file. The actual key is fetched via the `op` CLI each time it's needed. You must have the [1Password CLI](https://developer.1password.com/docs/cli/) installed and signed in.

### 4. password-store integration

If you use [password-store](https://www.passwordstore.org/) (`pass`), the CLI can retrieve your API key the same way — without storing it on disk:

```bash
sl auth login --pass --item "Email/simplelogin"
```

This stores a `ps://` reference in the config file. The actual key is fetched from the first line of `pass show <path>` each time it's needed. You must have `pass` installed and the entry must exist in your store.

### Self-hosted instances

If you run a self-hosted SimpleLogin instance, pass your URL at login:
Expand Down Expand Up @@ -138,6 +148,7 @@ Most commands support `--json` and `--jq` flags for machine-readable output. Man
| `sl auth login --key <key>` | Store API key directly |
| `sl auth login --key <key> --url <url>` | Authenticate against a self-hosted instance |
| `sl auth login --1password --vault <v> --item <i>` | Use 1Password integration |
| `sl auth login --pass --item <path>` | Use password-store integration |
| `sl auth logout` | Remove stored credentials |
| `sl auth status` | Show current user, key source, and API URL |

Expand Down Expand Up @@ -327,6 +338,8 @@ api_key: sl_xxxxxxxxxxxxx
api_base: https://sl.example.com # only for self-hosted instances
# or for 1Password:
op_ref: op://Personal/SimpleLogin API Key/credential
# or for password-store:
pass_ref: ps://Email/simplelogin
```

## Automated Upstream Tracking
Expand Down
3 changes: 2 additions & 1 deletion cmd/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ var Cmd = &cobra.Command{
Short: "Manage authentication",
Long: `Manage authentication for the SimpleLogin CLI.

The CLI supports three authentication methods:
The CLI supports four authentication methods:
1. Environment variables (SIMPLELOGIN_API_KEY or SL_API_KEY)
2. Direct API key stored in config file
3. 1Password integration via the op CLI
4. Password-store integration via the pass CLI

Use "sl auth login" to configure authentication, "sl auth status" to
verify your current session, and "sl auth logout" to clear stored credentials.`,
Expand Down
58 changes: 53 additions & 5 deletions cmd/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,23 @@ var loginCmd = &cobra.Command{
Short: "Authenticate with SimpleLogin",
Long: `Store your SimpleLogin API key for CLI access.

There are three ways to authenticate:
There are four ways to authenticate:

1. Direct API key:
Provide your API key directly. It will be stored in
$XDG_CONFIG_HOME/simplelogin/config.yml (defaults to ~/.config/simplelogin/config.yml).

2. 1Password integration:
Store a reference to your API key in 1Password. The CLI will
use the "op" CLI to retrieve the key on each request. This is
the most secure option as the key is never stored on disk.
use the "op" CLI to retrieve the key on each request. The key
is never stored on disk.

3. Interactive:
3. Password-store integration:
Store a reference to your API key in password-store (pass). The CLI
will use the "pass" CLI to retrieve the key on each request. The key
is never stored on disk.

4. Interactive:
If no flags are provided, you will be prompted to enter your
API key interactively.

Expand All @@ -43,6 +48,9 @@ of your server (e.g. https://sl.example.com).`,
# Login with 1Password integration
sl auth login --1password --vault Personal --item "SimpleLogin API Key"

# Login with password-store integration
sl auth login --pass --item "Email/simplelogin"

# Login to a self-hosted instance
sl auth login --key sl_xxxxxxxxxxxxx --url https://sl.example.com

Expand All @@ -55,6 +63,7 @@ var (
loginKey string
loginURL string
login1Password bool
loginPass bool
loginVault string
loginItem string
)
Expand All @@ -63,8 +72,9 @@ func init() {
loginCmd.Flags().StringVar(&loginKey, "key", "", "API key to store (note: value will appear in shell history and ps output; prefer interactive or --1password)")
loginCmd.Flags().StringVar(&loginURL, "url", "", "Base URL of a self-hosted SimpleLogin instance (e.g. https://sl.example.com)")
loginCmd.Flags().BoolVar(&login1Password, "1password", false, "Use 1Password integration")
loginCmd.Flags().BoolVar(&loginPass, "pass", false, "Use password-store integration")
loginCmd.Flags().StringVar(&loginVault, "vault", "", "1Password vault name")
loginCmd.Flags().StringVar(&loginItem, "item", "", "1Password item name")
loginCmd.Flags().StringVar(&loginItem, "item", "", "Credential store item name or path (1Password item name or password-store path)")
}

func runLogin(cmd *cobra.Command, args []string) error {
Expand All @@ -75,6 +85,10 @@ func runLogin(cmd *cobra.Command, args []string) error {
}
}

if login1Password && loginPass {
return fmt.Errorf("--1password and --pass are mutually exclusive")
}

if login1Password {
if loginVault == "" || loginItem == "" {
return fmt.Errorf("--vault and --item are required with --1password")
Expand Down Expand Up @@ -106,6 +120,40 @@ func runLogin(cmd *cobra.Command, args []string) error {
return nil
}

if loginPass {
if loginItem == "" {
return fmt.Errorf("--item is required with --pass")
}
if loginVault != "" {
return fmt.Errorf("--vault is only valid with --1password, not --pass")
}

if err := intauth.SavePassRef(loginItem); err != nil {
return fmt.Errorf("failed to save password-store reference: %w", err)
}

// Validate by trying to get the key and calling the API
key, err := intauth.GetAPIKey()
if err != nil {
output.PrintWarning("password-store reference saved, but could not validate: %v", err)
output.PrintWarning("Make sure the 'pass' CLI is installed and the entry exists.")
return nil
}

client := api.NewClient(key, intauth.GetAPIBase())
info, _, err := client.GetUserInfo()
if err != nil {
output.PrintWarning("password-store reference saved, but API validation failed: %v", err)
return nil
}

output.PrintSuccess("Authenticated as %s (%s) via password-store", info.Name, info.Email)
if loginURL != "" {
output.PrintSuccess("Using custom API URL: %s", intauth.GetAPIBase())
}
return nil
}

key := loginKey
if key == "" {
if !output.IsInteractive() {
Expand Down
2 changes: 1 addition & 1 deletion cmd/auth/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
var logoutCmd = &cobra.Command{
Use: "logout",
Short: "Remove stored credentials",
Long: `Remove stored API key and 1Password references from the config file.
Long: `Remove stored API key, 1Password and password-store references from the config file.

This command clears stored credentials from $XDG_CONFIG_HOME/simplelogin/config.yml (defaults to ~/.config/simplelogin/config.yml).
It also attempts to invalidate the API session on the server.
Expand Down
4 changes: 3 additions & 1 deletion cmd/auth/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var statusCmd = &cobra.Command{
user's name and email, and the source of the API key.

This is useful to verify which account is active and how the CLI is
obtaining the API key (environment variable, 1Password, or config file).`,
obtaining the API key (environment variable, 1Password, password-store, or config file).`,
Example: ` # Check authentication status
sl auth status

Expand Down Expand Up @@ -54,6 +54,8 @@ func runStatus(cmd *cobra.Command, args []string) error {
source = "SL_API_KEY env var"
} else if intauth.GetOPRef() != "" {
source = "1Password (" + intauth.GetOPRef() + ")"
} else if intauth.GetPassRef() != "" {
source = "password-store (" + intauth.GetPassRef() + ")"
}

client := api.NewClient(key, intauth.GetAPIBase())
Expand Down
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ Authentication:
1. SIMPLELOGIN_API_KEY environment variable
2. SL_API_KEY environment variable
3. 1Password CLI (if configured via sl auth login --1password)
4. Config file ($XDG_CONFIG_HOME/simplelogin/config.yml, defaults to ~/.config/simplelogin/config.yml)
4. Password-store CLI (if configured via sl auth login --pass)
5. Config file ($XDG_CONFIG_HOME/simplelogin/config.yml, defaults to ~/.config/simplelogin/config.yml)

Run "sl auth login" to get started.

Expand Down
47 changes: 42 additions & 5 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Config struct {
APIKey string `yaml:"api_key"`
APIBase string `yaml:"api_base"`
OPRef string `yaml:"op_ref"`
PassRef string `yaml:"pass_ref"`
}

var configDir string
Expand Down Expand Up @@ -77,7 +78,8 @@ func saveConfig(cfg Config) error {
// 1. SIMPLELOGIN_API_KEY env var
// 2. SL_API_KEY env var
// 3. 1Password via op CLI (if op_ref is set in config)
// 4. api_key from config file
// 4. password-store via pass CLI (if pass_ref is set in config)
// 5. api_key from config file
func GetAPIKey() (string, error) {
// 1. SIMPLELOGIN_API_KEY env var
if key := os.Getenv("SIMPLELOGIN_API_KEY"); key != "" {
Expand Down Expand Up @@ -106,7 +108,22 @@ func GetAPIKey() (string, error) {
}
}

// 4. Config file api_key
// 4. password-store via pass CLI — password is on the first line of output
if cfg.PassRef != "" {
if passPath, err := exec.LookPath("pass"); err == nil && passPath != "" {
passArg := strings.TrimPrefix(cfg.PassRef, "ps://")
cmd := exec.Command("pass", "show", passArg)
out, err := cmd.Output()
if err == nil {
key := strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0])
if key != "" {
return key, nil
}
}
}
}

// 5. Config file api_key
if cfg.APIKey != "" {
return cfg.APIKey, nil
}
Expand All @@ -118,8 +135,9 @@ func GetAPIKey() (string, error) {
func SaveAPIKey(key string) error {
cfg := loadConfig()
cfg.APIKey = key
// Remove op_ref if setting direct key
// Remove credential store refs if setting direct key
cfg.OPRef = ""
cfg.PassRef = ""
return saveConfig(cfg)
}

Expand All @@ -128,19 +146,38 @@ func SaveOPRef(vault, item string) error {
ref := fmt.Sprintf("op://%s/%s/credential", vault, item)
cfg := loadConfig()
cfg.OPRef = ref
// Remove direct api_key if setting 1Password ref
// Remove other credential sources when setting 1Password ref
cfg.APIKey = ""
cfg.PassRef = ""
return saveConfig(cfg)
}

// ClearConfig removes the API key and op_ref from the config file.
// SavePassRef stores the password-store path in the config file.
func SavePassRef(path string) error {
ref := "ps://" + path
cfg := loadConfig()
cfg.PassRef = ref
// Remove other credential sources when setting password-store ref
cfg.APIKey = ""
cfg.OPRef = ""
return saveConfig(cfg)
}

// GetPassRef returns the stored password-store path, if any.
func GetPassRef() string {
cfg := loadConfig()
return cfg.PassRef
}

// ClearConfig removes all stored credentials from the config file.
func ClearConfig() error {
if _, err := os.Stat(ConfigPath()); os.IsNotExist(err) {
return nil
}
cfg := loadConfig()
cfg.APIKey = ""
cfg.OPRef = ""
cfg.PassRef = ""
return saveConfig(cfg)
}

Expand Down
Loading
Loading