fix: handle 2FA on the Bitwarden account during API login [#442]
bw login --apikey prompts for a TOTP code when 2FA is enabled on the
BW account. The previous code didn't pass one, so it would hang or
fail. Now generates a TOTP from BW_TOTP_SECRET and passes via --code.
Changes:
- BitwardenHelper.__init__ accepts totp_secret param
- login() generates a pyotp code and passes --code when secret is set
- provision-agent.py passes BW_TOTP_SECRET from environment
- docker-compose.yml and .env.example updated for the new var
- BW_PASSWORD removed from the login env (only needed for unlock via stdin)
The BW account's own TOTP secret lives in ~/.config/bw/env alongside
the other BW access info — the one exception (can't store BW's 2FA in
BW itself).
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -3,5 +3,8 @@ BW_CLIENTID=
|
|||||||
BW_CLIENTSECRET=
|
BW_CLIENTSECRET=
|
||||||
BW_PASSWORD=
|
BW_PASSWORD=
|
||||||
|
|
||||||
|
# TOTP secret for the BW account's own 2FA (required if 2FA is enabled)
|
||||||
|
BW_TOTP_SECRET=
|
||||||
|
|
||||||
# Set to true for debugging (shows browser window — requires display)
|
# Set to true for debugging (shows browser window — requires display)
|
||||||
HEADFUL=false
|
HEADFUL=false
|
||||||
|
|||||||
+17
-4
@@ -22,10 +22,11 @@ from typing import Optional
|
|||||||
class BitwardenHelper:
|
class BitwardenHelper:
|
||||||
"""Wrapper around the Bitwarden CLI for credential management."""
|
"""Wrapper around the Bitwarden CLI for credential management."""
|
||||||
|
|
||||||
def __init__(self, client_id: str, client_secret: str, password: str):
|
def __init__(self, client_id: str, client_secret: str, password: str, totp_secret: str = ""):
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
self.client_secret = client_secret
|
self.client_secret = client_secret
|
||||||
self.password = password
|
self.password = password
|
||||||
|
self.totp_secret = totp_secret
|
||||||
self.session: Optional[str] = None
|
self.session: Optional[str] = None
|
||||||
|
|
||||||
def _run_bw(self, args: list[str], capture: bool = True) -> str:
|
def _run_bw(self, args: list[str], capture: bool = True) -> str:
|
||||||
@@ -46,17 +47,29 @@ class BitwardenHelper:
|
|||||||
return result.stdout.strip() if capture else ""
|
return result.stdout.strip() if capture else ""
|
||||||
|
|
||||||
def login(self) -> None:
|
def login(self) -> None:
|
||||||
"""Authenticate via API key and unlock the vault."""
|
"""Authenticate via API key and unlock the vault.
|
||||||
|
|
||||||
|
If 2FA is enabled on the account, generates a TOTP code from
|
||||||
|
self.totp_secret and passes it via --code.
|
||||||
|
"""
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["BW_CLIENTID"] = self.client_id
|
env["BW_CLIENTID"] = self.client_id
|
||||||
env["BW_CLIENTSECRET"] = self.client_secret
|
env["BW_CLIENTSECRET"] = self.client_secret
|
||||||
env["BW_PASSWORD"] = self.password
|
|
||||||
|
login_cmd = ["bw", "login", "--apikey"]
|
||||||
|
login_input = ""
|
||||||
|
|
||||||
|
if self.totp_secret:
|
||||||
|
import pyotp
|
||||||
|
totp_code = pyotp.TOTP(self.totp_secret).now()
|
||||||
|
login_cmd += ["--code", totp_code]
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["bw", "login", "--apikey"],
|
login_cmd,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
env=env,
|
env=env,
|
||||||
|
input=login_input,
|
||||||
)
|
)
|
||||||
if result.returncode != 0 and "already" not in result.stderr.lower():
|
if result.returncode != 0 and "already" not in result.stderr.lower():
|
||||||
raise RuntimeError(f"BW login failed: {result.stderr.strip()}")
|
raise RuntimeError(f"BW login failed: {result.stderr.strip()}")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ services:
|
|||||||
- BW_CLIENTID=${BW_CLIENTID}
|
- BW_CLIENTID=${BW_CLIENTID}
|
||||||
- BW_CLIENTSECRET=${BW_CLIENTSECRET}
|
- BW_CLIENTSECRET=${BW_CLIENTSECRET}
|
||||||
- BW_PASSWORD=${BW_PASSWORD}
|
- BW_PASSWORD=${BW_PASSWORD}
|
||||||
|
- BW_TOTP_SECRET=${BW_TOTP_SECRET:-}
|
||||||
- HEADFUL=${HEADFUL:-false}
|
- HEADFUL=${HEADFUL:-false}
|
||||||
volumes:
|
volumes:
|
||||||
- ./agents.yaml:/app/agents.yaml:ro
|
- ./agents.yaml:/app/agents.yaml:ro
|
||||||
|
|||||||
@@ -619,6 +619,7 @@ def main():
|
|||||||
client_id=os.environ["BW_CLIENTID"],
|
client_id=os.environ["BW_CLIENTID"],
|
||||||
client_secret=os.environ["BW_CLIENTSECRET"],
|
client_secret=os.environ["BW_CLIENTSECRET"],
|
||||||
password=os.environ["BW_PASSWORD"],
|
password=os.environ["BW_PASSWORD"],
|
||||||
|
totp_secret=os.environ.get("BW_TOTP_SECRET", ""),
|
||||||
)
|
)
|
||||||
log.info("Connecting to Bitwarden...")
|
log.info("Connecting to Bitwarden...")
|
||||||
bw.login()
|
bw.login()
|
||||||
|
|||||||
Reference in New Issue
Block a user