SSO with SAML (mod_shibboleth) and Flask


(fmarco76) #1

Hi all,

I have developed a small sso application with flask in order to allow the the authentication using SAML in conjunction with mod_shibboleth. If you interested this is the link:

Cheers,
Marco


Please visit our Discourse Forum! (Directory)
(Manthan Mallikarjun) #2

Sorry, I have no experience with python, but why not just port discourse/single_sign_on.rb at master · discourse/discourse · GitHub into a python library so people can use that instead?


(fmarco76) #3

I am not a Ruby expert so I started from the protocol description instead of the code. The code can be modified to be a library but it is so short that people interested can easily integrate the main methods in their code.

Actually, the only similar code to what I did is https://meta.discourse.org/t/saml-authentication-proxy-for-discourse-sso/27864 but it tries to implement SAML protocol with many limitation so I decide to do something integrated with shibboleth supporting the full protocol.


(Bruce Becker) #4

I’m not the author of the code - that was @fmarco76 and he can comment in more detail - but I think the reason we didn’t just port the SSO was that he wanted specifically to use python. We have a lot of experience with identity federations (this is decentralised authentication, not central SSO), so I guess he wanted to do it right by his standards :smile:


(Paolo G. Giarrusso) #5

This looks interesting, but even after skimming the README I’m not sure it’s what I need.
I have little clue on Shibboleth (not sure what’s mod_shibboleth or SAML), so I’ll just ask:

  1. Does this allow a Discourse instance to delegate authentication to a Shibboleth provider? (As a tip, this could be put at the README beginning).
  2. Does this require admin access to the Shibboleth server (that I don’t have), or is it just an intermediate service between Discourse and Shibboleth? Or what’s the architecture?

(fmarco76) #6

Hi Paolo,

Discourse allows SSO with other services using its own protocol has described here.
This require the other service to provide user information for the authentication.

However, the service I have to connect with Discourse use SAML for authentication and this
already provide SSO so I would use this protocol for SSO.

In order to use SAML SSO I developed this little python application which perform the SSO with other services using SAML and then perform Discourse SSO to allow user to get in.

mod_shibboleth is an implementation of SAML running with apache httpd server so you can easily integrate any application behind the httpd daemon with apache.

Developing this application I tried to be generic enough so you can use any authentication method provided by the httpd server (SAML, OpenID Connect, LDAP, Kerberos, etc…) by configuring the correct map among the attributes coming from the authentication with the attributes to provide to Discourse.

Therefore, this run as standalone application and work in the middle between Discourse and the external authentication. You configure Discourse to perform SSO with this application and then configure this application to perform SSO with the external protocol. Nevertheless, we configure everything in the same machine so even if it is a separate application the user has the impression that everything is in the same place but the IdentityProvider (if you use SAML or OpenID Connect).

Let me know if you need extra information.

P.S.: Ciao Paolo, come stai? Ci siamo conosciuti mentre facevo il dottorato (co-relatore era Tramontana). Dove ti trovi?


#7

@fmarco76

I have followed the instructions in here GitHub - fmarco76/DiscourseSSO: SSO Discourse Application to allow SAML authentication and I am having issues with one particular error

ImportError: no module named flask.

When logging in from Discourse, it navigates to the IdP where I login using my credentials and on upon return, I get the above error.

I have installed flask using pip install flask and it is under /var/loca/lib/python2.7/site-packages directory. I have also set the $PYTHONPATH variable to include

/usr/local/lib/python2.7:/usr/local/lib/python2.7/site-packages

I did send an email to fmarco76 with more details of the issue. I am not a python expert and I need to know what to read, where to look, and what are the possible reasons for such errors.

I run the test_sso.py and firstly I got same ImportError complaining about flask but, after adding the PYTHONPATH I got a different error:

‘import site’ failed; use -v for traceback

TypeError: cannot create weak reference to ‘classobj’ object

Some hints on how to resolve this issue would be very much appreciated. Thanks guys.


(fmarco76) #8

Hi Raf,
it seems a library error. What version of flask are you using?

Did you run the tests with tox or manually? If manually, could you try
to run with tox:

$ tox -e2.7

This will create a virtualenv with the correct library. If this is OK then the
problem could be a wrong version or path.

Cheers


#9

Thanks for your reply.

I did pip install tox and then run the command you provided, some tests passed and some failed, see below:

2.7 create: /var/www/DiscourseSSO/.tox/2.7
2.7 installdeps: pytest, pytest-capturelog, -r/var/www/DiscourseSSO/requirements.txt, pytest-cov
2.7 develop-inst: /var/www/DiscourseSSO
2.7 installed: coverage==4.0.3,-e git+https://github.com/fmarco76/DiscourseSSO@98217c569ae640de28387a1436dc3457c10dc9cf#egg=DiscourseSSO,Flask==0.10.1,itsdangerous==0.24,Jinja2==2.8,MarkupSafe==0.23,py==1.4.31,pytest==2.8.5,pytest-capturelog==0.7,pytest-cov==2.2.0,Werkzeug==0.11.3,wheel==0.26.0
2.7 runtests: PYTHONHASHSEED='2121325688'
2.7 runtests: commands[0] | py.test --cov=src --cov-report=term-missing -vv
============================================================== test session starts ==============================================================
platform linux2 -- Python 2.7.8, pytest-2.8.5, py-1.4.31, pluggy-0.3.1 -- /var/www/DiscourseSSO/.tox/2.7/bin/python2.7
cachedir: .cache
rootdir: /var/www/DiscourseSSO, inifile: setup.cfg
plugins: cov-2.2.0, capturelog-0.7
collected 11 items

CHANGELOG.rst SKIPPED
README.rst SKIPPED
tests/test_sso.py::Test_sso::test_payload_check FAILED
tests/test_sso.py::Test_sso::test_bad_payload_sig PASSED
tests/test_sso.py::Test_sso::test_no_payload PASSED
tests/test_sso.py::Test_sso::test_no_hash PASSED
tests/test_sso.py::Test_sso::test_authentication_no_shibboleth_attributes PASSED
tests/test_sso.py::Test_sso::test_authentication_no_previous_session PASSED
tests/test_sso.py::Test_sso::test_authentication_generation FAILED
tests/test_sso.py::Test_sso::test_authentication_generation_with_full_name FAILED
tests/test_sso.py::Test_sso::test_error_page_403 PASSED

--- UPDATE --- 

Speaking of version and path, that could be the issue. I have CentOS 6.7 with python 2.6 and I have followed instructions to install python2.7. I probably need to read on virtualenv and then see how DiscourseSSO fits into that. I definitely did install virtualenv when setting up DiscourseSSO (didn’t knew if I had too and how).


(fmarco76) #10

In my machine I have no problem with the test. I am using debian with python 2.7.

What is the error you get on the failed tests?

tox is creating the virtualenv where the tests are executed so it should work unless
there is some problem with global libraries different from Flask.


#11

One of 400 Bad Request and others seem to be assertions, see below:

=================================================================== FAILURES ====================================================================
__________________________________________________________ Test_sso.test_payload_check __________________________________________________________
tests/test_sso.py:41: in test_payload_check
    res = sso.payload_check()
src/discourseSSO/sso.py:63: in payload_check
    abort(400)
.tox/2.7/lib/python2.7/site-packages/werkzeug/exceptions.py:646: in __call__
    raise self.mapping[code](*args, **kwargs)
E   BadRequest: 400: Bad Request
----------------------------------------------------------------- Captured log ------------------------------------------------------------------
sso.py                      48 DEBUG    Request to login with payload="bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI=
" signature="2828aa29899722b35a2f191d34ef9b3ce695e0e6eeec47deb46d588d70c7cb56"
sso.py                      53 DEBUG    Session Secret Key: vWr,-n7NlGPv9SyIGBMr4ehwThUY92DpWPqIuh2NP_6Of-_8b3,h
sso.py                      55 DEBUG    SSO Secret Key: **MyDiscourseSecretKey-changed**
sso.py                      61 DEBUG    Calculated hash: 04268f724982a4478f400788344bc41b36c9f2b7d9c19338a9d622e2b9a
____________________________________________________ Test_sso.test_authentication_generation ____________________________________________________
tests/test_sso.py:116: in test_authentication_generation
    assert resp.location == ('http://discuss.example.com/session/'
E   assert 'https://myserver_.....70226ba89229c' == 'http://discus...6ad458d312ffd'
E     - https://myserver_url (changed)/session/sso_login?sso=bm9uY2U9Y2I2ODZmI1U4YzAwZmYxMzk1ZjBjMGImbmFtZT0mdXNlcm5hbWU9aGVs%0AbG8xMjMmZW1haWw9dGVzdCU0MHRlc3QuY29tJmV4dGVybmFsX2lkPWhlbGxvMTIz%0A&sig=9688ec2fe02f0d5a77652161a8f40508940f6b41cf34567c9cd70226ba89229c
E     + http://discuss.example.com/session/sso_login?sso=bm9uY2U9Y2I2ODI1MWVlZmI1TU4YzAwZmYxMzk1ZjBjMGImbmFtZT1zYW0mdXNlcm5hbWU9%0Ac2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRlcm5hbF9pZD1oZWxsbzEyMw%3D%3D%0A&sig=5292265340422c9ce2d528e25d2927a2e24b481c3e91fa353516ad458d312ffd
----------------------------------------------------------------- Captured log ------------------------------------------------------------------
sso.py                      96 DEBUG    Authenticating "" with username "hello123" and email "test@test.com"
sso.py                     104 DEBUG    Query string to return: nonce=cb68251eefb5211e58c00ff1395f0c0b&name=&username=hello123&email=test%40test.com&external_id=hello123
sso.py                     106 DEBUG    Base64 query string to return: bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT0mdXNlcm5hbWU9aGVs
bG8xMjMmZW1haWw9dGVzdCU0MHRlc3QuY29tJmV4dGVybmFsX2lkPWhlbGxvMTIz

sso.py                     108 DEBUG    URLEnc query string to return: bm9uY2U9Y2I2ODI1MWVZmYxMzk1ZjBjMGImbmFtZT0mdXNlcm5hbWU9aGVs%0AbG8xMjMmZW1haWw9dGVzdCU0MHRlc3QuY29tJmV4dGVybmFsX2lkPWhlbGxvMTIz%0A
sso.py                     114 DEBUG    Signature: 9688ec2fe02f0d5a77652161a8f40508940f6b41cf34567c9cd70226ba89229c
____________________________________________ Test_sso.test_authentication_generation_with_full_name _____________________________________________
tests/test_sso.py:138: in test_authentication_generation_with_full_name
    assert resp.location == ('http://discuss.example.com/session/'
E   assert 'https://myser...70226ba89229c' == 'http://discus...51ad12e87862b'
E     - https://myserverurl__(changed)/session/sso_login?sso=bm9uY2U9Y2I2ODI1MzAwZmYxMzk1ZjBjMGImbmFtZT0mdXNlcm5hbWU9aGVs%0AbG8xMjMmZW1haWw9dGVzdCU0MHRlc3QuY29tJmV4dGVybmFsX2lkPWhlbGxvMTIz%0A&sig=9688ec2fe02f052161a8f40508940f6b41cf34567c9cd70226ba89229c
E     + http://discuss.example.com/session/sso_login?sso=bm9uY2U9Y2I2ODI1MWVmYxMzk1ZjBjMGImbmFtZT1zYW0gYmlnJnVzZXJu%0AYW1lPXNhbWJpZ19iNjQyJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRlcm5hbF9pZD1oZWxsbzEy%0AMw%3D%3D%0A&sig=2371c654bbfbc5b322340a8fc80147ba00cb29a751ad12e87862b
----------------------------------------------------------------- Captured log ------------------------------------------------------------------
sso.py                      96 DEBUG    Authenticating "" with username "hello123" and email "test@test.com"
sso.py                     104 DEBUG    Query string to return: nonce=cb68251eefb5211e58c00ff1395f0c0b&name=&username=hello123&email=test%40test.com&external_id=hello123
sso.py                     106 DEBUG    Base64 query string to return: bm9uY2U9Y2I2ODI1MWVlZmZmYxMzk1ZjBjMGImbmFtZT0mdXNlcm5hbWU9aGVs
bG8xMjMmZW1haWw9dGVzdCU0MHRlc3QuY29tJmV4dGVybmFsX2lkPWhlbGxvMTIz

sso.py                     108 DEBUG    URLEnc query string to return: bm9uY2U9Y2I2ODI1MWmYxMzk1ZjBjMGImbmFtZT0mdXNlcm5hbWU9aGVs%0AbG8xMjMmZW1haWw9dGVzdCU0MHRlc3QuY29tJmV4dGVybmFsX2lkPWhlbGxvMTIz%0A
sso.py                     114 DEBUG    Signature: 9688ec2fe02f0d5a77652161a8f40508940f6b41cf34567c9cd70226ba89229c
------------------------------------------------ coverage: platform linux2, python 2.7.8-final-0 ------------------------------------------------
Name                                  Stmts   Miss Branch BrPart     Cover   Missing
------------------------------------------------------------------------------------
src/discourseSSO/__init__.py              1      0      0      0   100.00%
src/discourseSSO/config.py                4      0      0      0   100.00%
src/discourseSSO/default.py               8      0      0      0   100.00%
src/discourseSSO/sso.py                  54      5     14      3    88.24%   64-66, 85, 90, 62->64, 84->85, 87->90
------------------------------------------------------------------------------------
TOTAL                                    67      5     14      3    90.12%
============================================================ short test summary info ============================================================
FAIL tests/test_sso.py::Test_sso::()::test_payload_check
FAIL tests/test_sso.py::Test_sso::()::test_authentication_generation
FAIL tests/test_sso.py::Test_sso::()::test_authentication_generation_with_full_name
SKIP [2] /var/www/DiscourseSSO/.tox/2.7/lib/python2.7/site-packages/_pytest/doctest.py:165: all tests skipped by +SKIP option
======================================= 3 failed, 6 passed, 2 skipped, 1 pytest-warnings in 0.36 seconds ========================================
ERROR: InvocationError: '/var/www/DiscourseSSO/.tox/2.7/bin/py.test --cov=src --cov-report=term-missing -vv'
____________________________________________________________________ summary ____________________________________________________________________
ERROR:   2.7: commands failed

I must say, that is really good and detailed level of testing.

I have changed some of the values of sig and pyload and server url for security reasons. Not sure if it is a security risk but, just in case.


(fmarco76) #12

You have to change the value in production but not in test. Could you run again the test with the original values? (You can clone the repo in a different location and run tox on that)


#13

When I said I changed some of the values, I did not mean before running tests. I changed the values in the output before replying in here. I did no changes when running the tests against production.


(fmarco76) #14

But I am sure you changed something in the config.py before to run the tests because at
least the url is different.

The test work only with default configuration. After it is run you can change the values for your
production environment. So could you clone in a different location and run the tests?

Additionally, the error is not related with libraries but with the computation of value and this make evident that your libraries are not properly installed IMHO.


#15

Yes, I did change the values in the config.py and now I know what you mean. I did a fresh clone of the repo and while inside the test directory I run the tests using tox command and all the tests passed successfully.

Can you please provide some more details in regards to computation of value, so I can dig more on this and figure out the issue.


(fmarco76) #16

The failed test was checking if the returned value of the signature was correct. In this case the
test contain an already computed signature and the outcome of the call should match. The computation follow the indication made from discourse documentation.

However, if the test pass correctly but the wsgi with apache has the error then there is a problem
with your flask installation: the version is wrong, something is missing or other. Try to clean your python installation and install a brand new Flask or try to use the virtualenv with apache since it
work (at least the test show that it works). I cannot help on this because I have never used virtualenv and apache together.


Sign up and local authentication disappeared after enabling SSO-based authentication
#17

Thanks, I definitely have loads of hints to look into. I will have a look into Flask which I also believe could be the issue because it clearly says in the httpd logs that it fails to load Flask module.

Cheers, Raf.


#18

Seems like I made some progress. I now see the error page after supplying my SSO credentials

Your Identity Provider has authenticated your account but the SAML assertion does not include the attributes needed by Discourse to let you in. Please contact your Identity Provider administrator and ask to release the following attribute:

    eduPersonPrincipalName (mandatory)
    mail (mandatory)
    sn (optional)
    givenName (optional)

I have the following mapping in the config.py

# Attribute to read from the environment after user validation
DISCOURSE_USER_MAP = {
    'name': ['displayName'],
    'username': 'eppn',
    'external_id': 'eppn',
    'email': 'mail'
}

I have checked /Shibboleth.sso/Session of our SP and the Session summary shows the following

Attributes
affiliation: 1 value(s)
eppn: 1 value(s)
persistent-id: 1 value(s)

Does the above mean that the IdP has not released some attribute that is mandatory? I bet mail? How else can I confirm which attributes are released by the IdP?

One last question what should be the DISCOURSE_URL in the config.py? Should that be the base URL of the Discourse server or as mentioned in Official Single Sign On for Discourse

Redirect back to http://discourse_site/session/sso_login?sso=payload&sig=sig


(fmarco76) #19

Yes, it almost works now. If I remember correctly the email is mandatory.
I am not sure about the name.

The session summary is OK to check the attribute, alternatively they should be logged in the transaction.log file in /var/logs/shibboleth. To see the attribute values you should write a web page
showing the variables provided by apache and protect the page with shibboleth (I am sure there
are several example page in internet).

If you have not specific filter (I think it is the default for the SPs) you have to ask the IdP administrator to release the mail attribute to your SP and the authentication should work.


#20

Thank you Marco, finally done. It is working like a charm. :grinning: I have to make changes to the attribute-map.xml as well (uncomment the entries for attributes such as displayName and mail).