~roadmr/canonical-identity-provider/non-drifting-totp

« back to all changes in this revision

Viewing changes to src/ubuntu_sso_saml/tests/test_processors.py

Actually honor SAML AuthnRequests' NameIDPolicy format.

This was done to accommodate SPs (Bomgar) which request persistent names,
indicating so in their AuthnRequest. SSO usually ignores this and always
sends "email"-policy names, which most SPs handle fine but Bomgar fails with.

The fully-correct thing to do would be to always honor the AuthnRequest and
support most/all of the SAML-specified policies, but that'd be a large effort
and risks changing semantics for other SPs, which would prevent people from
logging in, which would be bad.

Another quirk is that, while we respond saying we're giving a persistent
identifier, our identifier (the e-mail address) does NOT actually conform
to persistent semantics per SAML spec's section 8.3; we are sending the
same value (the e-mail address), just saying it's "persistent" as requested
by the SP. We do have a persistent identifier (the OpenID) but we can't send that
because then it gets sent as the username, identifier, and email address. Again,
support for this can be added to our django-saml2-idp fork but it's more work
for something that at the moment is required only by one SP.

Due do the above, in order to support Bomgar (and possibly other SPs) in a more
selective way, we only honor a non-email NameIDPolicy if:
    - the SP is configured to honor this (a boolean in the SPConfig)
    - the requested NameID policy is "persistent".

This allows us to switch this on only for very specific SPs for which we
have more control and fully understand the consequences of "lying" with our
"persistent" support.

In all other cases, we continue ignoring/overriding this and always sending
our response with "email" policy.

(As a rant, the best thing to do would be to trash our hacky SAML library
and integrate OneLogin's SAML library which is fully standards-compliant,
which is however a huge undertaking we would have to consider and prioritize)


Merged from https://code.launchpad.net/~roadmr/canonical-identity-provider/support-saml-persistent-nameid-policy-format/+merge/361982

Show diffs side-by-side

added added

removed removed

Lines of Context:
1154
1154
                    >
1155
1155
    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">{issuer}</saml:Issuer>
1156
1156
    <samlp:NameIDPolicy xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
1157
 
                        Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
 
1157
                        Format="urn:oasis:names:tc:SAML:1.1:nameid-format:{nameid_format}"
1158
1158
                        AllowCreate="true"
1159
1159
                        />
1160
1160
    <samlp:RequestedAuthnContext xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
1178
1178
                    >
1179
1179
    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">{issuer}</saml:Issuer>
1180
1180
    <samlp:NameIDPolicy xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
1181
 
                        Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
 
1181
                        Format="urn:oasis:names:tc:SAML:1.1:nameid-format:{nameid_format}"
1182
1182
                        AllowCreate="true"
1183
1183
                        />
1184
1184
    <samlp:RequestedAuthnContext xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
1220
1220
        self.assertNotIn('<saml:AudienceRestriction>', samlresponse)
1221
1221
 
1222
1222
    def assert_saml_response_email(self, samlresponse, email):
1223
 
        self.assertIn(email, samlresponse)
1224
 
 
1225
 
    def get_request_data(self, compressed=True, has_acs_url=True):
 
1223
        self.assert_nameid(samlresponse, email, "email")
 
1224
 
 
1225
    def assert_nameid(self, samlresponse, email, the_format):
 
1226
        # The_format can be "email" or "persistent" currently.
 
1227
        full_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:" + the_format
 
1228
        samlsoup = PyQuery(samlresponse.replace('xmlns:', 'xmlnamespace:'))
 
1229
        nameids = samlsoup.find('NameID')
 
1230
        self.assertEqual(1, len(nameids))
 
1231
        self.assertEqual(full_format, nameids[0].attrib['format'])
 
1232
        self.assertEqual(email, nameids[0].text)
 
1233
 
 
1234
    def get_request_data(
 
1235
            self,
 
1236
            compressed=True,
 
1237
            has_acs_url=True,
 
1238
            nameid_format="emailAddress"):
1226
1239
        data = dict(**self.REQUEST_DATA)
1227
1240
        params = {
1228
1241
            'audience': self.AUDIENCE,
1229
1242
            'issuer': self.REQUEST_ISSUER,
1230
1243
            'relay_state': self.RELAY_STATE,
 
1244
            'nameid_format': nameid_format,
1231
1245
        }
1232
1246
        if has_acs_url:
1233
1247
            params['acs_url'] = self.ACS_URL
1271
1285
        self.assert_successful_saml_response(samlresponse)
1272
1286
        self.assert_unrestricted_saml_response(samlresponse)
1273
1287
 
 
1288
    def test_nameid_honor_custom_format_persistent_requested(self):
 
1289
        # make sure there is a saml config which honors nameid format from
 
1290
        # authnrequest
 
1291
        self.setup_saml_sp(honor_authnrequest_nameidpolicy_format=True)
 
1292
 
 
1293
        data = self.get_request_data(nameid_format="persistent")
 
1294
        samlresponse = self.do_saml_request(data=data)
 
1295
        self.assert_successful_saml_response(samlresponse)
 
1296
        self.assert_nameid(samlresponse, self.login_email, "persistent")
 
1297
 
 
1298
    def test_nameid_dont_honor_custom_format_persistent_requested(self):
 
1299
        self.setup_saml_sp(honor_authnrequest_nameidpolicy_format=False)
 
1300
 
 
1301
        data = self.get_request_data(nameid_format="persistent")
 
1302
        samlresponse = self.do_saml_request(data=data)
 
1303
        self.assert_successful_saml_response(samlresponse)
 
1304
        self.assert_nameid(samlresponse, self.login_email, "email")
 
1305
 
 
1306
    def test_nameid_honor_custom_format_email_requested(self):
 
1307
        # make sure there is a saml config which honors nameid format from
 
1308
        # authnrequest
 
1309
        self.setup_saml_sp(honor_authnrequest_nameidpolicy_format=True)
 
1310
 
 
1311
        data = self.get_request_data(nameid_format="email")
 
1312
        samlresponse = self.do_saml_request(data=data)
 
1313
        self.assert_successful_saml_response(samlresponse)
 
1314
        self.assert_nameid(samlresponse, self.login_email, "email")
 
1315
 
 
1316
    def test_nameid_dont_honor_custom_format_email_requested(self):
 
1317
        self.setup_saml_sp(honor_authnrequest_nameidpolicy_format=False)
 
1318
 
 
1319
        data = self.get_request_data(nameid_format="email")
 
1320
        samlresponse = self.do_saml_request(data=data)
 
1321
        self.assert_successful_saml_response(samlresponse)
 
1322
        self.assert_nameid(samlresponse, self.login_email, "email")
 
1323
 
 
1324
    def test_nameid_honor_custom_format_bogus_requested(self):
 
1325
        # make sure there is a saml config which honors nameid format from
 
1326
        # authnrequest
 
1327
        self.setup_saml_sp(honor_authnrequest_nameidpolicy_format=True)
 
1328
 
 
1329
        data = self.get_request_data(nameid_format="nonexistentandbogus")
 
1330
        samlresponse = self.do_saml_request(data=data)
 
1331
        self.assert_successful_saml_response(samlresponse)
 
1332
        self.assert_nameid(samlresponse, self.login_email, "email")
 
1333
 
 
1334
    def test_nameid_dont_honor_custom_format_bogus_requested(self):
 
1335
        self.setup_saml_sp(honor_authnrequest_nameidpolicy_format=False)
 
1336
 
 
1337
        data = self.get_request_data(nameid_format="nonexistentandbogus")
 
1338
        samlresponse = self.do_saml_request(data=data)
 
1339
        self.assert_successful_saml_response(samlresponse)
 
1340
        self.assert_nameid(samlresponse, self.login_email, "email")
 
1341
 
1274
1342
    def test_authnrequest_unknown_sp(self):
1275
1343
        data = self.get_request_data()
1276
1344
        response = self.client.get('/+saml', data=data, follow=True)