mirror of https://github.com/jumpserver/jumpserver
Fix rbac (#7699)
* perf: 优化 suggesstion * perf: 修改 migrations * feat: 添加OIDC认证逻辑 * perf: 修改 backend * perf: 优化认证backends * perf: 优化认证backends * perf: 优化CAS认证, 用户多域名进行访问时回调到各自域名 Co-authored-by: ibuler <ibuler@qq.com>pull/7700/head
parent
02ca473492
commit
edfca5eb24
|
@ -0,0 +1,51 @@
|
|||
from django.contrib.auth.backends import BaseBackend
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class JMSBaseAuthBackend:
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return True
|
||||
|
||||
def has_perm(self, user_obj, perm, obj=None):
|
||||
return False
|
||||
|
||||
# can authenticate
|
||||
def username_can_authenticate(self, username):
|
||||
return self.allow_authenticate(username=username)
|
||||
|
||||
def user_can_authenticate(self, user):
|
||||
if not self.allow_authenticate(user=user):
|
||||
return False
|
||||
is_valid = getattr(user, 'is_valid', None)
|
||||
return is_valid or is_valid is None
|
||||
|
||||
@property
|
||||
def backend_path(self):
|
||||
return f'{self.__module__}.{self.__class__.__name__}'
|
||||
|
||||
def allow_authenticate(self, user=None, username=None):
|
||||
if user:
|
||||
allowed_backends = user.get_allowed_auth_backends()
|
||||
else:
|
||||
allowed_backends = User.get_user_allowed_auth_backends(username)
|
||||
if allowed_backends is None:
|
||||
# 特殊值 None 表示没有限制
|
||||
return True
|
||||
allow = self.backend_path in allowed_backends
|
||||
if not allow:
|
||||
info = 'User {} skip authentication backend {}, because it not in {}'
|
||||
info = info.format(username, self.backend_path, ','.join(allowed_backends))
|
||||
logger.debug(info)
|
||||
return allow
|
||||
|
||||
|
||||
class JMSModelBackend(JMSBaseAuthBackend, ModelBackend):
|
||||
pass
|
|
@ -1,3 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .backends import *
|
||||
|
||||
# 保证 utils 中的模块进行初始化
|
||||
from . import utils
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django_cas_ng.backends import CASBackend as _CASBackend
|
||||
from django.conf import settings
|
||||
|
||||
from ..base import JMSBaseAuthBackend
|
||||
|
||||
__all__ = ['CASBackend']
|
||||
|
||||
|
||||
class CASBackend(_CASBackend):
|
||||
def user_can_authenticate(self, user):
|
||||
return True
|
||||
class CASBackend(JMSBaseAuthBackend, _CASBackend):
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_CAS
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
from django_cas_ng import utils
|
||||
from django_cas_ng.utils import (
|
||||
django_settings, get_protocol,
|
||||
urllib_parse, REDIRECT_FIELD_NAME, get_redirect_url
|
||||
)
|
||||
|
||||
|
||||
def get_service_url(request, redirect_to=None):
|
||||
"""
|
||||
重写 get_service url 方法, CAS_ROOT_PROXIED_AS 为空时, 支持跳转回当前访问的域名地址
|
||||
"""
|
||||
"""Generates application django service URL for CAS"""
|
||||
if getattr(django_settings, 'CAS_ROOT_PROXIED_AS', None):
|
||||
service = django_settings.CAS_ROOT_PROXIED_AS + request.path
|
||||
else:
|
||||
protocol = get_protocol(request)
|
||||
host = request.get_host()
|
||||
service = urllib_parse.urlunparse(
|
||||
(protocol, host, request.path, '', '', ''),
|
||||
)
|
||||
if not django_settings.CAS_STORE_NEXT:
|
||||
if '?' in service:
|
||||
service += '&'
|
||||
else:
|
||||
service += '?'
|
||||
service += urllib_parse.urlencode({
|
||||
REDIRECT_FIELD_NAME: redirect_to or get_redirect_url(request)
|
||||
})
|
||||
return service
|
||||
|
||||
|
||||
utils.get_service_url = get_service_url
|
|
@ -8,13 +8,14 @@ from django.core.cache import cache
|
|||
from django.utils.translation import ugettext as _
|
||||
from six import text_type
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework import authentication, exceptions
|
||||
from common.auth import signature
|
||||
|
||||
from common.utils import get_object_or_none, make_signature, http_to_unixtime
|
||||
from ..models import AccessKey, PrivateToken
|
||||
from .base import JMSBaseAuthBackend, JMSModelBackend
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
@ -28,21 +29,6 @@ def get_request_date_header(request):
|
|||
return date
|
||||
|
||||
|
||||
class JMSModelBackend(ModelBackend):
|
||||
def has_perm(self, user_obj, perm, obj=None):
|
||||
return False
|
||||
|
||||
def user_can_authenticate(self, user):
|
||||
return True
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
user = UserModel._default_manager.get(pk=user_id)
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
return user if user.is_valid else None
|
||||
|
||||
|
||||
class AccessKeyAuthentication(authentication.BaseAuthentication):
|
||||
"""App使用Access key进行签名认证, 目前签名算法比较简单,
|
||||
app注册或者手动建立后,会生成 access_key_id 和 access_key_secret,
|
||||
|
@ -167,7 +153,7 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
|
|||
return self.keyword
|
||||
|
||||
|
||||
class PrivateTokenAuthentication(authentication.TokenAuthentication):
|
||||
class PrivateTokenAuthentication(JMSBaseAuthBackend, authentication.TokenAuthentication):
|
||||
model = PrivateToken
|
||||
|
||||
|
||||
|
@ -215,46 +201,3 @@ class SignatureAuthentication(signature.SignatureAuthentication):
|
|||
except AccessKey.DoesNotExist:
|
||||
return None, None
|
||||
|
||||
|
||||
class SSOAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
def authenticate(self, request, sso_token=None, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class WeComAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class DingTalkAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class FeiShuAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationTokenAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
|
@ -1,43 +1,38 @@
|
|||
# coding:utf-8
|
||||
#
|
||||
|
||||
import warnings
|
||||
import ldap
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
||||
from django_auth_ldap.backend import _LDAPUser, LDAPBackend, LDAPSettings
|
||||
from django_auth_ldap.backend import _LDAPUser, LDAPBackend
|
||||
from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion
|
||||
|
||||
from users.utils import construct_user_email
|
||||
from common.const import LDAP_AD_ACCOUNT_DISABLE
|
||||
from .base import JMSBaseAuthBackend
|
||||
|
||||
logger = _LDAPConfig.get_logger()
|
||||
|
||||
|
||||
class LDAPAuthorizationBackend(LDAPBackend):
|
||||
class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
|
||||
"""
|
||||
Override this class to override _LDAPUser to LDAPUser
|
||||
"""
|
||||
@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 is_enabled():
|
||||
return settings.AUTH_LDAP
|
||||
|
||||
def get_or_build_user(self, username, ldap_user):
|
||||
"""
|
||||
This must return a (User, built) 2-tuple for the given LDAP user.
|
||||
This must return a (User, built) 2-tuple for the given LDAP user.
|
||||
|
||||
username is the Django-friendly username of the user. ldap_user.dn is
|
||||
the user's DN and ldap_user.attrs contains all of their LDAP
|
||||
attributes.
|
||||
username is the Django-friendly username of the user. ldap_user.dn is
|
||||
the user's DN and ldap_user.attrs contains all of their LDAP
|
||||
attributes.
|
||||
|
||||
The returned User object may be an unsaved model instance.
|
||||
The returned User object may be an unsaved model instance.
|
||||
|
||||
"""
|
||||
"""
|
||||
model = self.get_user_model()
|
||||
|
||||
if self.settings.USER_QUERY_FIELD:
|
||||
|
|
|
@ -0,0 +1,288 @@
|
|||
"""
|
||||
OpenID Connect relying party (RP) authentication backends
|
||||
=========================================================
|
||||
|
||||
This modules defines backends allowing to authenticate a user using a specific token endpoint
|
||||
of an OpenID Connect provider (OP).
|
||||
|
||||
"""
|
||||
|
||||
import base64
|
||||
import requests
|
||||
from rest_framework.exceptions import ParseError
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.db import transaction
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
|
||||
from ..base import JMSBaseAuthBackend
|
||||
from .utils import validate_and_return_id_token, build_absolute_uri
|
||||
from .decorator import ssl_verification
|
||||
from .signals import (
|
||||
openid_create_or_update_user, openid_user_login_failed, openid_user_login_success
|
||||
)
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class UserMixin:
|
||||
|
||||
@transaction.atomic
|
||||
def get_or_create_user_from_claims(self, request, claims):
|
||||
log_prompt = "Get or Create user from claims [ActionForUser]: {}"
|
||||
logger.debug(log_prompt.format('start'))
|
||||
|
||||
sub = claims['sub']
|
||||
name = claims.get('name', sub)
|
||||
username = claims.get('preferred_username', sub)
|
||||
email = claims.get('email', "{}@{}".format(username, 'jumpserver.openid'))
|
||||
logger.debug(
|
||||
log_prompt.format(
|
||||
"sub: {}|name: {}|username: {}|email: {}".format(sub, name, username, email)
|
||||
)
|
||||
)
|
||||
|
||||
user, created = get_user_model().objects.get_or_create(
|
||||
username=username, defaults={"name": name, "email": email}
|
||||
)
|
||||
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
|
||||
logger.debug(log_prompt.format("Send signal => openid create or update user"))
|
||||
openid_create_or_update_user.send(
|
||||
sender=self.__class__, request=request, user=user, created=created,
|
||||
name=name, username=username, email=email
|
||||
)
|
||||
return user, created
|
||||
|
||||
|
||||
class OIDCBaseBackend(UserMixin, JMSBaseAuthBackend, ModelBackend):
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_OPENID
|
||||
|
||||
|
||||
class OIDCAuthCodeBackend(OIDCBaseBackend):
|
||||
""" Allows to authenticate users using an OpenID Connect Provider (OP).
|
||||
|
||||
This authentication backend is able to authenticate users in the case of the OpenID Connect
|
||||
Authorization Code flow. The ``authenticate`` method provided by this backend is likely to be
|
||||
called when the callback URL is requested by the OpenID Connect Provider (OP). Thus it will
|
||||
call the OIDC provider again in order to request a valid token using the authorization code that
|
||||
should be available in the request parameters associated with the callback call.
|
||||
|
||||
"""
|
||||
|
||||
@ssl_verification
|
||||
def authenticate(self, request, nonce=None, **kwargs):
|
||||
""" Authenticates users in case of the OpenID Connect Authorization code flow. """
|
||||
log_prompt = "Process authenticate [OIDCAuthCodeBackend]: {}"
|
||||
logger.debug(log_prompt.format('start'))
|
||||
|
||||
# NOTE: the request object is mandatory to perform the authentication using an authorization
|
||||
# code provided by the OIDC supplier.
|
||||
if (nonce is None and settings.AUTH_OPENID_USE_NONCE) or request is None:
|
||||
logger.debug(log_prompt.format('Request or nonce value is missing'))
|
||||
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 settings.AUTH_OPENID_USE_STATE) or code is None:
|
||||
logger.debug(log_prompt.format('Authorization code or state value is missing'))
|
||||
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.
|
||||
logger.debug(log_prompt.format('Prepares token payload'))
|
||||
token_payload = {
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
|
||||
# Prepares the token headers that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
logger.debug(log_prompt.format('Prepares token headers'))
|
||||
basic_token = "{}:{}".format(settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET)
|
||||
headers = {"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())}
|
||||
|
||||
# Calls the token endpoint.
|
||||
logger.debug(log_prompt.format('Call the token endpoint'))
|
||||
token_response = requests.post(
|
||||
settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload, headers=headers
|
||||
)
|
||||
try:
|
||||
token_response.raise_for_status()
|
||||
token_response_data = token_response.json()
|
||||
except Exception as e:
|
||||
error = "Json token response error, token response " \
|
||||
"content is: {}, error is: {}".format(token_response.content, str(e))
|
||||
logger.debug(log_prompt.format(error))
|
||||
raise ParseError(error)
|
||||
|
||||
# Validates the token.
|
||||
logger.debug(log_prompt.format('Validate ID 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:
|
||||
logger.debug(log_prompt.format(
|
||||
'ID Token is missing, raw id token is: {}'.format(raw_id_token))
|
||||
)
|
||||
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.
|
||||
# https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
if settings.AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS:
|
||||
logger.debug(log_prompt.format('ID Token in claims'))
|
||||
claims = id_token
|
||||
else:
|
||||
# Fetches the claims (user information) from the userinfo endpoint provided by the OP.
|
||||
logger.debug(log_prompt.format('Fetches the claims from the userinfo endpoint'))
|
||||
claims_response = requests.get(
|
||||
settings.AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT,
|
||||
headers={'Authorization': 'Bearer {0}'.format(access_token)}
|
||||
)
|
||||
try:
|
||||
claims_response.raise_for_status()
|
||||
claims = claims_response.json()
|
||||
except Exception as e:
|
||||
error = "Json claims response error, claims response " \
|
||||
"content is: {}, error is: {}".format(claims_response.content, str(e))
|
||||
logger.debug(log_prompt.format(error))
|
||||
raise ParseError(error)
|
||||
|
||||
logger.debug(log_prompt.format('Get or create user from claims'))
|
||||
user, created = self.get_or_create_user_from_claims(request, claims)
|
||||
|
||||
logger.debug(log_prompt.format('Update or create oidc user'))
|
||||
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('OpenID user login success'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login success'))
|
||||
openid_user_login_success.send(sender=self.__class__, request=request, user=user)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('OpenID user login failed'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=user.username,
|
||||
reason="User is invalid"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
||||
|
||||
@ssl_verification
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
try:
|
||||
return self._authenticate(request, username, password, **kwargs)
|
||||
except Exception as e:
|
||||
error = f'Authenticate exception: {e}'
|
||||
logger.error(error, exc_info=True)
|
||||
return
|
||||
|
||||
def _authenticate(self, request, username=None, password=None, **kwargs):
|
||||
"""
|
||||
https://oauth.net/2/
|
||||
https://aaronparecki.com/oauth-2-simplified/#password
|
||||
"""
|
||||
log_prompt = "Process authenticate [OIDCAuthPasswordBackend]: {}"
|
||||
logger.debug(log_prompt.format('start'))
|
||||
request_timeout = 15
|
||||
|
||||
if not username or not password:
|
||||
logger.debug(log_prompt.format('Username or password is missing'))
|
||||
return
|
||||
|
||||
# Prepares the token payload that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
logger.debug(log_prompt.format('Prepares token payload'))
|
||||
token_payload = {
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
|
||||
'grant_type': 'password',
|
||||
'username': username,
|
||||
'password': password,
|
||||
}
|
||||
|
||||
# Calls the token endpoint.
|
||||
logger.debug(log_prompt.format('Call the token endpoint'))
|
||||
token_response = requests.post(settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload, timeout=request_timeout)
|
||||
try:
|
||||
token_response.raise_for_status()
|
||||
token_response_data = token_response.json()
|
||||
except Exception as e:
|
||||
error = "Json token response error, token response " \
|
||||
"content is: {}, error is: {}".format(token_response.content, str(e))
|
||||
logger.debug(log_prompt.format(error))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason=error
|
||||
)
|
||||
return
|
||||
|
||||
# Retrieves the access token
|
||||
access_token = token_response_data.get('access_token')
|
||||
|
||||
# Fetches the claims (user information) from the userinfo endpoint provided by the OP.
|
||||
logger.debug(log_prompt.format('Fetches the claims from the userinfo endpoint'))
|
||||
claims_response = requests.get(
|
||||
settings.AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT,
|
||||
headers={'Authorization': 'Bearer {0}'.format(access_token)},
|
||||
timeout=request_timeout
|
||||
)
|
||||
try:
|
||||
claims_response.raise_for_status()
|
||||
claims = claims_response.json()
|
||||
except Exception as e:
|
||||
error = "Json claims response error, claims response " \
|
||||
"content is: {}, error is: {}".format(claims_response.content, str(e))
|
||||
logger.debug(log_prompt.format(error))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason=error
|
||||
)
|
||||
return
|
||||
|
||||
logger.debug(log_prompt.format('Get or create user from claims'))
|
||||
user, created = self.get_or_create_user_from_claims(request, claims)
|
||||
|
||||
logger.debug(log_prompt.format('Update or create oidc user'))
|
||||
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('OpenID user login success'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login success'))
|
||||
openid_user_login_success.send(
|
||||
sender=self.__class__, request=request, user=user
|
||||
)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('OpenID user login failed'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason="User is invalid"
|
||||
)
|
||||
return None
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
|
||||
import warnings
|
||||
import contextlib
|
||||
import requests
|
||||
|
||||
from django.conf import settings
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
from .utils import get_logger
|
||||
|
||||
__all__ = ['ssl_verification']
|
||||
|
||||
old_merge_environment_settings = requests.Session.merge_environment_settings
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
@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
|
|
@ -1,10 +1,106 @@
|
|||
from jms_oidc_rp.middleware import OIDCRefreshIDTokenMiddleware as _OIDCRefreshIDTokenMiddleware
|
||||
import time
|
||||
import requests
|
||||
import requests.exceptions
|
||||
|
||||
from django.core.exceptions import MiddlewareNotUsed
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from common.utils import get_logger
|
||||
|
||||
from .utils import validate_and_return_id_token
|
||||
from .decorator import ssl_verification
|
||||
|
||||
|
||||
class OIDCRefreshIDTokenMiddleware(_OIDCRefreshIDTokenMiddleware):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class OIDCRefreshIDTokenMiddleware:
|
||||
""" Allows to periodically refresh the ID token associated with the authenticated user. """
|
||||
|
||||
def __init__(self, get_response):
|
||||
if not settings.AUTH_OPENID:
|
||||
raise MiddlewareNotUsed
|
||||
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
# Refreshes tokens only in the applicable cases.
|
||||
if request.method == 'GET' and not request.is_ajax() and request.user.is_authenticated and settings.AUTH_OPENID:
|
||||
self.refresh_token(request)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
@ssl_verification
|
||||
def refresh_token(self, request):
|
||||
""" Refreshes the token of the current user. """
|
||||
|
||||
log_prompt = "Process refresh Token: {}"
|
||||
# logger.debug(log_prompt.format('Start'))
|
||||
|
||||
# NOTE: SHARE_SESSION is False means that the user does not share sessions
|
||||
# with other applications
|
||||
if not settings.AUTH_OPENID_SHARE_SESSION:
|
||||
logger.debug(log_prompt.format('Not share session'))
|
||||
return
|
||||
|
||||
# NOTE: no refresh token in the session means that the user wasn't authentified using the
|
||||
# OpenID Connect provider (OP).
|
||||
refresh_token = request.session.get('oidc_auth_refresh_token')
|
||||
if refresh_token is None:
|
||||
logger.debug(log_prompt.format('Refresh token is missing'))
|
||||
return
|
||||
|
||||
id_token_exp_timestamp = request.session.get('oidc_auth_id_token_exp_timestamp', None)
|
||||
now_timestamp = time.time()
|
||||
# Returns immediately if the token is still valid.
|
||||
if id_token_exp_timestamp is not None and id_token_exp_timestamp > now_timestamp:
|
||||
# logger.debug(log_prompt.format('Returns immediately because token is still valid'))
|
||||
return
|
||||
|
||||
# Prepares the token payload that will be used to request a new token from the token
|
||||
# endpoint.
|
||||
refresh_token = request.session.pop('oidc_auth_refresh_token')
|
||||
token_payload = {
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': refresh_token,
|
||||
'scope': settings.AUTH_OPENID_SCOPES,
|
||||
}
|
||||
|
||||
# Calls the token endpoint.
|
||||
logger.debug(log_prompt.format('Calls the token endpoint'))
|
||||
token_response = requests.post(settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload)
|
||||
try:
|
||||
token_response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logger.debug(log_prompt.format('Request exception http error: {}'.format(str(e))))
|
||||
logger.debug(log_prompt.format('Logout'))
|
||||
auth.logout(request)
|
||||
return
|
||||
token_response_data = token_response.json()
|
||||
|
||||
# Validates the token.
|
||||
logger.debug(log_prompt.format('Validate ID Token'))
|
||||
raw_id_token = token_response_data.get('id_token')
|
||||
id_token = validate_and_return_id_token(raw_id_token, validate_nonce=False)
|
||||
|
||||
# If the token cannot be validated we have to log out the current user.
|
||||
if id_token is None:
|
||||
logger.debug(log_prompt.format('ID Token is None'))
|
||||
auth.logout(request)
|
||||
logger.debug(log_prompt.format('Logout'))
|
||||
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
|
||||
|
||||
# Saves the new expiration timestamp.
|
||||
request.session['oidc_auth_id_token_exp_timestamp'] = \
|
||||
time.time() + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
"""
|
||||
OpenID Connect relying party (RP) signals
|
||||
=========================================
|
||||
|
||||
This modules defines Django signals that can be triggered during the OpenID Connect
|
||||
authentication process.
|
||||
|
||||
"""
|
||||
|
||||
from django.dispatch import Signal
|
||||
|
||||
|
||||
openid_create_or_update_user = Signal(
|
||||
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
|
||||
)
|
||||
openid_user_login_success = Signal(providing_args=['request', 'user'])
|
||||
openid_user_login_failed = Signal(providing_args=['request', 'username', 'reason'])
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
"""
|
||||
OpenID Connect relying party (RP) URLs
|
||||
======================================
|
||||
|
||||
This modules defines the URLs allowing to perform OpenID Connect flows on a Relying Party (RP).
|
||||
It defines three main endpoints: the authentication request endpoint, the authentication
|
||||
callback endpoint and the end session endpoint.
|
||||
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', views.OIDCAuthRequestView.as_view(), name='login'),
|
||||
path('callback/', views.OIDCAuthCallbackView.as_view(), name='login-callback'),
|
||||
path('logout/', views.OIDCEndSessionView.as_view(), name='logout'),
|
||||
]
|
|
@ -0,0 +1,126 @@
|
|||
"""
|
||||
OpenID Connect relying party (RP) utilities
|
||||
===========================================
|
||||
|
||||
This modules defines utilities allowing to manipulate ID tokens and other common helpers.
|
||||
|
||||
"""
|
||||
|
||||
import datetime as dt
|
||||
from calendar import timegm
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.encoding import force_bytes, smart_bytes
|
||||
from jwkest import JWKESTException
|
||||
from jwkest.jwk import KEYS
|
||||
from jwkest.jws import JWS
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def validate_and_return_id_token(jws, nonce=None, validate_nonce=True):
|
||||
""" Validates the id_token according to the OpenID Connect specification. """
|
||||
log_prompt = "Validate ID Token: {}"
|
||||
logger.debug(log_prompt.format('Get shared key'))
|
||||
shared_key = settings.AUTH_OPENID_CLIENT_ID \
|
||||
if settings.AUTH_OPENID_PROVIDER_SIGNATURE_ALG == 'HS256' \
|
||||
else settings.AUTH_OPENID_PROVIDER_SIGNATURE_KEY # RS256
|
||||
|
||||
try:
|
||||
# Decodes the JSON Web Token and raise an error if the signature is invalid.
|
||||
logger.debug(log_prompt.format('Verify compact jwk'))
|
||||
id_token = JWS().verify_compact(force_bytes(jws), _get_jwks_keys(shared_key))
|
||||
except JWKESTException as e:
|
||||
logger.debug(log_prompt.format('Verify compact jwkest exception: {}'.format(str(e))))
|
||||
return
|
||||
|
||||
# Validates the claims embedded in the id_token.
|
||||
logger.debug(log_prompt.format('Validate claims'))
|
||||
_validate_claims(id_token, nonce=nonce, validate_nonce=validate_nonce)
|
||||
|
||||
return id_token
|
||||
|
||||
|
||||
def _get_jwks_keys(shared_key):
|
||||
""" Returns JWKS keys used to decrypt id_token values. """
|
||||
# The OpenID Connect Provider (OP) uses RSA keys to sign/enrypt ID tokens and generate public
|
||||
# keys allowing to decrypt them. These public keys are exposed through the 'jwks_uri' and should
|
||||
# be used to decrypt the JWS - JSON Web Signature.
|
||||
log_prompt = "Get jwks keys: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
jwks_keys = KEYS()
|
||||
logger.debug(log_prompt.format('Load from provider jwks endpoint'))
|
||||
jwks_keys.load_from_url(settings.AUTH_OPENID_PROVIDER_JWKS_ENDPOINT)
|
||||
# Adds the shared key (which can correspond to the client_secret) as an oct key so it can be
|
||||
# used for HMAC signatures.
|
||||
logger.debug(log_prompt.format('Add key'))
|
||||
jwks_keys.add({'key': smart_bytes(shared_key), 'kty': 'oct'})
|
||||
logger.debug(log_prompt.format('End'))
|
||||
return jwks_keys
|
||||
|
||||
|
||||
def _validate_claims(id_token, nonce=None, validate_nonce=True):
|
||||
""" Validates the claims embedded in the JSON Web Token. """
|
||||
log_prompt = "Validate claims: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
|
||||
iss_parsed_url = urlparse(id_token['iss'])
|
||||
provider_parsed_url = urlparse(settings.AUTH_OPENID_PROVIDER_ENDPOINT)
|
||||
if iss_parsed_url.netloc != provider_parsed_url.netloc:
|
||||
logger.debug(log_prompt.format('Invalid issuer'))
|
||||
raise SuspiciousOperation('Invalid issuer')
|
||||
|
||||
if isinstance(id_token['aud'], str):
|
||||
id_token['aud'] = [id_token['aud']]
|
||||
|
||||
if settings.AUTH_OPENID_CLIENT_ID not in id_token['aud']:
|
||||
logger.debug(log_prompt.format('Invalid audience'))
|
||||
raise SuspiciousOperation('Invalid audience')
|
||||
|
||||
if len(id_token['aud']) > 1 and 'azp' not in id_token:
|
||||
logger.debug(log_prompt.format('Incorrect id_token: azp'))
|
||||
raise SuspiciousOperation('Incorrect id_token: azp')
|
||||
|
||||
if 'azp' in id_token and id_token['azp'] != settings.AUTH_OPENID_CLIENT_ID:
|
||||
raise SuspiciousOperation('Incorrect id_token: azp')
|
||||
|
||||
utc_timestamp = timegm(dt.datetime.utcnow().utctimetuple())
|
||||
if utc_timestamp > id_token['exp']:
|
||||
logger.debug(log_prompt.format('Signature has expired'))
|
||||
raise SuspiciousOperation('Signature has expired')
|
||||
|
||||
if 'nbf' in id_token and utc_timestamp < id_token['nbf']:
|
||||
logger.debug(log_prompt.format('Incorrect id_token: nbf'))
|
||||
raise SuspiciousOperation('Incorrect id_token: nbf')
|
||||
|
||||
# Verifies that the token was issued in the allowed timeframe.
|
||||
if utc_timestamp > id_token['iat'] + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE:
|
||||
logger.debug(log_prompt.format('Incorrect id_token: iat'))
|
||||
raise SuspiciousOperation('Incorrect id_token: iat')
|
||||
|
||||
# Validate the nonce to ensure the request was not modified if applicable.
|
||||
id_token_nonce = id_token.get('nonce', None)
|
||||
if validate_nonce and settings.AUTH_OPENID_USE_NONCE and id_token_nonce != nonce:
|
||||
logger.debug(log_prompt.format('Incorrect id_token: nonce'))
|
||||
raise SuspiciousOperation('Incorrect id_token: nonce')
|
||||
|
||||
logger.debug(log_prompt.format('End'))
|
||||
|
||||
|
||||
def build_absolute_uri(request, path=None):
|
||||
"""
|
||||
Build absolute redirect uri
|
||||
"""
|
||||
if path is None:
|
||||
path = '/'
|
||||
|
||||
if settings.BASE_SITE_URL:
|
||||
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
|
||||
else:
|
||||
redirect_uri = request.build_absolute_uri(path)
|
||||
return redirect_uri
|
|
@ -0,0 +1,222 @@
|
|||
"""
|
||||
OpenID Connect relying party (RP) views
|
||||
=======================================
|
||||
|
||||
This modules defines views allowing to start the authorization and authentication process in
|
||||
order to authenticate a specific user. The most important views are: the "login" allowing to
|
||||
authenticate the users using the OP and get an authorizartion code, the callback view allowing
|
||||
to retrieve a valid token for the considered user and the logout view.
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
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 django.views.generic import View
|
||||
|
||||
from .utils import get_logger, build_absolute_uri
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class OIDCAuthRequestView(View):
|
||||
""" Allows to start the authorization flow in order to authenticate the end-user.
|
||||
|
||||
This view acts as the main endpoint to trigger the authentication process involving the OIDC
|
||||
provider (OP). It prepares an authentication request that will be sent to the authorization
|
||||
server in order to authenticate the end-user.
|
||||
|
||||
"""
|
||||
|
||||
http_method_names = ['get', ]
|
||||
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
|
||||
log_prompt = "Process GET requests [OIDCAuthRequestView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
|
||||
# Defines common parameters used to bootstrap the authentication request.
|
||||
logger.debug(log_prompt.format('Construct request params'))
|
||||
authentication_request_params = request.GET.dict()
|
||||
authentication_request_params.update({
|
||||
'scope': settings.AUTH_OPENID_SCOPES,
|
||||
'response_type': 'code',
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
})
|
||||
|
||||
# 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:
|
||||
logger.debug(log_prompt.format('Use state'))
|
||||
state = get_random_string(settings.AUTH_OPENID_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 settings.AUTH_OPENID_USE_NONCE:
|
||||
logger.debug(log_prompt.format('Use nonce'))
|
||||
nonce = get_random_string(settings.AUTH_OPENID_NONCE_LENGTH)
|
||||
authentication_request_params.update({'nonce': nonce, })
|
||||
request.session['oidc_auth_nonce'] = nonce
|
||||
|
||||
# Stores the "next" URL in the session if applicable.
|
||||
logger.debug(log_prompt.format('Stores next url in the session'))
|
||||
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.
|
||||
logger.debug(log_prompt.format('Construct redirect url'))
|
||||
query = urlencode(authentication_request_params)
|
||||
redirect_url = '{url}?{query}'.format(
|
||||
url=settings.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT, query=query)
|
||||
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
|
||||
|
||||
class OIDCAuthCallbackView(View):
|
||||
""" Allows to complete the authentication process.
|
||||
|
||||
This view acts as the main endpoint to complete the authentication process involving the OIDC
|
||||
provider (OP). It checks the request sent by the OIDC provider in order to determine whether the
|
||||
considered was successfully authentified or not and authenticates the user at the current
|
||||
application level if applicable.
|
||||
|
||||
"""
|
||||
|
||||
http_method_names = ['get', ]
|
||||
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
log_prompt = "Process GET requests [OIDCAuthCallbackView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
callback_params = request.GET
|
||||
|
||||
# Retrieve the state value that was previously generated. No state means that we cannot
|
||||
# authenticate the user (so a failure should be returned).
|
||||
state = request.session.get('oidc_auth_state', None)
|
||||
|
||||
# Retrieve the nonce that was previously generated and remove it from the current session.
|
||||
# If no nonce is available (while the USE_NONCE setting is set to True) this means that the
|
||||
# authentication cannot be performed and so we have redirect the user to a failure URL.
|
||||
nonce = request.session.pop('oidc_auth_nonce', None)
|
||||
|
||||
# NOTE: a redirect to the failure page should be return if some required GET parameters are
|
||||
# missing or if no state can be retrieved from the current session.
|
||||
|
||||
if (
|
||||
((nonce and settings.AUTH_OPENID_USE_NONCE) or not settings.AUTH_OPENID_USE_NONCE)
|
||||
and
|
||||
(
|
||||
(state and settings.AUTH_OPENID_USE_STATE and 'state' in callback_params)
|
||||
or
|
||||
(not settings.AUTH_OPENID_USE_STATE)
|
||||
)
|
||||
and
|
||||
('code' in callback_params)
|
||||
):
|
||||
# Ensures that the passed state values is the same as the one that was previously
|
||||
# generated when forging the authorization request. This is necessary to mitigate
|
||||
# Cross-Site Request Forgery (CSRF, XSRF).
|
||||
if settings.AUTH_OPENID_USE_STATE and callback_params['state'] != state:
|
||||
logger.debug(log_prompt.format('Invalid OpenID Connect callback state value'))
|
||||
raise SuspiciousOperation('Invalid OpenID Connect callback state value')
|
||||
|
||||
# Authenticates the end-user.
|
||||
next_url = request.session.get('oidc_auth_next_url', None)
|
||||
logger.debug(log_prompt.format('Process authenticate'))
|
||||
user = auth.authenticate(nonce=nonce, request=request)
|
||||
if user and user.is_valid:
|
||||
logger.debug(log_prompt.format('Login: {}'.format(user)))
|
||||
auth.login(self.request, user)
|
||||
# Stores an expiration timestamp in the user's session. This value will be used if
|
||||
# the project is configured to periodically refresh user's token.
|
||||
self.request.session['oidc_auth_id_token_exp_timestamp'] = \
|
||||
time.time() + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE
|
||||
# Stores the "session_state" value that can be passed by the OpenID Connect provider
|
||||
# in order to maintain a consistent session state across the OP and the related
|
||||
# relying parties (RP).
|
||||
self.request.session['oidc_auth_session_state'] = \
|
||||
callback_params.get('session_state', None)
|
||||
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(
|
||||
next_url or settings.AUTH_OPENID_AUTHENTICATION_REDIRECT_URI
|
||||
)
|
||||
|
||||
if 'error' in callback_params:
|
||||
logger.debug(
|
||||
log_prompt.format('Error in callback params: {}'.format(callback_params['error']))
|
||||
)
|
||||
# If we receive an error in the callback GET parameters, this means that the
|
||||
# authentication could not be performed at the OP level. In that case we have to logout
|
||||
# the current user because we could've obtained this error after a prompt=none hit on
|
||||
# OpenID Connect Provider authenticate endpoint.
|
||||
logger.debug(log_prompt.format('Logout'))
|
||||
auth.logout(request)
|
||||
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(settings.AUTH_OPENID_AUTHENTICATION_FAILURE_REDIRECT_URI)
|
||||
|
||||
|
||||
class OIDCEndSessionView(View):
|
||||
""" Allows to end the session of any user authenticated using OpenID Connect.
|
||||
|
||||
This view acts as the main endpoint to end the session of an end-user that was authenticated
|
||||
using the OIDC provider (OP). It calls the "end-session" endpoint provided by the provider if
|
||||
applicable.
|
||||
|
||||
"""
|
||||
|
||||
http_method_names = ['get', 'post', ]
|
||||
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
log_prompt = "Process GET requests [OIDCEndSessionView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
return self.post(request)
|
||||
|
||||
def post(self, request):
|
||||
""" Processes POST requests. """
|
||||
log_prompt = "Process POST requests [OIDCEndSessionView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
|
||||
logout_url = settings.LOGOUT_REDIRECT_URL or '/'
|
||||
|
||||
# Log out the current user.
|
||||
if request.user.is_authenticated:
|
||||
logger.debug(log_prompt.format('Current user is authenticated'))
|
||||
try:
|
||||
logout_url = self.provider_end_session_url \
|
||||
if settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT else logout_url
|
||||
except KeyError: # pragma: no cover
|
||||
logout_url = logout_url
|
||||
logger.debug(log_prompt.format('Log out the current user: {}'.format(request.user)))
|
||||
auth.logout(request)
|
||||
|
||||
# Redirects the user to the appropriate URL.
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(logout_url)
|
||||
|
||||
@property
|
||||
def provider_end_session_url(self):
|
||||
""" Returns the end-session URL. """
|
||||
q = QueryDict(mutable=True)
|
||||
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \
|
||||
build_absolute_uri(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
|
||||
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \
|
||||
self.request.session['oidc_auth_id_token']
|
||||
return '{}?{}'.format(settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT, q.urlencode())
|
|
@ -1,4 +0,0 @@
|
|||
"""
|
||||
使用下面的工程,进行jumpserver 的 oidc 认证
|
||||
https://github.com/BaiJiangJie/jumpserver-django-oidc-rp
|
||||
"""
|
|
@ -1,13 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
|
||||
from .base import JMSBaseAuthBackend
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
__all__ = ['PublicKeyAuthBackend']
|
||||
|
||||
|
||||
class PublicKeyAuthBackend:
|
||||
class PublicKeyAuthBackend(JMSBaseAuthBackend):
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.TERMINAL_PUBLIC_KEY_AUTH
|
||||
|
||||
def authenticate(self, request, username=None, public_key=None, **kwargs):
|
||||
if not public_key:
|
||||
return None
|
||||
|
@ -22,15 +29,6 @@ class PublicKeyAuthBackend:
|
|||
self.user_can_authenticate(user):
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def user_can_authenticate(user):
|
||||
"""
|
||||
Reject users with is_active=False. Custom user models that don't have
|
||||
that attribute are allowed.
|
||||
"""
|
||||
is_active = getattr(user, 'is_active', None)
|
||||
return is_active or is_active is None
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
user = UserModel._default_manager.get(pk=user_id)
|
||||
|
|
|
@ -6,6 +6,8 @@ from django.contrib.auth import get_user_model
|
|||
from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend
|
||||
from django.conf import settings
|
||||
|
||||
from .base import JMSBaseAuthBackend
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -39,11 +41,17 @@ class CreateUserMixin:
|
|||
return None
|
||||
|
||||
|
||||
class RadiusBackend(CreateUserMixin, RADIUSBackend):
|
||||
class RadiusBaseBackend(CreateUserMixin, JMSBaseAuthBackend):
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_RADIUS
|
||||
|
||||
|
||||
class RadiusBackend(RadiusBaseBackend, RADIUSBackend):
|
||||
def authenticate(self, request, username='', password='', **kwargs):
|
||||
return super().authenticate(request, username=username, password=password)
|
||||
|
||||
|
||||
class RadiusRealmBackend(CreateUserMixin, RADIUSRealmBackend):
|
||||
class RadiusRealmBackend(RadiusBaseBackend, RADIUSRealmBackend):
|
||||
def authenticate(self, request, username='', password='', realm=None, **kwargs):
|
||||
return super().authenticate(request, username=username, password=password, realm=realm)
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .backends import *
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
||||
from common.utils import get_logger
|
||||
|
@ -10,16 +10,17 @@ from .signals import (
|
|||
saml2_user_authenticated, saml2_user_authentication_failed,
|
||||
saml2_create_or_update_user
|
||||
)
|
||||
from ..base import JMSBaseAuthBackend
|
||||
|
||||
__all__ = ['SAML2Backend']
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class SAML2Backend(ModelBackend):
|
||||
def user_can_authenticate(self, user):
|
||||
is_valid = getattr(user, 'is_valid', None)
|
||||
return is_valid or is_valid is None
|
||||
class SAML2Backend(JMSBaseAuthBackend):
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_SAML2
|
||||
|
||||
@transaction.atomic
|
||||
def get_or_create_from_saml_data(self, request, **saml_user_data):
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
from django.conf import settings
|
||||
|
||||
from .base import JMSBaseAuthBackend
|
||||
|
||||
|
||||
class SSOAuthentication(JMSBaseAuthBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_SSO
|
||||
|
||||
def authenticate(self, request, sso_token=None, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class WeComAuthentication(JMSBaseAuthBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_WECOM
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class DingTalkAuthentication(JMSBaseAuthBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_DINGTALK
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class FeiShuAuthentication(JMSBaseAuthBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_FEISHU
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationTokenAuthentication(JMSBaseAuthBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
|
@ -12,9 +12,10 @@ from django.contrib import auth
|
|||
from django.utils.translation import ugettext as _
|
||||
from rest_framework.request import Request
|
||||
from django.contrib.auth import (
|
||||
BACKEND_SESSION_KEY, _get_backends,
|
||||
PermissionDenied, user_login_failed, _clean_credentials
|
||||
BACKEND_SESSION_KEY, load_backend,
|
||||
PermissionDenied, user_login_failed, _clean_credentials,
|
||||
)
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.shortcuts import reverse, redirect, get_object_or_404
|
||||
|
||||
from common.utils import get_request_ip, get_logger, bulk_get, FlashMessageUtil
|
||||
|
@ -29,27 +30,38 @@ from .const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def check_backend_can_auth(username, backend_path, allowed_auth_backends):
|
||||
if allowed_auth_backends is not None and backend_path not in allowed_auth_backends:
|
||||
logger.debug('Skip user auth backend: {}, {} not in'.format(
|
||||
username, backend_path, ','.join(allowed_auth_backends)
|
||||
))
|
||||
return False
|
||||
return True
|
||||
def _get_backends(return_tuples=False):
|
||||
backends = []
|
||||
for backend_path in settings.AUTHENTICATION_BACKENDS:
|
||||
backend = load_backend(backend_path)
|
||||
# 检查 backend 是否启用
|
||||
if not backend.is_enabled():
|
||||
continue
|
||||
backends.append((backend, backend_path) if return_tuples else backend)
|
||||
if not backends:
|
||||
raise ImproperlyConfigured(
|
||||
'No authentication backends have been defined. Does '
|
||||
'AUTHENTICATION_BACKENDS contain anything?'
|
||||
)
|
||||
return backends
|
||||
|
||||
|
||||
auth._get_backends = _get_backends
|
||||
|
||||
|
||||
def authenticate(request=None, **credentials):
|
||||
"""
|
||||
If the given credentials are valid, return a User object.
|
||||
之所以 hack 这个 auticate
|
||||
"""
|
||||
username = credentials.get('username')
|
||||
allowed_auth_backends = User.get_user_allowed_auth_backends(username)
|
||||
|
||||
for backend, backend_path in _get_backends(return_tuples=True):
|
||||
# 预先检查,不浪费认证时间
|
||||
if not check_backend_can_auth(username, backend_path, allowed_auth_backends):
|
||||
if not backend.username_can_authenticate(username):
|
||||
continue
|
||||
|
||||
# 原生
|
||||
backend_signature = inspect.signature(backend.authenticate)
|
||||
try:
|
||||
backend_signature.bind(request, **credentials)
|
||||
|
@ -63,21 +75,17 @@ def authenticate(request=None, **credentials):
|
|||
break
|
||||
if user is None:
|
||||
continue
|
||||
# 如果是 None, 证明没有检查过, 需要再次检查
|
||||
if allowed_auth_backends is None:
|
||||
# 有些 authentication 参数中不带 username, 之后还要再检查
|
||||
allowed_auth_backends = user.get_allowed_auth_backends()
|
||||
if not check_backend_can_auth(user.username, backend_path, allowed_auth_backends):
|
||||
continue
|
||||
|
||||
# 再次检查遇检查中遗漏的用户
|
||||
if not backend.user_can_authenticate(user):
|
||||
continue
|
||||
|
||||
# Annotate the user object with the path of the backend.
|
||||
user.backend = backend_path
|
||||
return user
|
||||
|
||||
# The credentials supplied are invalid to all backends, fire signal
|
||||
user_login_failed.send(
|
||||
sender=__name__, credentials=_clean_credentials(credentials), request=request
|
||||
)
|
||||
user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)
|
||||
|
||||
|
||||
auth.authenticate = authenticate
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.core.cache import cache
|
|||
from django.dispatch import receiver
|
||||
from django_cas_ng.signals import cas_user_authenticated
|
||||
|
||||
from jms_oidc_rp.signals import openid_user_login_failed, openid_user_login_success
|
||||
from authentication.backends.oidc.signals import openid_user_login_failed, openid_user_login_success
|
||||
|
||||
from authentication.backends.saml2.signals import (
|
||||
saml2_user_authenticated, saml2_user_authentication_failed
|
||||
|
|
|
@ -55,7 +55,7 @@ urlpatterns = [
|
|||
|
||||
# openid
|
||||
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
|
||||
path('openid/', include(('jms_oidc_rp.urls', 'authentication'), namespace='openid')),
|
||||
path('openid/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='openid')),
|
||||
path('saml2/', include(('authentication.backends.saml2.urls', 'authentication'), namespace='saml2')),
|
||||
path('captcha/', include('captcha.urls')),
|
||||
]
|
||||
|
|
|
@ -184,7 +184,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
@staticmethod
|
||||
def get_forgot_password_url():
|
||||
forgot_password_url = reverse('authentication:forgot-password')
|
||||
has_other_auth_backend = settings.AUTHENTICATION_BACKENDS[0] != settings.AUTH_BACKEND_MODEL
|
||||
has_other_auth_backend = settings.AUTHENTICATION_BACKENDS[1] != settings.AUTH_BACKEND_MODEL
|
||||
if has_other_auth_backend and settings.FORGOT_PASSWORD_URL:
|
||||
forgot_password_url = settings.FORGOT_PASSWORD_URL
|
||||
return forgot_password_url
|
||||
|
|
|
@ -74,6 +74,13 @@ AUTH_OPENID_ALWAYS_UPDATE_USER = CONFIG.AUTH_OPENID_ALWAYS_UPDATE_USER
|
|||
AUTH_OPENID_AUTH_LOGIN_URL_NAME = 'authentication:openid:login'
|
||||
AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:openid:login-callback'
|
||||
AUTH_OPENID_AUTH_LOGOUT_URL_NAME = 'authentication:openid:logout'
|
||||
# Other default
|
||||
AUTH_OPENID_STATE_LENGTH = 32
|
||||
AUTH_OPENID_NONCE_LENGTH = 32
|
||||
AUTH_OPENID_AUTHENTICATION_REDIRECT_URI = '/'
|
||||
AUTH_OPENID_AUTHENTICATION_FAILURE_REDIRECT_URI = '/'
|
||||
AUTH_OPENID_PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER = 'post_logout_redirect_uri'
|
||||
AUTH_OPENID_PROVIDER_END_SESSION_ID_TOKEN_PARAMETER = 'id_token_hint'
|
||||
# ==============================================================================
|
||||
|
||||
# Radius Auth
|
||||
|
@ -138,39 +145,35 @@ TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
|||
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS
|
||||
|
||||
|
||||
AUTH_BACKEND_MODEL = 'authentication.backends.api.JMSModelBackend'
|
||||
AUTH_BACKEND_MODEL = 'authentication.backends.base.JMSModelBackend'
|
||||
RBAC_BACKEND = 'rbac.backends.RBACBackend'
|
||||
AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend'
|
||||
AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend'
|
||||
AUTH_BACKEND_OIDC_PASSWORD = 'jms_oidc_rp.backends.OIDCAuthPasswordBackend'
|
||||
AUTH_BACKEND_OIDC_CODE = 'jms_oidc_rp.backends.OIDCAuthCodeBackend'
|
||||
AUTH_BACKEND_OIDC_PASSWORD = 'authentication.backends.oidc.backends.OIDCAuthPasswordBackend'
|
||||
AUTH_BACKEND_OIDC_CODE = 'authentication.backends.oidc.backends.OIDCAuthCodeBackend'
|
||||
AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend'
|
||||
AUTH_BACKEND_CAS = 'authentication.backends.cas.CASBackend'
|
||||
AUTH_BACKEND_SSO = 'authentication.backends.api.SSOAuthentication'
|
||||
AUTH_BACKEND_WECOM = 'authentication.backends.api.WeComAuthentication'
|
||||
AUTH_BACKEND_DINGTALK = 'authentication.backends.api.DingTalkAuthentication'
|
||||
AUTH_BACKEND_FEISHU = 'authentication.backends.api.FeiShuAuthentication'
|
||||
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.api.AuthorizationTokenAuthentication'
|
||||
AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend'
|
||||
AUTH_BACKEND_CAS = 'authentication.backends.cas.backends.CASBackend'
|
||||
AUTH_BACKEND_SSO = 'authentication.backends.sso.SSOAuthentication'
|
||||
AUTH_BACKEND_WECOM = 'authentication.backends.sso.WeComAuthentication'
|
||||
AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication'
|
||||
AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication'
|
||||
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication'
|
||||
AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.backends.SAML2Backend'
|
||||
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
AUTH_BACKEND_MODEL, RBAC_BACKEND, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_WECOM,
|
||||
AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, AUTH_BACKEND_AUTH_TOKEN,
|
||||
AUTH_BACKEND_SSO,
|
||||
# 只做权限校验
|
||||
RBAC_BACKEND,
|
||||
# 密码形式
|
||||
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_LDAP, AUTH_BACKEND_RADIUS,
|
||||
# 跳转形式
|
||||
AUTH_BACKEND_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2,
|
||||
# 扫码模式
|
||||
AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU,
|
||||
# Token模式
|
||||
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO,
|
||||
]
|
||||
|
||||
if AUTH_CAS:
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_CAS)
|
||||
if AUTH_OPENID:
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_OIDC_PASSWORD)
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_OIDC_CODE)
|
||||
if AUTH_RADIUS:
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_RADIUS)
|
||||
if AUTH_SAML2:
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_SAML2)
|
||||
|
||||
|
||||
ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH
|
||||
ONLY_ALLOW_AUTH_FROM_SOURCE = CONFIG.ONLY_ALLOW_AUTH_FROM_SOURCE
|
||||
|
||||
|
|
|
@ -57,7 +57,6 @@ INSTALLED_APPS = [
|
|||
'notifications.apps.NotificationsConfig',
|
||||
'rbac.apps.RBACConfig',
|
||||
'common.apps.CommonConfig',
|
||||
'jms_oidc_rp',
|
||||
'rest_framework',
|
||||
'rest_framework_swagger',
|
||||
'drf_yasg',
|
||||
|
@ -116,7 +115,6 @@ TEMPLATES = [
|
|||
'django.template.context_processors.media',
|
||||
'jumpserver.context_processor.jumpserver_processor',
|
||||
'orgs.context_processor.org_processor',
|
||||
'jms_oidc_rp.context_processors.oidc',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -26,11 +26,11 @@ REST_FRAMEWORK = {
|
|||
),
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
# 'rest_framework.authentication.BasicAuthentication',
|
||||
'authentication.backends.api.AccessKeyAuthentication',
|
||||
'authentication.backends.api.AccessTokenAuthentication',
|
||||
'authentication.backends.api.PrivateTokenAuthentication',
|
||||
'authentication.backends.api.SignatureAuthentication',
|
||||
'authentication.backends.api.SessionAuthentication',
|
||||
'authentication.backends.drf.AccessKeyAuthentication',
|
||||
'authentication.backends.drf.AccessTokenAuthentication',
|
||||
'authentication.backends.drf.PrivateTokenAuthentication',
|
||||
'authentication.backends.drf.SignatureAuthentication',
|
||||
'authentication.backends.drf.SessionAuthentication',
|
||||
),
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
'django_filters.rest_framework.DjangoFilterBackend',
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from authentication.backends.base import JMSBaseAuthBackend
|
||||
|
||||
|
||||
class RBACBackend(ModelBackend):
|
||||
class RBACBackend(JMSBaseAuthBackend):
|
||||
""" 只做权限校验 """
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return True
|
||||
|
||||
def authenticate(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
def username_can_authenticate(self, username):
|
||||
return False
|
||||
|
||||
def has_perm(self, user_obj, perm, obj=None):
|
||||
if not user_obj.is_active:
|
||||
return False
|
||||
|
||||
raise PermissionDenied()
|
||||
has_perm = perm in user_obj.perms
|
||||
if not has_perm:
|
||||
raise PermissionDenied()
|
||||
return has_perm
|
||||
#
|
||||
|
||||
# def has_module_perms(self, user_obj, app_label):
|
||||
# return True
|
||||
|
|
|
@ -16,7 +16,6 @@ exclude_permissions = (
|
|||
('contenttypes', '*', '*', '*'),
|
||||
('django_cas_ng', '*', '*', '*'),
|
||||
('django_celery_beat', '*', '*', '*'),
|
||||
('jms_oidc_rp', '*', '*', '*'),
|
||||
('admin', '*', '*', '*'),
|
||||
('sessions', '*', '*', '*'),
|
||||
('notifications', '*', '*', '*'),
|
||||
|
|
|
@ -107,7 +107,7 @@ class Setting(models.Model):
|
|||
# 添加
|
||||
if setting.cleaned_value and not has:
|
||||
logger.debug('Add auth backend: {}'.format(name))
|
||||
settings.AUTHENTICATION_BACKENDS.insert(0, backend)
|
||||
settings.AUTHENTICATION_BACKENDS.insert(1, backend)
|
||||
|
||||
# 去掉
|
||||
if not setting.cleaned_value and has:
|
||||
|
|
|
@ -10,7 +10,7 @@ __all__ = [
|
|||
class CASSettingSerializer(serializers.Serializer):
|
||||
AUTH_CAS = serializers.BooleanField(required=False, label=_('Enable CAS Auth'))
|
||||
CAS_SERVER_URL = serializers.CharField(required=False, max_length=1024, label=_('Server url'))
|
||||
CAS_ROOT_PROXIED_AS = serializers.CharField(required=False, max_length=1024, label=_('Proxy server url'))
|
||||
CAS_ROOT_PROXIED_AS = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=1024, label=_('Proxy server url'))
|
||||
CAS_LOGOUT_COMPLETELY = serializers.BooleanField(required=False, label=_('Logout completely'))
|
||||
CAS_VERSION = serializers.IntegerField(required=False, label=_('Version'), min_value=1, max_value=3)
|
||||
CAS_USERNAME_ATTRIBUTE = serializers.CharField(required=False, max_length=1024, label=_('Username attr'))
|
||||
|
|
|
@ -520,14 +520,27 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
|||
|
||||
SOURCE_BACKEND_MAPPING = {
|
||||
Source.local: [
|
||||
settings.AUTH_BACKEND_MODEL, settings.AUTH_BACKEND_PUBKEY,
|
||||
settings.AUTH_BACKEND_WECOM, settings.AUTH_BACKEND_DINGTALK,
|
||||
settings.AUTH_BACKEND_MODEL,
|
||||
settings.AUTH_BACKEND_PUBKEY,
|
||||
settings.AUTH_BACKEND_WECOM,
|
||||
settings.AUTH_BACKEND_DINGTALK,
|
||||
],
|
||||
Source.ldap: [
|
||||
settings.AUTH_BACKEND_LDAP
|
||||
],
|
||||
Source.openid: [
|
||||
settings.AUTH_BACKEND_OIDC_PASSWORD,
|
||||
settings.AUTH_BACKEND_OIDC_CODE
|
||||
],
|
||||
Source.radius: [
|
||||
settings.AUTH_BACKEND_RADIUS
|
||||
],
|
||||
Source.cas: [
|
||||
settings.AUTH_BACKEND_CAS
|
||||
],
|
||||
Source.saml2: [
|
||||
settings.AUTH_BACKEND_SAML2
|
||||
],
|
||||
Source.ldap: [settings.AUTH_BACKEND_LDAP],
|
||||
Source.openid: [settings.AUTH_BACKEND_OIDC_PASSWORD, settings.AUTH_BACKEND_OIDC_CODE],
|
||||
Source.radius: [settings.AUTH_BACKEND_RADIUS],
|
||||
Source.cas: [settings.AUTH_BACKEND_CAS],
|
||||
Source.saml2: [settings.AUTH_BACKEND_SAML2],
|
||||
}
|
||||
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
|
@ -728,7 +741,6 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
|||
@classmethod
|
||||
def get_user_allowed_auth_backends(cls, username):
|
||||
if not settings.ONLY_ALLOW_AUTH_FROM_SOURCE or not username:
|
||||
# return settings.AUTHENTICATION_BACKENDS
|
||||
return None
|
||||
user = cls.objects.filter(username=username).first()
|
||||
if not user:
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.core.exceptions import PermissionDenied
|
|||
from django_cas_ng.signals import cas_user_authenticated
|
||||
from django.db.models.signals import post_save
|
||||
|
||||
from jms_oidc_rp.signals import openid_create_or_update_user
|
||||
from authentication.backends.oidc.signals import openid_create_or_update_user
|
||||
|
||||
from authentication.backends.saml2.signals import saml2_create_or_update_user
|
||||
from common.utils import get_logger
|
||||
|
|
|
@ -96,7 +96,6 @@ ipython
|
|||
huaweicloud-sdk-python==1.0.21
|
||||
django-redis==4.11.0
|
||||
python-redis-lock==3.7.0
|
||||
jumpserver-django-oidc-rp==0.3.7.8
|
||||
django-mysql==3.9.0
|
||||
gmssl==3.2.1
|
||||
azure-mgmt-compute==4.6.2
|
||||
|
|
Loading…
Reference in New Issue