Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OM-128 Implement IServiceProvider methods for MPay integration #17

Merged
merged 4 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 29 additions & 46 deletions msystems/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@

DEFAULT_CFG = {
# URL to be redirected to after successful login
'base_login_redirect': "",
'saml_config': {
'mpass_login_redirect': "",
# Mpass configurations
'mpass_config': {
# Strict mode: SAML responses must be validated strictly.
"strict": True,
# Set this to True for debugging purposes.
"debug": False,
# Service provider settinhs
# Service provider settings
"sp": {
# entityId, acs and sls urls are validated by IdP
"entityId": "",
# callback url for login attemps
# callback url for login attempts
"assertionConsumerService": {
"url": "",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
Expand All @@ -23,69 +24,49 @@
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
},
# X509 certificate for the SP, PEM string format
# -----BEGIN CERTIFICATE-----
# some base 64 string
# -----END CERTIFICATE-----
"x509cert": "",
# RSA private key, PEM string format
# -----BEGIN PRIVATE KEY-----
# some base 64 string
# -----END PRIVATE KEY-----
"privateKey": ""
},
"idp": {
"entityId": "",
# login endpoint to redirect to
# login endpoint to redirect to from openIMIS
malinowskikam marked this conversation as resolved.
Show resolved Hide resolved
"singleSignOnService": {
"url": "",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
},
# endpoint to call after logout from openimis
# endpoint to call after logout from openIMIS
"singleLogoutService": {
"url": "",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
# Idp public X509 certificate
"x509cert": ""
},

# Advanced security options, comment out for default
# Advanced security options
"security": {
# "nameIdEncrypted": false,
"authnRequestsSigned": True,
"logoutRequestSigned": True,
"logoutResponseSigned": True,
"signMetadata": True,
# "wantMessagesSigned": false,
# "wantAssertionsSigned": false,
# "wantNameId": true,
# "wantNameIdEncrypted": false,
# "wantAssertionsEncrypted": false,
# "allowSingleLabelDomains": false,
# "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
# "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256",
# "rejectDeprecatedAlgorithm": true
},

# TODO
# Additional info for metadata
# "contactPerson": {
# "technical": {
# "givenName": "technical_name",
# "emailAddress": "[email protected]"
# },
# "support": {
# "givenName": "support_name",
# "emailAddress": "[email protected]"
# }
# },
# "organization": {
# "en-US": {
# "name": "sp_test",
# "displayname": "SP test",
# "url": "http://sp.example.com"
# }
# }
},
# Mpay configurations
'mpay_config': {
'service_id': "SERVICE1",
# The same as mpass cert
'service_certificate': "",
# The same as mpass private key
'service_private_key': "",
# Mpay certificate, PEM string format
'mpay_cert': "",
# Default account info for voucher payments
'mpay_destination_account': {
'BankCode': "",
'BankFiscalCode': "",
'BankAccount': "",
'BeneficiaryName': ""
}
}
}

Expand All @@ -102,8 +83,10 @@ class MsystemsConfig(AppConfig):
ENROLMENT_OFFICER = 'Enrolment Officer'
##### ------------------ ####

saml_config = None
base_login_redirect = None
mpass_config = None
mpass_login_redirect = None

mpay_config = None

def ready(self):
from core.models import ModuleConfiguration
Expand Down
Empty file added msystems/services/__init__.py
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def _update_policyholder(self, user: User, policyholder: PolicyHolder, name: str
policyholder.trade_name = name
policyholder.save(username=user.username)

def _delete_old_user_policyholders(self, user: User, policyholders: List[PolicyHolder] ):
def _delete_old_user_policyholders(self, user: User, policyholders: List[PolicyHolder]):
for phu in PolicyHolderUser.objects.filter(~Q(policy_holder__in=policyholders), user=user, is_deleted=False):
phu.delete(username=user.username)

Expand Down
11 changes: 11 additions & 0 deletions msystems/services/xml_signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from zeep.wsse.signature import _make_sign_key, _sign_envelope_with_key, _make_verify_key, _verify_envelope_with_key


def sign_envelope(envelope, key, cert):
key = _make_sign_key(key, cert, None)
return _sign_envelope_with_key(envelope, key, None, None)


def verify_envelope(envelope, cert):
key = _make_verify_key(cert)
return _verify_envelope_with_key(envelope, key)
Empty file added msystems/soap/__init__.py
Empty file.
123 changes: 123 additions & 0 deletions msystems/soap/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from spyne.model.primitive import Unicode, DateTime, Decimal, Boolean
from spyne.model.complex import ComplexModel, Array
from spyne.model.enum import Enum

namespace = 'https://zilieri.gov.md'

Currency = Enum('MDL', 'EUR', 'USD', type_name='Currency')
CustomerType = Enum('Unspecified', 'Person', 'Organisation', type_name='CustomerType')
OrderStatus = Enum('Active', 'PartiallyPaid', 'Paid', 'Completed', 'Expired', 'Canceled', 'Refunding',
'Refunded', type_name='OrderStatus')
PropertyType = Enum('string', 'idn', 'tc', type_name='Type')


class OrderProperty(ComplexModel):
namespace = namespace
__type_name__ = 'OrderProperty'

Name = Unicode.customize(min_occurs=1, max_occurs=1, max_len=36, nillable=False)
DisplayName = Unicode.customize(min_occurs=0, max_occurs=1, max_len=36, nillable=False)
Value = Unicode.customize(min_occurs=1, max_occurs=1, max_len=255, nillable=False)
Modifiable = Boolean.customize(min_occurs=0, max_occurs=1, nillable=False, default=False)
Required = Boolean.customize(min_occurs=0, max_occurs=1, nillable=False, default=False)
Type = PropertyType.customize(min_occurs=0, max_occurs=1, nillable=False, default=PropertyType.string)


class PaymentAccount(ComplexModel):
__namespace__ = namespace
__type_name__ = 'PaymentAccount'

ConfigurationCode = Unicode.customize(min_occurs=0, max_occurs=1, max_len=36, nillable=False)
BankCode = Unicode.customize(min_occurs=1, max_occurs=1, max_len=20, nillable=False)
BankFiscalCode = Unicode.customize(min_occurs=1, max_occurs=1, max_len=20, nillable=False)
BankAccount = Unicode.customize(min_occurs=1, max_occurs=1, max_len=24, nillable=False)
BeneficiaryName = Unicode.customize(min_occurs=1, max_occurs=1, max_len=60, nillable=False)


class OrderDetailsQuery(ComplexModel):
__namespace__ = namespace
__type_name__ = 'OrderDetailsQuery'

OrderKey = Unicode.customize(min_occurs=1, max_occurs=1, max_len=36, nillable=False)
ServiceID = Unicode.customize(min_occurs=1, max_occurs=1, max_len=36, nillable=False)
Language = Unicode.customize(min_occurs=0, max_occurs=1, max_len=2, nillable=False)


class OrderLine(ComplexModel):
__namespace__ = namespace
__type_name__ = 'OrderLine'

LineID = Unicode.customize(min_occurs=1, max_occurs=1, max_len=36, nillable=False)
Reason = Unicode.customize(min_occurs=1, max_occurs=1, max_len=50, nillable=False)
AmountDue = Decimal.customize(min_occurs=0, max_occurs=1, nillable=False)
AllowPartialPayment = Boolean.customize(min_occurs=0, max_occurs=1, nillable=False, default=False)
AllowAdvancePayment = Boolean.customize(min_occurs=0, max_occurs=1, nillable=False, default=False)
DestinationAccount = PaymentAccount.customize(min_occurs=1, max_occurs=1, nillable=False)
Properties = (Array(OrderProperty.customize(min_occurs=0, max_occurs="unbounded", nillable=False))
.customize(min_occurs=0, max_occurs=1, nillable=False))


class OrderDetails(ComplexModel):
__namespace__ = namespace
__type_name__ = 'OrderDetails'

ServiceID = Unicode.customize(min_occurs=1, max_occurs=1, max_len=36, nillable=False)
OrderKey = Unicode.customize(min_occurs=1, max_occurs=1, max_len=36, nillable=False)
Reason = Unicode.customize(min_occurs=1, max_occurs=1, max_len=50, nillable=False)
Status = OrderStatus.customize(min_occurs=1, max_occurs=1, nillable=False)
IssuedAt = DateTime.customize(min_occurs=0, max_occurs=1, nillable=False)
DueDate = DateTime.customize(min_occurs=0, max_occurs=1, nillable=False)
TotalAmountDue = Decimal.customize(min_occurs=0, max_occurs=1, nillable=False)
Currency = Currency.customize(min_occurs=1, max_occurs=1, nillable=False)
AllowPartialPayment = Boolean.customize(min_occurs=0, max_occurs=1, nillable=False, default=False)
AllowAdvancePayment = Boolean.customize(min_occurs=0, max_occurs=1, nillable=False, default=False)
CustomerType = CustomerType.customize(min_occurs=0, max_occurs=1, nillable=False, default=CustomerType.Unspecified)
CustomerID = Unicode.customize(min_occurs=0, max_occurs=1, max_len=13, nillable=False)
CustomerName = Unicode.customize(min_occurs=0, max_occurs=1, max_len=60, nillable=False)
Lines = (Array(OrderLine.customize(min_occurs=1, max_occurs="unbounded", nillable=False))
.customize(min_occurs=1, max_occurs=1, nillable=False))
Properties = (Array(OrderProperty.customize(min_occurs=0, max_occurs="unbounded", nillable=False))
.customize(min_occurs=0, max_occurs=1, nillable=False))


class GetOrderDetailsResult(ComplexModel):
__namespace__ = namespace
__type_name__ = 'GetOrderDetailsResult'

OrderDetails = OrderDetails.customize()


class PaymentProperty(ComplexModel):
__namespace__ = namespace
__type_name__ = 'PaymentProperty'

Name = Unicode.customize(min_occurs=1, max_occurs=1, max_len=36, nillable=False)
Value = Unicode.customize(min_occurs=0, max_occurs=1, max_len=255, nillable=False)


class PaymentConfirmationLine(ComplexModel):
__namespace__ = namespace
__type_name__ = 'PaymentConfirmationLine'

LineID = Unicode.customize(min_occurs=1, max_occurs=1, max_len=36, nillable=False)
Amount = Decimal.customize(min_occurs=1, max_occurs=1, nillable=False)
DestinationAccount = PaymentAccount.customize(min_occurs=1, max_occurs=1, nillable=False)
Properties = (Array(PaymentProperty.customize(min_occurs=0, max_occurs="unbounded", nillable=False))
.customize(min_occurs=0, max_occurs=1, nillable=False))


class PaymentConfirmation(ComplexModel):
__namespace__ = namespace
__type_name__ = 'PaymentConfirmation'

OrderKey = Unicode.customize(min_occurs=1, max_occurs=1, max_len=36, nillable=False)
ServiceID = Unicode.customize(min_occurs=1, max_occurs=1, max_len=36, nillable=False)
InvoiceID = Unicode.customize(min_occurs=0, max_occurs=1, max_len=36, nillable=False)
PaymentID = Unicode.customize(min_occurs=1, max_occurs=1, max_len=36, nillable=False)
PaidAt = DateTime.customize(min_occurs=1, max_occurs=1, nillable=False)
TotalAmount = Decimal.customize(min_occurs=1, max_occurs=1, nillable=False)
Currency = Currency.customize(min_occurs=1, max_occurs=1, nillable=False)

Lines = Array(PaymentConfirmationLine.customize(min_occurs=1, max_occurs="unbounded", nillable=False))
Properties = (Array(PaymentProperty.customize(min_occurs=0, max_occurs="unbounded", nillable=False))
.customize(min_occurs=0, max_occurs=1, nillable=False))
2 changes: 1 addition & 1 deletion msystems/tests/saml_user_service_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from location.models import Location
from msystems.apps import MsystemsConfig

from msystems.services import SamlUserService
from msystems.services.saml_user_service import SamlUserService
from msystems.tests.data import example_username, example_user_data, example_user_data_multiple_ph
from core.models import User, InteractiveUser, UserRole, Role
from policyholder.models import PolicyHolder
Expand Down
12 changes: 7 additions & 5 deletions msystems/urls.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from django.urls import path, include
from msystems import views
from django.views.decorators.csrf import csrf_exempt

from msystems.views import mpass, mpay

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

urlpatterns = [
path("saml/", include(saml_urls)),
path("mpay/", csrf_exempt(mpay.mpay_app))
]
Empty file added msystems/views/__init__.py
Empty file.
20 changes: 10 additions & 10 deletions msystems/views.py → msystems/views/mpass.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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 msystems.services.saml_user_service import SamlUserService
from onelogin.saml2.auth import OneLogin_Saml2_Auth, OneLogin_Saml2_Settings
from graphql_jwt.decorators import jwt_cookie
from graphql_jwt.shortcuts import get_token, create_refresh_token
Expand All @@ -31,14 +31,14 @@ def _build_auth(request, slo_workaround=False) -> OneLogin_Saml2_Auth:
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)
return OneLogin_Saml2_Auth(req, MsystemsConfig.mpass_config)


@require_GET
def login(request):
# From python3-saml django example
auth = _build_auth(request)
login_request = auth.login(return_to=MsystemsConfig.base_login_redirect)
login_request = auth.login(return_to=MsystemsConfig.mpass_login_redirect)
return redirect(login_request)


Expand All @@ -56,7 +56,7 @@ def logout(request):
def metadata(request):
# from python3-saml docs
saml_settings = OneLogin_Saml2_Settings(
settings=MsystemsConfig.saml_config, sp_validation_only=True)
settings=MsystemsConfig.mpass_config, sp_validation_only=True)
saml_metadata = saml_settings.get_sp_metadata()
errors = saml_settings.validate_metadata(saml_metadata)

Expand All @@ -78,7 +78,7 @@ def _handle_acs_login(request):
if errors:
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)
return redirect(MsystemsConfig.mpass_login_redirect)

username = auth.get_nameid()
user_data = auth.get_attributes()
Expand All @@ -92,7 +92,7 @@ def _handle_acs_login(request):
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)
return redirect(MsystemsConfig.mpass_login_redirect)


def _handle_acs_logout(request):
Expand All @@ -104,15 +104,15 @@ def _handle_acs_logout(request):
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)
return redirect(MsystemsConfig.mpass_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)
return redirect(MsystemsConfig.mpass_login_redirect)


# Saml have its own csrf protection, django not needed
Expand All @@ -135,5 +135,5 @@ def acs(request):
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.
# Currently, the only valid RelayState base_login_redirect
return relay_state == MsystemsConfig.base_login_redirect
# Currently, the only valid RelayState mpass_login_redirect
return relay_state == MsystemsConfig.mpass_login_redirect
Loading
Loading