Commit 5b6d5aeb authored by Cage, Gregory's avatar Cage, Gregory
Browse files

Refactor oidc_pam to verif jwt using jwks by default. rename oidc-pam

parent e9f38ada
Loading
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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
@@ -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/
+13 −61
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@
'''
PAM module for authenticating users via a OIDC token
'''
import base64
import json
import jwt
import os
@@ -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):
@@ -26,7 +27,6 @@ def pam_sm_setcred(pamh, _flags, _argv):
    '''
    Default
    '''
    logit("setcred")
    return pamh.PAM_SUCCESS


@@ -34,7 +34,6 @@ def pam_sm_acct_mgmt(pamh, _flags, _argv):
    '''
    Default
    '''
    logit("acct mgmt")
    return pamh.PAM_SUCCESS


@@ -42,7 +41,6 @@ def pam_sm_open_session(pamh, _flags, _argv):
    '''
    Default
    '''
    logit("open session")
    return pamh.PAM_SUCCESS


@@ -50,7 +48,6 @@ def pam_sm_close_session(pamh, _flags, _argv):
    '''
    Default
    '''
    logit("close session")
    return pamh.PAM_SUCCESS


@@ -58,7 +55,6 @@ def pam_sm_chauthtok(pamh, _flags, _argv):
    '''
    Default
    '''
    logit("chauthtok")
    return pamh.PAM_SUCCESS


@@ -66,7 +62,6 @@ def pam_sm_authenticate(pamh, _flags, _argv):
    '''
    Authenticates a user via an OIDC token
    '''   
    logit("trying")
    
    # build access token

@@ -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:
@@ -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,
@@ -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__))
@@ -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
+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
@@ -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
+3 −3
Original line number Diff line number Diff line
@@ -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.
@@ -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