mirror of https://github.com/jumpserver/jumpserver
Dev oidc (#3941)
* [Update] oidc_rp获取token添加headers base64编码 * [Update] 移除对oidc_rp的支持 * [Update] 移除对oidc_rp的支持2 * [Update] 修改OpenID配置(添加新配置项,并对旧配置项做兼容) * [Update] 移除所有与Keycloak相关的模块 * [Update] 添加jumpserver-django-oidc-rp的使用 * [Update] 更新登录重定向地址(oidc) * [Update] oidc添加一些配置参数;处理用户登录/创建/更新等信号 * [Update] 修改退出登录逻辑 * [Update] 添加oidc user登录成功的信号机制 * [Update] 修改mfa认证choices内容 (otp => code) * [Update] 添加OpenID backend password 认证失败信号机制;修改引入common包问题 * [Update] 用户Token/Auth API 校验用户时,传入request参数(解决登录成功日志记录的问题) * [Update] 添加依赖jumpserver-django-oidc-rp==0.3.7.1 * [Update] oidc认证模块说明pull/3942/head
parent
5d433456d4
commit
7833ff6671
|
@ -0,0 +1,4 @@
|
|||
"""
|
||||
使用下面的工程,进行jumpserver 的 oidc 认证
|
||||
https://github.com/BaiJiangJie/jumpserver-django-oidc-rp
|
||||
"""
|
|
@ -1,181 +0,0 @@
|
|||
import requests
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.urls import reverse
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from oidc_rp.conf import settings as oidc_rp_settings
|
||||
from oidc_rp.models import OIDCUser
|
||||
from oidc_rp.signals import oidc_user_created
|
||||
from oidc_rp.backends import OIDCAuthBackend
|
||||
from oidc_rp.utils import validate_and_return_id_token
|
||||
|
||||
|
||||
__all__ = ['OIDCAuthCodeBackend', 'OIDCAuthPasswordBackend']
|
||||
|
||||
|
||||
class OIDCAuthCodeBackend(OIDCAuthBackend):
|
||||
def authenticate(self, request, nonce=None, **kwargs):
|
||||
""" Authenticates users in case of the OpenID Connect Authorization code flow. """
|
||||
# NOTE: the request object is mandatory to perform the authentication using an authorization
|
||||
# code provided by the OIDC supplier.
|
||||
if (nonce is None and oidc_rp_settings.USE_NONCE) or request is None:
|
||||
return
|
||||
|
||||
# Fetches required GET parameters from the HTTP request object.
|
||||
state = request.GET.get('state')
|
||||
code = request.GET.get('code')
|
||||
|
||||
# Don't go further if the state value or the authorization code is not present in the GET
|
||||
# parameters because we won't be able to get a valid token for the user in that case.
|
||||
if (state is None and oidc_rp_settings.USE_STATE) or code is None:
|
||||
raise SuspiciousOperation('Authorization code or state value is missing')
|
||||
|
||||
# Prepares the token payload that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
token_payload = {
|
||||
'client_id': oidc_rp_settings.CLIENT_ID,
|
||||
'client_secret': oidc_rp_settings.CLIENT_SECRET,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': request.build_absolute_uri(
|
||||
reverse(settings.OIDC_RP_LOGIN_CALLBACK_URL_NAME)
|
||||
),
|
||||
}
|
||||
|
||||
# Calls the token endpoint.
|
||||
token_response = requests.post(oidc_rp_settings.PROVIDER_TOKEN_ENDPOINT, data=token_payload)
|
||||
token_response.raise_for_status()
|
||||
token_response_data = token_response.json()
|
||||
|
||||
# Validates the token.
|
||||
raw_id_token = token_response_data.get('id_token')
|
||||
id_token = validate_and_return_id_token(raw_id_token, nonce)
|
||||
if id_token is None:
|
||||
return
|
||||
|
||||
# Retrieves the access token and refresh token.
|
||||
access_token = token_response_data.get('access_token')
|
||||
refresh_token = token_response_data.get('refresh_token')
|
||||
|
||||
# Stores the ID token, the related access token and the refresh token in the session.
|
||||
request.session['oidc_auth_id_token'] = raw_id_token
|
||||
request.session['oidc_auth_access_token'] = access_token
|
||||
request.session['oidc_auth_refresh_token'] = refresh_token
|
||||
|
||||
# If the id_token contains userinfo scopes and claims we don't have to hit the userinfo
|
||||
# endpoint.
|
||||
if oidc_rp_settings.ID_TOKEN_INCLUDE_USERINFO:
|
||||
userinfo_data = id_token
|
||||
else:
|
||||
# Fetches the user information from the userinfo endpoint provided by the OP.
|
||||
userinfo_response = requests.get(
|
||||
oidc_rp_settings.PROVIDER_USERINFO_ENDPOINT,
|
||||
headers={'Authorization': 'Bearer {0}'.format(access_token)})
|
||||
userinfo_response.raise_for_status()
|
||||
userinfo_data = userinfo_response.json()
|
||||
|
||||
# Tries to retrieve a corresponding user in the local database and creates it if applicable.
|
||||
try:
|
||||
oidc_user = OIDCUser.objects.select_related('user').get(sub=userinfo_data.get('sub'))
|
||||
except OIDCUser.DoesNotExist:
|
||||
oidc_user = create_oidc_user_from_claims(userinfo_data)
|
||||
oidc_user_created.send(sender=self.__class__, request=request, oidc_user=oidc_user)
|
||||
else:
|
||||
update_oidc_user_from_claims(oidc_user, userinfo_data)
|
||||
|
||||
# Runs a custom user details handler if applicable. Such handler could be responsible for
|
||||
# creating / updating whatever is necessary to manage the considered user (eg. a profile).
|
||||
user_details_handler(oidc_user, userinfo_data)
|
||||
|
||||
return oidc_user.user
|
||||
|
||||
|
||||
class OIDCAuthPasswordBackend(ModelBackend):
|
||||
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
|
||||
if username is None and password is None:
|
||||
return
|
||||
|
||||
# Prepares the token payload that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
token_payload = {
|
||||
'client_id': oidc_rp_settings.CLIENT_ID,
|
||||
'client_secret': oidc_rp_settings.CLIENT_SECRET,
|
||||
'grant_type': 'password',
|
||||
'username': username,
|
||||
'password': password,
|
||||
}
|
||||
|
||||
token_response = requests.post(oidc_rp_settings.PROVIDER_TOKEN_ENDPOINT, data=token_payload)
|
||||
token_response.raise_for_status()
|
||||
token_response_data = token_response.json()
|
||||
|
||||
access_token = token_response_data.get('access_token')
|
||||
|
||||
# Fetches the user information from the userinfo endpoint provided by the OP.
|
||||
userinfo_response = requests.get(
|
||||
oidc_rp_settings.PROVIDER_USERINFO_ENDPOINT,
|
||||
headers={'Authorization': 'Bearer {0}'.format(access_token)})
|
||||
userinfo_response.raise_for_status()
|
||||
userinfo_data = userinfo_response.json()
|
||||
|
||||
# Tries to retrieve a corresponding user in the local database and creates it if applicable.
|
||||
try:
|
||||
oidc_user = OIDCUser.objects.select_related('user').get(sub=userinfo_data.get('sub'))
|
||||
except OIDCUser.DoesNotExist:
|
||||
oidc_user = create_oidc_user_from_claims(userinfo_data)
|
||||
oidc_user_created.send(sender=self.__class__, request=request, oidc_user=oidc_user)
|
||||
else:
|
||||
update_oidc_user_from_claims(oidc_user, userinfo_data)
|
||||
|
||||
# Runs a custom user details handler if applicable. Such handler could be responsible for
|
||||
# creating / updating whatever is necessary to manage the considered user (eg. a profile).
|
||||
user_details_handler(oidc_user, userinfo_data)
|
||||
|
||||
return oidc_user.user
|
||||
|
||||
|
||||
def get_or_create_user(username, email):
|
||||
user, created = get_user_model().objects.get_or_create(username=username)
|
||||
return user
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_oidc_user_from_claims(claims):
|
||||
"""
|
||||
Creates an ``OIDCUser`` instance using the claims extracted
|
||||
from an id_token.
|
||||
"""
|
||||
sub = claims['sub']
|
||||
email = claims.get('email')
|
||||
username = claims.get('preferred_username')
|
||||
user = get_or_create_user(username, email)
|
||||
oidc_user = OIDCUser.objects.create(user=user, sub=sub, userinfo=claims)
|
||||
|
||||
return oidc_user
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def update_oidc_user_from_claims(oidc_user, claims):
|
||||
"""
|
||||
Updates an ``OIDCUser`` instance using the claims extracted
|
||||
from an id_token.
|
||||
"""
|
||||
oidc_user.userinfo = claims
|
||||
oidc_user.save()
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def user_details_handler(oidc_user, userinfo_data):
|
||||
name = userinfo_data.get('name')
|
||||
username = userinfo_data.get('preferred_username')
|
||||
email = userinfo_data.get('email')
|
||||
oidc_user.user.name = name or username
|
||||
oidc_user.user.username = username
|
||||
oidc_user.user.email = email
|
||||
oidc_user.user.save()
|
|
@ -1,10 +0,0 @@
|
|||
from django.urls import path
|
||||
from oidc_rp import views as oidc_rp_views
|
||||
from .views import OverwriteOIDCAuthRequestView, OverwriteOIDCEndSessionView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', OverwriteOIDCAuthRequestView.as_view(), name='oidc-login'),
|
||||
path('callback/', oidc_rp_views.OIDCAuthCallbackView.as_view(), name='oidc-callback'),
|
||||
path('logout/', OverwriteOIDCEndSessionView.as_view(), name='oidc-logout'),
|
||||
]
|
|
@ -1,77 +0,0 @@
|
|||
from django.conf import settings
|
||||
from django.http import HttpResponseRedirect, QueryDict
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.http import is_safe_url, urlencode
|
||||
|
||||
from oidc_rp.conf import settings as oidc_rp_settings
|
||||
from oidc_rp.views import OIDCEndSessionView, OIDCAuthRequestView
|
||||
|
||||
__all__ = ['OverwriteOIDCAuthRequestView', 'OverwriteOIDCEndSessionView']
|
||||
|
||||
|
||||
class OverwriteOIDCAuthRequestView(OIDCAuthRequestView):
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
# Defines common parameters used to bootstrap the authentication request.
|
||||
authentication_request_params = request.GET.dict()
|
||||
authentication_request_params.update({
|
||||
'scope': oidc_rp_settings.SCOPES,
|
||||
'response_type': 'code',
|
||||
'client_id': oidc_rp_settings.CLIENT_ID,
|
||||
'redirect_uri': request.build_absolute_uri(
|
||||
reverse(settings.OIDC_RP_LOGIN_CALLBACK_URL_NAME)
|
||||
),
|
||||
})
|
||||
|
||||
# States should be used! They are recommended in order to maintain state between the
|
||||
# authentication request and the callback.
|
||||
if oidc_rp_settings.USE_STATE:
|
||||
state = get_random_string(oidc_rp_settings.STATE_LENGTH)
|
||||
authentication_request_params.update({'state': state})
|
||||
request.session['oidc_auth_state'] = state
|
||||
|
||||
# Nonces should be used too! In that case the generated nonce is stored both in the
|
||||
# authentication request parameters and in the user's session.
|
||||
if oidc_rp_settings.USE_NONCE:
|
||||
nonce = get_random_string(oidc_rp_settings.NONCE_LENGTH)
|
||||
authentication_request_params.update({'nonce': nonce, })
|
||||
request.session['oidc_auth_nonce'] = nonce
|
||||
|
||||
# Stores the "next" URL in the session if applicable.
|
||||
next_url = request.GET.get('next')
|
||||
request.session['oidc_auth_next_url'] = next_url \
|
||||
if is_safe_url(url=next_url, allowed_hosts=(request.get_host(), )) else None
|
||||
|
||||
# Redirects the user to authorization endpoint.
|
||||
query = urlencode(authentication_request_params)
|
||||
redirect_url = '{url}?{query}'.format(
|
||||
url=oidc_rp_settings.PROVIDER_AUTHORIZATION_ENDPOINT, query=query)
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
|
||||
|
||||
class OverwriteOIDCEndSessionView(OIDCEndSessionView):
|
||||
def post(self, request):
|
||||
""" Processes POST requests. """
|
||||
logout_url = settings.LOGOUT_REDIRECT_URL or '/'
|
||||
|
||||
try:
|
||||
logout_url = self.provider_end_session_url \
|
||||
if oidc_rp_settings.PROVIDER_END_SESSION_ENDPOINT else logout_url
|
||||
except KeyError: # pragma: no cover
|
||||
logout_url = logout_url
|
||||
|
||||
# Redirects the user to the appropriate URL.
|
||||
return HttpResponseRedirect(logout_url)
|
||||
|
||||
@property
|
||||
def provider_end_session_url(self):
|
||||
""" Returns the end-session URL. """
|
||||
q = QueryDict(mutable=True)
|
||||
q[oidc_rp_settings.PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \
|
||||
self.request.build_absolute_uri(settings.LOGOUT_REDIRECT_URL or '/')
|
||||
if self.request.session.get('oidc_auth_id_token'):
|
||||
q[oidc_rp_settings.PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \
|
||||
self.request.session['oidc_auth_id_token']
|
||||
return '{}?{}'.format(oidc_rp_settings.PROVIDER_END_SESSION_ENDPOINT, q.urlencode())
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .backends import *
|
||||
from .middleware import *
|
||||
from .utils import *
|
||||
from .decorator import *
|
|
@ -1,82 +0,0 @@
|
|||
# coding:utf-8
|
||||
#
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from .utils import new_client
|
||||
from .models import OIDT_ACCESS_TOKEN
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
logger = get_logger(__file__)
|
||||
client = new_client()
|
||||
|
||||
|
||||
__all__ = [
|
||||
'OpenIDAuthorizationCodeBackend', 'OpenIDAuthorizationPasswordBackend',
|
||||
]
|
||||
|
||||
|
||||
class BaseOpenIDAuthorizationBackend(object):
|
||||
@staticmethod
|
||||
def user_can_authenticate(user):
|
||||
"""
|
||||
Reject users with is_active=False. Custom user models that don't have
|
||||
that attribute are allowed.
|
||||
"""
|
||||
is_valid = getattr(user, 'is_valid', None)
|
||||
return is_valid or is_valid is None
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
user = UserModel._default_manager.get(pk=user_id)
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
|
||||
return user if self.user_can_authenticate(user) else None
|
||||
|
||||
|
||||
class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend):
|
||||
def authenticate(self, request, **kwargs):
|
||||
logger.info('Authentication OpenID code backend')
|
||||
code = kwargs.get('code')
|
||||
redirect_uri = kwargs.get('redirect_uri')
|
||||
if not code or not redirect_uri:
|
||||
logger.info('Authenticate failed: No code or No redirect uri')
|
||||
return None
|
||||
try:
|
||||
oidt_profile = client.update_or_create_from_code(
|
||||
code=code, redirect_uri=redirect_uri
|
||||
)
|
||||
except Exception as e:
|
||||
logger.info('Authenticate failed: get oidt_profile: {}'.format(e))
|
||||
return None
|
||||
else:
|
||||
# Check openid user single logout or not with access_token
|
||||
request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token
|
||||
user = oidt_profile.user
|
||||
logger.info('Authenticate success: user -> {}'.format(user))
|
||||
return user if self.user_can_authenticate(user) else None
|
||||
|
||||
|
||||
class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend):
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
logger.info('Authentication OpenID password backend')
|
||||
if not username:
|
||||
logger.info('Authenticate failed: Not username')
|
||||
return None
|
||||
try:
|
||||
oidt_profile = client.update_or_create_from_password(
|
||||
username=username, password=password
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
logger.info('Authenticate failed: get oidt_profile: {}'.format(e))
|
||||
return None
|
||||
else:
|
||||
user = oidt_profile.user
|
||||
logger.info('Authenticate success: user -> {}'.format(user))
|
||||
return user if self.user_can_authenticate(user) else None
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
|
||||
import warnings
|
||||
import contextlib
|
||||
|
||||
import requests
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
from django.conf import settings
|
||||
|
||||
__all__ = [
|
||||
'ssl_verification',
|
||||
]
|
||||
|
||||
old_merge_environment_settings = requests.Session.merge_environment_settings
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def no_ssl_verification():
|
||||
"""
|
||||
https://stackoverflow.com/questions/15445981/
|
||||
how-do-i-disable-the-security-certificate-check-in-python-requests
|
||||
"""
|
||||
opened_adapters = set()
|
||||
|
||||
def merge_environment_settings(self, url, proxies, stream, verify, cert):
|
||||
# Verification happens only once per connection so we need to close
|
||||
# all the opened adapters once we're done. Otherwise, the effects of
|
||||
# verify=False persist beyond the end of this context manager.
|
||||
opened_adapters.add(self.get_adapter(url))
|
||||
_settings = old_merge_environment_settings(
|
||||
self, url, proxies, stream, verify, cert
|
||||
)
|
||||
_settings['verify'] = False
|
||||
return _settings
|
||||
|
||||
requests.Session.merge_environment_settings = merge_environment_settings
|
||||
try:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', InsecureRequestWarning)
|
||||
yield
|
||||
finally:
|
||||
requests.Session.merge_environment_settings = old_merge_environment_settings
|
||||
for adapter in opened_adapters:
|
||||
try:
|
||||
adapter.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def ssl_verification(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
if not settings.AUTH_OPENID_IGNORE_SSL_VERIFICATION:
|
||||
return func(*args, **kwargs)
|
||||
with no_ssl_verification():
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
|
@ -1,41 +0,0 @@
|
|||
# coding:utf-8
|
||||
#
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
|
||||
from common.utils import get_logger
|
||||
from .utils import new_client
|
||||
from .models import OIDT_ACCESS_TOKEN
|
||||
|
||||
BACKEND_OPENID_AUTH_CODE = 'OpenIDAuthorizationCodeBackend'
|
||||
logger = get_logger(__file__)
|
||||
__all__ = ['OpenIDAuthenticationMiddleware']
|
||||
|
||||
|
||||
class OpenIDAuthenticationMiddleware(MiddlewareMixin):
|
||||
"""
|
||||
Check openid user single logout (with access_token)
|
||||
"""
|
||||
def process_request(self, request):
|
||||
# Don't need openid auth if AUTH_OPENID is False
|
||||
if not settings.AUTH_OPENID:
|
||||
return
|
||||
# Don't need openid auth if no shared session enabled
|
||||
if not settings.AUTH_OPENID_SHARE_SESSION:
|
||||
return
|
||||
# Don't need check single logout if user not authenticated
|
||||
if not request.user.is_authenticated:
|
||||
return
|
||||
elif not request.session[BACKEND_SESSION_KEY].endswith(
|
||||
BACKEND_OPENID_AUTH_CODE):
|
||||
return
|
||||
# Check openid user single logout or not with access_token
|
||||
try:
|
||||
client = new_client()
|
||||
client.get_userinfo(token=request.session.get(OIDT_ACCESS_TOKEN))
|
||||
except Exception as e:
|
||||
logout(request)
|
||||
logger.error(e)
|
|
@ -1,185 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.db import transaction
|
||||
from django.contrib.auth import get_user_model
|
||||
from keycloak.realm import KeycloakRealm
|
||||
from keycloak.keycloak_openid import KeycloakOpenID
|
||||
from users.utils import construct_user_email
|
||||
|
||||
from .signals import post_create_or_update_openid_user
|
||||
from .decorator import ssl_verification
|
||||
|
||||
OIDT_ACCESS_TOKEN = 'oidt_access_token'
|
||||
|
||||
|
||||
class Nonce(object):
|
||||
"""
|
||||
The openid-login is stored in cache as a temporary object, recording the
|
||||
user's redirect_uri and next_pat
|
||||
"""
|
||||
def __init__(self, redirect_uri, next_path):
|
||||
import uuid
|
||||
self.state = uuid.uuid4()
|
||||
self.redirect_uri = redirect_uri
|
||||
self.next_path = next_path
|
||||
|
||||
|
||||
class OpenIDTokenProfile(object):
|
||||
def __init__(self, user, access_token, refresh_token):
|
||||
"""
|
||||
:param user: User object
|
||||
:param access_token:
|
||||
:param refresh_token:
|
||||
"""
|
||||
self.user = user
|
||||
self.access_token = access_token
|
||||
self.refresh_token = refresh_token
|
||||
|
||||
def __str__(self):
|
||||
return "{}'s OpenID token profile".format(self.user.username)
|
||||
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, server_url, realm_name, client_id, client_secret):
|
||||
self.server_url = server_url
|
||||
self.realm_name = realm_name
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
self._openid_client = None
|
||||
self._realm = None
|
||||
self._openid_connect_client = None
|
||||
|
||||
@property
|
||||
def realm(self):
|
||||
if self._realm is None:
|
||||
self._realm = KeycloakRealm(
|
||||
server_url=self.server_url,
|
||||
realm_name=self.realm_name,
|
||||
headers={}
|
||||
)
|
||||
return self._realm
|
||||
|
||||
@property
|
||||
def openid_connect_client(self):
|
||||
"""
|
||||
:rtype: keycloak.openid_connect.KeycloakOpenidConnect
|
||||
"""
|
||||
if self._openid_connect_client is None:
|
||||
self._openid_connect_client = self.realm.open_id_connect(
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret
|
||||
)
|
||||
return self._openid_connect_client
|
||||
|
||||
@property
|
||||
def openid_client(self):
|
||||
"""
|
||||
:rtype: keycloak.keycloak_openid.KeycloakOpenID
|
||||
"""
|
||||
if self._openid_client is None:
|
||||
self._openid_client = KeycloakOpenID(
|
||||
server_url='%sauth/' % self.server_url,
|
||||
realm_name=self.realm_name,
|
||||
client_id=self.client_id,
|
||||
client_secret_key=self.client_secret,
|
||||
)
|
||||
return self._openid_client
|
||||
|
||||
@ssl_verification
|
||||
def get_url(self, name):
|
||||
return self.openid_connect_client.get_url(name=name)
|
||||
|
||||
def get_url_end_session_endpoint(self):
|
||||
return self.get_url(name='end_session_endpoint')
|
||||
|
||||
@ssl_verification
|
||||
def get_authorization_url(self, redirect_uri, scope, state):
|
||||
url = self.openid_connect_client.authorization_url(
|
||||
redirect_uri=redirect_uri, scope=scope, state=state
|
||||
)
|
||||
return url
|
||||
|
||||
@ssl_verification
|
||||
def get_userinfo(self, token):
|
||||
user_info = self.openid_connect_client.userinfo(token=token)
|
||||
return user_info
|
||||
|
||||
@ssl_verification
|
||||
def authorization_code(self, code, redirect_uri):
|
||||
token_response = self.openid_connect_client.authorization_code(
|
||||
code=code, redirect_uri=redirect_uri
|
||||
)
|
||||
return token_response
|
||||
|
||||
@ssl_verification
|
||||
def authorization_password(self, username, password):
|
||||
token_response = self.openid_client.token(
|
||||
username=username, password=password
|
||||
)
|
||||
return token_response
|
||||
|
||||
def update_or_create_from_code(self, code, redirect_uri):
|
||||
"""
|
||||
Update or create an user based on an authentication code.
|
||||
Response as specified in:
|
||||
https://tools.ietf.org/html/rfc6749#section-4.1.4
|
||||
:param str code: authentication code
|
||||
:param str redirect_uri:
|
||||
:rtype: OpenIDTokenProfile
|
||||
"""
|
||||
token_response = self.authorization_code(code, redirect_uri)
|
||||
return self._update_or_create(token_response=token_response)
|
||||
|
||||
def update_or_create_from_password(self, username, password):
|
||||
"""
|
||||
Update or create an user based on an authentication username and password.
|
||||
:param str username: authentication username
|
||||
:param str password: authentication password
|
||||
:return: OpenIDTokenProfile
|
||||
"""
|
||||
token_response = self.authorization_password(username, password)
|
||||
return self._update_or_create(token_response=token_response)
|
||||
|
||||
def _update_or_create(self, token_response):
|
||||
"""
|
||||
Update or create an user based on a token response.
|
||||
`token_response` contains the items returned by the OpenIDConnect Token API
|
||||
end-point:
|
||||
- id_token
|
||||
- access_token
|
||||
- expires_in
|
||||
- refresh_token
|
||||
- refresh_expires_in
|
||||
:param dict token_response:
|
||||
:rtype: OpenIDTokenProfile
|
||||
"""
|
||||
userinfo = self.get_userinfo(token=token_response['access_token'])
|
||||
with transaction.atomic():
|
||||
name = userinfo.get('name', '')
|
||||
username = userinfo.get('preferred_username', '')
|
||||
email = userinfo.get('email', '')
|
||||
email = construct_user_email(username, email)
|
||||
|
||||
user, created = get_user_model().objects.update_or_create(
|
||||
username=username,
|
||||
defaults={
|
||||
'name': name, 'email': email,
|
||||
'first_name': userinfo.get('given_name', ''),
|
||||
'last_name': userinfo.get('family_name', ''),
|
||||
}
|
||||
)
|
||||
oidt_profile = OpenIDTokenProfile(
|
||||
user=user,
|
||||
access_token=token_response['access_token'],
|
||||
refresh_token=token_response['refresh_token'],
|
||||
)
|
||||
if user:
|
||||
post_create_or_update_openid_user.send(
|
||||
sender=user.__class__, user=user, created=created
|
||||
)
|
||||
|
||||
return oidt_profile
|
||||
|
||||
def __str__(self):
|
||||
return self.client_id
|
|
@ -1,5 +0,0 @@
|
|||
from django.dispatch import Signal
|
||||
|
||||
|
||||
post_create_or_update_openid_user = Signal(providing_args=('user',))
|
||||
post_openid_login_success = Signal(providing_args=('user', 'request'))
|
|
@ -1,11 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', views.OpenIDLoginView.as_view(), name='openid-login'),
|
||||
path('login/complete/', views.OpenIDLoginCompleteView.as_view(),
|
||||
name='openid-login-complete'),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.conf import settings
|
||||
from .models import Client
|
||||
|
||||
__all__ = ['new_client']
|
||||
|
||||
|
||||
def new_client():
|
||||
"""
|
||||
:return: authentication.models.Client
|
||||
"""
|
||||
return Client(
|
||||
server_url=settings.AUTH_OPENID_SERVER_URL,
|
||||
realm_name=settings.AUTH_OPENID_REALM_NAME,
|
||||
client_id=settings.AUTH_OPENID_CLIENT_ID,
|
||||
client_secret=settings.AUTH_OPENID_CLIENT_SECRET
|
||||
)
|
|
@ -1,71 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.views.generic.base import RedirectView
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.http.response import (
|
||||
HttpResponseBadRequest,
|
||||
HttpResponseServerError,
|
||||
HttpResponseRedirect
|
||||
)
|
||||
|
||||
from .utils import new_client
|
||||
from .models import Nonce
|
||||
from .signals import post_openid_login_success
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
client = new_client()
|
||||
|
||||
__all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView']
|
||||
|
||||
|
||||
class OpenIDLoginView(RedirectView):
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
redirect_uri = settings.BASE_SITE_URL + \
|
||||
str(settings.AUTH_OPENID_LOGIN_COMPLETE_URL)
|
||||
nonce = Nonce(
|
||||
redirect_uri=redirect_uri,
|
||||
next_path=self.request.GET.get('next')
|
||||
)
|
||||
cache.set(str(nonce.state), nonce, 24*3600)
|
||||
|
||||
self.request.session['openid_state'] = str(nonce.state)
|
||||
authorization_url = client.get_authorization_url(
|
||||
redirect_uri=nonce.redirect_uri,
|
||||
scope='code',
|
||||
state=str(nonce.state)
|
||||
)
|
||||
return authorization_url
|
||||
|
||||
|
||||
class OpenIDLoginCompleteView(RedirectView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'error' in request.GET:
|
||||
return HttpResponseServerError(self.request.GET['error'])
|
||||
if 'code' not in self.request.GET and 'state' not in self.request.GET:
|
||||
return HttpResponseBadRequest(content='Code or State is empty')
|
||||
if self.request.GET['state'] != self.request.session['openid_state']:
|
||||
return HttpResponseBadRequest(content='State invalid')
|
||||
nonce = cache.get(self.request.GET['state'])
|
||||
if not nonce:
|
||||
return HttpResponseBadRequest(content='State failure')
|
||||
|
||||
user = authenticate(
|
||||
request=self.request,
|
||||
code=self.request.GET['code'],
|
||||
redirect_uri=nonce.redirect_uri
|
||||
)
|
||||
cache.delete(str(nonce.state))
|
||||
if not user:
|
||||
return HttpResponseBadRequest(content='Authenticate user failed')
|
||||
|
||||
login(self.request, user)
|
||||
post_openid_login_success.send(
|
||||
sender=self.__class__, user=user, request=self.request
|
||||
)
|
||||
return HttpResponseRedirect(nonce.next_path or '/')
|
||||
|
|
@ -171,7 +171,7 @@ class MFARequiredError(NeedMoreInfoError):
|
|||
'error': self.error,
|
||||
'msg': self.msg,
|
||||
'data': {
|
||||
'choices': ['otp'],
|
||||
'choices': ['code'],
|
||||
'url': reverse('api-auth:mfa-challenge')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,8 +62,7 @@ class AuthMixin:
|
|||
password = request.POST.get('password', '')
|
||||
public_key = request.POST.get('public_key', '')
|
||||
user, error = check_user_valid(
|
||||
username=username, password=password,
|
||||
public_key=public_key
|
||||
request=request, username=username, password=password, public_key=public_key
|
||||
)
|
||||
ip = self.get_request_ip()
|
||||
if not user:
|
||||
|
|
|
@ -1,55 +1,15 @@
|
|||
from django.http.request import QueryDict
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth.signals import user_logged_out
|
||||
from django_auth_ldap.backend import populate_user
|
||||
|
||||
from oidc_rp.signals import oidc_user_created
|
||||
from users.models import User
|
||||
from .backends.openid import new_client
|
||||
from .backends.openid.signals import (
|
||||
post_create_or_update_openid_user, post_openid_login_success
|
||||
)
|
||||
from .signals import post_auth_success
|
||||
from jms_oidc_rp.signals import oidc_user_login_success, oidc_user_login_failed
|
||||
|
||||
from .signals import post_auth_success, post_auth_failed
|
||||
|
||||
|
||||
@receiver(user_logged_out)
|
||||
def on_user_logged_out(sender, request, user, **kwargs):
|
||||
query = QueryDict('', mutable=True)
|
||||
query.update({
|
||||
'redirect_uri': settings.BASE_SITE_URL
|
||||
})
|
||||
# openid (keycloak)
|
||||
if settings.AUTH_OPENID and settings.AUTH_OPENID_SHARE_SESSION:
|
||||
client = new_client()
|
||||
end_session_endpoint = client.get_url_end_session_endpoint()
|
||||
openid_logout_url = "%s?%s" % (end_session_endpoint, query.urlencode())
|
||||
request.COOKIES['next'] = openid_logout_url
|
||||
return
|
||||
@receiver(oidc_user_login_success)
|
||||
def on_oidc_user_login_success(sender, request, user, **kwargs):
|
||||
post_auth_success.send(sender, user=user, request=request)
|
||||
|
||||
|
||||
@receiver(post_create_or_update_openid_user)
|
||||
def on_post_create_or_update_openid_user(sender, user=None, created=True, **kwargs):
|
||||
if created and user and user.username != 'admin':
|
||||
user.source = user.SOURCE_OPENID
|
||||
user.save()
|
||||
|
||||
|
||||
@receiver(post_openid_login_success)
|
||||
def on_openid_login_success(sender, user=None, request=None, **kwargs):
|
||||
post_auth_success.send(sender=sender, user=user, request=request)
|
||||
|
||||
|
||||
@receiver(populate_user)
|
||||
def on_ldap_create_user(sender, user, ldap_user, **kwargs):
|
||||
if user and user.username not in ['admin']:
|
||||
exists = User.objects.filter(username=user.username).exists()
|
||||
if not exists:
|
||||
user.source = user.SOURCE_LDAP
|
||||
user.save()
|
||||
|
||||
|
||||
@receiver(oidc_user_created)
|
||||
def on_oidc_user_created(sender, request, oidc_user, **kwargs):
|
||||
oidc_user.user.source = User.SOURCE_OPENID
|
||||
oidc_user.user.save()
|
||||
@receiver(oidc_user_login_failed)
|
||||
def on_oidc_user_login_failed(sender, username, request, reason, **kwargs):
|
||||
post_auth_failed.send(sender, username=username, request=request, reason=reason)
|
||||
|
|
|
@ -52,21 +52,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if AUTH_OPENID or AUTH_OIDC_RP %}
|
||||
{% if AUTH_OPENID %}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<p class="text-muted text-center">{% trans "More login options" %}</p>
|
||||
<div>
|
||||
{% if AUTH_OIDC_RP %}
|
||||
<button type="button" class="btn btn-default btn-sm btn-block" onclick="location.href='{% url 'authentication:oidc-rp:oidc-login' %}'">
|
||||
<i class="fa fa-openid"></i>
|
||||
{% trans 'OpenID' %}
|
||||
</button>
|
||||
{% elif AUTH_OPENID %}
|
||||
<button type="button" class="btn btn-default btn-sm btn-block" onclick="location.href='{% url 'authentication:openid:openid-login' %}'">
|
||||
<button type="button" class="btn btn-default btn-sm btn-block" onclick="location.href='{% url 'authentication:oidc:login' %}'">
|
||||
<i class="fa fa-openid"></i>
|
||||
{% trans 'Keycloak' %}
|
||||
{% trans 'OpenID' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ urlpatterns = [
|
|||
path('logout/', views.UserLogoutView.as_view(), name='logout'),
|
||||
|
||||
# openid
|
||||
path('openid/', include(('authentication.backends.openid.urls', 'authentication'), namespace='openid')),
|
||||
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
|
||||
path('oidc-rp/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='oidc-rp')),
|
||||
path('oidc/', include(('jms_oidc_rp.urls', 'authentication'), namespace='oidc')),
|
||||
]
|
||||
|
|
|
@ -58,11 +58,9 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
if self.request.GET.get("admin", 0):
|
||||
return None
|
||||
if settings.AUTH_OPENID:
|
||||
redirect_url = reverse("authentication:openid:openid-login")
|
||||
redirect_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME)
|
||||
elif settings.AUTH_CAS:
|
||||
redirect_url = reverse(settings.CAS_LOGIN_URL_NAME)
|
||||
elif settings.AUTH_OIDC_RP:
|
||||
redirect_url = reverse(settings.OIDC_RP_LOGIN_URL_NAME)
|
||||
|
||||
if redirect_url:
|
||||
query_string = request.GET.urlencode()
|
||||
|
@ -113,7 +111,6 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
context = {
|
||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||
'AUTH_OPENID': settings.AUTH_OPENID,
|
||||
'AUTH_OIDC_RP': settings.AUTH_OIDC_RP,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -188,24 +185,18 @@ class UserLogoutView(TemplateView):
|
|||
|
||||
@staticmethod
|
||||
def get_backend_logout_url():
|
||||
if settings.AUTH_OPENID:
|
||||
return settings.AUTH_OPENID_AUTH_LOGOUT_URL_NAME
|
||||
# if settings.AUTH_CAS:
|
||||
# return settings.CAS_LOGOUT_URL_NAME
|
||||
|
||||
# oidc rp
|
||||
if settings.AUTH_OIDC_RP:
|
||||
return reverse(settings.OIDC_RP_LOGOUT_URL_NAME)
|
||||
return None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
auth_logout(request)
|
||||
|
||||
backend_logout_url = self.get_backend_logout_url()
|
||||
if backend_logout_url:
|
||||
return redirect(backend_logout_url)
|
||||
|
||||
next_uri = request.COOKIES.get("next")
|
||||
if next_uri:
|
||||
return redirect(next_uri)
|
||||
|
||||
auth_logout(request)
|
||||
response = super().get(request, *args, **kwargs)
|
||||
return response
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
3. 程序需要, 用户需要更改的写到本config中
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
import errno
|
||||
|
@ -15,6 +16,7 @@ import json
|
|||
import yaml
|
||||
from importlib import import_module
|
||||
from django.urls import reverse_lazy
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PROJECT_DIR = os.path.dirname(BASE_DIR)
|
||||
|
@ -36,6 +38,38 @@ def import_string(dotted_path):
|
|||
) from err
|
||||
|
||||
|
||||
def is_absolute_uri(uri):
|
||||
""" 判断一个uri是否是绝对地址 """
|
||||
if not isinstance(uri, str):
|
||||
return False
|
||||
|
||||
result = re.match(r'^http[s]?://.*', uri)
|
||||
if result is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def build_absolute_uri(base, uri):
|
||||
""" 构建绝对uri地址 """
|
||||
if uri is None:
|
||||
return base
|
||||
|
||||
if isinstance(uri, int):
|
||||
uri = str(uri)
|
||||
|
||||
if not isinstance(uri, str):
|
||||
return base
|
||||
|
||||
if is_absolute_uri(uri):
|
||||
return uri
|
||||
|
||||
parsed_base = urlparse(base)
|
||||
url = "{}://{}".format(parsed_base.scheme, parsed_base.netloc)
|
||||
path = '{}/{}/'.format(parsed_base.path.strip('/'), uri.strip('/'))
|
||||
return urljoin(url, path)
|
||||
|
||||
|
||||
class DoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
@ -134,26 +168,35 @@ class Config(dict):
|
|||
'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False,
|
||||
'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1,
|
||||
|
||||
# OpenID 配置参数
|
||||
# OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8)
|
||||
'AUTH_OPENID': False,
|
||||
'AUTH_OPENID_CLIENT_ID': 'client-id',
|
||||
'AUTH_OPENID_CLIENT_SECRET': 'client-secret',
|
||||
'AUTH_OPENID_SHARE_SESSION': True,
|
||||
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
|
||||
# OpenID 新配置参数 (version >= 1.5.8)
|
||||
'AUTH_OPENID_PROVIDER_ENDPOINT': 'https://op-example.com/',
|
||||
'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://op-example.com/authorize',
|
||||
'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT': 'https://op-example.com/token',
|
||||
'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT': 'https://op-example.com/jwks',
|
||||
'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT': 'https://op-example.com/userinfo',
|
||||
'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT': 'https://op-example.com/logout',
|
||||
'AUTH_OPENID_PROVIDER_SIGNATURE_ALG': 'HS256',
|
||||
'AUTH_OPENID_PROVIDER_SIGNATURE_KEY': None,
|
||||
'AUTH_OPENID_PROVIDER_CLAIMS_NAME': None,
|
||||
'AUTH_OPENID_PROVIDER_CLAIMS_USERNAME': None,
|
||||
'AUTH_OPENID_PROVIDER_CLAIMS_EMAIL': None,
|
||||
'AUTH_OPENID_SCOPES': 'openid profile email',
|
||||
'AUTH_OPENID_ID_TOKEN_MAX_AGE': 60,
|
||||
'AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO': True,
|
||||
'AUTH_OPENID_USE_STATE': True,
|
||||
'AUTH_OPENID_USE_NONCE': True,
|
||||
'AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION': True,
|
||||
# OpenID 旧配置参数 (version <= 1.5.8 (discarded))
|
||||
'BASE_SITE_URL': 'http://localhost:8080',
|
||||
'AUTH_OPENID_SERVER_URL': 'http://openid',
|
||||
'AUTH_OPENID_REALM_NAME': 'jumpserver',
|
||||
'AUTH_OPENID_CLIENT_ID': 'jumpserver',
|
||||
'AUTH_OPENID_CLIENT_SECRET': '',
|
||||
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
|
||||
'AUTH_OPENID_SHARE_SESSION': True,
|
||||
|
||||
|
||||
'AUTH_OIDC_RP': False,
|
||||
'OIDC_RP_CLIENT_ID': 'client-id',
|
||||
'OIDC_RP_CLIENT_SECRET': 'client-secret',
|
||||
'OIDC_RP_PROVIDER_ENDPOINT': 'https://op-endpoint.com',
|
||||
'OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://op-endpoint.com/authorize',
|
||||
'OIDC_RP_PROVIDER_TOKEN_ENDPOINT': 'https://op-endpoint.com/token',
|
||||
'OIDC_RP_PROVIDER_JWKS_ENDPOINT': 'https://op-endpoint.com/jwk',
|
||||
'OIDC_RP_PROVIDER_USERINFO_ENDPOINT': 'https://op-endpoint.com/userinfo',
|
||||
'OIDC_RP_PROVIDER_END_SESSION_ENDPOINT': 'https://op-endpoint.com/logout',
|
||||
'OIDC_RP_ID_TOKEN_MAX_AGE': 60,
|
||||
'AUTH_OPENID_REALM_NAME': None,
|
||||
|
||||
'AUTH_RADIUS': False,
|
||||
'RADIUS_SERVER': 'localhost',
|
||||
|
@ -217,6 +260,88 @@ class Config(dict):
|
|||
'ORG_CHANGE_TO_URL': ''
|
||||
}
|
||||
|
||||
def compatible_auth_openid_of_key(self):
|
||||
"""
|
||||
兼容OpenID旧配置 (即 version <= 1.5.8)
|
||||
因为旧配置只支持OpenID协议的Keycloak实现,
|
||||
所以只需要根据旧配置和Keycloak的Endpoint说明文档,
|
||||
构造出新配置中标准OpenID协议中所需的Endpoint即可
|
||||
(Keycloak说明文档参考: https://www.keycloak.org/docs/latest/securing_apps/)
|
||||
"""
|
||||
if not self.AUTH_OPENID:
|
||||
return
|
||||
|
||||
realm_name = self.AUTH_OPENID_REALM_NAME
|
||||
if realm_name is None:
|
||||
return
|
||||
|
||||
compatible_keycloak_config = [
|
||||
(
|
||||
'AUTH_OPENID_PROVIDER_ENDPOINT',
|
||||
self.AUTH_OPENID_SERVER_URL
|
||||
),
|
||||
(
|
||||
'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT',
|
||||
'/realms/{}/protocol/openid-connect/auth'.format(realm_name)
|
||||
),
|
||||
(
|
||||
'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT',
|
||||
'/realms/{}/protocol/openid-connect/token'.format(realm_name)
|
||||
),
|
||||
(
|
||||
'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT',
|
||||
'/realms/{}/protocol/openid-connect/certs'.format(realm_name)
|
||||
),
|
||||
(
|
||||
'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT',
|
||||
'/realms/{}/protocol/openid-connect/userinfo'.format(realm_name)
|
||||
),
|
||||
(
|
||||
'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT',
|
||||
'/realms/{}/protocol/openid-connect/logout'.format(realm_name)
|
||||
)
|
||||
]
|
||||
for key, value in compatible_keycloak_config:
|
||||
self[key] = value
|
||||
|
||||
def compatible_auth_openid_of_value(self):
|
||||
"""
|
||||
兼容值的绝对路径、相对路径
|
||||
(key 为 AUTH_OPENID_PROVIDER_*_ENDPOINT 的配置)
|
||||
"""
|
||||
if not self.AUTH_OPENID:
|
||||
return
|
||||
|
||||
base = self.AUTH_OPENID_PROVIDER_ENDPOINT
|
||||
config = list(self.items())
|
||||
for key, value in config:
|
||||
result = re.match(r'^AUTH_OPENID_PROVIDER_.*_ENDPOINT$', key)
|
||||
if result is None:
|
||||
continue
|
||||
if value is None:
|
||||
# None 在 url 中有特殊含义 (比如对于: end_session_endpoint)
|
||||
continue
|
||||
value = build_absolute_uri(base, value)
|
||||
self[key] = value
|
||||
|
||||
def compatible(self):
|
||||
"""
|
||||
对配置做兼容处理
|
||||
1. 对`key`的兼容 (例如:版本升级)
|
||||
2. 对`value`做兼容 (例如:True、true、1 => True)
|
||||
|
||||
处理顺序要保持先对key做处理, 再对value做处理,
|
||||
因为处理value的时候,只根据最新版本支持的key进行
|
||||
"""
|
||||
parts = ['key', 'value']
|
||||
targets = ['auth_openid']
|
||||
for part in parts:
|
||||
for target in targets:
|
||||
method_name = 'compatible_{}_of_{}'.format(target, part)
|
||||
method = getattr(self, method_name, None)
|
||||
if method is not None:
|
||||
method()
|
||||
|
||||
def convert_type(self, k, v):
|
||||
default_value = self.defaults.get(k)
|
||||
if default_value is None:
|
||||
|
@ -305,11 +430,8 @@ class DynamicConfig:
|
|||
if self.static_config.get('AUTH_CAS'):
|
||||
backends.insert(0, 'authentication.backends.cas.CASBackend')
|
||||
if self.static_config.get('AUTH_OPENID'):
|
||||
backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend')
|
||||
backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend')
|
||||
if self.static_config.get('AUTH_OIDC_RP'):
|
||||
backends.insert(0, 'authentication.backends.oidc.backends.OIDCAuthCodeBackend')
|
||||
backends.insert(0, 'authentication.backends.oidc.backends.OIDCAuthPasswordBackend',)
|
||||
backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend')
|
||||
backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthPasswordBackend')
|
||||
if self.static_config.get('AUTH_RADIUS'):
|
||||
backends.insert(0, 'authentication.backends.radius.RadiusBackend')
|
||||
return backends
|
||||
|
@ -490,9 +612,9 @@ class ConfigManager:
|
|||
|
||||
manager = cls(root_path=root_path)
|
||||
if manager.load_from_object():
|
||||
return manager.config
|
||||
config = manager.config
|
||||
elif manager.load_from_yml():
|
||||
return manager.config
|
||||
config = manager.config
|
||||
else:
|
||||
msg = """
|
||||
|
||||
|
@ -502,6 +624,10 @@ class ConfigManager:
|
|||
"""
|
||||
raise ImportError(msg)
|
||||
|
||||
# 对config进行兼容处理
|
||||
config.compatible()
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def get_dynamic_config(cls, config):
|
||||
return DynamicConfig(config)
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
#
|
||||
import os
|
||||
import ldap
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from ..const import CONFIG, DYNAMIC, PROJECT_DIR
|
||||
|
||||
|
@ -43,39 +42,37 @@ AUTH_LDAP_SYNC_INTERVAL = CONFIG.AUTH_LDAP_SYNC_INTERVAL
|
|||
AUTH_LDAP_SYNC_CRONTAB = CONFIG.AUTH_LDAP_SYNC_CRONTAB
|
||||
AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS
|
||||
|
||||
# openid
|
||||
# Auth OpenID settings
|
||||
BASE_SITE_URL = CONFIG.BASE_SITE_URL
|
||||
|
||||
# ==============================================================================
|
||||
# 认证 OpenID 配置参数
|
||||
# 参考: https://django-oidc-rp.readthedocs.io/en/stable/settings.html
|
||||
# ==============================================================================
|
||||
AUTH_OPENID = CONFIG.AUTH_OPENID
|
||||
AUTH_OPENID_SERVER_URL = CONFIG.AUTH_OPENID_SERVER_URL
|
||||
AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME
|
||||
AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID
|
||||
AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET
|
||||
AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION
|
||||
AUTH_OPENID_PROVIDER_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_ENDPOINT
|
||||
AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT
|
||||
AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT
|
||||
AUTH_OPENID_PROVIDER_JWKS_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_JWKS_ENDPOINT
|
||||
AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT
|
||||
AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT
|
||||
AUTH_OPENID_PROVIDER_SIGNATURE_ALG = CONFIG.AUTH_OPENID_PROVIDER_SIGNATURE_ALG
|
||||
AUTH_OPENID_PROVIDER_SIGNATURE_KEY = CONFIG.AUTH_OPENID_PROVIDER_SIGNATURE_KEY
|
||||
AUTH_OPENID_PROVIDER_CLAIMS_NAME = CONFIG.AUTH_OPENID_PROVIDER_CLAIMS_NAME
|
||||
AUTH_OPENID_PROVIDER_CLAIMS_USERNAME = CONFIG.AUTH_OPENID_PROVIDER_CLAIMS_USERNAME
|
||||
AUTH_OPENID_PROVIDER_CLAIMS_EMAIL = CONFIG.AUTH_OPENID_PROVIDER_CLAIMS_EMAIL
|
||||
AUTH_OPENID_SCOPES = CONFIG.AUTH_OPENID_SCOPES
|
||||
AUTH_OPENID_ID_TOKEN_MAX_AGE = CONFIG.AUTH_OPENID_ID_TOKEN_MAX_AGE
|
||||
AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO = CONFIG.AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO
|
||||
AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION
|
||||
AUTH_OPENID_LOGIN_URL = reverse_lazy("authentication:openid:openid-login")
|
||||
AUTH_OPENID_LOGIN_COMPLETE_URL = reverse_lazy("authentication:openid:openid-login-complete")
|
||||
|
||||
# oidc rp
|
||||
# jumpserver
|
||||
AUTH_OIDC_RP = CONFIG.AUTH_OIDC_RP
|
||||
if AUTH_OIDC_RP:
|
||||
# 优先使用AUTH_OIDC_RP
|
||||
AUTH_OPENID = False
|
||||
OIDC_RP_LOGIN_URL_NAME = "authentication:oidc-rp:oidc-login"
|
||||
OIDC_RP_LOGIN_CALLBACK_URL_NAME = "authentication:oidc-rp:oidc-callback"
|
||||
OIDC_RP_LOGOUT_URL_NAME = "authentication:oidc-rp:oidc-logout"
|
||||
# https://django-oidc-rp.readthedocs.io/en/stable/settings.html#required-settings
|
||||
OIDC_RP_CLIENT_ID = CONFIG.OIDC_RP_CLIENT_ID
|
||||
OIDC_RP_CLIENT_SECRET = CONFIG.OIDC_RP_CLIENT_SECRET
|
||||
OIDC_RP_PROVIDER_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_ENDPOINT
|
||||
OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT
|
||||
OIDC_RP_PROVIDER_TOKEN_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_TOKEN_ENDPOINT
|
||||
OIDC_RP_PROVIDER_JWKS_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_JWKS_ENDPOINT
|
||||
OIDC_RP_PROVIDER_USERINFO_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_USERINFO_ENDPOINT
|
||||
OIDC_RP_PROVIDER_END_SESSION_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_END_SESSION_ENDPOINT
|
||||
OIDC_RP_ID_TOKEN_MAX_AGE = CONFIG.OIDC_RP_ID_TOKEN_MAX_AGE
|
||||
|
||||
AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION
|
||||
AUTH_OPENID_USE_STATE = CONFIG.AUTH_OPENID_USE_STATE
|
||||
AUTH_OPENID_USE_NONCE = CONFIG.AUTH_OPENID_USE_NONCE
|
||||
AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION = CONFIG.AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION
|
||||
AUTH_OPENID_AUTH_LOGIN_URL_NAME = 'authentication:oidc:login'
|
||||
AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:oidc:login-callback'
|
||||
AUTH_OPENID_AUTH_LOGOUT_URL_NAME = 'authentication:oidc:logout'
|
||||
# ==============================================================================
|
||||
|
||||
# Radius Auth
|
||||
AUTH_RADIUS = CONFIG.AUTH_RADIUS
|
||||
|
|
|
@ -48,6 +48,7 @@ INSTALLED_APPS = [
|
|||
'authentication.apps.AuthenticationConfig', # authentication
|
||||
'applications.apps.ApplicationsConfig',
|
||||
'tickets.apps.TicketsConfig',
|
||||
'jms_oidc_rp',
|
||||
'rest_framework',
|
||||
'rest_framework_swagger',
|
||||
'drf_yasg',
|
||||
|
@ -56,7 +57,6 @@ INSTALLED_APPS = [
|
|||
'django_filters',
|
||||
'bootstrap3',
|
||||
'captcha',
|
||||
'oidc_rp',
|
||||
'django_celery_beat',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.admin',
|
||||
|
@ -76,13 +76,12 @@ MIDDLEWARE = [
|
|||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'authentication.backends.openid.middleware.OpenIDAuthenticationMiddleware',
|
||||
'jms_oidc_rp.middleware.OIDCRefreshIDTokenMiddleware',
|
||||
'django_cas_ng.middleware.CASMiddleware',
|
||||
'jumpserver.middleware.TimezoneMiddleware',
|
||||
'jumpserver.middleware.DemoMiddleware',
|
||||
'jumpserver.middleware.RequestMiddleware',
|
||||
'orgs.middleware.OrgMiddleware',
|
||||
'oidc_rp.middleware.OIDCRefreshIDTokenMiddleware',
|
||||
]
|
||||
|
||||
|
||||
|
@ -105,7 +104,7 @@ TEMPLATES = [
|
|||
'django.template.context_processors.media',
|
||||
'jumpserver.context_processor.jumpserver_processor',
|
||||
'orgs.context_processor.org_processor',
|
||||
'oidc_rp.context_processors.oidc',
|
||||
'jms_oidc_rp.context_processors.oidc',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -18,4 +18,3 @@ def get_current_request():
|
|||
|
||||
|
||||
current_request = LocalProxy(partial(_find, 'current_request'))
|
||||
|
||||
|
|
|
@ -3,12 +3,19 @@
|
|||
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django_auth_ldap.backend import populate_user
|
||||
from django.conf import settings
|
||||
from django_cas_ng.signals import cas_user_authenticated
|
||||
|
||||
from jms_oidc_rp.signals import oidc_user_created, oidc_user_updated
|
||||
from jms_oidc_rp.backends import get_userinfo_from_claims
|
||||
|
||||
from common.utils import get_logger
|
||||
from .utils import construct_user_email
|
||||
from .signals import post_user_create
|
||||
from .models import User
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
|
@ -37,3 +44,30 @@ def on_cas_user_authenticated(sender, user, created, **kwargs):
|
|||
if created:
|
||||
user.source = user.SOURCE_CAS
|
||||
user.save()
|
||||
|
||||
|
||||
@receiver(populate_user)
|
||||
def on_ldap_create_user(sender, user, ldap_user, **kwargs):
|
||||
if user and user.username not in ['admin']:
|
||||
exists = User.objects.filter(username=user.username).exists()
|
||||
if not exists:
|
||||
user.source = user.SOURCE_LDAP
|
||||
user.save()
|
||||
|
||||
|
||||
@receiver(oidc_user_created)
|
||||
def on_oidc_user_created(sender, request, oidc_user, **kwargs):
|
||||
oidc_user.user.source = User.SOURCE_OPENID
|
||||
oidc_user.user.save()
|
||||
|
||||
|
||||
@receiver(oidc_user_updated)
|
||||
def on_oidc_user_updated(sender, request, oidc_user, **kwargs):
|
||||
if not settings.AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION:
|
||||
return
|
||||
name, username, email = get_userinfo_from_claims(oidc_user.userinfo)
|
||||
email = construct_user_email(username, email)
|
||||
oidc_user.user.name = name
|
||||
oidc_user.user.username = username
|
||||
oidc_user.user.email = email
|
||||
oidc_user.user.save()
|
||||
|
|
|
@ -326,7 +326,7 @@ def get_source_choices():
|
|||
]
|
||||
if settings.AUTH_LDAP:
|
||||
choices.append((User.SOURCE_LDAP, choices_all[User.SOURCE_LDAP]))
|
||||
if settings.AUTH_OPENID or settings.AUTH_OIDC_RP:
|
||||
if settings.AUTH_OPENID:
|
||||
choices.append((User.SOURCE_OPENID, choices_all[User.SOURCE_OPENID]))
|
||||
if settings.AUTH_RADIUS:
|
||||
choices.append((User.SOURCE_RADIUS, choices_all[User.SOURCE_RADIUS]))
|
||||
|
|
|
@ -29,7 +29,6 @@ BOOTSTRAP_TOKEN:
|
|||
# 使用单文件sqlite数据库
|
||||
# DB_ENGINE: sqlite3
|
||||
# DB_NAME:
|
||||
|
||||
# MySQL or postgres setting like:
|
||||
# 使用Mysql作为数据库
|
||||
DB_ENGINE: mysql
|
||||
|
@ -55,11 +54,7 @@ REDIS_PORT: 6379
|
|||
# REDIS_DB_CACHE: 4
|
||||
|
||||
# Use OpenID authorization
|
||||
#
|
||||
# 配置说明: 如果您使用的是Keycloak作为OP,可以使用方式1或方式2; 如果OP不是Keycloak, 请使用方式2
|
||||
#
|
||||
# 方式1: OpenID认证 (基于 oidc 协议的 keycloak 的实现)
|
||||
#
|
||||
# 使用OpenID 来进行认证设置
|
||||
# BASE_SITE_URL: http://localhost:8080
|
||||
# AUTH_OPENID: false # True or False
|
||||
# AUTH_OPENID_SERVER_URL: https://openid-auth-server.com/
|
||||
|
@ -68,19 +63,6 @@ REDIS_PORT: 6379
|
|||
# AUTH_OPENID_CLIENT_SECRET: client-secret
|
||||
# AUTH_OPENID_IGNORE_SSL_VERIFICATION: True
|
||||
# AUTH_OPENID_SHARE_SESSION: True
|
||||
#
|
||||
# 方式2: OpenID认证 (使用标准 oidc 协议进行认证)
|
||||
# 配置参数详细信息参考: https://django-oidc-rp.readthedocs.io/en/stable/settings.html
|
||||
#
|
||||
# AUTH_OIDC_RP: False
|
||||
# OIDC_RP_CLIENT_ID: client-id
|
||||
# OIDC_RP_CLIENT_SECRET: client-secret
|
||||
# OIDC_RP_PROVIDER_ENDPOINT: https://op-endpoint.com
|
||||
# OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT: https://op-endpoint.com/authorize
|
||||
# OIDC_RP_PROVIDER_TOKEN_ENDPOINT: https://op-endpoint.com/token
|
||||
# OIDC_RP_PROVIDER_JWKS_ENDPOINT: https://op-endpoint.com/jwk
|
||||
# OIDC_RP_PROVIDER_USERINFO_ENDPOINT: https://op-endpoint.com/userinfo
|
||||
# OIDC_RP_PROVIDER_END_SESSION_ENDPOINT: https://op-endpoint.com/logout
|
||||
|
||||
# Use Radius authorization
|
||||
# 使用Radius来认证
|
||||
|
|
|
@ -74,8 +74,6 @@ Werkzeug==0.15.3
|
|||
drf-nested-routers==0.91
|
||||
aliyun-python-sdk-core-v3==2.9.1
|
||||
aliyun-python-sdk-ecs==4.10.1
|
||||
python-keycloak==0.13.3
|
||||
python-keycloak-client==0.1.3
|
||||
rest_condition==1.0.3
|
||||
python-ldap==3.1.0
|
||||
tencentcloud-sdk-python==3.0.40
|
||||
|
@ -98,4 +96,4 @@ ipython
|
|||
huaweicloud-sdk-python==1.0.21
|
||||
django-redis==4.11.0
|
||||
python-redis-lock==3.5.0
|
||||
django-oidc-rp==0.3.4
|
||||
jumpserver-django-oidc-rp==0.3.7.1
|
||||
|
|
Loading…
Reference in New Issue