diff --git a/asyncssh/connection.py b/asyncssh/connection.py index 3174b36..a8e4264 100644 --- a/asyncssh/connection.py +++ b/asyncssh/connection.py @@ -3637,6 +3637,15 @@ async def password_auth_requested(self) -> Optional[str]: if self._password is not None: password: Optional[str] = self._password + + if callable(password): + password = cast(Callable[[], Optional[str]], password)() + + if inspect.isawaitable(password): + password = await cast(Awaitable[Optional[str]], password) + else: + password = cast(Optional[str], password) + self._password = None else: result = self._owner.password_auth_requested() @@ -7195,9 +7204,10 @@ class SSHClientConnectionOptions(SSHConnectionOptions): the currently logged in user on the local machine will be used. :param password: (optional) The password to use for client password authentication or - keyboard-interactive authentication which prompts for a password. - If this is not specified, client password authentication will - not be performed. + keyboard-interactive authentication which prompts for a password, + or a `callable` or coroutine which returns the password to use. + If this is not specified or set to `None`, client password + authentication will not be performed. :param client_host_keysign: (optional) Whether or not to use `ssh-keysign` to sign host-based authentication requests. If set to `True`, an attempt will be diff --git a/tests/test_connection_auth.py b/tests/test_connection_auth.py index 75c8c47..e93b1da 100644 --- a/tests/test_connection_auth.py +++ b/tests/test_connection_auth.py @@ -1795,6 +1795,36 @@ async def test_password_auth(self): async with self.connect(username='pw', password='pw', client_keys=None): pass + @asynctest + async def test_password_auth_callable(self): + """Test connecting with a callable for password authentication""" + + async with self.connect(username='pw', password=lambda: 'pw', + client_keys=None): + pass + + @asynctest + async def test_password_auth_async_callable(self): + """Test connecting with an async callable for password authentication""" + + async def get_password(): + return 'pw' + + async with self.connect(username='pw', password=get_password, + client_keys=None): + pass + + @asynctest + async def test_password_auth_awaitable(self): + """Test connecting with an awaitable for password authentication""" + + async def get_password(): + return 'pw' + + async with self.connect(username='pw', password=get_password(), + client_keys=None): + pass + @asynctest async def test_password_auth_disabled(self): """Test connecting with password authentication disabled"""