mirror of
https://github.com/os-mnemo/pan-globalprotect-okta
synced 2026-07-31 07:44:38 +02:00
Feature: Allow Yubikey webauthn authentication, if configured as 2FA in Okta.
Just insert your YubiKey and press the blinking button, when asked to do so. Needs `fido2` packages to work (`pip install fido2`).
This commit is contained in:
+75
-9
@@ -27,10 +27,10 @@
|
||||
THE SOFTWARE.
|
||||
"""
|
||||
from __future__ import print_function, unicode_literals
|
||||
import io, os, sys, re, json, base64, getpass, subprocess, shlex, signal
|
||||
import io, os, sys, re, json, base64, getpass, subprocess, shlex, signal, tempfile, traceback
|
||||
|
||||
from lxml import etree
|
||||
import requests
|
||||
import tempfile
|
||||
|
||||
if sys.version_info >= (3,):
|
||||
from urllib.parse import urlparse, urljoin
|
||||
@@ -42,6 +42,25 @@ else:
|
||||
binary_type = str
|
||||
input = raw_input
|
||||
|
||||
# Optional: fido2 support (webauthn via Yubikey)
|
||||
have_fido = False
|
||||
try:
|
||||
from fido2.utils import websafe_decode, websafe_encode
|
||||
from fido2.hid import CtapHidDevice
|
||||
from fido2.client import Fido2Client
|
||||
have_fido = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Optional: pyotp support
|
||||
have_pyotp = False
|
||||
try:
|
||||
import pyotp
|
||||
have_pyotp = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
to_b = lambda v: v if isinstance(v, binary_type) else v.encode('utf-8')
|
||||
to_u = lambda v: v if isinstance(v, text_type) else v.decode('utf-8')
|
||||
|
||||
@@ -155,7 +174,7 @@ def load_conf(cf):
|
||||
def mfa_priority(conf, ftype, fprovider):
|
||||
if ftype == 'token:software:totp':
|
||||
ftype = 'totp'
|
||||
if ftype not in ['totp', 'sms']:
|
||||
if ftype not in ['totp', 'sms', 'webauthn']:
|
||||
return 0
|
||||
mfa_order = conf.get('mfa_order', '')
|
||||
if ftype in mfa_order:
|
||||
@@ -163,7 +182,7 @@ def mfa_priority(conf, ftype, fprovider):
|
||||
else:
|
||||
priority = 0
|
||||
value = conf.get('{0}.{1}'.format(ftype, fprovider))
|
||||
if ftype == 'sms':
|
||||
if ftype in ('sms', 'webauthn'):
|
||||
if not (value or '').lower() in ['1', 'true']:
|
||||
value = None
|
||||
line_nr = conf.get('{0}.{1}.line'.format(ftype, fprovider), 0)
|
||||
@@ -367,6 +386,8 @@ def okta_mfa(conf, s, j):
|
||||
r = okta_mfa_totp(conf, s, f, state_token)
|
||||
elif ftype == 'sms':
|
||||
r = okta_mfa_sms(conf, s, f, state_token)
|
||||
elif ftype == 'webauthn':
|
||||
r = okta_mfa_webauthn(conf, s, f, state_token)
|
||||
else:
|
||||
r = None
|
||||
if r is not None:
|
||||
@@ -380,9 +401,7 @@ def okta_mfa_totp(conf, s, factor, state_token):
|
||||
if len(secret) == 0:
|
||||
code = input('{0} TOTP: '.format(provider)).strip()
|
||||
else:
|
||||
try:
|
||||
import pyotp
|
||||
except ImportError:
|
||||
if not have_pyotp:
|
||||
err('Need pyotp package, consider doing \'pip install pyotp\' (or similar)')
|
||||
totp = pyotp.TOTP(secret)
|
||||
code = totp.now()
|
||||
@@ -403,7 +422,7 @@ def okta_mfa_sms(conf, s, factor, state_token):
|
||||
provider = factor.get('provider', '')
|
||||
data = {
|
||||
'factorId': factor.get('id'),
|
||||
'stateToken': state_token,
|
||||
'stateToken': state_token
|
||||
}
|
||||
log('mfa {0} sms request [okta_url]'.format(provider))
|
||||
h, j = send_req(conf, s, 'sms mfa (1)', factor.get('url'), data, json=True,
|
||||
@@ -417,6 +436,52 @@ def okta_mfa_sms(conf, s, factor, state_token):
|
||||
expected_url=conf.get('okta_url'), verify=conf.get('okta_url_cert'))
|
||||
return j
|
||||
|
||||
def okta_mfa_webauthn(conf, s, factor, state_token):
|
||||
if not have_fido:
|
||||
err('Need fido2 package(s) for webauthn. Consider doing `pip install fido2` (or similar)')
|
||||
devices = list(CtapHidDevice.list_devices())
|
||||
if not devices:
|
||||
err('webauthn configured, but no U2F devices found')
|
||||
return None
|
||||
provider = factor.get('provider', '')
|
||||
log('mfa {0} challenge request [okta_url]'.format(provider))
|
||||
data = {
|
||||
'stateToken': state_token
|
||||
}
|
||||
h, j = send_req(conf, s, 'webauthn mfa challenge', factor.get('url'), data, json=True,
|
||||
expected_url=conf.get('okta_url'), verify=conf.get('okta_url_cert'))
|
||||
factor = j['_embedded']['factor']
|
||||
profile = factor['profile']
|
||||
purl = list(urlparse(conf.get('okta_url')))
|
||||
rpid = purl[1].split(':')[0]
|
||||
origin = '{0}://{1}'.format(purl[0], rpid)
|
||||
challenge = factor['_embedded']['challenge']['challenge']
|
||||
credentialId = websafe_decode(profile['credentialId'])
|
||||
allow_list = [{'type': 'public-key', 'id': credentialId}]
|
||||
for dev in devices:
|
||||
client = Fido2Client(dev, origin)
|
||||
print('!!! Touch the flashing U2F device to authenticate... !!!')
|
||||
try:
|
||||
result = client.get_assertion(rpid, challenge, allow_list)
|
||||
dbg(conf.get('debug'), 'assertion.result', result)
|
||||
break
|
||||
except:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
result = None
|
||||
if not result:
|
||||
return None
|
||||
assertion, client_data = result[0][0], result[1] # only one cred in allowList, so only one response.
|
||||
data = {
|
||||
'stateToken': state_token,
|
||||
'clientData': to_u(base64.b64encode(client_data)),
|
||||
'signatureData': to_u(base64.b64encode(assertion.signature)),
|
||||
'authenticatorData': to_u(base64.b64encode(assertion.auth_data))
|
||||
}
|
||||
log('mfa {0} signature request [okta_url]'.format(provider))
|
||||
h, j = send_req(conf, s, 'uf2 mfa signature', j['_links']['next']['href'], data, json=True,
|
||||
expected_url=conf.get('okta_url'), verify=conf.get('okta_url_cert'))
|
||||
return j
|
||||
|
||||
def okta_redirect(conf, s, session_token, redirect_url):
|
||||
rc = 0
|
||||
form_url, form_data = None, {}
|
||||
@@ -589,7 +654,8 @@ def main():
|
||||
cp.communicate()
|
||||
else:
|
||||
print('{0} | {1}'.format(pcmd, cmd))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user