Unverified Commit 43412d7e authored by BaiJiangJie's avatar BaiJiangJie Committed by GitHub

[Update] 优化OpenID登录逻辑,配置文件添加禁用证书认证选项 (#2854)

* [Update] 优化OpenID登录逻辑,配置文件添加禁用证书认证选项

* [Update] 优化OpenID细节

* [Update] 优化OpenID, 可配置是否启用共享Session选项

* [Update] 配置文件添加OpenID默认配置项
parent 320b17c8
...@@ -4,3 +4,4 @@ ...@@ -4,3 +4,4 @@
from .backends import * from .backends import *
from .middleware import * from .middleware import *
from .utils import * from .utils import *
from .decorator import *
...@@ -20,7 +20,6 @@ __all__ = [ ...@@ -20,7 +20,6 @@ __all__ = [
class BaseOpenIDAuthorizationBackend(object): class BaseOpenIDAuthorizationBackend(object):
@staticmethod @staticmethod
def user_can_authenticate(user): def user_can_authenticate(user):
""" """
...@@ -40,25 +39,20 @@ class BaseOpenIDAuthorizationBackend(object): ...@@ -40,25 +39,20 @@ class BaseOpenIDAuthorizationBackend(object):
class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend): class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend):
def authenticate(self, request, **kwargs): def authenticate(self, request, **kwargs):
logger.info('Authentication OpenID code backend') logger.info('Authentication OpenID code backend')
code = kwargs.get('code') code = kwargs.get('code')
redirect_uri = kwargs.get('redirect_uri') redirect_uri = kwargs.get('redirect_uri')
if not code or not redirect_uri: if not code or not redirect_uri:
logger.info('Authenticate failed: No code or No redirect uri') logger.info('Authenticate failed: No code or No redirect uri')
return None return None
try: try:
oidt_profile = client.update_or_create_from_code( oidt_profile = client.update_or_create_from_code(
code=code, redirect_uri=redirect_uri code=code, redirect_uri=redirect_uri
) )
except Exception as e: except Exception as e:
logger.info('Authenticate failed: get oidt_profile: {}'.format(e)) logger.info('Authenticate failed: get oidt_profile: {}'.format(e))
return None
else: else:
# Check openid user single logout or not with access_token # Check openid user single logout or not with access_token
request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token
...@@ -68,25 +62,19 @@ class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend): ...@@ -68,25 +62,19 @@ class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend):
class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend): class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend):
def authenticate(self, request, username=None, password=None, **kwargs): def authenticate(self, request, username=None, password=None, **kwargs):
logger.info('Authentication OpenID password backend') logger.info('Authentication OpenID password backend')
if not username:
if not settings.AUTH_OPENID:
logger.info('Authenticate failed: AUTH_OPENID is False')
return None
elif not username:
logger.info('Authenticate failed: Not username') logger.info('Authenticate failed: Not username')
return None return None
try: try:
oidt_profile = client.update_or_create_from_password( oidt_profile = client.update_or_create_from_password(
username=username, password=password username=username, password=password
) )
except Exception as e: except Exception as e:
logger.error(e, exc_info=True)
logger.info('Authenticate failed: get oidt_profile: {}'.format(e)) logger.info('Authenticate failed: get oidt_profile: {}'.format(e))
return None
else: else:
user = oidt_profile.user user = oidt_profile.user
logger.info('Authenticate success: user -> {}'.format(user)) logger.info('Authenticate success: user -> {}'.format(user))
......
# 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
...@@ -19,24 +19,23 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin): ...@@ -19,24 +19,23 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin):
""" """
Check openid user single logout (with access_token) Check openid user single logout (with access_token)
""" """
def process_request(self, request): def process_request(self, request):
# Don't need openid auth if AUTH_OPENID is False # Don't need openid auth if AUTH_OPENID is False
if not settings.AUTH_OPENID: if not settings.AUTH_OPENID:
return 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 # Don't need check single logout if user not authenticated
if not request.user.is_authenticated: if not request.user.is_authenticated:
return return
elif not request.session[BACKEND_SESSION_KEY].endswith( elif not request.session[BACKEND_SESSION_KEY].endswith(
BACKEND_OPENID_AUTH_CODE): BACKEND_OPENID_AUTH_CODE):
return return
# Check openid user single logout or not with access_token # Check openid user single logout or not with access_token
client = new_client()
try: try:
client.openid_connect_client.userinfo( client = new_client()
token=request.session.get(OIDT_ACCESS_TOKEN) client.get_userinfo(token=request.session.get(OIDT_ACCESS_TOKEN))
)
except Exception as e: except Exception as e:
logout(request) logout(request)
logger.error(e) logger.error(e)
...@@ -7,12 +7,24 @@ from keycloak.realm import KeycloakRealm ...@@ -7,12 +7,24 @@ from keycloak.realm import KeycloakRealm
from keycloak.keycloak_openid import KeycloakOpenID from keycloak.keycloak_openid import KeycloakOpenID
from .signals import post_create_openid_user from .signals import post_create_openid_user
from .decorator import ssl_verification
OIDT_ACCESS_TOKEN = 'oidt_access_token' 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): def __init__(self, user, access_token, refresh_token):
""" """
:param user: User object :param user: User object
...@@ -28,80 +40,109 @@ class OpenIDTokenProfile(object): ...@@ -28,80 +40,109 @@ class OpenIDTokenProfile(object):
class Client(object): class Client(object):
def __init__(self, server_url, realm_name, client_id, client_secret): def __init__(self, server_url, realm_name, client_id, client_secret):
self.server_url = server_url self.server_url = server_url
self.realm_name = realm_name self.realm_name = realm_name
self.client_id = client_id self.client_id = client_id
self.client_secret = client_secret self.client_secret = client_secret
self.realm = self.new_realm() self._openid_client = None
self.openid_client = self.new_openid_client() self._realm = None
self.openid_connect_client = self.new_openid_connect_client() self._openid_connect_client = None
def new_realm(self): @property
return KeycloakRealm( def realm(self):
server_url=self.server_url, if self._realm is None:
realm_name=self.realm_name, self._realm = KeycloakRealm(
headers={} 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 :rtype: keycloak.openid_connect.KeycloakOpenidConnect
""" """
openid_connect = self.realm.open_id_connect( if self._openid_connect_client is None:
client_id=self.client_id, self._openid_connect_client = self.realm.open_id_connect(
client_secret=self.client_secret client_id=self.client_id,
) client_secret=self.client_secret
return openid_connect )
return self._openid_connect_client
def new_openid_client(self): @property
def openid_client(self):
""" """
:rtype: keycloak.keycloak_openid.KeycloakOpenID :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')
return KeycloakOpenID( @ssl_verification
server_url='%sauth/' % self.server_url, def get_authorization_url(self, redirect_uri, scope, state):
realm_name=self.realm_name, url = self.openid_connect_client.authorization_url(
client_id=self.client_id, redirect_uri=redirect_uri, scope=scope, state=state
client_secret_key=self.client_secret,
) )
return url
def update_or_create_from_password(self, username, password): @ssl_verification
""" def get_userinfo(self, token):
Update or create an user based on an authentication username and password. user_info = self.openid_connect_client.userinfo(token=token)
return user_info
:param str username: authentication username @ssl_verification
:param str password: authentication password def authorization_code(self, code, redirect_uri):
:return: OpenIDTokenProfile 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( token_response = self.openid_client.token(
username=username, password=password username=username, password=password
) )
return token_response
return self._update_or_create(token_response=token_response)
def update_or_create_from_code(self, code, redirect_uri): def update_or_create_from_code(self, code, redirect_uri):
""" """
Update or create an user based on an authentication code. Update or create an user based on an authentication code.
Response as specified in: Response as specified in:
https://tools.ietf.org/html/rfc6749#section-4.1.4 https://tools.ietf.org/html/rfc6749#section-4.1.4
:param str code: authentication code :param str code: authentication code
:param str redirect_uri: :param str redirect_uri:
:rtype: OpenIDTokenProfile :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( def update_or_create_from_password(self, username, password):
code=code, redirect_uri=redirect_uri) """
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) return self._update_or_create(token_response=token_response)
def _update_or_create(self, token_response): def _update_or_create(self, token_response):
""" """
Update or create an user based on a token response. Update or create an user based on a token response.
`token_response` contains the items returned by the OpenIDConnect Token API `token_response` contains the items returned by the OpenIDConnect Token API
end-point: end-point:
- id_token - id_token
...@@ -109,14 +150,10 @@ class Client(object): ...@@ -109,14 +150,10 @@ class Client(object):
- expires_in - expires_in
- refresh_token - refresh_token
- refresh_expires_in - refresh_expires_in
:param dict token_response: :param dict token_response:
:rtype: OpenIDTokenProfile :rtype: OpenIDTokenProfile
""" """
userinfo = self.get_userinfo(token=token_response['access_token'])
userinfo = self.openid_connect_client.userinfo(
token=token_response['access_token'])
with transaction.atomic(): with transaction.atomic():
user, _ = get_user_model().objects.update_or_create( user, _ = get_user_model().objects.update_or_create(
username=userinfo.get('preferred_username', ''), username=userinfo.get('preferred_username', ''),
...@@ -126,13 +163,11 @@ class Client(object): ...@@ -126,13 +163,11 @@ class Client(object):
'last_name': userinfo.get('family_name', '') 'last_name': userinfo.get('family_name', '')
} }
) )
oidt_profile = OpenIDTokenProfile( oidt_profile = OpenIDTokenProfile(
user=user, user=user,
access_token=token_response['access_token'], access_token=token_response['access_token'],
refresh_token=token_response['refresh_token'], refresh_token=token_response['refresh_token'],
) )
if user: if user:
post_create_openid_user.send(sender=user.__class__, user=user) post_create_openid_user.send(sender=user.__class__, user=user)
...@@ -140,17 +175,3 @@ class Client(object): ...@@ -140,17 +175,3 @@ class Client(object):
def __str__(self): def __str__(self):
return self.client_id 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
...@@ -24,7 +24,6 @@ __all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView'] ...@@ -24,7 +24,6 @@ __all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView']
class OpenIDLoginView(RedirectView): class OpenIDLoginView(RedirectView):
def get_redirect_url(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
redirect_uri = settings.BASE_SITE_URL + str(settings.LOGIN_COMPLETE_URL) redirect_uri = settings.BASE_SITE_URL + str(settings.LOGIN_COMPLETE_URL)
nonce = Nonce( nonce = Nonce(
...@@ -32,42 +31,36 @@ class OpenIDLoginView(RedirectView): ...@@ -32,42 +31,36 @@ class OpenIDLoginView(RedirectView):
next_path=self.request.GET.get('next') next_path=self.request.GET.get('next')
) )
cache.set(str(nonce.state), nonce, 24*3600) cache.set(str(nonce.state), nonce, 24*3600)
self.request.session['openid_state'] = str(nonce.state) self.request.session['openid_state'] = str(nonce.state)
authorization_url = client.openid_connect_client.\ authorization_url = client.get_authorization_url(
authorization_url( redirect_uri=nonce.redirect_uri,
redirect_uri=nonce.redirect_uri, scope='code', scope='code',
state=str(nonce.state) state=str(nonce.state)
) )
return authorization_url return authorization_url
class OpenIDLoginCompleteView(RedirectView): class OpenIDLoginCompleteView(RedirectView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if 'error' in request.GET: if 'error' in request.GET:
return HttpResponseServerError(self.request.GET['error']) return HttpResponseServerError(self.request.GET['error'])
if 'code' not in self.request.GET and 'state' not in self.request.GET: 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']: if self.request.GET['state'] != self.request.session['openid_state']:
return HttpResponseBadRequest() return HttpResponseBadRequest(content='State invalid')
nonce = cache.get(self.request.GET['state']) nonce = cache.get(self.request.GET['state'])
if not nonce: if not nonce:
return HttpResponseBadRequest() return HttpResponseBadRequest(content='State failure')
user = authenticate( user = authenticate(
request=self.request, request=self.request,
code=self.request.GET['code'], code=self.request.GET['code'],
redirect_uri=nonce.redirect_uri redirect_uri=nonce.redirect_uri
) )
cache.delete(str(nonce.state)) cache.delete(str(nonce.state))
if not user: if not user:
return HttpResponseBadRequest() return HttpResponseBadRequest(content='Authenticate user failed')
login(self.request, user) login(self.request, user)
post_openid_login_success.send( post_openid_login_success.send(
......
...@@ -18,19 +18,17 @@ from .signals import post_auth_success, post_auth_failed ...@@ -18,19 +18,17 @@ from .signals import post_auth_success, post_auth_failed
def on_user_logged_out(sender, request, user, **kwargs): def on_user_logged_out(sender, request, user, **kwargs):
if not settings.AUTH_OPENID: if not settings.AUTH_OPENID:
return return
if not settings.AUTH_OPENID_SHARE_SESSION:
return
query = QueryDict('', mutable=True) query = QueryDict('', mutable=True)
query.update({ query.update({
'redirect_uri': settings.BASE_SITE_URL 'redirect_uri': settings.BASE_SITE_URL
}) })
client = new_client() client = new_client()
openid_logout_url = "%s?%s" % ( openid_logout_url = "%s?%s" % (
client.openid_connect_client.get_url( client.get_url_end_session_endpoint(),
name='end_session_endpoint'),
query.urlencode() query.urlencode()
) )
request.COOKIES['next'] = openid_logout_url request.COOKIES['next'] = openid_logout_url
......
...@@ -344,6 +344,8 @@ defaults = { ...@@ -344,6 +344,8 @@ defaults = {
'SESSION_COOKIE_AGE': 3600 * 24, 'SESSION_COOKIE_AGE': 3600 * 24,
'SESSION_EXPIRE_AT_BROWSER_CLOSE': False, 'SESSION_EXPIRE_AT_BROWSER_CLOSE': False,
'AUTH_OPENID': False, 'AUTH_OPENID': False,
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
'AUTH_OPENID_SHARE_SESSION': False,
'OTP_VALID_WINDOW': 0, 'OTP_VALID_WINDOW': 0,
'OTP_ISSUER_NAME': 'Jumpserver', 'OTP_ISSUER_NAME': 'Jumpserver',
'EMAIL_SUFFIX': 'jumpserver.org', 'EMAIL_SUFFIX': 'jumpserver.org',
......
...@@ -456,6 +456,8 @@ AUTH_OPENID_SERVER_URL = CONFIG.AUTH_OPENID_SERVER_URL ...@@ -456,6 +456,8 @@ AUTH_OPENID_SERVER_URL = CONFIG.AUTH_OPENID_SERVER_URL
AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME
AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID
AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET 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 = [ AUTH_OPENID_BACKENDS = [
'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend', 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend',
'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend', 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend',
......
...@@ -61,6 +61,8 @@ REDIS_PORT: 6379 ...@@ -61,6 +61,8 @@ REDIS_PORT: 6379
# AUTH_OPENID_REALM_NAME: realm-name # AUTH_OPENID_REALM_NAME: realm-name
# AUTH_OPENID_CLIENT_ID: client-id # AUTH_OPENID_CLIENT_ID: client-id
# AUTH_OPENID_CLIENT_SECRET: client-secret # AUTH_OPENID_CLIENT_SECRET: client-secret
# AUTH_OPENID_IGNORE_SSL_VERIFICATION: True
# AUTH_OPENID_SHARE_SESSION: False
# #
# Use Radius authorization # Use Radius authorization
# 使用Radius来认证 # 使用Radius来认证
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment