Loading .gitignore +1 −0 Original line number Diff line number Diff line .idea build cmake-build-debug ssh_client/test_asure.py No newline at end of file server_side/dockerfiles/Dockerfile.python +18 −8 Original line number Diff line number Diff line FROM ubuntu:22.04 AS no2fa FROM ubuntu:20.04 AS no2fa RUN apt-get update && apt-get install -y ssh libpam-python curl python2 sudo ARG DEBIAN_FRONTEND=noninteractive ARG TEST_USER=test ENV TZ=America/New_York RUN apt-get update && apt-get install -y ssh libpam-python curl python2 sudo vim python2-dev build-essential RUN curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py && python2 get-pip.py RUN pip2 config set global.target /lib/python2.7 && pip2 install requests RUN apt-get install -y libffi-dev libssl-dev RUN pip2 config set global.target /lib/python2.7 \ && pip2 install requests \ && pip2 install pyjwt==1.7.1 \ && pip2 install cryptography==2.3 env TEST_USER=$TEST_USER RUN useradd test RUN mkhomedir_helper test RUN echo test:123 | chpasswd RUN useradd $TEST_USER RUN mkhomedir_helper $TEST_USER RUN echo $TEST_USER:123 | chpasswd RUN mkdir /run/sshd COPY python/oidc-pam.py /etc/security/oidc/oidc-pam.py COPY python/oidc_pam.py /etc/security/oidc/oidc_pam.py COPY python/sshd /etc/pam.d/ COPY sshd_pam.conf /etc/ssh/sshd_config.d/ COPY start_no2fa.sh /tmp/oidc/ Loading server_side/oidc-pam.json +4 −5 Original line number Diff line number Diff line { "client_id": "galaxy", "client_secret":"coR3eIu4hEaxNwveSbXjsiHdHijYtRuf", "introspection_url": "http://host.docker.internal:8080/realms/ndip/protocol/openid-connect/token/introspect", "provider": "azure", "verification_type": "jwks_url", "jwks_url": "https://login.microsoftonline.com/db3dbd43-4c4b-4544-9f8a-0553f9f5f25e/discovery/v2.0/keys", "introspection_url": "", "check_2fa": false, "enable_log": true, "log_file": "/tmp/oidc.log" } No newline at end of file server_side/python/oidc-pam.py→server_side/python/oidc_pam.py +54 −31 Original line number Diff line number Diff line Loading @@ -5,12 +5,16 @@ ''' PAM module for authenticating users via a OIDC token ''' import base64 import json import jwt import os import sys import requests import logging from cryptography.hazmat.backends import default_backend from cryptography.x509 import load_der_x509_certificate logging.basicConfig(filename='/tmp/oidc.log', encoding='utf-8', level=logging.DEBUG) Loading Loading @@ -57,17 +61,8 @@ def pam_sm_authenticate(pamh, _flags, _argv): ''' Authenticates a user via an OIDC token ''' # Load config file and build access token try: config_dpath = os.path.dirname(os.path.realpath(__file__)) config_fpath = os.path.join(config_dpath, 'oidc-pam.json') config_fd = open(config_fpath, 'r') config = config_fd.read() config_fd.close() config = json.loads(config) except Exception as error: logit('Error loading configuration: %s' % error) return pamh.PAM_AUTH_ERR # build access token use_first_pass = 'use_first_pass' in _argv # get user&token Loading Loading @@ -96,30 +91,58 @@ def pam_sm_authenticate(pamh, _flags, _argv): except pamh.exception as error: return error.pam_result return verify_token_jwt(pamh, user, access_token) def verify_token_jwt(pamh, user, access_token): config = load_config() try: url = config['introspection_url'] logit(access_token) data = {'token': access_token.strip(), 'client_id': config['client_id'], 'client_secret': config['client_secret']} response = requests.post(url, data=data, timeout=5) if response.status_code != requests.status_codes.codes.ok: logit('Error checking introspecting token, server returned %d %s' % response.status_code, response.text) return pamh.PAM_AUTH_ERR token_info = response.json() if 'active' not in token_info or token_info['active'] != True: logit('Error checking introspecting token, token %s invalid, server response: %s' % ( access_token, response.text)) return pamh.PAM_AUTH_ERR if 'preferred_username' not in token_info or token_info['preferred_username'] != user: logit('wrong user name in token: %s, expected %s' % (access_token, user)) # Obtain appropriate cert from JWK URI jwks_url = config['jwks_url'] key_set = requests.get(jwks_url, timeout=5) encoded_header, rest = access_token.split('.', 1) headerobj = json.loads(base64.b64decode(encoded_header+ '==').decode('utf8')) key_id = headerobj['kid'] for key in key_set.json()['keys']: if key['kid'] == key_id: x5c = key['x5c'][0] break else: raise jwt.DecodeError('Cannot find kid ' + key_id) cert = load_der_x509_certificate(base64.b64decode(x5c), default_backend()) # Decode token (exp date is checked automatically) decoded_token = jwt.decode( access_token, key=cert.public_key(), algorithms=['RS256'], options={'exp': True, 'verify_aud': False} ) # Check if correct user if decoded_token['preferred_username'].split('@',1)[0] != user: logit('SSH user does not match token user: %s (ssh) !=v %s (token)' % (user, decoded_token['preferred_username'])) return pamh.PAM_AUTH_ERR # Check if two factor authenticated if config['check_2fa']: if 'session_attribute' not in token_info or token_info['session_attribute'] != '2fa': if 'mfa' not in decoded_token['amr']: logit('missing 2fa in token: %s ' % access_token) return pamh.PAM_AUTH_ERR except Exception as error: logit('Error introspecting token %s, error: %s' % (access_token, error)) logit('Error verifying jwt token %s, error: %s' % (access_token, error)) return pamh.PAM_AUTH_ERR logit('Login successful for user %s, token %s' % (user, access_token)) return pamh.PAM_SUCCESS def load_config(): config_dpath = os.path.dirname(os.path.realpath(__file__)) config_fpath = os.path.join(config_dpath, 'oidc-pam.json') config_fd = open(config_fpath, 'r') config = config_fd.read() config_fd.close() config = json.loads(config) return config server_side/python/sshd +2 −2 Original line number Diff line number Diff line # PAM configuration for the Secure Shell service auth [success=1 default=ignore] pam_unix.so auth sufficient pam_python.so /etc/security/oidc/oidc-pam.py use_first_pass auth sufficient pam_python.so /etc/security/oidc/oidc_pam.py use_first_pass # Standard Un*x authentication. @include common-auth Loading @@ -15,7 +15,7 @@ account required pam_nologin.so # account required pam_access.so # Standard Un*x authorization. #account sufficient pam_python.so /opt/oidc/oidc-pam.py #account sufficient pam_python.so /opt/oidc/oidc_pam.py @include common-account # SELinux needs to be the first session rule. This ensures that any Loading Loading
.gitignore +1 −0 Original line number Diff line number Diff line .idea build cmake-build-debug ssh_client/test_asure.py No newline at end of file
server_side/dockerfiles/Dockerfile.python +18 −8 Original line number Diff line number Diff line FROM ubuntu:22.04 AS no2fa FROM ubuntu:20.04 AS no2fa RUN apt-get update && apt-get install -y ssh libpam-python curl python2 sudo ARG DEBIAN_FRONTEND=noninteractive ARG TEST_USER=test ENV TZ=America/New_York RUN apt-get update && apt-get install -y ssh libpam-python curl python2 sudo vim python2-dev build-essential RUN curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py && python2 get-pip.py RUN pip2 config set global.target /lib/python2.7 && pip2 install requests RUN apt-get install -y libffi-dev libssl-dev RUN pip2 config set global.target /lib/python2.7 \ && pip2 install requests \ && pip2 install pyjwt==1.7.1 \ && pip2 install cryptography==2.3 env TEST_USER=$TEST_USER RUN useradd test RUN mkhomedir_helper test RUN echo test:123 | chpasswd RUN useradd $TEST_USER RUN mkhomedir_helper $TEST_USER RUN echo $TEST_USER:123 | chpasswd RUN mkdir /run/sshd COPY python/oidc-pam.py /etc/security/oidc/oidc-pam.py COPY python/oidc_pam.py /etc/security/oidc/oidc_pam.py COPY python/sshd /etc/pam.d/ COPY sshd_pam.conf /etc/ssh/sshd_config.d/ COPY start_no2fa.sh /tmp/oidc/ Loading
server_side/oidc-pam.json +4 −5 Original line number Diff line number Diff line { "client_id": "galaxy", "client_secret":"coR3eIu4hEaxNwveSbXjsiHdHijYtRuf", "introspection_url": "http://host.docker.internal:8080/realms/ndip/protocol/openid-connect/token/introspect", "provider": "azure", "verification_type": "jwks_url", "jwks_url": "https://login.microsoftonline.com/db3dbd43-4c4b-4544-9f8a-0553f9f5f25e/discovery/v2.0/keys", "introspection_url": "", "check_2fa": false, "enable_log": true, "log_file": "/tmp/oidc.log" } No newline at end of file
server_side/python/oidc-pam.py→server_side/python/oidc_pam.py +54 −31 Original line number Diff line number Diff line Loading @@ -5,12 +5,16 @@ ''' PAM module for authenticating users via a OIDC token ''' import base64 import json import jwt import os import sys import requests import logging from cryptography.hazmat.backends import default_backend from cryptography.x509 import load_der_x509_certificate logging.basicConfig(filename='/tmp/oidc.log', encoding='utf-8', level=logging.DEBUG) Loading Loading @@ -57,17 +61,8 @@ def pam_sm_authenticate(pamh, _flags, _argv): ''' Authenticates a user via an OIDC token ''' # Load config file and build access token try: config_dpath = os.path.dirname(os.path.realpath(__file__)) config_fpath = os.path.join(config_dpath, 'oidc-pam.json') config_fd = open(config_fpath, 'r') config = config_fd.read() config_fd.close() config = json.loads(config) except Exception as error: logit('Error loading configuration: %s' % error) return pamh.PAM_AUTH_ERR # build access token use_first_pass = 'use_first_pass' in _argv # get user&token Loading Loading @@ -96,30 +91,58 @@ def pam_sm_authenticate(pamh, _flags, _argv): except pamh.exception as error: return error.pam_result return verify_token_jwt(pamh, user, access_token) def verify_token_jwt(pamh, user, access_token): config = load_config() try: url = config['introspection_url'] logit(access_token) data = {'token': access_token.strip(), 'client_id': config['client_id'], 'client_secret': config['client_secret']} response = requests.post(url, data=data, timeout=5) if response.status_code != requests.status_codes.codes.ok: logit('Error checking introspecting token, server returned %d %s' % response.status_code, response.text) return pamh.PAM_AUTH_ERR token_info = response.json() if 'active' not in token_info or token_info['active'] != True: logit('Error checking introspecting token, token %s invalid, server response: %s' % ( access_token, response.text)) return pamh.PAM_AUTH_ERR if 'preferred_username' not in token_info or token_info['preferred_username'] != user: logit('wrong user name in token: %s, expected %s' % (access_token, user)) # Obtain appropriate cert from JWK URI jwks_url = config['jwks_url'] key_set = requests.get(jwks_url, timeout=5) encoded_header, rest = access_token.split('.', 1) headerobj = json.loads(base64.b64decode(encoded_header+ '==').decode('utf8')) key_id = headerobj['kid'] for key in key_set.json()['keys']: if key['kid'] == key_id: x5c = key['x5c'][0] break else: raise jwt.DecodeError('Cannot find kid ' + key_id) cert = load_der_x509_certificate(base64.b64decode(x5c), default_backend()) # Decode token (exp date is checked automatically) decoded_token = jwt.decode( access_token, key=cert.public_key(), algorithms=['RS256'], options={'exp': True, 'verify_aud': False} ) # Check if correct user if decoded_token['preferred_username'].split('@',1)[0] != user: logit('SSH user does not match token user: %s (ssh) !=v %s (token)' % (user, decoded_token['preferred_username'])) return pamh.PAM_AUTH_ERR # Check if two factor authenticated if config['check_2fa']: if 'session_attribute' not in token_info or token_info['session_attribute'] != '2fa': if 'mfa' not in decoded_token['amr']: logit('missing 2fa in token: %s ' % access_token) return pamh.PAM_AUTH_ERR except Exception as error: logit('Error introspecting token %s, error: %s' % (access_token, error)) logit('Error verifying jwt token %s, error: %s' % (access_token, error)) return pamh.PAM_AUTH_ERR logit('Login successful for user %s, token %s' % (user, access_token)) return pamh.PAM_SUCCESS def load_config(): config_dpath = os.path.dirname(os.path.realpath(__file__)) config_fpath = os.path.join(config_dpath, 'oidc-pam.json') config_fd = open(config_fpath, 'r') config = config_fd.read() config_fd.close() config = json.loads(config) return config
server_side/python/sshd +2 −2 Original line number Diff line number Diff line # PAM configuration for the Secure Shell service auth [success=1 default=ignore] pam_unix.so auth sufficient pam_python.so /etc/security/oidc/oidc-pam.py use_first_pass auth sufficient pam_python.so /etc/security/oidc/oidc_pam.py use_first_pass # Standard Un*x authentication. @include common-auth Loading @@ -15,7 +15,7 @@ account required pam_nologin.so # account required pam_access.so # Standard Un*x authorization. #account sufficient pam_python.so /opt/oidc/oidc-pam.py #account sufficient pam_python.so /opt/oidc/oidc_pam.py @include common-account # SELinux needs to be the first session rule. This ensures that any Loading