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 @@
from .backends import *
from .middleware import *
from .utils import *
from .decorator import *
......@@ -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))
......
# 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):
"""
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)
......@@ -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()
def new_realm(self):
return KeycloakRealm(
server_url=self.server_url,
realm_name=self.realm_name,
headers={}
)
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
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
@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(
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_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
......@@ -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(
......
......@@ -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
......
......@@ -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',
......
......@@ -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',
......
......@@ -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来认证
......
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