Skip to content

Commit

Permalink
Allow authentication context requests
Browse files Browse the repository at this point in the history
  • Loading branch information
duskobogdanovski committed Jan 27, 2021
1 parent 6670cd0 commit 663a587
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 15 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ idp.xml
.vscode/
.pre-commit-config.yaml
.flake8

*.pem
test-signed.xml
test-signed-encrypted.xml
37 changes: 23 additions & 14 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -123,22 +123,31 @@ Optional::
# Default: <Not set>
ckanext.saml2auth.sp.name_id_policy_format = urn:oasis:names:tc:SAML:2.0:nameid-format:persistent

# Entity ID (also know as Issuer)
# Define the entity ID. Default is urn:mace:umu.se:saml:ckan:sp
ckanext.saml2auth.entity_id = urn:gov:gsa:SAML:2.0.profiles:sp:sso:gsa:catalog-dev

# Signed responses and assertions
ckanext.saml2auth.want_response_signed = False
ckanext.saml2auth.want_assertions_signed = False
ckanext.saml2auth.want_assertions_or_response_signed = True
# Entity ID (also know as Issuer)
# Define the entity ID. Default is urn:mace:umu.se:saml:ckan:sp
ckanext.saml2auth.entity_id = urn:gov:gsa:SAML:2.0.profiles:sp:sso:gsa:catalog-dev

# Signed responses and assertions
ckanext.saml2auth.want_response_signed = False
ckanext.saml2auth.want_assertions_signed = False
ckanext.saml2auth.want_assertions_or_response_signed = True
# Cert & key files
ckanext.saml2auth.key_file_path = /path/to/mykey.pem
ckanext.saml2auth.cert_file_path = /path/to/mycert.pem
# Cert & key files
ckanext.saml2auth.key_file_path = /path/to/mykey.pem
ckanext.saml2auth.cert_file_path = /path/to/mycert.pem
# Attribute map directory
ckanext.saml2auth.attribute_map_dir = /path/to/dir/attributemaps

# Attribute map directory
ckanext.saml2auth.attribute_map_dir = /path/to/dir/attributemaps

# Authentication context request before redirect to login
# e.g. to ask for a PIV card with login.gov provider (https://developers.login.gov/oidc/#aal-values) use:
ckanext.saml2auth.requested_authn_context = http://idmanagement.gov/ns/assurance/aal/3?hspd12=true
# You can use multiple context separated by spaces
ckanext.saml2auth.requested_authn_context = req1 req2

# Define the comparison value for RequestedAuthnContext
# Comparison could be one of this: exact, minimum, maximum or better
ckanext.saml2auth.requested_authn_context_comparison = exact

----------------------
Developer installation
Expand Down
19 changes: 19 additions & 0 deletions ckanext/saml2auth/tests/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# encoding: utf-8
import os
import pytest
from ckanext.saml2auth.views.saml2auth import saml2login


here = os.path.dirname(os.path.abspath(__file__))
extras_folder = os.path.join(here, 'extras')


@pytest.mark.ckan_config(u'ckanext.saml2auth.requested_authn_context_comparison', 'bad_value')
@pytest.mark.ckan_config(u'ckanext.saml2auth.requested_authn_context', 'req1 req2')
@pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.location', u'local')
@pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.local_path',
os.path.join(extras_folder, 'provider0', 'idp.xml'))
def test_empty_comparison():
with pytest.raises(ValueError) as e:
saml2login()
assert 'Unexpected comparison' in e
23 changes: 23 additions & 0 deletions ckanext/saml2auth/tests/test_spconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pytest

from ckanext.saml2auth.spconfig import get_config
from ckanext.saml2auth.views.saml2auth import _get_requested_authn_contexts


@pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.location', u'local')
Expand Down Expand Up @@ -76,3 +77,25 @@ def test_read_acs_endpoint():

acs_endpoint = get_config()[u'service'][u'sp'][u'endpoints'][u'assertion_consumer_service'][0]
assert acs_endpoint.endswith('/my/acs/endpoint')


@pytest.mark.ckan_config(u'ckanext.saml2auth.requested_authn_context', u'req1')
def test_one_requested_authn_context():

contexts = _get_requested_authn_contexts()
assert contexts[0] == u'req1'


@pytest.mark.ckan_config(u'ckanext.saml2auth.requested_authn_context', u'req1 req2')
def test_two_requested_authn_context():

contexts = _get_requested_authn_contexts()
assert u'req1' in contexts
assert u'req2' in contexts


@pytest.mark.ckan_config(u'ckanext.saml2auth.requested_authn_context', None)
def test_empty_requested_authn_context():

contexts = _get_requested_authn_contexts()
assert contexts == []
32 changes: 31 additions & 1 deletion ckanext/saml2auth/views/saml2auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import logging
from flask import Blueprint
from saml2 import entity
from saml2.saml import AuthnContextClassRef
from saml2.samlp import RequestedAuthnContext

import ckan.plugins.toolkit as toolkit
import ckan.model as model
Expand All @@ -20,6 +22,15 @@
saml2auth = Blueprint(u'saml2auth', __name__)


def _get_requested_authn_contexts():
requested_authn_contexts = config.get('ckanext.saml2auth.requested_authn_context',
None)
if requested_authn_contexts is None:
return []

return requested_authn_contexts.strip().split()


def process_user(email, saml_id, firstname, lastname):
""" Check if CKAN-SAML user exists for the current SAML login """

Expand Down Expand Up @@ -159,7 +170,26 @@ def saml2login():
configured identity provider for authentication
'''
client = h.saml_client(sp_config())
reqid, info = client.prepare_for_authenticate()
requested_authn_contexts = _get_requested_authn_contexts()

if len(requested_authn_contexts) > 0:
comparison = config.get('ckanext.saml2auth.requested_authn_context_comparison',
'minimum')
if comparison not in ['exact', 'minimum', 'maximum', 'better']:
error = 'Unexpected comparison value {}'.format(comparison)
raise ValueError(error)
requested_authn_context = RequestedAuthnContext(comparison=comparison)

for item in requested_authn_contexts:
context_class_ref = AuthnContextClassRef()
context_class_ref.text = item
log.debug('requested_authn_context added {}'.format(item))
requested_authn_context.authn_context_class_ref.append(context_class_ref)

reqid, info = client.prepare_for_authenticate(
requested_authn_context=requested_authn_context)
else:
reqid, info = client.prepare_for_authenticate()

redirect_url = None
for key, value in info[u'headers']:
Expand Down

0 comments on commit 663a587

Please sign in to comment.