mirror of https://github.com/zulip/zulip.git
auth: Add initial SAML authentication support.
There are a few outstanding issues that we expect to resolve beforce including this in a release, but this is good checkpoint to merge. This PR is a collaboration with Tim Abbott. Fixes #716.
This commit is contained in:
parent
82f923c27a
commit
4dc3ed36c3
|
@ -34,6 +34,97 @@ Each of these requires one to a handful of lines of configuration in
|
|||
`settings.py`, as well as a secret in `zulip-secrets.conf`. Details
|
||||
are documented in your `settings.py`.
|
||||
|
||||
## SAML
|
||||
|
||||
Zulip 2.1 and later has beta support for SAML authentication, used by
|
||||
Okta, OneLogin, and many other IdPs (identity providers). You can
|
||||
configure it as follows:
|
||||
|
||||
1. These instructions assume you have an installed Zulip server. You
|
||||
can have created an organization already using EmailAuthBackend, or
|
||||
plan to create the organization using SAML authentication.
|
||||
|
||||
1. Tell your IdP how to find your Zulip server:
|
||||
|
||||
* **SP Entity ID**: `https://yourzulipdomain.example.com`.
|
||||
* **SSO URL**:
|
||||
`https://yourzulipdomain.example.com/complete/saml/`. This is
|
||||
the "SAML ACS url" in SAML terminology.
|
||||
|
||||
The `Entity ID` should match the value of
|
||||
`SOCIAL_AUTH_SAML_SP_ENTITY_ID` computed in the Zulip settings.
|
||||
You can run on your Zulip server
|
||||
`/home/zulip/deployments/current/scripts/setup/get-django-setting
|
||||
SOCIAL_AUTH_SAML_SP_ENTITY_ID` to get the computed value.
|
||||
|
||||
2. Tell Zulip how to connect to your SAML provider server by filling
|
||||
out the section of `/etc/zulip/settings.py` on your Zulip server
|
||||
with the heading "SAML Authentication".
|
||||
* You will need to update `SOCIAL_AUTH_SAML_ORG_INFO` with your
|
||||
organization name (`displayname` may appear in the SAML
|
||||
authentication flow; `name` won't be displayed to humans).
|
||||
* Fill out `SOCIAL_AUTH_SAML_ENABLED_IDPS` with data provided by
|
||||
your identity provider. You may find [the python-social-auth
|
||||
SAML
|
||||
docs](https://python-social-auth-docs.readthedocs.io/en/latest/backends/saml.html)
|
||||
helpful. You'll need to obtain several values from your IdP's
|
||||
metadata and enter them on the right-hand side of this
|
||||
Python dictionary:
|
||||
1. Set the outer `idp_name` key to be an identifier for your IdP,
|
||||
e.g. `testshib` or `okta`. This field may be used later if
|
||||
Zulip adds support for declaring multiple IdPs here.
|
||||
2. The IdP should provide the `url` and `entity_id` values.
|
||||
3. Save the `x509cert` value to a file; you'll use it in the
|
||||
instructions below.
|
||||
4. The values needed in the `attr_` fields are often configurable
|
||||
in your IdP's interface when setting up SAML authentication
|
||||
(referred to as "Attribute Statements" with Okta, or
|
||||
"Attribute Mapping" with GSuite). You'll want to connect
|
||||
these so that Zulip gets the email address (used as a unique
|
||||
user ID) and name for the user.
|
||||
|
||||
3. Install the certificate(s) required for SAML authentication. You
|
||||
will definitely need the public certificate of your IdP. Some IdP
|
||||
providers also support the Zulip server (Service Provider) having
|
||||
a certificate used for encryption and signing. We detail these
|
||||
steps as optional below, because they aren't required for basic
|
||||
setup, and some IdPs like Okta don't fully support Service
|
||||
Provider certificates. You should install them as follows:
|
||||
|
||||
1. On your Zulip server, `mkdir -p /etc/zulip/saml/idps/`
|
||||
2. Put the IDP public certificate in `/etc/zulip/saml/idps/{idp_name}.crt`
|
||||
3. (Optional) Put the Zulip server public certificate in `/etc/zulip/saml/zulip-cert.crt`
|
||||
4. (Optional) Put the Zulip server private key in `/etc/zulip/saml/zulip-private-key.key`
|
||||
5. Set the proper permissions on these files and directories:
|
||||
|
||||
```
|
||||
chown -R zulip.zulip /etc/zulip/saml/
|
||||
find /etc/zulip/saml/ -type f -exec chmod 644 -- {} +
|
||||
chmod 640 /etc/zulip/saml/zulip-private-key.key
|
||||
```
|
||||
|
||||
4. (Optional) If you configured the optional public and private server
|
||||
certificates above, you can enable the additional setting
|
||||
`"authnRequestsSigned": True` in `SOCIAL_AUTH_SAML_SECURITY_CONFIG`
|
||||
to have the SAMLRequests the server will be issuing to the IdP
|
||||
signed using those certificates. Additionally, if the IdP supports
|
||||
it, you can upload the public certificate to enable encryption of
|
||||
assertions in the SAMLResponses the IdP will send about
|
||||
authenticated users.
|
||||
|
||||
5. Enable the `zproject.backends.SAMLAuthBackend` auth backend, in
|
||||
`AUTHENTICATION_BACKENDS` in `/etc/zulip/settings.py`.
|
||||
|
||||
6. [Restart the Zulip server](settings.html) to ensure your settings
|
||||
changes take effect. The Zulip login page should now have a button
|
||||
for SAML authentication that you can use to login or create an account
|
||||
(including when creating a new organization).
|
||||
|
||||
7. If the configuration was successful, the server's metadata can be
|
||||
found at `https://yourzulipdomain.example.com/saml/metadata.xml`. You
|
||||
can use this for verifying your configuration or provide it to your
|
||||
IdP.
|
||||
|
||||
```eval_rst
|
||||
.. _ldap:
|
||||
```
|
||||
|
|
|
@ -56,6 +56,29 @@ Here are the full procedures for dev:
|
|||
`social_auth_github_key` to the client ID and `social_auth_github_secret`
|
||||
to the client secret.
|
||||
|
||||
### SAML
|
||||
|
||||
* Register a SAML authentication with Okta at
|
||||
https://zulipchat-admin.okta.com/admin/apps/saml-wizard/create. Specify:
|
||||
* `http://localhost:9991/complete/saml/` for the "Single sign on URL"`.
|
||||
* `http://localhost:9991` for the "Audience URI (SP Entity ID)".
|
||||
* Skip "Default RelayState".
|
||||
* Skip "Name ID format".
|
||||
* Set 'Email` for "Application username format".
|
||||
* Provide "Attribute statements" of `email` to `user.email`,
|
||||
`first_name` to `user.firstName`, and `last_name` to `user.lastName`.
|
||||
* Assign at least one account to the in the "Assignments" tab. Uou'll
|
||||
be logging in using this email address in the development
|
||||
environment (so make sure that email has an account and can login
|
||||
to the target realm).
|
||||
* Visit the big "Setup instructions" button on the "Sign on" tab.
|
||||
* Edit `zproject/dev-secrets.conf` to add the two values provided:
|
||||
* Set `saml_url = http...` from "Identity Provider Single Sign-On
|
||||
URL".
|
||||
* Set `saml_entity_id = http://...` from "Identity Provider Issuer".
|
||||
* Download the certificate and put it at the path `zproject/dev_saml.cert`.
|
||||
* Now you should have working SAML authentication!
|
||||
|
||||
### When SSL is required
|
||||
|
||||
Some OAuth providers (such as Facebook) require HTTPS on the callback
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -632,6 +632,11 @@ button.login-social-button:active {
|
|||
box-shadow: 0px 1px 1px hsla(0, 0%, 0%, 0.3);
|
||||
}
|
||||
|
||||
.saml-wrapper button.login-social-button {
|
||||
background-image: url('/static/images/landing-page/logos/saml-icon.png');
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.google-wrapper button.login-social-button {
|
||||
background-image: url('/static/images/landing-page/logos/googl_e-icon.png');
|
||||
width: 100%;
|
||||
|
|
|
@ -80,6 +80,19 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if saml_error %}
|
||||
<p>
|
||||
SAML authentication is either not enabled or misconfigured. Have a look at
|
||||
our <a href="https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#SAML">setup guide</a>.
|
||||
</p>
|
||||
{% if development_environment %}
|
||||
<p>
|
||||
See also
|
||||
the <a href="https://zulip.readthedocs.io/en/latest/subsystems/auth.html#saml">SAML
|
||||
guide</a> for the development environment.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<p>After making your changes, remember to restart
|
||||
the Zulip server.</p>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.23 on 2019-09-19 00:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import bitfield.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('zerver', '0249_userprofile_role_finish'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='realm',
|
||||
name='authentication_methods',
|
||||
field=bitfield.models.BitField(['Google', 'Email', 'GitHub', 'LDAP', 'Dev', 'RemoteUser', 'AzureAD', 'SAML'], default=2147483647),
|
||||
),
|
||||
]
|
|
@ -139,7 +139,8 @@ class Realm(models.Model):
|
|||
MAX_GOOGLE_HANGOUTS_DOMAIN_LENGTH = 255 # This is just the maximum domain length by RFC
|
||||
INVITES_STANDARD_REALM_DAILY_MAX = 3000
|
||||
MESSAGE_VISIBILITY_LIMITED = 10000
|
||||
AUTHENTICATION_FLAGS = [u'Google', u'Email', u'GitHub', u'LDAP', u'Dev', u'RemoteUser', u'AzureAD']
|
||||
AUTHENTICATION_FLAGS = [u'Google', u'Email', u'GitHub', u'LDAP', u'Dev',
|
||||
u'RemoteUser', u'AzureAD', u'SAML']
|
||||
SUBDOMAIN_FOR_ROOT_DOMAIN = ''
|
||||
|
||||
# User-visible display name and description used on e.g. the organization homepage
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDazCCAlOgAwIBAgIUeoIaZ6d61p7x4Zth70B0228j29AwDQYJKoZIhvcNAQEL
|
||||
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xOTA5MjkwMTExMDFaFw0yOTA5
|
||||
MjgwMTExMDFaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQDnmQlCXt/Lwf1moKuP3rnfWHClwH7f3R0fjAaLARR7
|
||||
3rRSbvhoCWrmQ2w8ZO7vtDh+rivqqsIW0AjPcBRoPzzzXYGTi/1S7tmEQcOV/oMP
|
||||
WciDIkQ6Nzg/3m8vvyb92/cI7/nTTChKIQI3K6uwZ1b8mvWt2MWhxvUUZyWWzyGw
|
||||
J+JvWRtRznnUaPjg7hvYfDbekAQ1avKRQ0uTxkj3x7Dt6PxgO9zG14jXi6R+vfDA
|
||||
YzLpcB+DCP48mMVWmtSWUpVEKQHrHP+EKOr7j8FyOXl0FfqkVD3xmXvirwgwv/Ny
|
||||
FMWUCcZddbBBENOMty1dKTqnU0ei7IT7IHc7aWemibb9AgMBAAGjUzBRMB0GA1Ud
|
||||
DgQWBBRdGAHn9nAevooUt1kpppwOeaC2nTAfBgNVHSMEGDAWgBRdGAHn9nAevooU
|
||||
t1kpppwOeaC2nTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCs
|
||||
sJutJ32iQO2LhEtYA9tut0BoD/GAb6JFwPUX7HXcX5iouRCNgF6fsMgCFVlyRn5V
|
||||
xyPsuz7rjumEmKIfmEbhTz/WYYBPGI8R+7MgOVCXZX+8r2+GxDnLOI9g5ypsWV6B
|
||||
nyFjGF/ldFieY+nhFL7HZ5s4AXM+PNHoguG00qE5nLcD160w1wun5rTXQRyNWyrf
|
||||
Nzoc2kTnXY8RLKVqMx7FzakPJ4CfLak/c2iiAU2ug7tEW/3OGdZeb2m56b8dl4GY
|
||||
3YStArY9y4S+DT690fagfVrlkj0zaWHsWcWdyzkLdNWFOxNWv5nVI/rkyawl0OyO
|
||||
+nQdIbP+XrzXpGPVt5qL
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><saml2p:Response Destination="http://zulip.testserver/complete/saml/" ID="id544612569720442296425226" InResponseTo="ONELOGIN_a5fde8b09598814d7af2537f865d31c2f7aea831" IssueInstant="2019-09-25T01:02:02.120Z" Version="2.0" xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xs="http://www.w3.org/2001/XMLSchema"><saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://www.okta.com/exk1da4osrIL3Y7ip357</saml2:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#id544612569720442296425226"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"><ec:InclusiveNamespaces PrefixList="xs" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>P/e+Gdz179UAcrrPZW2R9hzxMlSAGwbZ+Ogksp7Rzlg=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>H1eepG122h3jzIqorofI6sr636xVFFtqsN0Vj5eb9YoFN3KMDH1AqzvGbzA+XEoT/1vle/D2n1A0qMv6UrnMy0EgrZlA+Mx3MgcQDhFoIqI7lV48I2aJ+G1+FvTrzt1hhfn6SBTorhc3M2+ST9z68V8mLsNXr82GveL/Ej5J4rxbQQ0Jxaic3luAkV0EhROqiSDwC7e/45II34e3sdtQ9bbnf3feDbovklb7Daa/NIqWpWX+0Y9qhHo1zx05oPiGZFtveJHiUbFXPpjR0r1juuG3HTGkORhRHCMYnpz73NsmuBTkAgYE+G0vUr0k5Sk28efS15ZZuAyiN+XCjl6SzQ==</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIDojCCAoqgAwIBAgIGAW0svbVqMA0GCSqGSIb3DQEBCwUAMIGRMQswCQYDVQQGEwJVUzETMBEG
|
||||
A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
|
||||
MBIGA1UECwwLU1NPUHJvdmlkZXIxEjAQBgNVBAMMCXp1bGlwY2hhdDEcMBoGCSqGSIb3DQEJARYN
|
||||
aW5mb0Bva3RhLmNvbTAeFw0xOTA5MTMyMjI3MTNaFw0yOTA5MTMyMjI4MTNaMIGRMQswCQYDVQQG
|
||||
EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UE
|
||||
CgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxEjAQBgNVBAMMCXp1bGlwY2hhdDEcMBoGCSqG
|
||||
SIb3DQEJARYNaW5mb0Bva3RhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3K
|
||||
E57ZDDVplrZO1RKpz9zFekhXZZFiPhW2TCTtoaI966sGaRCmV10cb1FCUxxI3ilcjY8G5irHYc5O
|
||||
D4S8+FeIaHb036VjtZZNCDkamE2zGZCix5wCpXhxhQrXkkPJbzO4IGW896O43FPwefGfYnPC8/Oj
|
||||
bZ0OUuR8KkNbgn2VnqwZtmb0EX5xrA+212UDyVQ7izVXOoBbvzeydLh8EWteEXjKBREKGBfCL9Kl
|
||||
x8JY7BlYrZx+13NeDQsL7bgTXMnTIp3MVP3xddRqsatwersRVGr9b/HzXxfwu/MU230swjsNlgLZ
|
||||
OXiYD43rNEkJRlFfMnlY8F3IoE1Mki2BJtMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAI8Xk13NT
|
||||
rjo417U1+wZjUKqvB7iw4zGapMqpGRVavGw9hZmSCs8/AJAkFZQKMhAR9GGqf7JHHj/4fNEQ+XVh
|
||||
YF1jCUR/X2VwUiBseDHaUKj7EZiX9tIFEI/6LVfPRjKNy1RkEXHo7Lg4RnctclZ1KU7mIZkPSk1J
|
||||
fShKIUhtvNaCYJ4OVkN+giQQ6u9HwBqoBYikOBhvgXfIlBFD5H1n7JqxOjWZNO7Rhhx+TjD/0Dmd
|
||||
BE04J2bv5zCllBWSv2e1YFi+5SBTBq6FaIMz5c8T4WRFpRlnDEvvEREeAsAbaDiYvpJlO6hOzHNj
|
||||
KVmHpmQsDhsgXP02tDsfTARf3EXhbA==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><saml2p:Status xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"><saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></saml2p:Status><saml2:Assertion ID="id54461256972849811250394622" IssueInstant="2019-09-25T01:02:02.120Z" Version="2.0" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xs="http://www.w3.org/2001/XMLSchema"><saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://www.okta.com/exk1da4osrIL3Y7ip357</saml2:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#id54461256972849811250394622"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"><ec:InclusiveNamespaces PrefixList="xs" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>J5+QPDVShpm1VpG+dAjbU5GNrRwkEVMQ3MthoFukiH4=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>RWvjlS6nr7MUm6JzSn71nD3nkM77bja8Mnfy8GZYL0tQwtLBdixgW+oUF8jSXId9/dkYKCcq1n3fxzyX1iMkAeF7YcUlmpfN56hRKECzdWmaaceCS7s15vTqN/Gy83AFWp6d/nBbyt25UnGtmOSJjU5+QmBBB/JJO2EjtCiJZlJkJy3V1nOU/PnJ5p3iutUNtn17gqVYKkWix8b95xdoOHEZLC/0w8pt6OOLePlg+HafCg0XA7jS3g4+vPagcAEhSBIEpX9rZVdaWZdpP5NxHbjtyG979n5tzx7ooVBebrEfPdneoQeZQabNU/jUeeWXBJNaQ3Rv59EidOaTI68LZA==</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIDojCCAoqgAwIBAgIGAW0svbVqMA0GCSqGSIb3DQEBCwUAMIGRMQswCQYDVQQGEwJVUzETMBEG
|
||||
A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
|
||||
MBIGA1UECwwLU1NPUHJvdmlkZXIxEjAQBgNVBAMMCXp1bGlwY2hhdDEcMBoGCSqGSIb3DQEJARYN
|
||||
aW5mb0Bva3RhLmNvbTAeFw0xOTA5MTMyMjI3MTNaFw0yOTA5MTMyMjI4MTNaMIGRMQswCQYDVQQG
|
||||
EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UE
|
||||
CgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxEjAQBgNVBAMMCXp1bGlwY2hhdDEcMBoGCSqG
|
||||
SIb3DQEJARYNaW5mb0Bva3RhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3K
|
||||
E57ZDDVplrZO1RKpz9zFekhXZZFiPhW2TCTtoaI966sGaRCmV10cb1FCUxxI3ilcjY8G5irHYc5O
|
||||
D4S8+FeIaHb036VjtZZNCDkamE2zGZCix5wCpXhxhQrXkkPJbzO4IGW896O43FPwefGfYnPC8/Oj
|
||||
bZ0OUuR8KkNbgn2VnqwZtmb0EX5xrA+212UDyVQ7izVXOoBbvzeydLh8EWteEXjKBREKGBfCL9Kl
|
||||
x8JY7BlYrZx+13NeDQsL7bgTXMnTIp3MVP3xddRqsatwersRVGr9b/HzXxfwu/MU230swjsNlgLZ
|
||||
OXiYD43rNEkJRlFfMnlY8F3IoE1Mki2BJtMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAI8Xk13NT
|
||||
rjo417U1+wZjUKqvB7iw4zGapMqpGRVavGw9hZmSCs8/AJAkFZQKMhAR9GGqf7JHHj/4fNEQ+XVh
|
||||
YF1jCUR/X2VwUiBseDHaUKj7EZiX9tIFEI/6LVfPRjKNy1RkEXHo7Lg4RnctclZ1KU7mIZkPSk1J
|
||||
fShKIUhtvNaCYJ4OVkN+giQQ6u9HwBqoBYikOBhvgXfIlBFD5H1n7JqxOjWZNO7Rhhx+TjD/0Dmd
|
||||
BE04J2bv5zCllBWSv2e1YFi+5SBTBq6FaIMz5c8T4WRFpRlnDEvvEREeAsAbaDiYvpJlO6hOzHNj
|
||||
KVmHpmQsDhsgXP02tDsfTARf3EXhbA==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><saml2:Subject xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">{email}</saml2:NameID><saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml2:SubjectConfirmationData InResponseTo="ONELOGIN_a5fde8b09598814d7af2537f865d31c2f7aea831" NotOnOrAfter="2019-09-25T01:07:02.120Z" Recipient="http://zulip.testserver/complete/saml/"/></saml2:SubjectConfirmation></saml2:Subject><saml2:Conditions NotBefore="2019-09-25T00:57:02.120Z" NotOnOrAfter="2019-09-25T01:07:02.120Z" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:AudienceRestriction><saml2:Audience>http://zulip.testserver</saml2:Audience></saml2:AudienceRestriction></saml2:Conditions><saml2:AuthnStatement AuthnInstant="2019-09-25T01:01:37.741Z" SessionIndex="ONELOGIN_a5fde8b09598814d7af2537f865d31c2f7aea831" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:AuthnContext><saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef></saml2:AuthnContext></saml2:AuthnStatement><saml2:AttributeStatement xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:Attribute Name="first_name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"><saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">{first_name}</saml2:AttributeValue></saml2:Attribute><saml2:Attribute Name="last_name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"><saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">{last_name}</saml2:AttributeValue></saml2:Attribute><saml2:Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"><saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">{email}</saml2:AttributeValue></saml2:Attribute></saml2:AttributeStatement></saml2:Assertion></saml2p:Response>
|
|
@ -0,0 +1,21 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDazCCAlOgAwIBAgIUdxMUceyfJ0JWc9+631OR8cJDxREwDQYJKoZIhvcNAQEL
|
||||
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xOTA5MjQyMjAwMDFaFw0yOTA5
|
||||
MjMyMjAwMDFaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQDk5VXGfX6rAwkCoNYTTCsmZLcmhNW8KXbr0+giMHJ2
|
||||
1wswiFZadOevRbgEKeB6b7d/4G0JNTtcTKrq3LqBX7YiQqRsUegBMf1Ev8Gsx8c3
|
||||
LJKreOd2NdN0CKgEYtm0YAkOwoM4Idg5JUzfDHOJN6Q/ktUsUD8/LERjDzrLGTte
|
||||
A/HThow++1HIUKQCubzJXqyehC9/+gz17bCRq5XBaKB5oxjq0c8tNU6rFXnbYVZJ
|
||||
/v6OpKfzzg+BV73VVQWtfT40Aco5kBhp6gCjj3N1UQHX9KgEmtQpBHSqb9YbWGcn
|
||||
CILJ7HUgRdRVf0TEj83bT/IEmgAw0DOaDbWIjmUTsOW3AgMBAAGjUzBRMB0GA1Ud
|
||||
DgQWBBSXe/5beCNwVuyWNfQLiET1HYrjaTAfBgNVHSMEGDAWgBSXe/5beCNwVuyW
|
||||
NfQLiET1HYrjaTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAQ
|
||||
n4tWLuO2pcJzN4s8xJdG19Qne+H2N1mfRgzfSOIAAHWYXuP8i2JXp+QgPRai/Jse
|
||||
54p3WRM2JWrGicjgx/XDAsDv+SyZB3uF1r5yPGdKt05VRC/wiGh3p0rosGr5+8B0
|
||||
lICHeKPgOg0pCX3k3iU15ZkT8OA+5IQmyonB3gxtAjkUKTTUjd5gnIY8KIbcfrTw
|
||||
iJMPLLumpUx1NMbEcmcB1HKT2YHwPUh93d6FA5WAr0y9swke4VhI2vXTovwnR9CU
|
||||
oXbZrHH231O6SDaccl0/qFp6FDuSogRt9oiZorw8kk1NPyx21mqfmCgZ6tAZ7SJZ
|
||||
0ppBMmyTLo1PL86rZcF9
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDk5VXGfX6rAwkC
|
||||
oNYTTCsmZLcmhNW8KXbr0+giMHJ21wswiFZadOevRbgEKeB6b7d/4G0JNTtcTKrq
|
||||
3LqBX7YiQqRsUegBMf1Ev8Gsx8c3LJKreOd2NdN0CKgEYtm0YAkOwoM4Idg5JUzf
|
||||
DHOJN6Q/ktUsUD8/LERjDzrLGTteA/HThow++1HIUKQCubzJXqyehC9/+gz17bCR
|
||||
q5XBaKB5oxjq0c8tNU6rFXnbYVZJ/v6OpKfzzg+BV73VVQWtfT40Aco5kBhp6gCj
|
||||
j3N1UQHX9KgEmtQpBHSqb9YbWGcnCILJ7HUgRdRVf0TEj83bT/IEmgAw0DOaDbWI
|
||||
jmUTsOW3AgMBAAECggEAfCCyB1X+3xZiSH6YGRbxP3zWpZjbn5KM3w6nkALdz/yG
|
||||
IOeOjLdg/Pe99uQOy9bRmBNIjfnEGyWoen0A1y/kQWgKaoNwYVWOlz219dDRA+a0
|
||||
EzEZtE00QnR/SQGiNeLuhoaNSl9wNm035q2F6h+2fpNN7x4Fbmi/HUkhBQrF2xEZ
|
||||
vK6WXb5uHiWYJ4wIEw5nc/DMY3NZ6vJgU+T6uMkj1llhlTtKFFNnRS9yrt46D0Bb
|
||||
yJEP+9RhmSkAjZyKICYNh/r7Me0CU3AOHnyAR6Tyz7KweupE91INclzPuL2HEd6R
|
||||
05TntfqC+yimwa0AY4dX+cUntr82B5KarWur94Uj4QKBgQD9az4OcIPV7qEobVwo
|
||||
eqfgJc9sN0806ESgzCxIRut/RPNykr76zE6tYPQbzAi9iQQYi+yXVkZddUWWPtht
|
||||
qTv9LIuGb0NqKLUQYx2tpyh0gFnJJHyKSmooIfoKDEkSLWyPzn2tKPrGOQ683tjX
|
||||
vMGNfUIyaHMsidRwOs58oiweswKBgQDnOibfvPjEVrSYRcimUmm8WS7tCBJFd5LL
|
||||
cL//NfHQ92SX6z33j71Qz80ezLIrHHizhjaxPTxXGny/unc8j3njM15ilZmNB9Lg
|
||||
wtTW3H+MamFF3nTI6/M1iaAM2OIY9nTwZv/UMMmBuVsNOa0mdo1FxxF9ik8MQibB
|
||||
vsO65YKe7QKBgQCamE2nKWSDoauWqgBKgWjgCLDc53DeacNUBLoO7ZTEcx/AiV0Q
|
||||
SorEohzIyFOcrHVfNB0ExZDvepcU7QnC/DaoYABN5ppNrL+oW47DXPIFADfFyQhg
|
||||
pLzV9sQ+VPhOqn9Ly0BH3nP9cNlYxump0nCRDBTSA34fcYWzYWyOA7C+mQKBgQCS
|
||||
UjpHW04Q8M1XjtFqbrx6c/U+Cd2GGCTMmIzm8zwTAHqnqDWOc2dZvCYRV3dn0JyQ
|
||||
/l2dyyJj/F709Qp/SEvZeqg/umtw04KeuKv3S5FrSeZEUIGWo7lEJ9MgTh7FrTBS
|
||||
8Nrza+wYKzNzKwxnSp4bid2Hk/5xw2rDL/SsUJBYAQKBgQDqeuuosCuynOZKF/Vy
|
||||
tMZ7ms1GQzfmnkh1uA+OdTMkxwN/DXbJJwo89EouPN+KVpdM8GlvWRZesrVtqSpO
|
||||
wK94UzIVrejv6sLUz33tTyRoMrPmkQ9r6KdpHrHwTAJzfceeqHwK1XUCEf/ht4A6
|
||||
0zaX2kWvylQBjsD293QfCerl3A==
|
||||
-----END PRIVATE KEY-----
|
|
@ -56,16 +56,19 @@ from zproject.backends import ZulipDummyBackend, EmailAuthBackend, \
|
|||
require_email_format_usernames, AUTH_BACKEND_NAME_MAP, \
|
||||
ZulipLDAPConfigurationError, ZulipLDAPExceptionOutsideDomain, \
|
||||
ZulipLDAPException, query_ldap, sync_user_from_ldap, SocialAuthMixin, \
|
||||
PopulateUserLDAPError
|
||||
PopulateUserLDAPError, SAMLAuthBackend, saml_auth_enabled
|
||||
|
||||
from zerver.views.auth import (maybe_send_to_registration,
|
||||
_subdomain_token_salt)
|
||||
from version import ZULIP_VERSION
|
||||
|
||||
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
||||
from onelogin.saml2.response import OneLogin_Saml2_Response
|
||||
from social_core.exceptions import AuthFailed, AuthStateForbidden
|
||||
from social_django.strategy import DjangoStrategy
|
||||
from social_django.storage import BaseDjangoStorage
|
||||
|
||||
import base64
|
||||
import json
|
||||
import urllib
|
||||
import ujson
|
||||
|
@ -956,6 +959,216 @@ class SocialAuthBase(ZulipTestCase):
|
|||
self.assertEqual(result.status_code, 302)
|
||||
self.assertIn('login', result.url)
|
||||
|
||||
class SAMLAuthBackendTest(SocialAuthBase):
|
||||
__unittest_skip__ = False
|
||||
|
||||
BACKEND_CLASS = SAMLAuthBackend
|
||||
LOGIN_URL = "/accounts/login/social/saml"
|
||||
SIGNUP_URL = "/accounts/register/social/saml"
|
||||
AUTHORIZATION_URL = "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO"
|
||||
AUTH_FINISH_URL = "/complete/saml/"
|
||||
CONFIG_ERROR_URL = "/config-error/saml"
|
||||
|
||||
# We have to define our own social_auth_test as the flow of SAML authentication
|
||||
# is different from the other social backends.
|
||||
def social_auth_test(self, account_data_dict: Dict[str, str],
|
||||
*, subdomain: Optional[str]=None,
|
||||
mobile_flow_otp: Optional[str]=None,
|
||||
is_signup: Optional[str]=None,
|
||||
next: str='',
|
||||
multiuse_object_key: str='',
|
||||
**extra_data: Any) -> HttpResponse:
|
||||
url, headers = self.prepare_login_url_and_headers(
|
||||
subdomain, mobile_flow_otp, is_signup, next, multiuse_object_key
|
||||
)
|
||||
|
||||
result = self.client_get(url, **headers)
|
||||
|
||||
expected_result_url_prefix = 'http://testserver/login/%s/' % (self.backend.name,)
|
||||
if settings.SOCIAL_AUTH_SUBDOMAIN is not None:
|
||||
expected_result_url_prefix = (
|
||||
'http://%s.testserver/login/%s/' % (settings.SOCIAL_AUTH_SUBDOMAIN, self.backend.name)
|
||||
)
|
||||
|
||||
if result.status_code != 302 or not result.url.startswith(expected_result_url_prefix):
|
||||
return result
|
||||
|
||||
result = self.client_get(result.url, **headers)
|
||||
|
||||
self.assertEqual(result.status_code, 302)
|
||||
assert self.AUTHORIZATION_URL in result.url
|
||||
assert "samlrequest" in result.url.lower()
|
||||
|
||||
self.client.cookies = result.cookies
|
||||
parsed_url = urllib.parse.urlparse(result.url)
|
||||
relay_state = urllib.parse.parse_qs(parsed_url.query)['RelayState'][0]
|
||||
# Make sure params are getting encoded into RelayState:
|
||||
data = SAMLAuthBackend.get_data_from_redis(relay_state)
|
||||
if next:
|
||||
self.assertEqual(data['next'], next)
|
||||
if is_signup:
|
||||
self.assertEqual(data['is_signup'], is_signup)
|
||||
|
||||
saml_response = self.generate_saml_response(**account_data_dict)
|
||||
post_params = {"SAMLResponse": saml_response, "RelayState": relay_state}
|
||||
# The mock below is necessary, so that python3-saml accepts our SAMLResponse,
|
||||
# and doesn't verify the cryptographic signatures etc., since generating
|
||||
# a perfectly valid SAMLResponse for the purpose of these tests would be too complex,
|
||||
# and we simply use one loaded from a fixture file.
|
||||
with mock.patch.object(OneLogin_Saml2_Response, 'is_valid', return_value=True):
|
||||
result = self.client_post(self.AUTH_FINISH_URL, post_params, **headers)
|
||||
|
||||
return result
|
||||
|
||||
def generate_saml_response(self, email: str, name: str) -> str:
|
||||
"""
|
||||
The samlresponse.txt fixture has a pre-generated SAMLResponse,
|
||||
with {email}, {first_name}, {last_name} placeholders, that can
|
||||
be filled out with the data we want.
|
||||
"""
|
||||
name_parts = name.split(' ')
|
||||
first_name = name_parts[0]
|
||||
last_name = name_parts[1]
|
||||
|
||||
unencoded_saml_response = self.fixture_data("samlresponse.txt", type="saml").format(
|
||||
email=email,
|
||||
first_name=first_name,
|
||||
last_name=last_name
|
||||
)
|
||||
# SAMLResponse needs to be base64-encoded.
|
||||
saml_response = base64.b64encode(unencoded_saml_response.encode()).decode() # type: str
|
||||
|
||||
return saml_response
|
||||
|
||||
def get_account_data_dict(self, email: str, name: str) -> Dict[str, Any]:
|
||||
return dict(email=email, name=name)
|
||||
|
||||
def test_social_auth_no_key(self) -> None:
|
||||
"""
|
||||
Since in the case of SAML there isn't a direct equivalent of CLIENT_KEY_SETTING,
|
||||
we override this test, to test for the case where the obligatory
|
||||
SOCIAL_AUTH_SAML_ENABLED_IDPS isn't configured.
|
||||
"""
|
||||
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
||||
with self.settings(SOCIAL_AUTH_SAML_ENABLED_IDPS=None):
|
||||
result = self.social_auth_test(account_data_dict,
|
||||
subdomain='zulip', next='/user_uploads/image')
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(result.url, self.CONFIG_ERROR_URL)
|
||||
|
||||
def test_saml_auth_works_without_private_public_keys(self) -> None:
|
||||
with self.settings(SOCIAL_AUTH_SAML_SP_PUBLIC_CERT='', SOCIAL_AUTH_SAML_SP_PRIVATE_KEY=''):
|
||||
self.test_social_auth_success()
|
||||
|
||||
def test_saml_auth_enabled(self) -> None:
|
||||
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.SAMLAuthBackend',)):
|
||||
self.assertTrue(saml_auth_enabled())
|
||||
result = self.client_get("/saml/metadata.xml")
|
||||
self.assert_in_success_response(
|
||||
['entityID="{}"'.format(settings.SOCIAL_AUTH_SAML_SP_ENTITY_ID)], result
|
||||
)
|
||||
|
||||
def test_social_auth_complete(self) -> None:
|
||||
with mock.patch.object(OneLogin_Saml2_Response, 'is_valid', return_value=True):
|
||||
with mock.patch.object(OneLogin_Saml2_Auth, 'is_authenticated', return_value=False), \
|
||||
mock.patch('zproject.backends.logging.info') as m:
|
||||
# This mock causes AuthFailed to be raised.
|
||||
saml_response = self.generate_saml_response(self.email, self.name)
|
||||
relay_state = SAMLAuthBackend.put_data_in_redis({"idp": "test_idp"})
|
||||
post_params = {"SAMLResponse": saml_response, "RelayState": relay_state}
|
||||
result = self.client_post('/complete/saml/', post_params)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertIn('login', result.url)
|
||||
m.assert_called_with("Authentication failed: SAML login failed: [] (None)")
|
||||
|
||||
def test_social_auth_complete_when_base_exc_is_raised(self) -> None:
|
||||
with mock.patch.object(OneLogin_Saml2_Response, 'is_valid', return_value=True):
|
||||
with mock.patch('social_core.backends.saml.SAMLAuth.auth_complete',
|
||||
side_effect=AuthStateForbidden('State forbidden')), \
|
||||
mock.patch('zproject.backends.logging.warning') as m:
|
||||
saml_response = self.generate_saml_response(self.email, self.name)
|
||||
relay_state = SAMLAuthBackend.put_data_in_redis({"idp": "test_idp"})
|
||||
post_params = {"SAMLResponse": saml_response, "RelayState": relay_state}
|
||||
result = self.client_post('/complete/saml/', post_params)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertIn('login', result.url)
|
||||
m.assert_called_with("Wrong state parameter given.")
|
||||
|
||||
def test_social_auth_complete_bad_params(self) -> None:
|
||||
# Simple GET for /complete/saml without the required parameters.
|
||||
# This tests the auth_complete wrapped in our SAMLAuthBackend,
|
||||
# ensuring it prevents this requests from causing an internal server error.
|
||||
with mock.patch('zproject.backends.logging.info') as m:
|
||||
result = self.client_get('/complete/saml/')
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertIn('login', result.url)
|
||||
m.assert_called_with("SAML authentication failed: missing RelayState.")
|
||||
|
||||
# Check that POSTing the RelayState, but with missing SAMLResponse,
|
||||
# doesn't cause errors either:
|
||||
with mock.patch('zproject.backends.logging.info') as m:
|
||||
relay_state = SAMLAuthBackend.put_data_in_redis({"idp": "test_idp"})
|
||||
post_params = {"RelayState": relay_state}
|
||||
result = self.client_post('/complete/saml/', post_params)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertIn('login', result.url)
|
||||
m.assert_called_with(
|
||||
# OneLogin_Saml2_Error exception:
|
||||
"SAML Response not found, Only supported HTTP_POST Binding"
|
||||
)
|
||||
|
||||
with mock.patch('zproject.backends.logging.info') as m:
|
||||
relay_state = SAMLAuthBackend.put_data_in_redis({"idp": "test_idp"})
|
||||
relay_state = relay_state[:-1] # Break the token by removing the last character
|
||||
post_params = {"RelayState": relay_state}
|
||||
result = self.client_post('/complete/saml/', post_params)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertIn('login', result.url)
|
||||
m.assert_called_with("SAML authentication failed: bad RelayState token.")
|
||||
|
||||
def test_social_auth_saml_bad_idp_param_on_login_page(self) -> None:
|
||||
with mock.patch('zproject.backends.logging.info') as m:
|
||||
result = self.client_get('/login/saml/')
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual('/login/', result.url)
|
||||
m.assert_called_with("/login/saml/ : Bad idp param.")
|
||||
|
||||
with mock.patch('zproject.backends.logging.info') as m:
|
||||
result = self.client_get('/login/saml/?idp=bad_idp')
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual('/login/', result.url)
|
||||
m.assert_called_with("/login/saml/ : Bad idp param.")
|
||||
|
||||
def test_social_auth_invalid_email(self) -> None:
|
||||
"""
|
||||
This test needs an override from the original class. For security reasons,
|
||||
the 'next' and 'mobile_flow_otp' params don't get passed on in the session
|
||||
if the authentication attempt failed. See SAMLAuthBackend.auth_complete for details.
|
||||
"""
|
||||
account_data_dict = self.get_account_data_dict(email="invalid", name=self.name)
|
||||
result = self.social_auth_test(account_data_dict,
|
||||
expect_choose_email_screen=True,
|
||||
subdomain='zulip', next='/user_uploads/image')
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(result.url, "/login/")
|
||||
|
||||
def test_social_auth_saml_multiple_idps_configured(self) -> None:
|
||||
"""
|
||||
Using multiple IdPs is not supported right now, and having multiple configured
|
||||
should lead to misconfiguration page.
|
||||
"""
|
||||
|
||||
with self.settings(SOCIAL_AUTH_SAML_ENABLED_IDPS={"test_idp1": {}, "test_idp2": {}}):
|
||||
# We don't need to put full idp configurations in the mock settings above
|
||||
# to trigger the error.
|
||||
with mock.patch("zerver.views.auth.logging.error") as mock_error:
|
||||
result = self.client_get("/accounts/login/social/saml")
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(result.url, '/config-error/saml')
|
||||
mock_error.assert_called_once_with(
|
||||
"SAML misconfigured - you have specified multiple IdPs. Only one IdP is supported."
|
||||
)
|
||||
|
||||
class GitHubAuthBackendTest(SocialAuthBase):
|
||||
__unittest_skip__ = False
|
||||
|
||||
|
|
|
@ -412,6 +412,14 @@ class ConfigErrorTest(ZulipTestCase):
|
|||
self.assert_not_in_success_response(["zproject/dev_settings.py"], result)
|
||||
self.assert_not_in_success_response(["zproject/dev-secrets.conf"], result)
|
||||
|
||||
@override_settings(SOCIAL_AUTH_SAML_ENABLED_IDPS=None)
|
||||
def test_saml_error(self) -> None:
|
||||
result = self.client_get("/accounts/login/social/saml")
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(result.url, '/config-error/saml')
|
||||
result = self.client_get(result.url)
|
||||
self.assert_in_success_response(["SAML authentication"], result)
|
||||
|
||||
def test_smtp_error(self) -> None:
|
||||
result = self.client_get("/config-error/smtp")
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
|
|
@ -7,7 +7,8 @@ from django.contrib.auth.views import password_reset as django_password_reset
|
|||
from django.urls import reverse
|
||||
from zerver.decorator import require_post, \
|
||||
process_client, do_login, log_view_func
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, \
|
||||
HttpResponseServerError
|
||||
from django.template.response import SimpleTemplateResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
@ -37,12 +38,14 @@ from zerver.models import PreregistrationUser, UserProfile, remote_user_to_email
|
|||
from zerver.signals import email_on_new_login
|
||||
from zproject.backends import password_auth_enabled, dev_auth_enabled, \
|
||||
ldap_auth_enabled, ZulipLDAPConfigurationError, ZulipLDAPAuthBackend, \
|
||||
AUTH_BACKEND_NAME_MAP, auth_enabled_helper
|
||||
AUTH_BACKEND_NAME_MAP, auth_enabled_helper, saml_auth_enabled
|
||||
from version import ZULIP_VERSION
|
||||
|
||||
import jwt
|
||||
import logging
|
||||
|
||||
from social_django.utils import load_backend, load_strategy
|
||||
|
||||
from two_factor.forms import BackupTokenForm
|
||||
from two_factor.views import LoginView as BaseTwoFactorLoginView
|
||||
|
||||
|
@ -315,7 +318,8 @@ def remote_user_jwt(request: HttpRequest) -> HttpResponse:
|
|||
return login_or_register_remote_user(request, email, user_profile, remote_user)
|
||||
|
||||
def oauth_redirect_to_root(request: HttpRequest, url: str,
|
||||
sso_type: str, is_signup: bool=False) -> HttpResponse:
|
||||
sso_type: str, is_signup: bool=False,
|
||||
extra_url_params: Dict[str, str]={}) -> HttpResponse:
|
||||
main_site_uri = settings.ROOT_DOMAIN_URI + url
|
||||
if settings.SOCIAL_AUTH_SUBDOMAIN is not None and sso_type == 'social':
|
||||
main_site_uri = (settings.EXTERNAL_URI_SCHEME +
|
||||
|
@ -342,23 +346,54 @@ def oauth_redirect_to_root(request: HttpRequest, url: str,
|
|||
if next:
|
||||
params['next'] = next
|
||||
|
||||
params = {**params, **extra_url_params}
|
||||
|
||||
return redirect(main_site_uri + '?' + urllib.parse.urlencode(params))
|
||||
|
||||
def start_social_login(request: HttpRequest, backend: str) -> HttpResponse:
|
||||
backend_url = reverse('social:begin', args=[backend])
|
||||
extra_url_params = {} # type: Dict[str, str]
|
||||
if backend == "saml":
|
||||
obligatory_saml_settings_list = [
|
||||
settings.SOCIAL_AUTH_SAML_SP_ENTITY_ID,
|
||||
settings.SOCIAL_AUTH_SAML_ORG_INFO,
|
||||
settings.SOCIAL_AUTH_SAML_TECHNICAL_CONTACT,
|
||||
settings.SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
|
||||
settings.SOCIAL_AUTH_SAML_ENABLED_IDPS
|
||||
]
|
||||
if any(not setting for setting in obligatory_saml_settings_list):
|
||||
return redirect_to_config_error("saml")
|
||||
|
||||
# This backend requires the name of the IdP (from the list of configured ones)
|
||||
# to be passed as the parameter.
|
||||
# Currently we support configuring only one IdP.
|
||||
# TODO: Support multiple IdPs. python-social-auth SAML (which we use here)
|
||||
# already supports that, so essentially only the UI for it on the login pages
|
||||
# needs to be figured out.
|
||||
if len(settings.SOCIAL_AUTH_SAML_ENABLED_IDPS) != 1:
|
||||
logging.error(
|
||||
"SAML misconfigured - you have specified multiple IdPs. Only one IdP is supported."
|
||||
)
|
||||
return redirect_to_config_error("saml")
|
||||
extra_url_params = {'idp': list(settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.keys())[0]}
|
||||
if (backend == "github") and not (settings.SOCIAL_AUTH_GITHUB_KEY and
|
||||
settings.SOCIAL_AUTH_GITHUB_SECRET):
|
||||
return redirect_to_config_error("github")
|
||||
if (backend == "google") and not (settings.SOCIAL_AUTH_GOOGLE_KEY and
|
||||
settings.SOCIAL_AUTH_GOOGLE_SECRET):
|
||||
return redirect_to_config_error("google")
|
||||
# TODO: Add a similar block of AzureAD.
|
||||
# TODO: Add a similar block for AzureAD.
|
||||
|
||||
return oauth_redirect_to_root(request, backend_url, 'social')
|
||||
return oauth_redirect_to_root(request, backend_url, 'social', extra_url_params=extra_url_params)
|
||||
|
||||
def start_social_signup(request: HttpRequest, backend: str) -> HttpResponse:
|
||||
backend_url = reverse('social:begin', args=[backend])
|
||||
return oauth_redirect_to_root(request, backend_url, 'social', is_signup=True)
|
||||
extra_url_params = {} # type: Dict[str, str]
|
||||
if backend == "saml":
|
||||
assert len(settings.SOCIAL_AUTH_SAML_ENABLED_IDPS) == 1
|
||||
extra_url_params = {'idp': list(settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.keys())[0]}
|
||||
return oauth_redirect_to_root(request, backend_url, 'social', is_signup=True,
|
||||
extra_url_params=extra_url_params)
|
||||
|
||||
def authenticate_remote_user(realm: Realm,
|
||||
email_address: Optional[str]) -> Optional[UserProfile]:
|
||||
|
@ -851,3 +886,25 @@ def password_reset(request: HttpRequest, **kwargs: Any) -> HttpResponse:
|
|||
template_name='zerver/reset.html',
|
||||
password_reset_form=ZulipPasswordResetForm,
|
||||
post_reset_redirect='/accounts/password/reset/done/')
|
||||
|
||||
@csrf_exempt
|
||||
def saml_sp_metadata(request: HttpRequest, **kwargs: Any) -> HttpResponse: # nocoverage
|
||||
"""
|
||||
This is the view function for generating our SP metadata
|
||||
for SAML authentication. It's meant for helping check the correctness
|
||||
of the configuration when setting up SAML, or for obtaining the XML metadata
|
||||
if the IdP requires it.
|
||||
Taken from https://python-social-auth.readthedocs.io/en/latest/backends/saml.html
|
||||
"""
|
||||
if not saml_auth_enabled():
|
||||
return redirect_to_config_error("saml")
|
||||
|
||||
complete_url = reverse('social:complete', args=("saml",))
|
||||
saml_backend = load_backend(load_strategy(request), "saml",
|
||||
complete_url)
|
||||
metadata, errors = saml_backend.generate_metadata_xml()
|
||||
if not errors:
|
||||
return HttpResponse(content=metadata,
|
||||
content_type='text/xml')
|
||||
|
||||
return HttpResponseServerError(content=', '.join(errors))
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import copy
|
||||
import logging
|
||||
import magic
|
||||
import ujson
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from django_auth_ldap.backend import LDAPBackend, _LDAPUser, ldap_error
|
||||
|
@ -28,12 +29,14 @@ from django.http import HttpResponse, HttpResponseRedirect
|
|||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from requests import HTTPError
|
||||
from onelogin.saml2.errors import OneLogin_Saml2_Error
|
||||
from social_core.backends.github import GithubOAuth2, GithubOrganizationOAuth2, \
|
||||
GithubTeamOAuth2
|
||||
from social_core.backends.azuread import AzureADOAuth2
|
||||
from social_core.backends.base import BaseAuth
|
||||
from social_core.backends.google import GoogleOAuth2
|
||||
from social_core.backends.oauth import BaseOAuth2
|
||||
from social_core.backends.saml import SAMLAuth
|
||||
from social_core.pipeline.partial import partial
|
||||
from social_core.exceptions import AuthFailed, SocialAuthBaseException
|
||||
|
||||
|
@ -44,11 +47,15 @@ from zerver.lib.avatar_hash import user_avatar_content_hash
|
|||
from zerver.lib.dev_ldap_directory import init_fakeldap
|
||||
from zerver.lib.request import JsonableError
|
||||
from zerver.lib.users import check_full_name, validate_user_custom_profile_field
|
||||
from zerver.lib.utils import generate_random_token
|
||||
from zerver.lib.redis_utils import get_redis_client
|
||||
from zerver.models import CustomProfileField, DisposableEmailError, DomainNotAllowedForRealmError, \
|
||||
EmailContainsPlusError, PreregistrationUser, UserProfile, Realm, custom_profile_fields_for_realm, \
|
||||
email_allowed_for_realm, get_default_stream_groups, get_user_profile_by_id, remote_user_to_email, \
|
||||
email_to_username, get_realm, get_user_by_delivery_email, supported_auth_backends
|
||||
|
||||
redis_client = get_redis_client()
|
||||
|
||||
# This first batch of methods is used by other code in Zulip to check
|
||||
# whether a given authentication backend is enabled for a given realm.
|
||||
# In each case, we both needs to check at the server level (via
|
||||
|
@ -96,6 +103,9 @@ def google_auth_enabled(realm: Optional[Realm]=None) -> bool:
|
|||
def github_auth_enabled(realm: Optional[Realm]=None) -> bool:
|
||||
return auth_enabled_helper(['GitHub'], realm)
|
||||
|
||||
def saml_auth_enabled(realm: Optional[Realm]=None) -> bool:
|
||||
return auth_enabled_helper(['SAML'], realm)
|
||||
|
||||
def any_social_backend_enabled(realm: Optional[Realm]=None) -> bool:
|
||||
"""Used by the login page process to determine whether to show the
|
||||
'OR' for login with Google"""
|
||||
|
@ -1003,6 +1013,124 @@ class GoogleAuthBackend(SocialAuthMixin, GoogleOAuth2):
|
|||
verified_emails.append(details["email"])
|
||||
return verified_emails
|
||||
|
||||
class SAMLAuthBackend(SocialAuthMixin, SAMLAuth):
|
||||
auth_backend_name = "SAML"
|
||||
standard_relay_params = ["subdomain", "multiuse_object_key", "mobile_flow_otp",
|
||||
"next", "is_signup"]
|
||||
REDIS_EXPIRATION_SECONDS = 60 * 15
|
||||
|
||||
def auth_url(self) -> str:
|
||||
"""Get the URL to which we must redirect in order to
|
||||
authenticate the user. Overriding the original SAMLAuth.auth_url.
|
||||
Runs when someone accesses the /login/saml/ endpoint."""
|
||||
try:
|
||||
idp_name = self.strategy.request_data()['idp']
|
||||
auth = self._create_saml_auth(idp=self.get_idp(idp_name))
|
||||
except KeyError:
|
||||
# If the above raise KeyError, it means invalid or no idp was specified,
|
||||
# we should log that and redirect to the login page.
|
||||
logging.info("/login/saml/ : Bad idp param.")
|
||||
return reverse('zerver.views.auth.login_page',
|
||||
kwargs = {'template_name': 'zerver/login.html'})
|
||||
|
||||
# This where we change things. We need to pass some params
|
||||
# (`mobile_flow_otp`, `next`, etc.) through RelayState, which
|
||||
# then the IdP will pass back to us so we can read those
|
||||
# parameters in the final part of the authentication flow, at
|
||||
# the /complete/saml/ endpoint.
|
||||
#
|
||||
# To protect against network eavesdropping of these
|
||||
# parameters, we send just a random token to the IdP in
|
||||
# RelayState, which is used as a key into our redis data store
|
||||
# for fetching the actual parameters after the IdP has
|
||||
# returned a successful authentication.
|
||||
params_to_relay = ["idp"] + self.standard_relay_params
|
||||
request_data = self.strategy.request_data().dict()
|
||||
data_to_relay = {
|
||||
key: request_data[key] for key in params_to_relay if key in request_data
|
||||
}
|
||||
relay_state = self.put_data_in_redis(data_to_relay)
|
||||
|
||||
return auth.login(return_to=relay_state)
|
||||
|
||||
@classmethod
|
||||
def put_data_in_redis(cls, data_to_relay: Dict[str, Any]) -> str:
|
||||
with redis_client.pipeline() as pipeline:
|
||||
token = generate_random_token(64)
|
||||
key = "saml_token_{}".format(token)
|
||||
pipeline.set(key, ujson.dumps(data_to_relay))
|
||||
pipeline.expire(key, cls.REDIS_EXPIRATION_SECONDS)
|
||||
pipeline.execute()
|
||||
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def get_data_from_redis(cls, key: str) -> Optional[Dict[str, Any]]:
|
||||
redis_data = None
|
||||
if key.startswith('saml_token_'):
|
||||
# Safety if statement, to not allow someone to poke around arbitrary redis keys here.
|
||||
redis_data = redis_client.get(key)
|
||||
if redis_data is None:
|
||||
# TODO: We will need some sort of user-facing message
|
||||
# about the authentication session having expired here.
|
||||
logging.info("SAML authentication failed: bad RelayState token.")
|
||||
return None
|
||||
|
||||
return ujson.loads(redis_data)
|
||||
|
||||
def auth_complete(self, *args: Any, **kwargs: Any) -> Optional[HttpResponse]:
|
||||
"""
|
||||
Additional ugly wrapping on top of auth_complete in SocialAuthMixin.
|
||||
We handle two things here:
|
||||
1. Working around bad RelayState or SAMLResponse parameters in the request.
|
||||
Both parameters should be present if the user came to /complete/saml/ through
|
||||
the IdP as intended. The errors can happen if someone simply types the endpoint into
|
||||
their browsers, or generally tries messing with it in some ways.
|
||||
|
||||
2. The first part of our SAML authentication flow will encode important parameters
|
||||
into the RelayState. We need to read them and set those values in the session,
|
||||
and then change the RelayState param to the idp_name, because that's what
|
||||
SAMLAuth.auth_complete() expects.
|
||||
"""
|
||||
if 'RelayState' not in self.strategy.request_data():
|
||||
logging.info("SAML authentication failed: missing RelayState.")
|
||||
return None
|
||||
|
||||
# Set the relevant params that we transported in the RelayState:
|
||||
redis_key = self.strategy.request_data()['RelayState']
|
||||
relayed_params = self.get_data_from_redis(redis_key)
|
||||
if relayed_params is None:
|
||||
return None
|
||||
|
||||
result = None
|
||||
try:
|
||||
for param, value in relayed_params.items():
|
||||
if param in self.standard_relay_params:
|
||||
self.strategy.session_set(param, value)
|
||||
|
||||
# super().auth_complete expects to have RelayState set to the idp_name,
|
||||
# so we need to replace this param.
|
||||
post_params = self.strategy.request.POST.copy()
|
||||
post_params['RelayState'] = relayed_params["idp"]
|
||||
self.strategy.request.POST = post_params
|
||||
|
||||
# Call the auth_complete method of SocialAuthMixIn
|
||||
result = super().auth_complete(*args, **kwargs) # type: ignore # monkey-patching
|
||||
except OneLogin_Saml2_Error as e:
|
||||
# This will be raised if SAMLResponse is missing.
|
||||
logging.info(str(e))
|
||||
# Fall through to returning None.
|
||||
finally:
|
||||
if result is None:
|
||||
for param in self.standard_relay_params:
|
||||
# If an attacker managed to eavesdrop on the RelayState token,
|
||||
# they may pass it here to the endpoint with an invalid SAMLResponse.
|
||||
# We remove these potentially sensitive parameters that we have set in the session
|
||||
# ealier, to avoid leaking their values.
|
||||
self.strategy.session_set(param, None)
|
||||
|
||||
return result
|
||||
|
||||
AUTH_BACKEND_NAME_MAP = {
|
||||
'Dev': DevAuthBackend,
|
||||
'Email': EmailAuthBackend,
|
||||
|
|
|
@ -45,6 +45,7 @@ AUTHENTICATION_BACKENDS = (
|
|||
'zproject.backends.EmailAuthBackend',
|
||||
'zproject.backends.GitHubAuthBackend',
|
||||
'zproject.backends.GoogleAuthBackend',
|
||||
'zproject.backends.SAMLAuthBackend',
|
||||
# 'zproject.backends.AzureADAuthBackend',
|
||||
)
|
||||
|
||||
|
@ -157,3 +158,6 @@ TERMS_OF_SERVICE = 'corporate/terms.md'
|
|||
# header. Important for SAML authentication in the development
|
||||
# environment.
|
||||
USE_X_FORWARDED_PORT = True
|
||||
|
||||
# Override the default SAML entity ID
|
||||
SOCIAL_AUTH_SAML_SP_ENTITY_ID = "http://localhost:9991/"
|
||||
|
|
|
@ -120,6 +120,7 @@ AUTHENTICATION_BACKENDS = (
|
|||
# 'zproject.backends.GoogleAuthBackend', # Google auth, setup below
|
||||
# 'zproject.backends.GitHubAuthBackend', # GitHub auth, setup below
|
||||
# 'zproject.backends.AzureADAuthBackend', # Microsoft Azure Active Directory auth, setup below
|
||||
# 'zproject.backends.SAMLAuthBackend', # SAML, setup below
|
||||
# 'zproject.backends.ZulipLDAPAuthBackend', # LDAP, setup below
|
||||
# 'zproject.backends.ZulipRemoteUserBackend', # Local SSO, setup docs on readthedocs
|
||||
) # type: Tuple[str, ...]
|
||||
|
@ -185,6 +186,63 @@ AUTHENTICATION_BACKENDS = (
|
|||
#
|
||||
#SOCIAL_AUTH_SUBDOMAIN = 'auth'
|
||||
|
||||
########
|
||||
# SAML Authentication
|
||||
#
|
||||
# For SAML authentication, you will need to configure the settings
|
||||
# below using information from your SAML Identity Provider, as
|
||||
# explained in:
|
||||
#
|
||||
# https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#saml
|
||||
#
|
||||
# You will need to modify these SAML settings:
|
||||
SOCIAL_AUTH_SAML_ORG_INFO = {
|
||||
"en-US": {
|
||||
"displayname": "Example Inc.",
|
||||
"name": "example",
|
||||
"url": "%s%s" % ('https://', EXTERNAL_HOST),
|
||||
}
|
||||
}
|
||||
SOCIAL_AUTH_SAML_ENABLED_IDPS = {
|
||||
# The fields are explained in detail here:
|
||||
# https://python-social-auth-docs.readthedocs.io/en/latest/backends/saml.html
|
||||
"idp_name": {
|
||||
# Configure entity_id and url according to information provided to you by your IdP:
|
||||
"entity_id": "https://idp.testshib.org/idp/shibboleth",
|
||||
"url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
|
||||
# The part below corresponds to what's likely referred to as something like
|
||||
# "Attribute Statements" (with Okta as your IdP) or "Attribute Mapping" (with G Suite).
|
||||
# The names on the right side need to correspond to the names under which
|
||||
# the IdP will send the user attributes. With these defaults, it's expected
|
||||
# that the user's email will be sent with the "email" attribute name,
|
||||
# the first name and the last name with the "first_name", "last_name" attribute names.
|
||||
"attr_user_permanent_id": "email",
|
||||
"attr_first_name": "first_name",
|
||||
"attr_last_name": "last_name",
|
||||
"attr_username": "email",
|
||||
"attr_email": "email",
|
||||
# The "x509cert" attribute is automatically read from
|
||||
# /etc/zulip/saml/idps/{idp_name}.crt; don't specify it here.
|
||||
}
|
||||
}
|
||||
|
||||
SOCIAL_AUTH_SAML_SECURITY_CONFIG = {
|
||||
# If you've set up the optional private and public server keys,
|
||||
# set this to True to enable signing of SAMLRequests using the
|
||||
# private key.
|
||||
"authnRequestsSigned": False,
|
||||
}
|
||||
|
||||
# These SAML settings you likely won't need to modify.
|
||||
SOCIAL_AUTH_SAML_SP_ENTITY_ID = 'https://' + EXTERNAL_HOST
|
||||
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = {
|
||||
"givenName": "Technical team",
|
||||
"emailAddress": ZULIP_ADMINISTRATOR,
|
||||
}
|
||||
SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {
|
||||
"givenName": "Support team",
|
||||
"emailAddress": ZULIP_ADMINISTRATOR,
|
||||
}
|
||||
|
||||
########
|
||||
# Azure Active Directory OAuth.
|
||||
|
@ -211,7 +269,6 @@ AUTHENTICATION_BACKENDS = (
|
|||
# SSO_APPEND_DOMAIN = "example.com")
|
||||
SSO_APPEND_DOMAIN = None # type: Optional[str]
|
||||
|
||||
|
||||
################
|
||||
# Miscellaneous settings.
|
||||
|
||||
|
|
|
@ -52,6 +52,13 @@ def get_config(section: str, key: str, default_value: Optional[Any]=None) -> Opt
|
|||
return config_file.get(section, key)
|
||||
return default_value
|
||||
|
||||
def get_from_file_if_exists(path: str) -> str:
|
||||
if os.path.exists(path):
|
||||
with open(path, "r") as f:
|
||||
return f.read()
|
||||
else:
|
||||
return ''
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = get_secret("secret_key")
|
||||
|
||||
|
@ -160,6 +167,14 @@ DEFAULT_SETTINGS = {
|
|||
'SOCIAL_AUTH_SUBDOMAIN': None,
|
||||
'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET': get_secret('azure_oauth2_secret'),
|
||||
'SOCIAL_AUTH_GOOGLE_KEY': get_secret('social_auth_google_key', development_only=True),
|
||||
# SAML:
|
||||
'SOCIAL_AUTH_SAML_SP_ENTITY_ID': None,
|
||||
'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT': '',
|
||||
'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY': '',
|
||||
'SOCIAL_AUTH_SAML_ORG_INFO': None,
|
||||
'SOCIAL_AUTH_SAML_TECHNICAL_CONTACT': None,
|
||||
'SOCIAL_AUTH_SAML_SUPPORT_CONTACT': None,
|
||||
'SOCIAL_AUTH_SAML_ENABLED_IDPS': {},
|
||||
# Historical name for SOCIAL_AUTH_GITHUB_KEY; still allowed in production.
|
||||
'GOOGLE_OAUTH2_CLIENT_ID': None,
|
||||
|
||||
|
@ -1355,6 +1370,26 @@ GOOGLE_OAUTH2_CLIENT_SECRET = get_secret('google_oauth2_client_secret')
|
|||
SOCIAL_AUTH_GOOGLE_KEY = SOCIAL_AUTH_GOOGLE_KEY or GOOGLE_OAUTH2_CLIENT_ID
|
||||
SOCIAL_AUTH_GOOGLE_SECRET = SOCIAL_AUTH_GOOGLE_SECRET or GOOGLE_OAUTH2_CLIENT_SECRET
|
||||
|
||||
if PRODUCTION:
|
||||
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = get_from_file_if_exists("/etc/zulip/saml/zulip-cert.crt")
|
||||
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = get_from_file_if_exists("/etc/zulip/saml/zulip-private-key.key")
|
||||
|
||||
for idp_name, idp_dict in SOCIAL_AUTH_SAML_ENABLED_IDPS.items():
|
||||
if DEVELOPMENT:
|
||||
idp_dict['entity_id'] = get_secret('saml_entity_id', '')
|
||||
idp_dict['url'] = get_secret('saml_url', '')
|
||||
idp_dict['x509cert_path'] = 'zproject/dev_saml.cert'
|
||||
|
||||
# Set `x509cert` if not specified already; also support an override path.
|
||||
if 'x509cert' in idp_dict:
|
||||
continue
|
||||
|
||||
if 'x509cert_path' in idp_dict:
|
||||
path = idp_dict['x509cert_path']
|
||||
else:
|
||||
path = "/etc/zulip/saml/idps/{}.crt".format(idp_name)
|
||||
idp_dict['x509cert'] = get_from_file_if_exists(path)
|
||||
|
||||
SOCIAL_AUTH_PIPELINE = [
|
||||
'social_core.pipeline.social_auth.social_details',
|
||||
'zproject.backends.social_auth_associate_user',
|
||||
|
|
|
@ -172,3 +172,38 @@ THUMBOR_SERVES_CAMO = True
|
|||
# Logging the emails while running the tests adds them
|
||||
# to /emails page.
|
||||
DEVELOPMENT_LOG_EMAILS = False
|
||||
|
||||
SOCIAL_AUTH_SAML_SP_ENTITY_ID = 'http://' + EXTERNAL_HOST
|
||||
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = get_from_file_if_exists("zerver/tests/fixtures/saml/zulip.crt")
|
||||
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = get_from_file_if_exists("zerver/tests/fixtures/saml/zulip.key")
|
||||
|
||||
SOCIAL_AUTH_SAML_ORG_INFO = {
|
||||
"en-US": {
|
||||
"name": "example",
|
||||
"displayname": "Example Inc.",
|
||||
"url": "%s%s" % ('http://', EXTERNAL_HOST),
|
||||
}
|
||||
}
|
||||
|
||||
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = {
|
||||
"givenName": "Tech Gal",
|
||||
"emailAddress": "technical@example.com"
|
||||
}
|
||||
|
||||
SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {
|
||||
"givenName": "Support Guy",
|
||||
"emailAddress": "support@example.com",
|
||||
}
|
||||
|
||||
SOCIAL_AUTH_SAML_ENABLED_IDPS = {
|
||||
"test_idp": {
|
||||
"entity_id": "https://idp.testshib.org/idp/shibboleth",
|
||||
"url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
|
||||
"x509cert": get_from_file_if_exists("zerver/tests/fixtures/saml/idp.crt"),
|
||||
"attr_user_permanent_id": "email",
|
||||
"attr_first_name": "first_name",
|
||||
"attr_last_name": "last_name",
|
||||
"attr_username": "email",
|
||||
"attr_email": "email",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -581,6 +581,9 @@ i18n_urls = [
|
|||
template_name='zerver/config_error.html',),
|
||||
{'dev_not_supported_error': True},
|
||||
name='dev_not_supported'),
|
||||
url(r'^config-error/saml$', TemplateView.as_view(
|
||||
template_name='zerver/config_error.html',),
|
||||
{'saml_error': True},),
|
||||
]
|
||||
|
||||
# Make a copy of i18n_urls so that they appear without prefix for english
|
||||
|
@ -709,6 +712,7 @@ urls += [
|
|||
|
||||
# Python Social Auth
|
||||
urls += [url(r'^', include('social_django.urls', namespace='social'))]
|
||||
urls += [url(r'^saml/metadata.xml$', zerver.views.auth.saml_sp_metadata)]
|
||||
|
||||
# User documentation site
|
||||
urls += [url(r'^help/(?P<article>.*)$',
|
||||
|
|
Loading…
Reference in New Issue