Skip to content

Commit

Permalink
OM-45 Implement single logout (#6)
Browse files Browse the repository at this point in the history
* OM-9 Added economic unit handling

* Added Log

* Moved logout to acs

* Removed sls

* Added SLO workaround

* Added SAMLRequest to workaround

* Fixed QueryDict check

* Turned od SLO workaround

* Fixed workaround

* OM-45 Implemented single logout

* OM-45 Removed user check for logout

* OM-45 Added role info to test payload

* OM-45 Updated CI script, style
  • Loading branch information
malinowskikam authored Oct 27, 2023
1 parent 2997ec0 commit cea0160
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 18 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
name: Module CI
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- main
- 'release/**'
- develop
- 'feature/**'
workflow_dispatch:
inputs:
comment:
description: Just a simple comment to know the purpose of the manual build
required: false

jobs:
call:
name: Default CI Flow
uses: openimis/openimis-be_py/.github/workflows/ci_module.yml@moldova
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
SONAR_PROJECT_KEY: openimis_openimis-be-msystems_py
SONAR_ORGANIZATION: openimis-1
SONAR_PROJECT_NAME: openimis-be-msystems_py
SONAR_PROJECT_VERSION: 1.0
SONAR_SOURCES: msystems
SONAR_EXCLUSIONS: "**/migrations/**,**/static/**,**/media/**,**/tests/**"
3 changes: 1 addition & 2 deletions msystems/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,11 @@ def __init__(self):
def login(self, username: str, user_data: dict):
with transaction.atomic():
try:
logger.debug("Successful SAML login, username=%s, user_data=%s", username, str(user_data))
user = self._get_or_create_user(username, user_data)
self._update_user_legal_entities(user, user_data)
return user
except BaseException as e:
# Extra logging for the development, should be removed for any real data usage
# as it will put personal information in logs
logger.debug("Successful SAML login handling failed, username=%s, user_data=%s", username,
str(user_data), exc_info=e)
raise
Expand Down
6 changes: 4 additions & 2 deletions msystems/tests/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
'BirthDate': ['1970-01-01'],
'OrganizationAdministrator': [
'Test Organisation 1 2345234523452',
]
],
'Role': ['Employer']
}

example_user_data_multiple_ph = {
Expand All @@ -15,5 +16,6 @@
'OrganizationAdministrator': [
'Test Organisation 1 2345234523452',
'Test Organisation 2 1234123412341'
]
],
'Role': ['Employer']
}
2 changes: 1 addition & 1 deletion msystems/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

saml_urls = [
path("login/", views.login),
path("logout/", views.logout),
path("metadata/", views.metadata),
path("acs/", views.acs),
path("sls/", views.sls),
]

urlpatterns = [
Expand Down
68 changes: 55 additions & 13 deletions msystems/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging

from django.http import HttpResponse, HttpResponseServerError
from django.http import HttpResponse, HttpResponseServerError, HttpResponseBadRequest
from django.shortcuts import redirect
from django.views.decorators.http import require_GET, require_POST
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.clickjacking import xframe_options_exempt
from msystems.apps import MsystemsConfig
from msystems.services import SamlUserService
from onelogin.saml2.auth import OneLogin_Saml2_Auth, OneLogin_Saml2_Settings
Expand All @@ -13,7 +14,7 @@
logger = logging.getLogger(__name__)


def _build_auth(request) -> OneLogin_Saml2_Auth:
def _build_auth(request, slo_workaround=False) -> OneLogin_Saml2_Auth:
# From python3-saml django example
req = {
'https': 'on' if request.is_secure() else 'off',
Expand All @@ -24,6 +25,12 @@ def _build_auth(request) -> OneLogin_Saml2_Auth:
# 'lowercase_urlencoding': True,
'post_data': request.POST.copy()
}
if slo_workaround:
# https://github.com/SAML-Toolkits/python3-saml/issues/205
if "SAMLResponse" in req["post_data"]:
req['get_data']['SAMLResponse'] = req["post_data"].get("SAMLResponse")
if "SAMLRequest" in req["post_data"]:
req['get_data']['SAMLRequest'] = req["post_data"].get("SAMLRequest")
return OneLogin_Saml2_Auth(req, MsystemsConfig.saml_config)


Expand All @@ -35,6 +42,16 @@ def login(request):
return redirect(login_request)


@require_GET
@jwt_cookie
def logout(request):
auth = _build_auth(request)
request.delete_jwt_cookie = True
request.delete_refresh_token_cookie = True
logout_request = auth.logout(name_id=request.user.username)
return redirect(logout_request)


@require_GET
def metadata(request):
# from python3-saml docs
Expand All @@ -53,20 +70,16 @@ def metadata(request):
return resp


# Saml have its own csrf protection, django not needed
@csrf_exempt
@jwt_cookie
@require_POST
def acs(request):
def _handle_acs_login(request):
auth = _build_auth(request)
auth.process_response()
errors = auth.get_errors()

if errors:
logger.error("Login attempt failed: %s\n%s", str(
errors[-1]), auth.get_last_error_reason())
logger.error("SAML Login failed: %s\n%s", str(errors[-1]), auth.get_last_error_reason())
# TODO Add information about failed login attempt for the user
return redirect(MsystemsConfig.base_login_redirect)

username = auth.get_nameid()
user_data = auth.get_attributes()

Expand All @@ -82,16 +95,45 @@ def acs(request):
return redirect(MsystemsConfig.base_login_redirect)


def _handle_acs_logout(request):
auth = _build_auth(request, slo_workaround=True)
# We do not use local session, we are telling the lib not to touch it
auth.process_slo(keep_local_session=True)
errors = auth.get_errors()

if errors:
logger.error("SAML Logout failed: %s\n%s", str(errors[-1]), auth.get_last_error_reason())
# TODO Add information about failed logout attempt for the user
return redirect(MsystemsConfig.base_login_redirect)

request.delete_jwt_cookie = True
request.delete_refresh_token_cookie = True

if 'RelayState' in request.POST and _validate_relay_state(request.POST['RelayState']):
return redirect(auth.redirect_to(request.POST['RelayState']))
else:
return redirect(MsystemsConfig.base_login_redirect)


# Saml have its own csrf protection, django not needed
@csrf_exempt
# This is required as mpass calls this endpoint from iframe
@xframe_options_exempt
@jwt_cookie
@require_POST
def sls(request):
# This will be removed
pass
def acs(request):
if 'SAMLResponse' in request.POST:
# Probably login response
return _handle_acs_login(request)
elif 'SAMLRequest' in request.POST:
# Probably logout request
return _handle_acs_logout(request)
else:
return HttpResponseBadRequest("Missing SAML payload")


def _validate_relay_state(relay_state):
# To avoid 'Open Redirect' attacks, before execute the redirection confirm
# the value of the 'RelayState' is a trusted URL.
# Currenly the only valid RelayState base_login_redirect
# Currently, the only valid RelayState base_login_redirect
return relay_state == MsystemsConfig.base_login_redirect

0 comments on commit cea0160

Please sign in to comment.