From 7833ff6671e9382e42d6b250e8eaffd63e1e96ce Mon Sep 17 00:00:00 2001 From: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com> Date: Sun, 26 Apr 2020 20:36:17 +0800 Subject: [PATCH] Dev oidc (#3941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] oidc_rp获取token添加headers base64编码 * [Update] 移除对oidc_rp的支持 * [Update] 移除对oidc_rp的支持2 * [Update] 修改OpenID配置(添加新配置项,并对旧配置项做兼容) * [Update] 移除所有与Keycloak相关的模块 * [Update] 添加jumpserver-django-oidc-rp的使用 * [Update] 更新登录重定向地址(oidc) * [Update] oidc添加一些配置参数;处理用户登录/创建/更新等信号 * [Update] 修改退出登录逻辑 * [Update] 添加oidc user登录成功的信号机制 * [Update] 修改mfa认证choices内容 (otp => code) * [Update] 添加OpenID backend password 认证失败信号机制;修改引入common包问题 * [Update] 用户Token/Auth API 校验用户时,传入request参数(解决登录成功日志记录的问题) * [Update] 添加依赖jumpserver-django-oidc-rp==0.3.7.1 * [Update] oidc认证模块说明 --- apps/authentication/backends/oidc/__init__.py | 4 + apps/authentication/backends/oidc/backends.py | 181 ----------------- apps/authentication/backends/oidc/urls.py | 10 - apps/authentication/backends/oidc/views.py | 77 -------- .../backends/openid/__init__.py | 7 - .../backends/openid/backends.py | 82 -------- .../backends/openid/decorator.py | 57 ------ .../backends/openid/middleware.py | 41 ---- apps/authentication/backends/openid/models.py | 185 ------------------ .../authentication/backends/openid/signals.py | 5 - apps/authentication/backends/openid/tests.py | 0 apps/authentication/backends/openid/urls.py | 11 -- apps/authentication/backends/openid/utils.py | 19 -- apps/authentication/backends/openid/views.py | 71 ------- apps/authentication/errors.py | 2 +- apps/authentication/mixins.py | 3 +- apps/authentication/signals_handlers.py | 56 +----- .../templates/authentication/login.html | 13 +- apps/authentication/urls/view_urls.py | 3 +- apps/authentication/views/login.py | 19 +- apps/jumpserver/conf.py | 174 +++++++++++++--- apps/jumpserver/settings/auth.py | 57 +++--- apps/jumpserver/settings/base.py | 7 +- apps/jumpserver/utils.py | 1 - apps/users/signals_handler.py | 34 ++++ apps/users/utils.py | 2 +- config_example.yml | 20 +- requirements/requirements.txt | 4 +- 28 files changed, 240 insertions(+), 905 deletions(-) delete mode 100644 apps/authentication/backends/oidc/backends.py delete mode 100644 apps/authentication/backends/oidc/urls.py delete mode 100644 apps/authentication/backends/oidc/views.py delete mode 100644 apps/authentication/backends/openid/__init__.py delete mode 100644 apps/authentication/backends/openid/backends.py delete mode 100644 apps/authentication/backends/openid/decorator.py delete mode 100644 apps/authentication/backends/openid/middleware.py delete mode 100644 apps/authentication/backends/openid/models.py delete mode 100644 apps/authentication/backends/openid/signals.py delete mode 100644 apps/authentication/backends/openid/tests.py delete mode 100644 apps/authentication/backends/openid/urls.py delete mode 100644 apps/authentication/backends/openid/utils.py delete mode 100644 apps/authentication/backends/openid/views.py diff --git a/apps/authentication/backends/oidc/__init__.py b/apps/authentication/backends/oidc/__init__.py index e69de29bb..a82161b8e 100644 --- a/apps/authentication/backends/oidc/__init__.py +++ b/apps/authentication/backends/oidc/__init__.py @@ -0,0 +1,4 @@ +""" +使用下面的工程,进行jumpserver 的 oidc 认证 +https://github.com/BaiJiangJie/jumpserver-django-oidc-rp +""" \ No newline at end of file diff --git a/apps/authentication/backends/oidc/backends.py b/apps/authentication/backends/oidc/backends.py deleted file mode 100644 index 2fa4a2555..000000000 --- a/apps/authentication/backends/oidc/backends.py +++ /dev/null @@ -1,181 +0,0 @@ -import requests -from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend -from django.core.exceptions import SuspiciousOperation -from django.conf import settings -from django.db import transaction -from django.urls import reverse -from django.utils.module_loading import import_string - -from oidc_rp.conf import settings as oidc_rp_settings -from oidc_rp.models import OIDCUser -from oidc_rp.signals import oidc_user_created -from oidc_rp.backends import OIDCAuthBackend -from oidc_rp.utils import validate_and_return_id_token - - -__all__ = ['OIDCAuthCodeBackend', 'OIDCAuthPasswordBackend'] - - -class OIDCAuthCodeBackend(OIDCAuthBackend): - def authenticate(self, request, nonce=None, **kwargs): - """ Authenticates users in case of the OpenID Connect Authorization code flow. """ - # NOTE: the request object is mandatory to perform the authentication using an authorization - # code provided by the OIDC supplier. - if (nonce is None and oidc_rp_settings.USE_NONCE) or request is None: - return - - # Fetches required GET parameters from the HTTP request object. - state = request.GET.get('state') - code = request.GET.get('code') - - # Don't go further if the state value or the authorization code is not present in the GET - # parameters because we won't be able to get a valid token for the user in that case. - if (state is None and oidc_rp_settings.USE_STATE) or code is None: - raise SuspiciousOperation('Authorization code or state value is missing') - - # Prepares the token payload that will be used to request an authentication token to the - # token endpoint of the OIDC provider. - token_payload = { - 'client_id': oidc_rp_settings.CLIENT_ID, - 'client_secret': oidc_rp_settings.CLIENT_SECRET, - 'grant_type': 'authorization_code', - 'code': code, - 'redirect_uri': request.build_absolute_uri( - reverse(settings.OIDC_RP_LOGIN_CALLBACK_URL_NAME) - ), - } - - # Calls the token endpoint. - token_response = requests.post(oidc_rp_settings.PROVIDER_TOKEN_ENDPOINT, data=token_payload) - token_response.raise_for_status() - token_response_data = token_response.json() - - # Validates the token. - raw_id_token = token_response_data.get('id_token') - id_token = validate_and_return_id_token(raw_id_token, nonce) - if id_token is None: - return - - # Retrieves the access token and refresh token. - access_token = token_response_data.get('access_token') - refresh_token = token_response_data.get('refresh_token') - - # Stores the ID token, the related access token and the refresh token in the session. - request.session['oidc_auth_id_token'] = raw_id_token - request.session['oidc_auth_access_token'] = access_token - request.session['oidc_auth_refresh_token'] = refresh_token - - # If the id_token contains userinfo scopes and claims we don't have to hit the userinfo - # endpoint. - if oidc_rp_settings.ID_TOKEN_INCLUDE_USERINFO: - userinfo_data = id_token - else: - # Fetches the user information from the userinfo endpoint provided by the OP. - userinfo_response = requests.get( - oidc_rp_settings.PROVIDER_USERINFO_ENDPOINT, - headers={'Authorization': 'Bearer {0}'.format(access_token)}) - userinfo_response.raise_for_status() - userinfo_data = userinfo_response.json() - - # Tries to retrieve a corresponding user in the local database and creates it if applicable. - try: - oidc_user = OIDCUser.objects.select_related('user').get(sub=userinfo_data.get('sub')) - except OIDCUser.DoesNotExist: - oidc_user = create_oidc_user_from_claims(userinfo_data) - oidc_user_created.send(sender=self.__class__, request=request, oidc_user=oidc_user) - else: - update_oidc_user_from_claims(oidc_user, userinfo_data) - - # Runs a custom user details handler if applicable. Such handler could be responsible for - # creating / updating whatever is necessary to manage the considered user (eg. a profile). - user_details_handler(oidc_user, userinfo_data) - - return oidc_user.user - - -class OIDCAuthPasswordBackend(ModelBackend): - - def authenticate(self, request, username=None, password=None, **kwargs): - - if username is None and password is None: - return - - # Prepares the token payload that will be used to request an authentication token to the - # token endpoint of the OIDC provider. - token_payload = { - 'client_id': oidc_rp_settings.CLIENT_ID, - 'client_secret': oidc_rp_settings.CLIENT_SECRET, - 'grant_type': 'password', - 'username': username, - 'password': password, - } - - token_response = requests.post(oidc_rp_settings.PROVIDER_TOKEN_ENDPOINT, data=token_payload) - token_response.raise_for_status() - token_response_data = token_response.json() - - access_token = token_response_data.get('access_token') - - # Fetches the user information from the userinfo endpoint provided by the OP. - userinfo_response = requests.get( - oidc_rp_settings.PROVIDER_USERINFO_ENDPOINT, - headers={'Authorization': 'Bearer {0}'.format(access_token)}) - userinfo_response.raise_for_status() - userinfo_data = userinfo_response.json() - - # Tries to retrieve a corresponding user in the local database and creates it if applicable. - try: - oidc_user = OIDCUser.objects.select_related('user').get(sub=userinfo_data.get('sub')) - except OIDCUser.DoesNotExist: - oidc_user = create_oidc_user_from_claims(userinfo_data) - oidc_user_created.send(sender=self.__class__, request=request, oidc_user=oidc_user) - else: - update_oidc_user_from_claims(oidc_user, userinfo_data) - - # Runs a custom user details handler if applicable. Such handler could be responsible for - # creating / updating whatever is necessary to manage the considered user (eg. a profile). - user_details_handler(oidc_user, userinfo_data) - - return oidc_user.user - - -def get_or_create_user(username, email): - user, created = get_user_model().objects.get_or_create(username=username) - return user - - -@transaction.atomic -def create_oidc_user_from_claims(claims): - """ - Creates an ``OIDCUser`` instance using the claims extracted - from an id_token. - """ - sub = claims['sub'] - email = claims.get('email') - username = claims.get('preferred_username') - user = get_or_create_user(username, email) - oidc_user = OIDCUser.objects.create(user=user, sub=sub, userinfo=claims) - - return oidc_user - - -@transaction.atomic -def update_oidc_user_from_claims(oidc_user, claims): - """ - Updates an ``OIDCUser`` instance using the claims extracted - from an id_token. - """ - oidc_user.userinfo = claims - oidc_user.save() - - -@transaction.atomic -def user_details_handler(oidc_user, userinfo_data): - name = userinfo_data.get('name') - username = userinfo_data.get('preferred_username') - email = userinfo_data.get('email') - oidc_user.user.name = name or username - oidc_user.user.username = username - oidc_user.user.email = email - oidc_user.user.save() diff --git a/apps/authentication/backends/oidc/urls.py b/apps/authentication/backends/oidc/urls.py deleted file mode 100644 index 173269d0d..000000000 --- a/apps/authentication/backends/oidc/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path -from oidc_rp import views as oidc_rp_views -from .views import OverwriteOIDCAuthRequestView, OverwriteOIDCEndSessionView - - -urlpatterns = [ - path('login/', OverwriteOIDCAuthRequestView.as_view(), name='oidc-login'), - path('callback/', oidc_rp_views.OIDCAuthCallbackView.as_view(), name='oidc-callback'), - path('logout/', OverwriteOIDCEndSessionView.as_view(), name='oidc-logout'), -] diff --git a/apps/authentication/backends/oidc/views.py b/apps/authentication/backends/oidc/views.py deleted file mode 100644 index 4db0c4138..000000000 --- a/apps/authentication/backends/oidc/views.py +++ /dev/null @@ -1,77 +0,0 @@ -from django.conf import settings -from django.http import HttpResponseRedirect, QueryDict -from django.urls import reverse -from django.utils.crypto import get_random_string -from django.utils.http import is_safe_url, urlencode - -from oidc_rp.conf import settings as oidc_rp_settings -from oidc_rp.views import OIDCEndSessionView, OIDCAuthRequestView - -__all__ = ['OverwriteOIDCAuthRequestView', 'OverwriteOIDCEndSessionView'] - - -class OverwriteOIDCAuthRequestView(OIDCAuthRequestView): - def get(self, request): - """ Processes GET requests. """ - # Defines common parameters used to bootstrap the authentication request. - authentication_request_params = request.GET.dict() - authentication_request_params.update({ - 'scope': oidc_rp_settings.SCOPES, - 'response_type': 'code', - 'client_id': oidc_rp_settings.CLIENT_ID, - 'redirect_uri': request.build_absolute_uri( - reverse(settings.OIDC_RP_LOGIN_CALLBACK_URL_NAME) - ), - }) - - # States should be used! They are recommended in order to maintain state between the - # authentication request and the callback. - if oidc_rp_settings.USE_STATE: - state = get_random_string(oidc_rp_settings.STATE_LENGTH) - authentication_request_params.update({'state': state}) - request.session['oidc_auth_state'] = state - - # Nonces should be used too! In that case the generated nonce is stored both in the - # authentication request parameters and in the user's session. - if oidc_rp_settings.USE_NONCE: - nonce = get_random_string(oidc_rp_settings.NONCE_LENGTH) - authentication_request_params.update({'nonce': nonce, }) - request.session['oidc_auth_nonce'] = nonce - - # Stores the "next" URL in the session if applicable. - next_url = request.GET.get('next') - request.session['oidc_auth_next_url'] = next_url \ - if is_safe_url(url=next_url, allowed_hosts=(request.get_host(), )) else None - - # Redirects the user to authorization endpoint. - query = urlencode(authentication_request_params) - redirect_url = '{url}?{query}'.format( - url=oidc_rp_settings.PROVIDER_AUTHORIZATION_ENDPOINT, query=query) - return HttpResponseRedirect(redirect_url) - - -class OverwriteOIDCEndSessionView(OIDCEndSessionView): - def post(self, request): - """ Processes POST requests. """ - logout_url = settings.LOGOUT_REDIRECT_URL or '/' - - try: - logout_url = self.provider_end_session_url \ - if oidc_rp_settings.PROVIDER_END_SESSION_ENDPOINT else logout_url - except KeyError: # pragma: no cover - logout_url = logout_url - - # Redirects the user to the appropriate URL. - return HttpResponseRedirect(logout_url) - - @property - def provider_end_session_url(self): - """ Returns the end-session URL. """ - q = QueryDict(mutable=True) - q[oidc_rp_settings.PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \ - self.request.build_absolute_uri(settings.LOGOUT_REDIRECT_URL or '/') - if self.request.session.get('oidc_auth_id_token'): - q[oidc_rp_settings.PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \ - self.request.session['oidc_auth_id_token'] - return '{}?{}'.format(oidc_rp_settings.PROVIDER_END_SESSION_ENDPOINT, q.urlencode()) - diff --git a/apps/authentication/backends/openid/__init__.py b/apps/authentication/backends/openid/__init__.py deleted file mode 100644 index 9ed3bea78..000000000 --- a/apps/authentication/backends/openid/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -# - -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 deleted file mode 100644 index 938566e2a..000000000 --- a/apps/authentication/backends/openid/backends.py +++ /dev/null @@ -1,82 +0,0 @@ -# coding:utf-8 -# - -from django.contrib.auth import get_user_model -from django.conf import settings - -from common.utils import get_logger -from .utils import new_client -from .models import OIDT_ACCESS_TOKEN - -UserModel = get_user_model() - -logger = get_logger(__file__) -client = new_client() - - -__all__ = [ - 'OpenIDAuthorizationCodeBackend', 'OpenIDAuthorizationPasswordBackend', -] - - -class BaseOpenIDAuthorizationBackend(object): - @staticmethod - def user_can_authenticate(user): - """ - Reject users with is_active=False. Custom user models that don't have - that attribute are allowed. - """ - is_valid = getattr(user, 'is_valid', None) - return is_valid or is_valid is None - - def get_user(self, user_id): - try: - user = UserModel._default_manager.get(pk=user_id) - except UserModel.DoesNotExist: - return None - - return user if self.user_can_authenticate(user) else None - - -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 - ) - 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 - user = oidt_profile.user - logger.info('Authenticate success: user -> {}'.format(user)) - return user if self.user_can_authenticate(user) else None - - -class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend): - def authenticate(self, request, username=None, password=None, **kwargs): - logger.info('Authentication OpenID password backend') - 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)) - return user if self.user_can_authenticate(user) else None - diff --git a/apps/authentication/backends/openid/decorator.py b/apps/authentication/backends/openid/decorator.py deleted file mode 100644 index 7286b7a2f..000000000 --- a/apps/authentication/backends/openid/decorator.py +++ /dev/null @@ -1,57 +0,0 @@ -# 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 deleted file mode 100644 index bacb4858c..000000000 --- a/apps/authentication/backends/openid/middleware.py +++ /dev/null @@ -1,41 +0,0 @@ -# coding:utf-8 -# - -from django.conf import settings -from django.contrib.auth import logout -from django.utils.deprecation import MiddlewareMixin -from django.contrib.auth import BACKEND_SESSION_KEY - -from common.utils import get_logger -from .utils import new_client -from .models import OIDT_ACCESS_TOKEN - -BACKEND_OPENID_AUTH_CODE = 'OpenIDAuthorizationCodeBackend' -logger = get_logger(__file__) -__all__ = ['OpenIDAuthenticationMiddleware'] - - -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 - try: - 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 deleted file mode 100644 index a945e8eb3..000000000 --- a/apps/authentication/backends/openid/models.py +++ /dev/null @@ -1,185 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from django.db import transaction -from django.contrib.auth import get_user_model -from keycloak.realm import KeycloakRealm -from keycloak.keycloak_openid import KeycloakOpenID -from users.utils import construct_user_email - -from .signals import post_create_or_update_openid_user -from .decorator import ssl_verification - -OIDT_ACCESS_TOKEN = 'oidt_access_token' - - -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 - :param access_token: - :param refresh_token: - """ - self.user = user - self.access_token = access_token - self.refresh_token = refresh_token - - def __str__(self): - return "{}'s OpenID token profile".format(self.user.username) - - -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._openid_client = None - self._realm = None - self._openid_connect_client = None - - @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 - - @property - def openid_connect_client(self): - """ - :rtype: keycloak.openid_connect.KeycloakOpenidConnect - """ - 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 - - @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 - - @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 - - @ssl_verification - def get_userinfo(self, token): - user_info = self.openid_connect_client.userinfo(token=token) - return user_info - - @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 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) - - 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 - - access_token - - expires_in - - refresh_token - - refresh_expires_in - :param dict token_response: - :rtype: OpenIDTokenProfile - """ - userinfo = self.get_userinfo(token=token_response['access_token']) - with transaction.atomic(): - name = userinfo.get('name', '') - username = userinfo.get('preferred_username', '') - email = userinfo.get('email', '') - email = construct_user_email(username, email) - - user, created = get_user_model().objects.update_or_create( - username=username, - defaults={ - 'name': name, 'email': email, - 'first_name': userinfo.get('given_name', ''), - '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_or_update_openid_user.send( - sender=user.__class__, user=user, created=created - ) - - return oidt_profile - - def __str__(self): - return self.client_id diff --git a/apps/authentication/backends/openid/signals.py b/apps/authentication/backends/openid/signals.py deleted file mode 100644 index ad81bca4a..000000000 --- a/apps/authentication/backends/openid/signals.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.dispatch import Signal - - -post_create_or_update_openid_user = Signal(providing_args=('user',)) -post_openid_login_success = Signal(providing_args=('user', 'request')) diff --git a/apps/authentication/backends/openid/tests.py b/apps/authentication/backends/openid/tests.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/authentication/backends/openid/urls.py b/apps/authentication/backends/openid/urls.py deleted file mode 100644 index 019529e12..000000000 --- a/apps/authentication/backends/openid/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# -from django.urls import path - -from . import views - -urlpatterns = [ - path('login/', views.OpenIDLoginView.as_view(), name='openid-login'), - path('login/complete/', views.OpenIDLoginCompleteView.as_view(), - name='openid-login-complete'), -] diff --git a/apps/authentication/backends/openid/utils.py b/apps/authentication/backends/openid/utils.py deleted file mode 100644 index 15160d224..000000000 --- a/apps/authentication/backends/openid/utils.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from django.conf import settings -from .models import Client - -__all__ = ['new_client'] - - -def new_client(): - """ - :return: authentication.models.Client - """ - return Client( - server_url=settings.AUTH_OPENID_SERVER_URL, - realm_name=settings.AUTH_OPENID_REALM_NAME, - client_id=settings.AUTH_OPENID_CLIENT_ID, - client_secret=settings.AUTH_OPENID_CLIENT_SECRET - ) diff --git a/apps/authentication/backends/openid/views.py b/apps/authentication/backends/openid/views.py deleted file mode 100644 index 89c935452..000000000 --- a/apps/authentication/backends/openid/views.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- -# - -import logging - -from django.conf import settings -from django.core.cache import cache -from django.views.generic.base import RedirectView -from django.contrib.auth import authenticate, login -from django.http.response import ( - HttpResponseBadRequest, - HttpResponseServerError, - HttpResponseRedirect -) - -from .utils import new_client -from .models import Nonce -from .signals import post_openid_login_success - -logger = logging.getLogger(__name__) -client = new_client() - -__all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView'] - - -class OpenIDLoginView(RedirectView): - def get_redirect_url(self, *args, **kwargs): - redirect_uri = settings.BASE_SITE_URL + \ - str(settings.AUTH_OPENID_LOGIN_COMPLETE_URL) - nonce = Nonce( - redirect_uri=redirect_uri, - 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.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(content='Code or State is empty') - if self.request.GET['state'] != self.request.session['openid_state']: - return HttpResponseBadRequest(content='State invalid') - nonce = cache.get(self.request.GET['state']) - if not nonce: - 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(content='Authenticate user failed') - - login(self.request, user) - post_openid_login_success.send( - sender=self.__class__, user=user, request=self.request - ) - return HttpResponseRedirect(nonce.next_path or '/') - diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 23323b15a..d782a05fc 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -171,7 +171,7 @@ class MFARequiredError(NeedMoreInfoError): 'error': self.error, 'msg': self.msg, 'data': { - 'choices': ['otp'], + 'choices': ['code'], 'url': reverse('api-auth:mfa-challenge') } } diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 1c4fb5aa1..5b3738c98 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -62,8 +62,7 @@ class AuthMixin: password = request.POST.get('password', '') public_key = request.POST.get('public_key', '') user, error = check_user_valid( - username=username, password=password, - public_key=public_key + request=request, username=username, password=password, public_key=public_key ) ip = self.get_request_ip() if not user: diff --git a/apps/authentication/signals_handlers.py b/apps/authentication/signals_handlers.py index 2d73ae9b7..645b202c2 100644 --- a/apps/authentication/signals_handlers.py +++ b/apps/authentication/signals_handlers.py @@ -1,55 +1,15 @@ -from django.http.request import QueryDict -from django.conf import settings from django.dispatch import receiver -from django.contrib.auth.signals import user_logged_out -from django_auth_ldap.backend import populate_user -from oidc_rp.signals import oidc_user_created -from users.models import User -from .backends.openid import new_client -from .backends.openid.signals import ( - post_create_or_update_openid_user, post_openid_login_success -) -from .signals import post_auth_success +from jms_oidc_rp.signals import oidc_user_login_success, oidc_user_login_failed +from .signals import post_auth_success, post_auth_failed -@receiver(user_logged_out) -def on_user_logged_out(sender, request, user, **kwargs): - query = QueryDict('', mutable=True) - query.update({ - 'redirect_uri': settings.BASE_SITE_URL - }) - # openid (keycloak) - if settings.AUTH_OPENID and settings.AUTH_OPENID_SHARE_SESSION: - client = new_client() - end_session_endpoint = client.get_url_end_session_endpoint() - openid_logout_url = "%s?%s" % (end_session_endpoint, query.urlencode()) - request.COOKIES['next'] = openid_logout_url - return +@receiver(oidc_user_login_success) +def on_oidc_user_login_success(sender, request, user, **kwargs): + post_auth_success.send(sender, user=user, request=request) -@receiver(post_create_or_update_openid_user) -def on_post_create_or_update_openid_user(sender, user=None, created=True, **kwargs): - if created and user and user.username != 'admin': - user.source = user.SOURCE_OPENID - user.save() - -@receiver(post_openid_login_success) -def on_openid_login_success(sender, user=None, request=None, **kwargs): - post_auth_success.send(sender=sender, user=user, request=request) - - -@receiver(populate_user) -def on_ldap_create_user(sender, user, ldap_user, **kwargs): - if user and user.username not in ['admin']: - exists = User.objects.filter(username=user.username).exists() - if not exists: - user.source = user.SOURCE_LDAP - user.save() - - -@receiver(oidc_user_created) -def on_oidc_user_created(sender, request, oidc_user, **kwargs): - oidc_user.user.source = User.SOURCE_OPENID - oidc_user.user.save() +@receiver(oidc_user_login_failed) +def on_oidc_user_login_failed(sender, username, request, reason, **kwargs): + post_auth_failed.send(sender, username=username, request=request, reason=reason) diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 21d775633..1f842d7a2 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -52,21 +52,14 @@ - {% if AUTH_OPENID or AUTH_OIDC_RP %} + {% if AUTH_OPENID %}

{% trans "More login options" %}

- {% if AUTH_OIDC_RP %} - - {% elif AUTH_OPENID %} - - {% endif %}
{% endif %} diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index c33ea453c..6c6d110d1 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -16,7 +16,6 @@ urlpatterns = [ path('logout/', views.UserLogoutView.as_view(), name='logout'), # openid - path('openid/', include(('authentication.backends.openid.urls', 'authentication'), namespace='openid')), path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')), - path('oidc-rp/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='oidc-rp')), + path('oidc/', include(('jms_oidc_rp.urls', 'authentication'), namespace='oidc')), ] diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index d27c4ccc3..c67cf2090 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -58,11 +58,9 @@ class UserLoginView(mixins.AuthMixin, FormView): if self.request.GET.get("admin", 0): return None if settings.AUTH_OPENID: - redirect_url = reverse("authentication:openid:openid-login") + redirect_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) elif settings.AUTH_CAS: redirect_url = reverse(settings.CAS_LOGIN_URL_NAME) - elif settings.AUTH_OIDC_RP: - redirect_url = reverse(settings.OIDC_RP_LOGIN_URL_NAME) if redirect_url: query_string = request.GET.urlencode() @@ -113,7 +111,6 @@ class UserLoginView(mixins.AuthMixin, FormView): context = { 'demo_mode': os.environ.get("DEMO_MODE"), 'AUTH_OPENID': settings.AUTH_OPENID, - 'AUTH_OIDC_RP': settings.AUTH_OIDC_RP, } kwargs.update(context) return super().get_context_data(**kwargs) @@ -188,24 +185,18 @@ class UserLogoutView(TemplateView): @staticmethod def get_backend_logout_url(): + if settings.AUTH_OPENID: + return settings.AUTH_OPENID_AUTH_LOGOUT_URL_NAME # if settings.AUTH_CAS: # return settings.CAS_LOGOUT_URL_NAME - - # oidc rp - if settings.AUTH_OIDC_RP: - return reverse(settings.OIDC_RP_LOGOUT_URL_NAME) + return None def get(self, request, *args, **kwargs): - auth_logout(request) - backend_logout_url = self.get_backend_logout_url() if backend_logout_url: return redirect(backend_logout_url) - next_uri = request.COOKIES.get("next") - if next_uri: - return redirect(next_uri) - + auth_logout(request) response = super().get(request, *args, **kwargs) return response diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 68e5d6e86..2c0b5bf5b 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -8,6 +8,7 @@ 3. 程序需要, 用户需要更改的写到本config中 """ import os +import re import sys import types import errno @@ -15,6 +16,7 @@ import json import yaml from importlib import import_module from django.urls import reverse_lazy +from urllib.parse import urljoin, urlparse BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) @@ -36,6 +38,38 @@ def import_string(dotted_path): ) from err +def is_absolute_uri(uri): + """ 判断一个uri是否是绝对地址 """ + if not isinstance(uri, str): + return False + + result = re.match(r'^http[s]?://.*', uri) + if result is None: + return False + + return True + + +def build_absolute_uri(base, uri): + """ 构建绝对uri地址 """ + if uri is None: + return base + + if isinstance(uri, int): + uri = str(uri) + + if not isinstance(uri, str): + return base + + if is_absolute_uri(uri): + return uri + + parsed_base = urlparse(base) + url = "{}://{}".format(parsed_base.scheme, parsed_base.netloc) + path = '{}/{}/'.format(parsed_base.path.strip('/'), uri.strip('/')) + return urljoin(url, path) + + class DoesNotExist(Exception): pass @@ -134,26 +168,35 @@ class Config(dict): 'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False, 'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1, + # OpenID 配置参数 + # OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8) 'AUTH_OPENID': False, + 'AUTH_OPENID_CLIENT_ID': 'client-id', + 'AUTH_OPENID_CLIENT_SECRET': 'client-secret', + 'AUTH_OPENID_SHARE_SESSION': True, + 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, + # OpenID 新配置参数 (version >= 1.5.8) + 'AUTH_OPENID_PROVIDER_ENDPOINT': 'https://op-example.com/', + 'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://op-example.com/authorize', + 'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT': 'https://op-example.com/token', + 'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT': 'https://op-example.com/jwks', + 'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT': 'https://op-example.com/userinfo', + 'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT': 'https://op-example.com/logout', + 'AUTH_OPENID_PROVIDER_SIGNATURE_ALG': 'HS256', + 'AUTH_OPENID_PROVIDER_SIGNATURE_KEY': None, + 'AUTH_OPENID_PROVIDER_CLAIMS_NAME': None, + 'AUTH_OPENID_PROVIDER_CLAIMS_USERNAME': None, + 'AUTH_OPENID_PROVIDER_CLAIMS_EMAIL': None, + 'AUTH_OPENID_SCOPES': 'openid profile email', + 'AUTH_OPENID_ID_TOKEN_MAX_AGE': 60, + 'AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO': True, + 'AUTH_OPENID_USE_STATE': True, + 'AUTH_OPENID_USE_NONCE': True, + 'AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION': True, + # OpenID 旧配置参数 (version <= 1.5.8 (discarded)) 'BASE_SITE_URL': 'http://localhost:8080', 'AUTH_OPENID_SERVER_URL': 'http://openid', - 'AUTH_OPENID_REALM_NAME': 'jumpserver', - 'AUTH_OPENID_CLIENT_ID': 'jumpserver', - 'AUTH_OPENID_CLIENT_SECRET': '', - 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, - 'AUTH_OPENID_SHARE_SESSION': True, - - - 'AUTH_OIDC_RP': False, - 'OIDC_RP_CLIENT_ID': 'client-id', - 'OIDC_RP_CLIENT_SECRET': 'client-secret', - 'OIDC_RP_PROVIDER_ENDPOINT': 'https://op-endpoint.com', - 'OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://op-endpoint.com/authorize', - 'OIDC_RP_PROVIDER_TOKEN_ENDPOINT': 'https://op-endpoint.com/token', - 'OIDC_RP_PROVIDER_JWKS_ENDPOINT': 'https://op-endpoint.com/jwk', - 'OIDC_RP_PROVIDER_USERINFO_ENDPOINT': 'https://op-endpoint.com/userinfo', - 'OIDC_RP_PROVIDER_END_SESSION_ENDPOINT': 'https://op-endpoint.com/logout', - 'OIDC_RP_ID_TOKEN_MAX_AGE': 60, + 'AUTH_OPENID_REALM_NAME': None, 'AUTH_RADIUS': False, 'RADIUS_SERVER': 'localhost', @@ -217,6 +260,88 @@ class Config(dict): 'ORG_CHANGE_TO_URL': '' } + def compatible_auth_openid_of_key(self): + """ + 兼容OpenID旧配置 (即 version <= 1.5.8) + 因为旧配置只支持OpenID协议的Keycloak实现, + 所以只需要根据旧配置和Keycloak的Endpoint说明文档, + 构造出新配置中标准OpenID协议中所需的Endpoint即可 + (Keycloak说明文档参考: https://www.keycloak.org/docs/latest/securing_apps/) + """ + if not self.AUTH_OPENID: + return + + realm_name = self.AUTH_OPENID_REALM_NAME + if realm_name is None: + return + + compatible_keycloak_config = [ + ( + 'AUTH_OPENID_PROVIDER_ENDPOINT', + self.AUTH_OPENID_SERVER_URL + ), + ( + 'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT', + '/realms/{}/protocol/openid-connect/auth'.format(realm_name) + ), + ( + 'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT', + '/realms/{}/protocol/openid-connect/token'.format(realm_name) + ), + ( + 'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT', + '/realms/{}/protocol/openid-connect/certs'.format(realm_name) + ), + ( + 'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT', + '/realms/{}/protocol/openid-connect/userinfo'.format(realm_name) + ), + ( + 'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT', + '/realms/{}/protocol/openid-connect/logout'.format(realm_name) + ) + ] + for key, value in compatible_keycloak_config: + self[key] = value + + def compatible_auth_openid_of_value(self): + """ + 兼容值的绝对路径、相对路径 + (key 为 AUTH_OPENID_PROVIDER_*_ENDPOINT 的配置) + """ + if not self.AUTH_OPENID: + return + + base = self.AUTH_OPENID_PROVIDER_ENDPOINT + config = list(self.items()) + for key, value in config: + result = re.match(r'^AUTH_OPENID_PROVIDER_.*_ENDPOINT$', key) + if result is None: + continue + if value is None: + # None 在 url 中有特殊含义 (比如对于: end_session_endpoint) + continue + value = build_absolute_uri(base, value) + self[key] = value + + def compatible(self): + """ + 对配置做兼容处理 + 1. 对`key`的兼容 (例如:版本升级) + 2. 对`value`做兼容 (例如:True、true、1 => True) + + 处理顺序要保持先对key做处理, 再对value做处理, + 因为处理value的时候,只根据最新版本支持的key进行 + """ + parts = ['key', 'value'] + targets = ['auth_openid'] + for part in parts: + for target in targets: + method_name = 'compatible_{}_of_{}'.format(target, part) + method = getattr(self, method_name, None) + if method is not None: + method() + def convert_type(self, k, v): default_value = self.defaults.get(k) if default_value is None: @@ -305,11 +430,8 @@ class DynamicConfig: if self.static_config.get('AUTH_CAS'): backends.insert(0, 'authentication.backends.cas.CASBackend') if self.static_config.get('AUTH_OPENID'): - backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend') - backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend') - if self.static_config.get('AUTH_OIDC_RP'): - backends.insert(0, 'authentication.backends.oidc.backends.OIDCAuthCodeBackend') - backends.insert(0, 'authentication.backends.oidc.backends.OIDCAuthPasswordBackend',) + backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend') + backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthPasswordBackend') if self.static_config.get('AUTH_RADIUS'): backends.insert(0, 'authentication.backends.radius.RadiusBackend') return backends @@ -490,9 +612,9 @@ class ConfigManager: manager = cls(root_path=root_path) if manager.load_from_object(): - return manager.config + config = manager.config elif manager.load_from_yml(): - return manager.config + config = manager.config else: msg = """ @@ -502,6 +624,10 @@ class ConfigManager: """ raise ImportError(msg) + # 对config进行兼容处理 + config.compatible() + return config + @classmethod def get_dynamic_config(cls, config): return DynamicConfig(config) diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index e46362638..4bb431bd5 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -2,7 +2,6 @@ # import os import ldap -from django.urls import reverse_lazy from ..const import CONFIG, DYNAMIC, PROJECT_DIR @@ -43,39 +42,37 @@ AUTH_LDAP_SYNC_INTERVAL = CONFIG.AUTH_LDAP_SYNC_INTERVAL AUTH_LDAP_SYNC_CRONTAB = CONFIG.AUTH_LDAP_SYNC_CRONTAB AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS -# openid -# Auth OpenID settings -BASE_SITE_URL = CONFIG.BASE_SITE_URL + +# ============================================================================== +# 认证 OpenID 配置参数 +# 参考: https://django-oidc-rp.readthedocs.io/en/stable/settings.html +# ============================================================================== AUTH_OPENID = CONFIG.AUTH_OPENID -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_PROVIDER_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_ENDPOINT +AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT +AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT +AUTH_OPENID_PROVIDER_JWKS_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_JWKS_ENDPOINT +AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT +AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT +AUTH_OPENID_PROVIDER_SIGNATURE_ALG = CONFIG.AUTH_OPENID_PROVIDER_SIGNATURE_ALG +AUTH_OPENID_PROVIDER_SIGNATURE_KEY = CONFIG.AUTH_OPENID_PROVIDER_SIGNATURE_KEY +AUTH_OPENID_PROVIDER_CLAIMS_NAME = CONFIG.AUTH_OPENID_PROVIDER_CLAIMS_NAME +AUTH_OPENID_PROVIDER_CLAIMS_USERNAME = CONFIG.AUTH_OPENID_PROVIDER_CLAIMS_USERNAME +AUTH_OPENID_PROVIDER_CLAIMS_EMAIL = CONFIG.AUTH_OPENID_PROVIDER_CLAIMS_EMAIL +AUTH_OPENID_SCOPES = CONFIG.AUTH_OPENID_SCOPES +AUTH_OPENID_ID_TOKEN_MAX_AGE = CONFIG.AUTH_OPENID_ID_TOKEN_MAX_AGE +AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO = CONFIG.AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION -AUTH_OPENID_LOGIN_URL = reverse_lazy("authentication:openid:openid-login") -AUTH_OPENID_LOGIN_COMPLETE_URL = reverse_lazy("authentication:openid:openid-login-complete") - -# oidc rp -# jumpserver -AUTH_OIDC_RP = CONFIG.AUTH_OIDC_RP -if AUTH_OIDC_RP: - # 优先使用AUTH_OIDC_RP - AUTH_OPENID = False -OIDC_RP_LOGIN_URL_NAME = "authentication:oidc-rp:oidc-login" -OIDC_RP_LOGIN_CALLBACK_URL_NAME = "authentication:oidc-rp:oidc-callback" -OIDC_RP_LOGOUT_URL_NAME = "authentication:oidc-rp:oidc-logout" -# https://django-oidc-rp.readthedocs.io/en/stable/settings.html#required-settings -OIDC_RP_CLIENT_ID = CONFIG.OIDC_RP_CLIENT_ID -OIDC_RP_CLIENT_SECRET = CONFIG.OIDC_RP_CLIENT_SECRET -OIDC_RP_PROVIDER_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_ENDPOINT -OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT -OIDC_RP_PROVIDER_TOKEN_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_TOKEN_ENDPOINT -OIDC_RP_PROVIDER_JWKS_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_JWKS_ENDPOINT -OIDC_RP_PROVIDER_USERINFO_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_USERINFO_ENDPOINT -OIDC_RP_PROVIDER_END_SESSION_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_END_SESSION_ENDPOINT -OIDC_RP_ID_TOKEN_MAX_AGE = CONFIG.OIDC_RP_ID_TOKEN_MAX_AGE - +AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION +AUTH_OPENID_USE_STATE = CONFIG.AUTH_OPENID_USE_STATE +AUTH_OPENID_USE_NONCE = CONFIG.AUTH_OPENID_USE_NONCE +AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION = CONFIG.AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION +AUTH_OPENID_AUTH_LOGIN_URL_NAME = 'authentication:oidc:login' +AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:oidc:login-callback' +AUTH_OPENID_AUTH_LOGOUT_URL_NAME = 'authentication:oidc:logout' +# ============================================================================== # Radius Auth AUTH_RADIUS = CONFIG.AUTH_RADIUS diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index c2b558742..d1fbb5a36 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -48,6 +48,7 @@ INSTALLED_APPS = [ 'authentication.apps.AuthenticationConfig', # authentication 'applications.apps.ApplicationsConfig', 'tickets.apps.TicketsConfig', + 'jms_oidc_rp', 'rest_framework', 'rest_framework_swagger', 'drf_yasg', @@ -56,7 +57,6 @@ INSTALLED_APPS = [ 'django_filters', 'bootstrap3', 'captcha', - 'oidc_rp', 'django_celery_beat', 'django.contrib.auth', 'django.contrib.admin', @@ -76,13 +76,12 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'authentication.backends.openid.middleware.OpenIDAuthenticationMiddleware', + 'jms_oidc_rp.middleware.OIDCRefreshIDTokenMiddleware', 'django_cas_ng.middleware.CASMiddleware', 'jumpserver.middleware.TimezoneMiddleware', 'jumpserver.middleware.DemoMiddleware', 'jumpserver.middleware.RequestMiddleware', 'orgs.middleware.OrgMiddleware', - 'oidc_rp.middleware.OIDCRefreshIDTokenMiddleware', ] @@ -105,7 +104,7 @@ TEMPLATES = [ 'django.template.context_processors.media', 'jumpserver.context_processor.jumpserver_processor', 'orgs.context_processor.org_processor', - 'oidc_rp.context_processors.oidc', + 'jms_oidc_rp.context_processors.oidc', ], }, }, diff --git a/apps/jumpserver/utils.py b/apps/jumpserver/utils.py index aa808d2f2..72a836892 100644 --- a/apps/jumpserver/utils.py +++ b/apps/jumpserver/utils.py @@ -18,4 +18,3 @@ def get_current_request(): current_request = LocalProxy(partial(_find, 'current_request')) - diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index aa1458c7d..17e11af84 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -3,12 +3,19 @@ from django.dispatch import receiver from django.db.models.signals import m2m_changed +from django_auth_ldap.backend import populate_user +from django.conf import settings from django_cas_ng.signals import cas_user_authenticated +from jms_oidc_rp.signals import oidc_user_created, oidc_user_updated +from jms_oidc_rp.backends import get_userinfo_from_claims + from common.utils import get_logger +from .utils import construct_user_email from .signals import post_user_create from .models import User + logger = get_logger(__file__) @@ -37,3 +44,30 @@ def on_cas_user_authenticated(sender, user, created, **kwargs): if created: user.source = user.SOURCE_CAS user.save() + + +@receiver(populate_user) +def on_ldap_create_user(sender, user, ldap_user, **kwargs): + if user and user.username not in ['admin']: + exists = User.objects.filter(username=user.username).exists() + if not exists: + user.source = user.SOURCE_LDAP + user.save() + + +@receiver(oidc_user_created) +def on_oidc_user_created(sender, request, oidc_user, **kwargs): + oidc_user.user.source = User.SOURCE_OPENID + oidc_user.user.save() + + +@receiver(oidc_user_updated) +def on_oidc_user_updated(sender, request, oidc_user, **kwargs): + if not settings.AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION: + return + name, username, email = get_userinfo_from_claims(oidc_user.userinfo) + email = construct_user_email(username, email) + oidc_user.user.name = name + oidc_user.user.username = username + oidc_user.user.email = email + oidc_user.user.save() diff --git a/apps/users/utils.py b/apps/users/utils.py index 9488b5877..0729115b6 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -326,7 +326,7 @@ def get_source_choices(): ] if settings.AUTH_LDAP: choices.append((User.SOURCE_LDAP, choices_all[User.SOURCE_LDAP])) - if settings.AUTH_OPENID or settings.AUTH_OIDC_RP: + if settings.AUTH_OPENID: choices.append((User.SOURCE_OPENID, choices_all[User.SOURCE_OPENID])) if settings.AUTH_RADIUS: choices.append((User.SOURCE_RADIUS, choices_all[User.SOURCE_RADIUS])) diff --git a/config_example.yml b/config_example.yml index cec3b7eb6..a2e7ccf1a 100644 --- a/config_example.yml +++ b/config_example.yml @@ -29,7 +29,6 @@ BOOTSTRAP_TOKEN: # 使用单文件sqlite数据库 # DB_ENGINE: sqlite3 # DB_NAME: - # MySQL or postgres setting like: # 使用Mysql作为数据库 DB_ENGINE: mysql @@ -55,11 +54,7 @@ REDIS_PORT: 6379 # REDIS_DB_CACHE: 4 # Use OpenID authorization -# -# 配置说明: 如果您使用的是Keycloak作为OP,可以使用方式1或方式2; 如果OP不是Keycloak, 请使用方式2 -# -# 方式1: OpenID认证 (基于 oidc 协议的 keycloak 的实现) -# +# 使用OpenID 来进行认证设置 # BASE_SITE_URL: http://localhost:8080 # AUTH_OPENID: false # True or False # AUTH_OPENID_SERVER_URL: https://openid-auth-server.com/ @@ -68,19 +63,6 @@ REDIS_PORT: 6379 # AUTH_OPENID_CLIENT_SECRET: client-secret # AUTH_OPENID_IGNORE_SSL_VERIFICATION: True # AUTH_OPENID_SHARE_SESSION: True -# -# 方式2: OpenID认证 (使用标准 oidc 协议进行认证) -# 配置参数详细信息参考: https://django-oidc-rp.readthedocs.io/en/stable/settings.html -# -# AUTH_OIDC_RP: False -# OIDC_RP_CLIENT_ID: client-id -# OIDC_RP_CLIENT_SECRET: client-secret -# OIDC_RP_PROVIDER_ENDPOINT: https://op-endpoint.com -# OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT: https://op-endpoint.com/authorize -# OIDC_RP_PROVIDER_TOKEN_ENDPOINT: https://op-endpoint.com/token -# OIDC_RP_PROVIDER_JWKS_ENDPOINT: https://op-endpoint.com/jwk -# OIDC_RP_PROVIDER_USERINFO_ENDPOINT: https://op-endpoint.com/userinfo -# OIDC_RP_PROVIDER_END_SESSION_ENDPOINT: https://op-endpoint.com/logout # Use Radius authorization # 使用Radius来认证 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 467a04fcb..81019d249 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -74,8 +74,6 @@ Werkzeug==0.15.3 drf-nested-routers==0.91 aliyun-python-sdk-core-v3==2.9.1 aliyun-python-sdk-ecs==4.10.1 -python-keycloak==0.13.3 -python-keycloak-client==0.1.3 rest_condition==1.0.3 python-ldap==3.1.0 tencentcloud-sdk-python==3.0.40 @@ -98,4 +96,4 @@ ipython huaweicloud-sdk-python==1.0.21 django-redis==4.11.0 python-redis-lock==3.5.0 -django-oidc-rp==0.3.4 +jumpserver-django-oidc-rp==0.3.7.1 -- GitLab