Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding MultiFactorAuthHandler class #37

Merged
merged 1 commit into from
Aug 19, 2024
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
101 changes: 58 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ supports it.**
If you don't need the Jump Host features but DO need to handle multi-factor authentication,
these next examples are for you.

You can use the ```simple_auth_handler``` or ```MagicAuthHandler``` to handle your
You can use the ```simple_auth_handler``` or ```MultiFactorAuthHandler``` to handle your
authentication to a single host without ever proxying another SSH session through it ('jumping'.)
Just pick an authentication approach and go.

Expand Down Expand Up @@ -89,31 +89,33 @@ print(output)
```


### Authentication Handler Example 2: MultiFactor Authentication using the MagicAuthHandler
### Authentication Handler Example 2: MultiFactor Authentication using the MultiFactorAuthHandler

The ```MagicAuthHandler``` class is a more advanced handler that can be used to accomplish complex
authentication sessions with automation -- even through MFA infrastructure. This is accomplished by
feeding the handler a sequence of responses which will be required during the authentication
session, such as a password and OTP. Each item in the sequence should be a Python list.
The ```MultiFactorAuthHandler``` class is a more advanced handler that can be used to accomplish
complex authentication sessions with automation -- even through MFA infrastructure. This is
accomplished by seeding the handler with a sequence of responses which will be required during
the authentication session, such as a password and OTP.

It goes without saying that, you must figure out what your MFA infrastructure is expecting and
provide the correct responses in the correct order. This is a powerful tool, but it can take a bit
of tinkering to get it right for your environment.

In this example, my MFA infrastucture is first going to require that I authenticate with
my password, and then I have to enter '1' to instruct the infrastructure to push an
authentication request to my mobile authenticator.
In this next example, my MFA infrastucture is first going to require that I authenticate with my
password, and then I have to enter '1' to instruct the infrastructure to push an authentication
request to my mobile authenticator.

#### Multi-Factor Authentication using the MagicAuthHandler
#### Multi-Factor Authentication using the MultiFactorAuthHandler
```python
from paramiko_jump import SSHJumpClient, MagicAuthHandler
from paramiko_jump import SSHJumpClient, MultiFactorAuthHandler

handler = MagicAuthHandler(['password'], ['1']) # Note that the handler responses are lists!
handler = MultiFactorAuthHandler()
handler.add('password') # Just add strings to the handler in the order they are needed
handler.add('1')

with SSHJumpClient(auth_handler=handler) as jumper:
jumper.connect(
hostname='somehost.example.com',
username='username',
username='username',
look_for_keys=False,
)
stdin, stdout, stderr = jumper.exec_command('uptime')
Expand All @@ -125,7 +127,7 @@ with SSHJumpClient(auth_handler=handler) as jumper:
## Simpler Authentication Schemes

In general, unless you want keyboard interactive authentication (See: ```simple_auth_handler```),
or you want to authenticate through 2FA/MFA infrastructure (See: ```MagicAuthHandler```),
or you want to authenticate through 2FA/MFA infrastructure (See: ```MultiFactorAuthHandler```),
you probably want to authenticate to a host in one of the following ways:


Expand Down Expand Up @@ -243,7 +245,7 @@ with SSHJumpClient(auth_handler=simple_auth_handler) as jumper:
```


### SSH Proxying Example 1b: Connect to a single target through a Jump Host
### SSH Proxying Example 1b: Connect to a single Target Host through a Jump Host

This example is functionally equivalent to Example 1a, with two key changes:

Expand Down Expand Up @@ -283,45 +285,58 @@ target.close()
```


### SSH Proxying Example 2: Open one Jump Channel, connect to multiple targets
### SSH Proxying Example 2: Open one jump channel, connect to multiple Target Hosts


#### Keyboard-Interactive Authentication on the Jump Host, Basic Authentication on the Target Hosts
#### MFA on the Jump Host, Basic Authentication on the Target Hosts
```python
from paramiko_jump import SSHJumpClient, simple_auth_handler
import os
from paramiko_jump import SSHJumpClient, MultiFactorAuthHandler

password = os.getenv('JUMP_HOST_PASSWORD')
handler = MultiFactorAuthHandler()
handler.add(password)
handler.add('1')

with SSHJumpClient(auth_handler=simple_auth_handler) as jumper:
with SSHJumpClient(auth_handler=handler) as jumper:
jumper.connect(
hostname='jump-host',
username='jump-user',
)

target1 = SSHJumpClient(jump_session=jumper)
target1.connect(
hostname='target-host1',
username='username',
password='password',
look_for_keys=False,
allow_agent=False,
)
stdin, stdout, stderr = target1.exec_command('sh ver')
output = stdout.readlines()
print(output)
target1.close()

target2 = SSHJumpClient(jump_session=jumper)
target2.connect(
hostname='target-host2',
username='username',
password='password',
look_for_keys=False,
allow_agent=False,
)
_, stdout, _ = target2.exec_command('sh ip int br')
output = stdout.readlines()
print(output)
target2.close()
with SSHJumpClient(jump_session=jumper) as target1:
target1.connect(
hostname='target-host1',
username='username',
password='password',
look_for_keys=False,
)
_, stdout, _ = target1.exec_command('uptime')
output = stdout.readlines()
print(output)

with SSHJumpClient(jump_session=jumper) as target2:
target2.connect(
hostname='target-host2',
username='username',
password='password',
look_for_keys=False,
)
_, stdout, _ = target2.exec_command('uptime')
output = stdout.readlines()
print(output)

with SSHJumpClient(jump_session=jumper) as target3:
target2.connect(
hostname='target-host3',
username='username',
password='password',
look_for_keys=False,
)
_, stdout, _ = target2.exec_command('uptime')
output = stdout.readlines()
print(output)
```


Expand Down
13 changes: 12 additions & 1 deletion paramiko_jump/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,22 @@

"""

import logging

from paramiko_jump.client import SSHJumpClient
from paramiko_jump.handler import MagicAuthHandler, simple_auth_handler
from paramiko_jump.handler import (
MagicAuthHandler,
MultiFactorAuthHandler,
simple_auth_handler,
)


logging.getLogger(__name__).addHandler(logging.NullHandler())


__all__ = (
'MagicAuthHandler',
'MultiFactorAuthHandler',
'SSHJumpClient',
'simple_auth_handler',
)
1 change: 1 addition & 0 deletions paramiko_jump/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'SSHJumpClient',
)


from typing import Callable, Optional

from paramiko.client import SSHClient
Expand Down
138 changes: 124 additions & 14 deletions paramiko_jump/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,34 @@
- simple_auth_handler, for simple keyboard-interactive use-cases
- MagicAuthHandler, for mimicking keyboard-interactive authentication
using pre-defined responses.


"""


from getpass import getpass
from typing import List, Sequence, Tuple


__all__ = (
'MagicAuthHandler',
'MultiFactorAuthHandler',
'simple_auth_handler',
)


_Prompt = Tuple[str, bool]
import logging

from getpass import getpass
from typing import (
List,
Optional,
Sequence,
Tuple,
)


_LOG = logging.getLogger(__name__)


def simple_auth_handler(
title: str,
instructions: str,
prompt_list: Sequence[_Prompt],
prompt_list: Sequence[Tuple[str, bool]],
) -> List[str]:
"""
Authentication callback, for keyboard-interactive
Expand All @@ -50,9 +56,9 @@ def simple_auth_handler(
"""
answers = []
if title:
print(title)
_LOG.info(title)
if instructions:
print(instructions)
_LOG.info(instructions)

for prompt, show_input in prompt_list:
input_ = input if show_input else getpass
Expand All @@ -62,10 +68,10 @@ def simple_auth_handler(

class MagicAuthHandler:
"""
Stateful auth handler for paramiko that will return a list of
auth parameters for every CLI prompt. This is useful for multi-
factor authentication where the auth parameters change with each
prompt (e.g. password followed by OTP).
Stateful auth handler for paramiko that will return an auth parameter
for each CLI prompt. This is useful for multi-factor authentication
where the auth parameters change with each prompt (e.g. password
followed by OTP).

Example
-------
Expand All @@ -77,9 +83,16 @@ class MagicAuthHandler:
['1234']


This class is deprecated by the ``MultiFactorAuthHandler``; use that
class instead for new use cases.
"""

def __init__(self, *items):
"""
:param items:
Auth responses to be returned by the handler. Each should be a
List.
"""
self._iterator = iter(items)

def __call__(self, *args, **kwargs):
Expand All @@ -93,3 +106,100 @@ def __iter__(self):

def __next__(self):
return next(self._iterator)


class MultiFactorAuthHandler:
"""
Stateful auth handler for paramiko that will return an auth parameter
for each CLI prompt. This is useful for multi-factor authentication
where the auth parameters change with each prompt (e.g. password
followed by OTP).

This class is a more flexible and feature-rich replacement for the
deprecated ``MagicAuthHandler``.

Example
-------
>>> from paramiko_jump import MultiFactorAuthHandler
>>> handler = MultiFactorAuthHandler()
>>> handler.add('password')
>>> handler.add('1234')
>>> handler()
['password']
>>> handler()
['1234']

Backwards-compatibility Example
-------------------------------
>>> from paramiko_jump import MultiFactorAuthHandler
>>> handler = MultiFactorAuthHandler(['password'], ['1234'])
>>> handler()
['password']
>>> handler()
['1234']
"""

def __init__(
self,
*auth_responses: List,
show_title: bool = False,
show_instructions: bool = False,
show_prompts: bool = False,

):
"""
:param auth_responses:
Optional auth responses to be returned by the handler.
Each item should be a List. This interface is backwards-
compatible with the ``MagicAuthHandler``

Alternatively, see the .add() method for an easier
approach to adding auth responses after instantiation,
without needing to pack them into Lists.
:param show_title:
If True, the title will be displayed.
:param show_instructions:
If True, the instructions will be displayed.
:param show_prompts:
If True, the authentication prompts will be displayed.
"""
self._auth_responses = [*auth_responses]
self._show_title = show_title
self._show_instructions = show_instructions
self._show_prompts = show_prompts

self._iterator = None

def __call__(
self,
title: Optional[str] = None,
instructions: Optional[str] = None,
prompt_list: Optional[List] = None,
):
if self._show_title:
_LOG.info(title)
if self._show_instructions:
_LOG.info(instructions)
if self._show_prompts and prompt_list:
for prompt, _ in prompt_list:
_LOG.info(prompt)
try:
return next(self)
except StopIteration:
return []

def __iter__(self):
return self

def __next__(self):
if self._iterator is None:
self._iterator = iter(self._auth_responses)

return next(self._iterator)

def add(self, auth_response: str) -> None:
"""
Add an auth response to the handler.
"""
# Pack it into a list as this is how Paramiko expects it.
self._auth_responses.append([auth_response])
Loading