Skip to content

Commit cf54041

Browse files
pkittenisMattCatz
andauthored
Python callback function support for keyboard interactive authentication (#225)
* Allow provided Python callback function to be used for keyboard interactive authentication. Keyboard-interactive events can have multiple steps. Tweak the existing `kbd_callback` to massage prompts into a format that an end user can handle from python. * Added `ssh2.session.Session.userauth_keyboardinteractive_callback` to maintain backwards compatibility. See new example script for usage. * Updated keyboard interactive auth with python callback function. Added tests. * Updated changelog. * Updated callback example script --------- Co-authored-by: Matthew Cather <mattbob4@gmail.com>
1 parent d233075 commit cf54041

File tree

5 files changed

+2853
-2077
lines changed

5 files changed

+2853
-2077
lines changed

Changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Changes
99

1010
* Added constants for session related flags under `ssh2.session`.
1111
* Added `ssh2.session.Session.flag` function for enabling/disabling session flags like compression support.
12+
* Added `ssh2.session.userauth_keyboardinteractive_callback` for authentication using Python callback function,
13+
for example for Oauth and other two-factor (2FA) or more factor authentication. Thanks @MattCatz .
1214

1315

1416
1.1.2

ci/integration_tests/test_session.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from unittest.mock import MagicMock
12
import os
23
import socket
34
from unittest import skipUnless
@@ -335,3 +336,7 @@ def test_non_blocking_multi_chan(self):
335336
self._wait_eagain(chan.wait_closed)
336337
chan = self._wait_eagain(self.session.open_session)
337338
self.assertIsInstance(chan, Channel)
339+
340+
def test_userauth_kb_with_callback(self):
341+
my_cb = MagicMock()
342+
self.assertRaises(AuthenticationError, self.session.userauth_keyboardinteractive_callback, self.user, my_cb)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/python
2+
3+
"""Example script for authentication with Python callback function using an OAuth token"""
4+
5+
from __future__ import print_function
6+
7+
import argparse
8+
import socket
9+
import os
10+
import pwd
11+
import functools
12+
13+
14+
from ssh2.session import Session
15+
16+
17+
USERNAME = pwd.getpwuid(os.geteuid()).pw_name
18+
19+
parser = argparse.ArgumentParser()
20+
21+
parser.add_argument('password', help="User password")
22+
parser.add_argument('oauth', help="OAUTH key to use for authentication")
23+
parser.add_argument('cmd', help="Command to run")
24+
parser.add_argument('--host', dest='host',
25+
default='localhost',
26+
help='Host to connect to')
27+
parser.add_argument('--port', dest='port', default=22, help="Port to connect on", type=int)
28+
parser.add_argument('-u', dest='user', default=USERNAME, help="User name to authenticate as")
29+
30+
31+
def oauth_handler(name, instruction, prompts, password, oauth):
32+
responses = []
33+
34+
for prompt in prompts:
35+
if "Password:" in prompt:
36+
responses.append(password)
37+
if "One-time password (OATH) for" in prompt:
38+
responses.append(oauth)
39+
40+
return responses
41+
42+
def main():
43+
args = parser.parse_args()
44+
45+
callback = functools.partial(oauth_handler,password=args.password,oauth=args.oauth)
46+
47+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
48+
sock.connect((args.host, args.port))
49+
s = Session()
50+
s.handshake(sock)
51+
s.userauth_keyboardinteractive_callback(args.user, callback)
52+
chan = s.open_session()
53+
chan.execute(args.cmd)
54+
size, data = chan.read()
55+
while size > 0:
56+
print(data)
57+
size, data = chan.read()
58+
59+
60+
if __name__ == "__main__":
61+
main()

0 commit comments

Comments
 (0)