Loading server_side/dockerfiles/Dockerfile.python +2 −2 Original line number Diff line number Diff line Loading @@ -7,7 +7,7 @@ RUN curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py && pyt RUN apt-get install -y libffi-dev RUN pip2 config set global.target /lib/python2.7 \ && pip2 install requests \ && pip2 install jwt==0.3.2 \ && pip2 install pyjwt==1.7.1 \ && pip2 install cryptography==2.3 RUN useradd test Loading @@ -15,7 +15,7 @@ RUN mkhomedir_helper test RUN echo test: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/python/oidc-pam.py→server_side/python/oidc_pam.py +13 −61 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ ''' PAM module for authenticating users via a OIDC token ''' import base64 import json import jwt import os Loading @@ -15,7 +16,7 @@ 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) logging.basicConfig(filename='oidc.log', encoding='utf-8', level=logging.DEBUG) def logit(data): Loading @@ -26,7 +27,6 @@ def pam_sm_setcred(pamh, _flags, _argv): ''' Default ''' logit("setcred") return pamh.PAM_SUCCESS Loading @@ -34,7 +34,6 @@ def pam_sm_acct_mgmt(pamh, _flags, _argv): ''' Default ''' logit("acct mgmt") return pamh.PAM_SUCCESS Loading @@ -42,7 +41,6 @@ def pam_sm_open_session(pamh, _flags, _argv): ''' Default ''' logit("open session") return pamh.PAM_SUCCESS Loading @@ -50,7 +48,6 @@ def pam_sm_close_session(pamh, _flags, _argv): ''' Default ''' logit("close session") return pamh.PAM_SUCCESS Loading @@ -58,7 +55,6 @@ def pam_sm_chauthtok(pamh, _flags, _argv): ''' Default ''' logit("chauthtok") return pamh.PAM_SUCCESS Loading @@ -66,7 +62,6 @@ def pam_sm_authenticate(pamh, _flags, _argv): ''' Authenticates a user via an OIDC token ''' logit("trying") # build access token Loading Loading @@ -97,21 +92,19 @@ def pam_sm_authenticate(pamh, _flags, _argv): except pamh.exception as error: return error.pam_result if (os.environ['PAM_OIDC_VERFIFICATION_TYPE'] == "jwks_url"): return verify_token_jwt(pamh, user, access_token) else: return verify_token_introspection(pamh, user, access_token) def verify_token_jwt(pamh, user, access_token): def verify_token_jwt(pamh, user, access_token, jwt_options): config = load_config_jwt(pamh) try: # Obtain appropriate cert from JWK URI jwks_url = config['jwks_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.b64_decode(encoded_header).decode('utf8')) 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: Loading @@ -121,15 +114,13 @@ def verify_token_jwt(pamh, user, access_token): raise jwt.DecodeError(f'Cannot find kid={kid}') cert = load_der_x509_certificate(base64.b64decode(x5c), default_backend()) # Decode token (exp date is checked automatically) decoded_token = jwt.decode( access_token, key=certificate.public_key(), key=cert.public_key(), algorithms=['RS256'], audience=self.setting('KEY') options=jwt_options ) # Check if correct user if decoded_token['preferred_username'] != user: logit('SSH user does not match token user: %s (ssh) !=v %s (token)' % (user, Loading @@ -148,37 +139,6 @@ def verify_token_jwt(pamh, user, access_token): logit('Login successful for user %s, token %s' % (user, access_token)) return pamh.PAM_SUCCESS def verify_token_introspection(pamh, user, access_token): logit('Attempting token verification through instrosepction URL.') config = load_config_introspection(pamh) 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)) return pamh.PAM_AUTH_ERR if config['check_2fa']: if 'session_attribute' not in token_info or token_info['session_attribute'] != '2fa': 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)) return pamh.PAM_AUTH_ERR logit('Login successful for user %s, token %s' % (user, access_token)) return pamh.PAM_SUCCESS def load_config(pamh): # Load config file config_dpath = os.path.dirname(os.path.realpath(__file__)) Loading @@ -194,13 +154,5 @@ def load_config_jwt(pamh): config = load_config(pamh) return next((config_item for config_item in config if config_item['verification_type'] == "jwks_url")) except Exception as error: logit('Error loading configuration for jwt verification: %s' % error)\ return pamh.PAM_AUTH_ERR def load_config_introspection(pamh): try: config = load_config(pamh) return next((config_item for config_item in config if config_item['verification_type'] == "introspection_url")) except Exception as error: logit('Error loading configuration for introspection verification: %s' % error)\ logit('Error loading configuration for jwt verification: %s' % error) return pamh.PAM_AUTH_ERR 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 server_side/python/sshd_2fa +3 −3 Original line number Diff line number Diff line Loading @@ -2,9 +2,9 @@ #auth [success=ignore default=1] pam_unix.so #auth [success=1 default=die] pam_google_authenticator.so #auth sufficient pam_python.so /etc/security/oidc/oidc-pam.py #auth sufficient pam_python.so /etc/security/oidc/oidc_pam.py auth [success=done default=ignore] pam_python.so /etc/security/oidc/oidc-pam.py auth [success=done default=ignore] pam_python.so /etc/security/oidc/oidc_pam.py auth [success=done default=die] pam_google_authenticator.so use_first_pass secret=${HOME}/auth/.google_authenticator # Standard Un*x authentication. Loading @@ -19,7 +19,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
server_side/dockerfiles/Dockerfile.python +2 −2 Original line number Diff line number Diff line Loading @@ -7,7 +7,7 @@ RUN curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py && pyt RUN apt-get install -y libffi-dev RUN pip2 config set global.target /lib/python2.7 \ && pip2 install requests \ && pip2 install jwt==0.3.2 \ && pip2 install pyjwt==1.7.1 \ && pip2 install cryptography==2.3 RUN useradd test Loading @@ -15,7 +15,7 @@ RUN mkhomedir_helper test RUN echo test: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/python/oidc-pam.py→server_side/python/oidc_pam.py +13 −61 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ ''' PAM module for authenticating users via a OIDC token ''' import base64 import json import jwt import os Loading @@ -15,7 +16,7 @@ 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) logging.basicConfig(filename='oidc.log', encoding='utf-8', level=logging.DEBUG) def logit(data): Loading @@ -26,7 +27,6 @@ def pam_sm_setcred(pamh, _flags, _argv): ''' Default ''' logit("setcred") return pamh.PAM_SUCCESS Loading @@ -34,7 +34,6 @@ def pam_sm_acct_mgmt(pamh, _flags, _argv): ''' Default ''' logit("acct mgmt") return pamh.PAM_SUCCESS Loading @@ -42,7 +41,6 @@ def pam_sm_open_session(pamh, _flags, _argv): ''' Default ''' logit("open session") return pamh.PAM_SUCCESS Loading @@ -50,7 +48,6 @@ def pam_sm_close_session(pamh, _flags, _argv): ''' Default ''' logit("close session") return pamh.PAM_SUCCESS Loading @@ -58,7 +55,6 @@ def pam_sm_chauthtok(pamh, _flags, _argv): ''' Default ''' logit("chauthtok") return pamh.PAM_SUCCESS Loading @@ -66,7 +62,6 @@ def pam_sm_authenticate(pamh, _flags, _argv): ''' Authenticates a user via an OIDC token ''' logit("trying") # build access token Loading Loading @@ -97,21 +92,19 @@ def pam_sm_authenticate(pamh, _flags, _argv): except pamh.exception as error: return error.pam_result if (os.environ['PAM_OIDC_VERFIFICATION_TYPE'] == "jwks_url"): return verify_token_jwt(pamh, user, access_token) else: return verify_token_introspection(pamh, user, access_token) def verify_token_jwt(pamh, user, access_token): def verify_token_jwt(pamh, user, access_token, jwt_options): config = load_config_jwt(pamh) try: # Obtain appropriate cert from JWK URI jwks_url = config['jwks_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.b64_decode(encoded_header).decode('utf8')) 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: Loading @@ -121,15 +114,13 @@ def verify_token_jwt(pamh, user, access_token): raise jwt.DecodeError(f'Cannot find kid={kid}') cert = load_der_x509_certificate(base64.b64decode(x5c), default_backend()) # Decode token (exp date is checked automatically) decoded_token = jwt.decode( access_token, key=certificate.public_key(), key=cert.public_key(), algorithms=['RS256'], audience=self.setting('KEY') options=jwt_options ) # Check if correct user if decoded_token['preferred_username'] != user: logit('SSH user does not match token user: %s (ssh) !=v %s (token)' % (user, Loading @@ -148,37 +139,6 @@ def verify_token_jwt(pamh, user, access_token): logit('Login successful for user %s, token %s' % (user, access_token)) return pamh.PAM_SUCCESS def verify_token_introspection(pamh, user, access_token): logit('Attempting token verification through instrosepction URL.') config = load_config_introspection(pamh) 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)) return pamh.PAM_AUTH_ERR if config['check_2fa']: if 'session_attribute' not in token_info or token_info['session_attribute'] != '2fa': 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)) return pamh.PAM_AUTH_ERR logit('Login successful for user %s, token %s' % (user, access_token)) return pamh.PAM_SUCCESS def load_config(pamh): # Load config file config_dpath = os.path.dirname(os.path.realpath(__file__)) Loading @@ -194,13 +154,5 @@ def load_config_jwt(pamh): config = load_config(pamh) return next((config_item for config_item in config if config_item['verification_type'] == "jwks_url")) except Exception as error: logit('Error loading configuration for jwt verification: %s' % error)\ return pamh.PAM_AUTH_ERR def load_config_introspection(pamh): try: config = load_config(pamh) return next((config_item for config_item in config if config_item['verification_type'] == "introspection_url")) except Exception as error: logit('Error loading configuration for introspection verification: %s' % error)\ logit('Error loading configuration for jwt verification: %s' % error) return pamh.PAM_AUTH_ERR
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
server_side/python/sshd_2fa +3 −3 Original line number Diff line number Diff line Loading @@ -2,9 +2,9 @@ #auth [success=ignore default=1] pam_unix.so #auth [success=1 default=die] pam_google_authenticator.so #auth sufficient pam_python.so /etc/security/oidc/oidc-pam.py #auth sufficient pam_python.so /etc/security/oidc/oidc_pam.py auth [success=done default=ignore] pam_python.so /etc/security/oidc/oidc-pam.py auth [success=done default=ignore] pam_python.so /etc/security/oidc/oidc_pam.py auth [success=done default=die] pam_google_authenticator.so use_first_pass secret=${HOME}/auth/.google_authenticator # Standard Un*x authentication. Loading @@ -19,7 +19,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