Source code for simple_openid_connect.integrations.django.views
"""View functions which handle openid authentication and their related callbacks"""importloggingimportsecretsfromdatetimeimportdatetime,timedelta,timezonefromhttpimportHTTPStatusfromtypingimportMappingfromdjango.confimportsettingsfromdjango.contrib.authimportlogin,logoutfromdjango.httpimport(HttpRequest,HttpResponse,HttpResponseBadRequest,HttpResponseRedirect,)fromdjango.shortcutsimportresolve_urlfromdjango.template.responseimportTemplateResponsefromdjango.utils.decoratorsimportmethod_decoratorfromdjango.viewsimportViewfromdjango.views.decorators.cacheimportcache_controlfromsimple_openid_connect.dataimport(FrontChannelLogoutNotification,IdToken,RpInitiatedLogoutRequest,TokenSuccessResponse,)fromsimple_openid_connect.integrations.django.appsimportOpenidAppConfigfromsimple_openid_connect.integrations.django.modelsimportOpenidSessionlogger=logging.getLogger(__name__)
[docs]classInvalidAuthStateError(Exception):""" Exception that is thrown when the LoginCallbackView is served and the user-agent has no authentication procedure currently in progress """
[docs]def__init__(self)->None:super().__init__(self,"User-Agent has no authentication procedures in progress so the login-callback will not be processed",)
[docs]classInvalidNonceError(Exception):""" Exception that is thrown when an authentication response contains an invalid or no nonce value """
[docs]def__init__(self)->None:super().__init__(self,"Authentication response contained an invalid or no nonce value")
[docs]classInitLoginView(View):""" The view which handles initiating a login. It essentially redirects the user agent to the Openid provider. """defget(self,request:HttpRequest)->HttpResponse:logout(request)if"next"inrequest.GET.keys():request.session["login_redirect_url"]=request.GET["next"]# save the login state into the session to prevent CSRF attacks# ref: https://simple-openid-connect.readthedocs.io/en/stable/nonce_and_state.htmlstate=secrets.token_urlsafe(32)request.session["openid_auth_state"]=state# save the time at which authentication was startedrequest.session["openid_auth_start_time"]=datetime.now(tz=timezone.utc).timestamp()# prevent replay attacks by generating and specifying a nonce# ref: https://simple-openid-connect.readthedocs.io/en/stable/nonce_and_state.htmlnonce=secrets.token_urlsafe(48)request.session["openid_auth_nonce"]=nonce# redirect the user-agent to the oidc providerclient=OpenidAppConfig.get_instance().get_client(request)redirect=client.authorization_code_flow.start_authentication(state=state,nonce=nonce)returnHttpResponseRedirect(redirect)
[docs]classLoginCallbackView(View):""" The view which handles login callbacks. It handles an authentication response from the Openid provider that is encoded in the current url by either logging the user in or rendering the error. Error rendering can be customized by overwriting the template *simple_openid_connect/login_failed.html* which receives the context `token_response` of type :class:`TokenErrorResponse <simple_openid_connect.data.TokenErrorResponse>`. """defget(self,request:HttpRequest)->HttpResponse:app_settings=OpenidAppConfig.get_instance().safe_settingsclient=OpenidAppConfig.get_instance().get_client(request)# prevent CSRF attacks by verifying the state parameter# ref: https://simple-openid-connect.readthedocs.io/en/stable/nonce_and_state.htmlifrequest.session.get("openid_auth_state",None)isNone:raiseInvalidAuthStateError()# don't allow login completion if the process was started too long agoifrequest.session.get("openid_auth_start_time",None)isNoneor(datetime.now(tz=timezone.utc)-datetime.fromtimestamp(request.session["openid_auth_start_time"],tz=timezone.utc))>timedelta(seconds=app_settings.OPENID_LOGIN_TIMEOUT):raiseInvalidAuthStateError()else:delrequest.session["openid_auth_start_time"]# exchange the passed code for tokenstoken_response=client.authorization_code_flow.handle_authentication_result(current_url=request.get_full_path(),state=request.session["openid_auth_state"],)ifnotisinstance(token_response,TokenSuccessResponse):returnTemplateResponse(request,"simple_openid_connect/login_failed.html",{"token_response":token_response,},status=HTTPStatus.UNAUTHORIZED,)# validate the received tokensid_token=IdToken.parse_jwt(token_response.id_token,client.provider_keys)id_token.validate_extern(client.provider_config.issuer,client.client_auth.client_id,nonce=request.session["openid_auth_nonce"],)# handle federated user information (create a new user if necessary or update local info) and log the user inuser=OpenidAppConfig.get_instance().user_mapper.handle_federated_userinfo(id_token)openid_session=user.openid.update_session(token_response,id_token)request.session["openid_session"]=openid_session.idlogin(request,user,backend=settings.AUTHENTICATION_BACKENDS[0])# redirect to the next get parameter if present, otherwise to the configured defaultif"login_redirect_url"inrequest.session.keys():returnHttpResponseRedirect(redirect_to=request.session["login_redirect_url"])else:returnHttpResponseRedirect(redirect_to=resolve_url(settings.LOGIN_REDIRECT_URL))
[docs]classLogoutView(View):""" The view which handles logging a user out. """defget(self,request:HttpRequest)->HttpResponse:session_id=request.session.get("openid_session")logout(request)client=OpenidAppConfig.get_instance().get_client(request)ifsettings.LOGOUT_REDIRECT_URLisnotNone:openid_session=(OpenidSession.objects.get(id=session_id)ifsession_idelseNone)logout_request=RpInitiatedLogoutRequest(post_logout_redirect_uri=request.build_absolute_uri(resolve_url(settings.LOGOUT_REDIRECT_URL)))ifopenid_sessionisnotNoneandopenid_session.raw_id_tokenisnotNone:logout_request.id_token_hint=openid_session.raw_id_tokenelse:logout_request.client_id=client.client_auth.client_idelse:logout_request=NonereturnHttpResponseRedirect(client.initiate_logout(logout_request))
[docs]classFrontChannelLogoutNotificationView(View):""" A view which handles Openid front-channel logout notifications by logging out the current session """@method_decorator(cache_control(no_store=True))defget(self,request:HttpRequest)->HttpResponse:logout(request)returnHttpResponse(status=200)