Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python Paramiko (Client) Multifactor Authentication

I'm attempting to use Paramiko (on Python 2.7) to connect to a host that uses multifactor authentication (username + password + one-time-password). The transport.auth_interactive function seems to be the way to do this (based on what I'm understanding from the documentation), but execution never reaches that point - the authentication fails on the client.connect line.

I seem to be missing something.

This is the code:

#!/usr/bin/env python

import paramiko
import getpass
import os
import logging

user = ""
pw = ""
mfa = ""

def inter_handler(title, instructions, prompt_list):
    resp = []

    for pr in prompt_list:
        if pr[0].strip() == "Username:":
            resp.append(user)
        elif pr[0].strip() == "Password:":
            resp.append(pw)
        elif pr[0].strip() == "OTP Code:":
            resp.append(mfa)
        
    return tuple(resp)


#Main Entry Point
if __name__ == "__main__":

    paramiko.util.log_to_file(os.path.expanduser('~/paramiko.log'), logging.DEBUG)

    user = raw_input("Username: ")
    pw = getpass.getpass("Password: ")
    mfa = raw_input("OTP Code:")

    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())   #Don't care about host keys
    client.connect("mfahost.example.com", port=22, username=user, password=pw, look_for_keys=False)
    client.get_transport().auth_interactive(user, inter_handler) 

    client.exec_command("touch ~/paramikotestfile")
like image 774
Erick Brown Avatar asked May 10 '17 22:05

Erick Brown


2 Answers

Answering my own question because I managed to figure it out, and thought I would share.

The short answer is that one has to create a socket, create a Paramiko client Transport on it, invoke auth_interactive, then open a session. The session provides a Paramiko Channel object which can be take exec_command calls just like the SSHClient object.

The code below is the full reference implementation:

 #!/usr/bin/env python

import paramiko     #Provides SSH functionality
import getpass      #Allows for secure prompting and collection of the user password
import os           #Used to setup the Paramiko log file
import logging      #Used to setup the Paramiko log file
import socket       #This method requires that we create our own socket

#Global variables are used to store these data because they're sent to the server by a callback
user = ""
pw = ""
mfa = ""

def inter_handler(title, instructions, prompt_list):
    """
    inter_handler: the callback for paramiko.transport.auth_interactive

    The prototype for this function is defined by Paramiko, so all of the
    arguments need to be there, even though we don't use 'title' or
    'instructions'.

    The function is expected to return a tuple of data containing the
    responses to the provided prompts. Experimental results suggests that
    there will be one call of this function per prompt, but the mechanism
    allows for multiple prompts to be sent at once, so it's best to assume
    that that can happen.

    Since tuples can't really be built on the fly, the responses are 
    collected in a list which is then converted to a tuple when it's time
    to return a value.

    Experiments suggest that the username prompt never happens. This makes
    sense, but the Username prompt is included here just in case.
    """

    resp = []  #Initialize the response container

    #Walk the list of prompts that the server sent that we need to answer
    for pr in prompt_list:
        #str() used to to make sure that we're dealing with a string rather than a unicode string
        #strip() used to get rid of any padding spaces sent by the server

        if str(pr[0]).strip() == "Username:":
            resp.append(user)
        elif str(pr[0]).strip() == "Password:":
            resp.append(pw)
        elif str(pr[0]).strip() == "OTP Code:":
            resp.append(mfa)

    return tuple(resp)  #Convert the response list to a tuple and return it


#Main Entry Point
if __name__ == "__main__":
    #Setup Paramiko logging; this is useful for troubleshooting
    paramiko.util.log_to_file(os.path.expanduser('~/paramiko.log'), logging.DEBUG)

    #Get the username, password, and MFA token code from the user
    user = raw_input("Username: ")
    pw = getpass.getpass("Password: ")
    mfa = raw_input("OTP Code: ")

    #Create a socket and connect it to port 22 on the host
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(("mfahost.example.com", 22))

    #Make a Paramiko Transport object using the socket
    ts = paramiko.Transport(sock)

    #Tell Paramiko that the Transport is going to be used as a client
    ts.start_client(timeout=10)

    #Begin authentication; note that the username and callback are passed
    ts.auth_interactive(user, inter_handler)

    #Opening a session creates a channel along the socket to the server
    chan = ts.open_session(timeout=10)

    #Now the channel can be used to execute commands
    chan.exec_command("touch ~/paramikotestfile")

like image 79
Erick Brown Avatar answered Nov 04 '22 21:11

Erick Brown


A more convenient way is the auth_interactive_dumb method. it is an auth_interactive that just outputs what the server asks to the console and sends back what you type. This way you dont have to store the password and dont have to write your own handler.

so basically

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("mfahost.example.com", 22))
ts = paramiko.Transport(sock)
ts.start_client(timeout=10)
ts.auth_interactive_dumb(user)
like image 36
MMeissner Avatar answered Nov 04 '22 22:11

MMeissner