* [Update] 添加django-oidc-rp支持

* [Update] 添加django-oidc-rp支持2

* [Update] 调试django-oidc-rp对keycloak的支持

* [Update] 调试django-oidc-rp对keycloak的支持2

* [Update] 修改oidc_rp创建用户/更新用户的功能

* [Update] oidc_rp添加支持password认证

* [Update] 重写oidc_rp end session view

* [Update] 优化 oidc_rp view backend url 等引用关系
This commit is contained in:
BaiJiangJie
2020-04-22 00:22:24 +08:00
committed by GitHub
parent 9febe488b5
commit 272701a8fd
10 changed files with 324 additions and 12 deletions

View File

@@ -0,0 +1,181 @@
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()

View File

@@ -0,0 +1,10 @@
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'),
]

View File

@@ -0,0 +1,77 @@
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())