diff --git a/apps/authentication/backends/openid/__init__.py b/apps/authentication/backends/openid/__init__.py index 2deaf3cae..9ed3bea78 100644 --- a/apps/authentication/backends/openid/__init__.py +++ b/apps/authentication/backends/openid/__init__.py @@ -4,3 +4,4 @@ from .backends import * from .middleware import * from .utils import * +from .decorator import * diff --git a/apps/authentication/backends/openid/backends.py b/apps/authentication/backends/openid/backends.py index b8e4ae609..f6285d1ed 100644 --- a/apps/authentication/backends/openid/backends.py +++ b/apps/authentication/backends/openid/backends.py @@ -20,7 +20,6 @@ __all__ = [ class BaseOpenIDAuthorizationBackend(object): - @staticmethod def user_can_authenticate(user): """ @@ -40,25 +39,20 @@ class BaseOpenIDAuthorizationBackend(object): class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend): - def authenticate(self, request, **kwargs): logger.info('Authentication OpenID code backend') - code = kwargs.get('code') redirect_uri = kwargs.get('redirect_uri') - if not code or not redirect_uri: logger.info('Authenticate failed: No code or No redirect uri') return None - try: oidt_profile = client.update_or_create_from_code( - code=code, redirect_uri=redirect_uri + code=code, redirect_uri=redirect_uri ) - except Exception as e: logger.info('Authenticate failed: get oidt_profile: {}'.format(e)) - + return None else: # Check openid user single logout or not with access_token request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token @@ -68,25 +62,19 @@ class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend): class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend): - def authenticate(self, request, username=None, password=None, **kwargs): logger.info('Authentication OpenID password backend') - - if not settings.AUTH_OPENID: - logger.info('Authenticate failed: AUTH_OPENID is False') - return None - elif not username: + if not username: logger.info('Authenticate failed: Not username') return None - try: oidt_profile = client.update_or_create_from_password( username=username, password=password ) - except Exception as e: + logger.error(e, exc_info=True) logger.info('Authenticate failed: get oidt_profile: {}'.format(e)) - + return None else: user = oidt_profile.user logger.info('Authenticate success: user -> {}'.format(user)) diff --git a/apps/authentication/backends/openid/decorator.py b/apps/authentication/backends/openid/decorator.py new file mode 100644 index 000000000..7286b7a2f --- /dev/null +++ b/apps/authentication/backends/openid/decorator.py @@ -0,0 +1,57 @@ +# coding: utf-8 +# + +import warnings +import contextlib + +import requests +from urllib3.exceptions import InsecureRequestWarning +from django.conf import settings + +__all__ = [ + 'ssl_verification', +] + +old_merge_environment_settings = requests.Session.merge_environment_settings + + +@contextlib.contextmanager +def no_ssl_verification(): + """ + https://stackoverflow.com/questions/15445981/ + how-do-i-disable-the-security-certificate-check-in-python-requests + """ + opened_adapters = set() + + def merge_environment_settings(self, url, proxies, stream, verify, cert): + # Verification happens only once per connection so we need to close + # all the opened adapters once we're done. Otherwise, the effects of + # verify=False persist beyond the end of this context manager. + opened_adapters.add(self.get_adapter(url)) + _settings = old_merge_environment_settings( + self, url, proxies, stream, verify, cert + ) + _settings['verify'] = False + return _settings + + requests.Session.merge_environment_settings = merge_environment_settings + try: + with warnings.catch_warnings(): + warnings.simplefilter('ignore', InsecureRequestWarning) + yield + finally: + requests.Session.merge_environment_settings = old_merge_environment_settings + for adapter in opened_adapters: + try: + adapter.close() + except: + pass + + +def ssl_verification(func): + def wrapper(*args, **kwargs): + if not settings.AUTH_OPENID_IGNORE_SSL_VERIFICATION: + return func(*args, **kwargs) + with no_ssl_verification(): + return func(*args, **kwargs) + return wrapper diff --git a/apps/authentication/backends/openid/middleware.py b/apps/authentication/backends/openid/middleware.py index 43c7dbd22..bacb4858c 100644 --- a/apps/authentication/backends/openid/middleware.py +++ b/apps/authentication/backends/openid/middleware.py @@ -19,24 +19,23 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin): """ Check openid user single logout (with access_token) """ - def process_request(self, request): # Don't need openid auth if AUTH_OPENID is False if not settings.AUTH_OPENID: return + # Don't need openid auth if no shared session enabled + if not settings.AUTH_OPENID_SHARE_SESSION: + return # Don't need check single logout if user not authenticated if not request.user.is_authenticated: return elif not request.session[BACKEND_SESSION_KEY].endswith( BACKEND_OPENID_AUTH_CODE): return - # Check openid user single logout or not with access_token - client = new_client() try: - client.openid_connect_client.userinfo( - token=request.session.get(OIDT_ACCESS_TOKEN) - ) + client = new_client() + client.get_userinfo(token=request.session.get(OIDT_ACCESS_TOKEN)) except Exception as e: logout(request) logger.error(e) diff --git a/apps/authentication/backends/openid/models.py b/apps/authentication/backends/openid/models.py index fd75ed870..b99ba402a 100644 --- a/apps/authentication/backends/openid/models.py +++ b/apps/authentication/backends/openid/models.py @@ -7,12 +7,24 @@ from keycloak.realm import KeycloakRealm from keycloak.keycloak_openid import KeycloakOpenID from .signals import post_create_openid_user +from .decorator import ssl_verification OIDT_ACCESS_TOKEN = 'oidt_access_token' -class OpenIDTokenProfile(object): +class Nonce(object): + """ + The openid-login is stored in cache as a temporary object, recording the + user's redirect_uri and next_pat + """ + def __init__(self, redirect_uri, next_path): + import uuid + self.state = uuid.uuid4() + self.redirect_uri = redirect_uri + self.next_path = next_path + +class OpenIDTokenProfile(object): def __init__(self, user, access_token, refresh_token): """ :param user: User object @@ -28,80 +40,109 @@ class OpenIDTokenProfile(object): class Client(object): - def __init__(self, server_url, realm_name, client_id, client_secret): self.server_url = server_url self.realm_name = realm_name self.client_id = client_id self.client_secret = client_secret - self.realm = self.new_realm() - self.openid_client = self.new_openid_client() - self.openid_connect_client = self.new_openid_connect_client() + self._openid_client = None + self._realm = None + self._openid_connect_client = None - def new_realm(self): - return KeycloakRealm( - server_url=self.server_url, - realm_name=self.realm_name, - headers={} - ) + @property + def realm(self): + if self._realm is None: + self._realm = KeycloakRealm( + server_url=self.server_url, + realm_name=self.realm_name, + headers={} + ) + return self._realm - def new_openid_connect_client(self): + @property + def openid_connect_client(self): """ :rtype: keycloak.openid_connect.KeycloakOpenidConnect """ - openid_connect = self.realm.open_id_connect( - client_id=self.client_id, - client_secret=self.client_secret - ) - return openid_connect + if self._openid_connect_client is None: + self._openid_connect_client = self.realm.open_id_connect( + client_id=self.client_id, + client_secret=self.client_secret + ) + return self._openid_connect_client - def new_openid_client(self): + @property + def openid_client(self): """ :rtype: keycloak.keycloak_openid.KeycloakOpenID """ + if self._openid_client is None: + self._openid_client = KeycloakOpenID( + server_url='%sauth/' % self.server_url, + realm_name=self.realm_name, + client_id=self.client_id, + client_secret_key=self.client_secret, + ) + return self._openid_client - return KeycloakOpenID( - server_url='%sauth/' % self.server_url, - realm_name=self.realm_name, - client_id=self.client_id, - client_secret_key=self.client_secret, + @ssl_verification + def get_url(self, name): + return self.openid_connect_client.get_url(name=name) + + def get_url_end_session_endpoint(self): + return self.get_url(name='end_session_endpoint') + + @ssl_verification + def get_authorization_url(self, redirect_uri, scope, state): + url = self.openid_connect_client.authorization_url( + redirect_uri=redirect_uri, scope=scope, state=state ) + return url - def update_or_create_from_password(self, username, password): - """ - Update or create an user based on an authentication username and password. + @ssl_verification + def get_userinfo(self, token): + user_info = self.openid_connect_client.userinfo(token=token) + return user_info - :param str username: authentication username - :param str password: authentication password - :return: OpenIDTokenProfile - """ + @ssl_verification + def authorization_code(self, code, redirect_uri): + token_response = self.openid_connect_client.authorization_code( + code=code, redirect_uri=redirect_uri + ) + return token_response + + @ssl_verification + def authorization_password(self, username, password): token_response = self.openid_client.token( username=username, password=password ) - - return self._update_or_create(token_response=token_response) + return token_response def update_or_create_from_code(self, code, redirect_uri): """ Update or create an user based on an authentication code. Response as specified in: - https://tools.ietf.org/html/rfc6749#section-4.1.4 - :param str code: authentication code :param str redirect_uri: :rtype: OpenIDTokenProfile """ + token_response = self.authorization_code(code, redirect_uri) + return self._update_or_create(token_response=token_response) - token_response = self.openid_connect_client.authorization_code( - code=code, redirect_uri=redirect_uri) - + def update_or_create_from_password(self, username, password): + """ + Update or create an user based on an authentication username and password. + :param str username: authentication username + :param str password: authentication password + :return: OpenIDTokenProfile + """ + token_response = self.authorization_password(username, password) return self._update_or_create(token_response=token_response) def _update_or_create(self, token_response): """ Update or create an user based on a token response. - `token_response` contains the items returned by the OpenIDConnect Token API end-point: - id_token @@ -109,14 +150,10 @@ class Client(object): - expires_in - refresh_token - refresh_expires_in - :param dict token_response: :rtype: OpenIDTokenProfile """ - - userinfo = self.openid_connect_client.userinfo( - token=token_response['access_token']) - + userinfo = self.get_userinfo(token=token_response['access_token']) with transaction.atomic(): user, _ = get_user_model().objects.update_or_create( username=userinfo.get('preferred_username', ''), @@ -126,13 +163,11 @@ class Client(object): 'last_name': userinfo.get('family_name', '') } ) - oidt_profile = OpenIDTokenProfile( user=user, access_token=token_response['access_token'], refresh_token=token_response['refresh_token'], ) - if user: post_create_openid_user.send(sender=user.__class__, user=user) @@ -140,17 +175,3 @@ class Client(object): def __str__(self): return self.client_id - - -class Nonce(object): - """ - The openid-login is stored in cache as a temporary object, recording the - user's redirect_uri and next_pat - """ - - def __init__(self, redirect_uri, next_path): - import uuid - self.state = uuid.uuid4() - self.redirect_uri = redirect_uri - self.next_path = next_path - diff --git a/apps/authentication/backends/openid/views.py b/apps/authentication/backends/openid/views.py index 45b5bfe23..5e51e7a38 100644 --- a/apps/authentication/backends/openid/views.py +++ b/apps/authentication/backends/openid/views.py @@ -24,7 +24,6 @@ __all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView'] class OpenIDLoginView(RedirectView): - def get_redirect_url(self, *args, **kwargs): redirect_uri = settings.BASE_SITE_URL + str(settings.LOGIN_COMPLETE_URL) nonce = Nonce( @@ -32,42 +31,36 @@ class OpenIDLoginView(RedirectView): next_path=self.request.GET.get('next') ) cache.set(str(nonce.state), nonce, 24*3600) + self.request.session['openid_state'] = str(nonce.state) - authorization_url = client.openid_connect_client.\ - authorization_url( - redirect_uri=nonce.redirect_uri, scope='code', - state=str(nonce.state) - ) + authorization_url = client.get_authorization_url( + redirect_uri=nonce.redirect_uri, + scope='code', + state=str(nonce.state) + ) return authorization_url class OpenIDLoginCompleteView(RedirectView): - def get(self, request, *args, **kwargs): if 'error' in request.GET: return HttpResponseServerError(self.request.GET['error']) - if 'code' not in self.request.GET and 'state' not in self.request.GET: - return HttpResponseBadRequest() - + return HttpResponseBadRequest(content='Code or State is empty') if self.request.GET['state'] != self.request.session['openid_state']: - return HttpResponseBadRequest() - + return HttpResponseBadRequest(content='State invalid') nonce = cache.get(self.request.GET['state']) - if not nonce: - return HttpResponseBadRequest() + return HttpResponseBadRequest(content='State failure') user = authenticate( request=self.request, code=self.request.GET['code'], redirect_uri=nonce.redirect_uri ) - cache.delete(str(nonce.state)) - if not user: - return HttpResponseBadRequest() + return HttpResponseBadRequest(content='Authenticate user failed') login(self.request, user) post_openid_login_success.send( diff --git a/apps/authentication/signals_handlers.py b/apps/authentication/signals_handlers.py index a0732894f..e401b7dc1 100644 --- a/apps/authentication/signals_handlers.py +++ b/apps/authentication/signals_handlers.py @@ -18,19 +18,17 @@ from .signals import post_auth_success, post_auth_failed def on_user_logged_out(sender, request, user, **kwargs): if not settings.AUTH_OPENID: return - + if not settings.AUTH_OPENID_SHARE_SESSION: + return query = QueryDict('', mutable=True) query.update({ 'redirect_uri': settings.BASE_SITE_URL }) - client = new_client() openid_logout_url = "%s?%s" % ( - client.openid_connect_client.get_url( - name='end_session_endpoint'), + client.get_url_end_session_endpoint(), query.urlencode() ) - request.COOKIES['next'] = openid_logout_url diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 0362a68e3..2e46e53c6 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -344,6 +344,8 @@ defaults = { 'SESSION_COOKIE_AGE': 3600 * 24, 'SESSION_EXPIRE_AT_BROWSER_CLOSE': False, 'AUTH_OPENID': False, + 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, + 'AUTH_OPENID_SHARE_SESSION': False, 'OTP_VALID_WINDOW': 0, 'OTP_ISSUER_NAME': 'Jumpserver', 'EMAIL_SUFFIX': 'jumpserver.org', diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 453b1b9e7..4b1f8b6eb 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -456,6 +456,8 @@ AUTH_OPENID_SERVER_URL = CONFIG.AUTH_OPENID_SERVER_URL AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET +AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION +AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION AUTH_OPENID_BACKENDS = [ 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend', 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend', diff --git a/config_example.yml b/config_example.yml index bf5cdacb3..54706ca9f 100644 --- a/config_example.yml +++ b/config_example.yml @@ -61,6 +61,8 @@ REDIS_PORT: 6379 # AUTH_OPENID_REALM_NAME: realm-name # AUTH_OPENID_CLIENT_ID: client-id # AUTH_OPENID_CLIENT_SECRET: client-secret +# AUTH_OPENID_IGNORE_SSL_VERIFICATION: True +# AUTH_OPENID_SHARE_SESSION: False # # Use Radius authorization # 使用Radius来认证