Commit 6e4d8054 authored by Yakubov, Sergey's avatar Yakubov, Sergey
Browse files

Merge branch 'gzi-ssh-azure' into 'main'

Gzi ssh azure

See merge request !3
parents 5a912936 78bf2fd2
Loading
Loading
Loading
Loading
Loading
+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
+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/
+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
+54 −31
Original line number Diff line number Diff line
@@ -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)


@@ -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
@@ -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
+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
Loading