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.
|
THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
from __future__ import print_function, unicode_literals
|
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
|
from lxml import etree
|
||||||
import requests
|
import requests
|
||||||
import tempfile
|
|
||||||
|
|
||||||
if sys.version_info >= (3,):
|
if sys.version_info >= (3,):
|
||||||
from urllib.parse import urlparse, urljoin
|
from urllib.parse import urlparse, urljoin
|
||||||
@@ -42,6 +42,25 @@ else:
|
|||||||
binary_type = str
|
binary_type = str
|
||||||
input = raw_input
|
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_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')
|
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):
|
def mfa_priority(conf, ftype, fprovider):
|
||||||
if ftype == 'token:software:totp':
|
if ftype == 'token:software:totp':
|
||||||
ftype = 'totp'
|
ftype = 'totp'
|
||||||
if ftype not in ['totp', 'sms']:
|
if ftype not in ['totp', 'sms', 'webauthn']:
|
||||||
return 0
|
return 0
|
||||||
mfa_order = conf.get('mfa_order', '')
|
mfa_order = conf.get('mfa_order', '')
|
||||||
if ftype in mfa_order:
|
if ftype in mfa_order:
|
||||||
@@ -163,7 +182,7 @@ def mfa_priority(conf, ftype, fprovider):
|
|||||||
else:
|
else:
|
||||||
priority = 0
|
priority = 0
|
||||||
value = conf.get('{0}.{1}'.format(ftype, fprovider))
|
value = conf.get('{0}.{1}'.format(ftype, fprovider))
|
||||||
if ftype == 'sms':
|
if ftype in ('sms', 'webauthn'):
|
||||||
if not (value or '').lower() in ['1', 'true']:
|
if not (value or '').lower() in ['1', 'true']:
|
||||||
value = None
|
value = None
|
||||||
line_nr = conf.get('{0}.{1}.line'.format(ftype, fprovider), 0)
|
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)
|
r = okta_mfa_totp(conf, s, f, state_token)
|
||||||
elif ftype == 'sms':
|
elif ftype == 'sms':
|
||||||
r = okta_mfa_sms(conf, s, f, state_token)
|
r = okta_mfa_sms(conf, s, f, state_token)
|
||||||
|
elif ftype == 'webauthn':
|
||||||
|
r = okta_mfa_webauthn(conf, s, f, state_token)
|
||||||
else:
|
else:
|
||||||
r = None
|
r = None
|
||||||
if r is not None:
|
if r is not None:
|
||||||
@@ -380,9 +401,7 @@ def okta_mfa_totp(conf, s, factor, state_token):
|
|||||||
if len(secret) == 0:
|
if len(secret) == 0:
|
||||||
code = input('{0} TOTP: '.format(provider)).strip()
|
code = input('{0} TOTP: '.format(provider)).strip()
|
||||||
else:
|
else:
|
||||||
try:
|
if not have_pyotp:
|
||||||
import pyotp
|
|
||||||
except ImportError:
|
|
||||||
err('Need pyotp package, consider doing \'pip install pyotp\' (or similar)')
|
err('Need pyotp package, consider doing \'pip install pyotp\' (or similar)')
|
||||||
totp = pyotp.TOTP(secret)
|
totp = pyotp.TOTP(secret)
|
||||||
code = totp.now()
|
code = totp.now()
|
||||||
@@ -403,7 +422,7 @@ def okta_mfa_sms(conf, s, factor, state_token):
|
|||||||
provider = factor.get('provider', '')
|
provider = factor.get('provider', '')
|
||||||
data = {
|
data = {
|
||||||
'factorId': factor.get('id'),
|
'factorId': factor.get('id'),
|
||||||
'stateToken': state_token,
|
'stateToken': state_token
|
||||||
}
|
}
|
||||||
log('mfa {0} sms request [okta_url]'.format(provider))
|
log('mfa {0} sms request [okta_url]'.format(provider))
|
||||||
h, j = send_req(conf, s, 'sms mfa (1)', factor.get('url'), data, json=True,
|
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'))
|
expected_url=conf.get('okta_url'), verify=conf.get('okta_url_cert'))
|
||||||
return j
|
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):
|
def okta_redirect(conf, s, session_token, redirect_url):
|
||||||
rc = 0
|
rc = 0
|
||||||
form_url, form_data = None, {}
|
form_url, form_data = None, {}
|
||||||
@@ -589,7 +654,8 @@ def main():
|
|||||||
cp.communicate()
|
cp.communicate()
|
||||||
else:
|
else:
|
||||||
print('{0} | {1}'.format(pcmd, cmd))
|
print('{0} | {1}'.format(pcmd, cmd))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user