From c121ac6b1d37c49842b4be4508f422c203c0cd42 Mon Sep 17 00:00:00 2001 From: jiangweidong Date: Tue, 13 Dec 2022 15:30:08 +0800 Subject: [PATCH 1/2] =?UTF-8?q?perf:=20OpenID=E6=94=AF=E6=8C=81PKCE?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/backends/oidc/backends.py | 4 ++- apps/authentication/backends/oidc/views.py | 29 ++++++++++++++++++- apps/jumpserver/conf.py | 2 ++ apps/settings/serializers/auth/oidc.py | 5 ++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/apps/authentication/backends/oidc/backends.py b/apps/authentication/backends/oidc/backends.py index 70e0f759c..f29bf95e5 100644 --- a/apps/authentication/backends/oidc/backends.py +++ b/apps/authentication/backends/oidc/backends.py @@ -88,7 +88,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend): """ @ssl_verification - def authenticate(self, request, nonce=None, **kwargs): + def authenticate(self, request, nonce=None, code_verifier=None, **kwargs): """ Authenticates users in case of the OpenID Connect Authorization code flow. """ log_prompt = "Process authenticate [OIDCAuthCodeBackend]: {}" logger.debug(log_prompt.format('start')) @@ -134,6 +134,8 @@ class OIDCAuthCodeBackend(OIDCBaseBackend): request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME) ) } + if settings.AUTH_OPENID_PKCE and code_verifier: + token_payload['code_verifier'] = code_verifier if settings.AUTH_OPENID_CLIENT_AUTH_METHOD == 'client_secret_post': token_payload.update({ 'client_id': settings.AUTH_OPENID_CLIENT_ID, diff --git a/apps/authentication/backends/oidc/views.py b/apps/authentication/backends/oidc/views.py index 78019ac33..88088245d 100644 --- a/apps/authentication/backends/oidc/views.py +++ b/apps/authentication/backends/oidc/views.py @@ -9,7 +9,10 @@ """ +import base64 +import hashlib import time +import secrets from django.conf import settings from django.contrib import auth @@ -38,6 +41,19 @@ class OIDCAuthRequestView(View): http_method_names = ['get', ] + @staticmethod + def gen_code_verifier(length=128): + # length range 43 ~ 128 + return secrets.token_urlsafe(length - 32) + + @staticmethod + def gen_code_challenge(code_verifier, code_challenge_method): + if code_challenge_method == 'plain': + return code_verifier + h = hashlib.sha256(code_verifier.encode('ascii')).digest() + b = base64.urlsafe_b64encode(h) + return b.decode('ascii')[:-1] + def get(self, request): """ Processes GET requests. """ @@ -56,6 +72,16 @@ class OIDCAuthRequestView(View): ) }) + if settings.AUTH_OPENID_PKCE: + code_verifier = self.gen_code_verifier() + code_challenge_method = settings.AUTH_OPENID_CODE_CHALLENGE_METHOD or 'S256' + code_challenge = self.gen_code_challenge(code_verifier, code_challenge_method) + authentication_request_params.update({ + 'code_challenge_method': code_challenge_method, + 'code_challenge': code_challenge + }) + request.session['oidc_auth_code_verifier'] = code_verifier + # States should be used! They are recommended in order to maintain state between the # authentication request and the callback. if settings.AUTH_OPENID_USE_STATE: @@ -138,8 +164,9 @@ class OIDCAuthCallbackView(View): # Authenticates the end-user. next_url = request.session.get('oidc_auth_next_url', None) + code_verifier = request.session.get('oidc_auth_code_verifier', None) logger.debug(log_prompt.format('Process authenticate')) - user = auth.authenticate(nonce=nonce, request=request) + user = auth.authenticate(nonce=nonce, request=request, code_verifier=code_verifier) if user and user.is_valid: logger.debug(log_prompt.format('Login: {}'.format(user))) auth.login(self.request, user) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index c2bfba805..d43384987 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -270,6 +270,8 @@ class Config(dict): 'AUTH_OPENID_USER_ATTR_MAP': { 'name': 'name', 'username': 'preferred_username', 'email': 'email' }, + 'AUTH_OPENID_PKCE': False, + 'AUTH_OPENID_CODE_CHALLENGE_METHOD': 'S256', # OpenID 新配置参数 (version >= 1.5.9) 'AUTH_OPENID_PROVIDER_ENDPOINT': 'https://oidc.example.com/', diff --git a/apps/settings/serializers/auth/oidc.py b/apps/settings/serializers/auth/oidc.py index ea25ca9d7..259cf9712 100644 --- a/apps/settings/serializers/auth/oidc.py +++ b/apps/settings/serializers/auth/oidc.py @@ -38,6 +38,11 @@ class CommonSettingSerializer(serializers.Serializer): help_text=_('User attr map present how to map OpenID user attr to ' 'jumpserver, username,name,email is jumpserver attr') ) + AUTH_OPENID_PKCE = serializers.BooleanField(required=False, label=_('Enable PKCE')) + AUTH_OPENID_CODE_CHALLENGE_METHOD = serializers.ChoiceField( + default='S256', label=_('Code challenge method'), + choices=(('S256', 'HS256'), ('plain', 'Plain')) + ) class KeycloakSettingSerializer(CommonSettingSerializer): From 6b33a54aef43d1dcbae9f65d22f9d4d4c0be0c9e Mon Sep 17 00:00:00 2001 From: jiangweidong Date: Tue, 13 Dec 2022 15:31:47 +0800 Subject: [PATCH 2/2] =?UTF-8?q?perf:=20OpenID=E6=94=AF=E6=8C=81PKCE?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/settings/auth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index b4f1d3f9f..f1fec05cd 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -77,6 +77,8 @@ AUTH_OPENID_USE_NONCE = CONFIG.AUTH_OPENID_USE_NONCE AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION AUTH_OPENID_ALWAYS_UPDATE_USER = CONFIG.AUTH_OPENID_ALWAYS_UPDATE_USER +AUTH_OPENID_PKCE = CONFIG.AUTH_OPENID_PKCE +AUTH_OPENID_CODE_CHALLENGE_METHOD = CONFIG.AUTH_OPENID_CODE_CHALLENGE_METHOD AUTH_OPENID_USER_ATTR_MAP = CONFIG.AUTH_OPENID_USER_ATTR_MAP AUTH_OPENID_AUTH_LOGIN_URL_NAME = 'authentication:openid:login' AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:openid:login-callback'