grafana/django-saml2-auth

No username or email provided.

santigandolfo opened this issue · 9 comments

Hi, Im tryng to create a POC to try to use SAML to authenticate users in a Django project, but I'm blocked after receiving a SAML Response.

This is my current SAML2_AUTH:

SAML2_AUTH = {
    'DEBUG': True,
    'METADATA_AUTO_CONF_URL': 'https://redacted.com/Gsuite/GoogleIDPMetadata.xml',
    'ASSERTION_URL': 'https://127.0.0.1:8000',
    'ENTITY_ID': 'https://127.0.0.1:8000/saml2_auth/acs/',
    'DEFAULT_NEXT_URL': '/',
    'USE_JWT': True,
    'FRONTEND_URL': 'http://127.0.0.1:5173/',
    'AUTHN_REQUESTS_SIGNED': False,  # Require each authentication request to be signed
    'LOGOUT_REQUESTS_SIGNED': False,  # Require each logout request to be signed
    'WANT_ASSERTIONS_SIGNED': False,  # Require each assertion to be signed
    'WANT_RESPONSE_SIGNED': False,  # Require response to be signed
    'TOKEN_REQUIRED': False,
    'NAME_ID_FORMAT': '<urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress>'
}

And this is the response I'm getting:

<?xml 
version="1.0" 
encoding="UTF-8" 
standalone="no"?>
<saml2p:Response 
    xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" 
    Destination="https://127.0.0.1:8000/saml2_auth/acs/" 
    ID="_2392de0148b141c290591d8e9e279eeb" 
    InResponseTo="id-6E7o8vsnlmwqeuJtd" 
    IssueInstant="2023-07-03T18:40:35.226Z" 
    Version="2.0">
    <saml2:Issuer 
        xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://accounts.google.com/o/saml2?
        idpid=C00xoo4uv
    </saml2:Issuer>
    <saml2p:Status>
        <saml2p:StatusCode 
            Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
    </saml2p:Status>
    <saml2:Assertion 
        xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" 
        ID="_509f9b945285e09236ad9555ab4d8fd1" 
        IssueInstant="2023-07-03T18:40:35.226Z" 
        Version="2.0">
        <saml2:Issuer>https://accounts.google.com/o/saml2?
            idpid=C00xoo4uv
        </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="#_509f9b945285e09236ad9555ab4d8fd1">
                    <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#"/>
                    </ds:Transforms>
                    <ds:DigestMethod 
                        Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
                    <ds:DigestValue>ncYSkItwHYfojUZrTOhKovFq9XWdyeELoLexeQkE6/
                        Y=
                    </ds:DigestValue>
                </ds:Reference>
            </ds:SignedInfo>
            <ds:SignatureValue>dDPiJ+fMd/cZVQ47MBcKwvmxfpvpCtXg4r/bkk796HJ129aQKZ+
jMJlDlC/h4isX3jK1KSaqFusSiTG16z4aX9aiuRjsnP2Uw1sseoxljLwuA59NpXI7LZStnSdvWt1
2Xk9+YFQRi/
                mk0aA0CuwAbqBWHv8kMbg3jEzlQ==
            </ds:SignatureValue>
            <ds:KeyInfo>
                <ds:X509Data>
                    <ds:X509SubjectName>
                        ST=California,
                        C=US,
                        OU=Google For Work,
                        CN=Google,
                        L=Mountain View,
                        O=Google Inc.
                    </ds:X509SubjectName>
                    <ds:X509Certificate>MIIDdDCCAlygAwIBAgIGAWqx7+csMA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ
bmMuMRYwFAYDVQQHEBIoEaOuX5YZVj0T8l8sSwlmDfY3NMKwK5PtpimHlklVAoy249d
tJE9gy6snxCvrRcJ7IQS0VhBM7X3aC6ZCAxibs7I9XRVUcZsbgqGpzR/NTSwTVCxLrBWjHvTH5
7ZBte+rQdbnJWBiZLaV+fVphyjOo7GPuPwmsMGzvNoYs</ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </ds:Signature>
        <saml2:Subject>
            <saml2:NameID 
                Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">santiago.gandolfo@redacted.com
            </saml2:NameID>
            <saml2:SubjectConfirmation 
                Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <saml2:SubjectConfirmationData 
                    InResponseTo="id-6E7o8vsnlmwqeuJtd" 
                    NotOnOrAfter="2023-07-03T18:45:35.226Z" 
                    Recipient="https://127.0.0.1:8000/saml2_auth/acs/"/>
            </saml2:SubjectConfirmation>
        </saml2:Subject>
        <saml2:Conditions 
            NotBefore="2023-07-03T18:35:35.226Z" 
            NotOnOrAfter="2023-07-03T18:45:35.226Z">
            <saml2:AudienceRestriction>
                <saml2:Audience>https://127.0.0.1:8000/saml2_auth/acs/</saml2:Audience>
            </saml2:AudienceRestriction>
        </saml2:Conditions>
        <saml2:AttributeStatement>
            <saml2:Attribute 
                Name="Groups">
                <saml2:AttributeValue 
                    xmlns:xs="http://www.w3.org/2001/XMLSchema" 
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                    xsi:type="xs:anyType">app_stats_access_full_test
                </saml2:AttributeValue>
            </saml2:Attribute>
        </saml2:AttributeStatement>
        <saml2:AuthnStatement 
            AuthnInstant="2023-07-03T14:02:48.000Z" 
            SessionIndex="_509f9b945285e09236ad9555ab4d8fd1">
            <saml2:AuthnContext>
                <saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef>
            </saml2:AuthnContext>
        </saml2:AuthnStatement>
    </saml2:Assertion>
</saml2p:Response>

My problem is that when I get redirected back to my app, the screen says:

Sorry, you are not allowed to access this app
To report a problem with your access please contact your system administrator

Error code: 1114

Reason: Username or email must be configured on the SAML app before logging in.

And the Django logs say:

 No username or email provided.

What am I doing wrong? I'm following this article (https://medium.com/cogito-engineering/enabling-sso-for-your-react-and-django-app-with-saml-2-0-754ef752acc1). The only difference in my case is that I'm using Google instead of Okta.

Hey @santigandolfo,

There are a few things I noticed and worked on:

  1. You must set ATTRIBUTES_MAP, otherwise the field mapping from IdP field names to Django user_model's fields won't work.
  2. Debugging is not working as expected in this library, so I am trying to fix it:
  3. I added a small guide to the README in the new PR that shows how to debug the request/response between IdP and Django.
  4. I haven't tested this library with Google SAML SSO, but would be happy to see if you can make it happen.
  5. I redacted sensitive information you posted in the original post and deleted the history. Next time, please remove any PII and sensitive information from your SAML assertion, configs, code snippets, logs and any other pieces of information.

Hi @mostafa, thank you for the help and for the redaction!
I've not touched the django User model, how should the mapping look like? I've tried all this alternatives for the email field, but nothing worked:

  •    'email': 'user.email'
    
  •    'email': 'email'
    
  •    'email': 'emailAddress'
    
  •    'email': 'emailAddress'
    
  • 'email': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress',

Looking at my saml Response, would you know how the ATTRIBUTES_MAP should look like? (I don't know how I could tell it that the email is the NameID or if that field should also be coming from in the AttributeStatement).

Finally, is there a way for it to not try to search/store the user in a database? (I'd prefer if I didn't have to store the user's information)

@santigandolfo
The attribute names from your IdP can be seen in the AttributeStatement. So this is how it should be constructed:

{
    "email field name from AttributeStatement": "email field name on the user model in Django",
    "other fields from AttributeStatement":  "other fields on the user model in Django"
}

Note that namespaces prepended to field names in the AttributeStatement are not supported at the moment.

What do you mean by not storing/searching the user info in a database?

This library assumes that you already have a user table/model and you want to map existing users. Otherwise, you might want to set CREATE_USER to True and set TRIGGERS.CREATE_USER to the custom user creation function you have. This only matters if you don't have an existing user to match against.

Hi @mostafa
In my case the AttributeStatement looks like this:

        <saml2:AttributeStatement>
            <saml2:Attribute 
                Name="Groups">
                <saml2:AttributeValue 
                    xmlns:xs="http://www.w3.org/2001/XMLSchema" 
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                    xsi:type="xs:anyType">app_stats_access_full_test
                </saml2:AttributeValue>
            </saml2:Attribute>
        </saml2:AttributeStatement>

So I don't have the email there. In my case it is in the Subject:

        <saml2:Subject>
            <saml2:NameID 
                Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">santiago.gandolfo@redacted.com
            </saml2:NameID>
            <saml2:SubjectConfirmation 
                Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <saml2:SubjectConfirmationData 
                    InResponseTo="id-6E7o8vsnlmwqeuJtd" 
                    NotOnOrAfter="2023-07-03T18:45:35.226Z" 
                    Recipient="https://127.0.0.1:8000/saml2_auth/acs/"/>
            </saml2:SubjectConfirmation>
        </saml2:Subject>

In order to do the mapping it can only be in the AttributeStatement?


In this POC I don't mind having the database create and search users in the Django database, but in real life I'd prefer to only have to manage users from the Google Console and Django to be a middleman to manage authorization on my React app (using drf-jwt). This could be solved using the triggers, right?
In any case, if I am forced to have the users in the database it wouldn't be that big of a problem, but what I would really like is to at least be able to make it work and not have my current `No username or email provided' proble,

You need to tell Google which fields (and their values) you want to send in the attribute statement, so Google sends them to your Django app, otherwise they won't show up in your attribute statement by default. I could find this guide that shows how to do that.

You are considered a service provider in this case, so you should have some notion of the user for auth and access control, so I suppose it is not possible, at least not with the help of this library, to just do the authentication.

You can use JWT for your React app. See all the config options starting with JWT_* and how to change signing algorithm of your JWT token or use custom token triggers.

I @mostafa thank you so much for your help. I talked to our security team and them made some changes on the Google side and now the AttributeStatement is coming like this and we've managed to complete the login flow:

        <saml2:AttributeStatement>
            <saml2:Attribute 
                Name="user.username">
                <saml2:AttributeValue 
                    xmlns:xs="http://www.w3.org/2001/XMLSchema" 
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                    xsi:type="xs:anyType">santiago.gandolfo@redacted.com
                </saml2:AttributeValue>
            </saml2:Attribute>
            <saml2:Attribute 
                Name="user.email">
                <saml2:AttributeValue 
                    xmlns:xs="http://www.w3.org/2001/XMLSchema" 
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                    xsi:type="xs:anyType">santiago.gandolfo@redacted.com
                </saml2:AttributeValue>
            </saml2:Attribute>
            <saml2:Attribute 
                Name="user.first_name">
                <saml2:AttributeValue 
                    xmlns:xs="http://www.w3.org/2001/XMLSchema" 
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                    xsi:type="xs:anyType">Santiago
                </saml2:AttributeValue>
            </saml2:Attribute>
            <saml2:Attribute 
                Name="user.last_name">
                <saml2:AttributeValue 
                    xmlns:xs="http://www.w3.org/2001/XMLSchema" 
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                    xsi:type="xs:anyType">Gandolfo
                </saml2:AttributeValue>
            </saml2:Attribute>
            <saml2:Attribute 
                Name="Groups">
                <saml2:AttributeValue 
                    xmlns:xs="http://www.w3.org/2001/XMLSchema" 
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                    xsi:type="xs:anyType">app_stats_access_full_test
                </saml2:AttributeValue>
            </saml2:Attribute>
        </saml2:AttributeStatement>

Now, the only "problem" that we are having is that the email field is not being saved correctly. We've tried using the default Django User model and this custom one:

from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
    """ Use a custom user model to avoid requiring a username and just use email """
    username = models.EmailField(unique=True)
    email = models.EmailField(unique=True)
    first_name = models.CharField(max_length=60)
    last_name = models.CharField(max_length=60)

But still the email field has nothing for the user in the database.

This is our configured attributes map:

    'ATTRIBUTES_MAP': {
        'email': 'user.email',
        'first_name': 'user.first_name',
        'last_name': 'user.last_name',
        'groups': 'Groups',
    },

Do you know what could be happening?

@santigandolfo
Happy to hear that you managed to make it work. For this "problem" of yours, I recommend using the TRIGGER.BEFORE_LOGIN hook function to set up the user in your system. It receives a user instance, which is a flattened attribute statement. Then you can update all the fields on your user model.

  1. Does the user exist in your platform before login?
  2. Are you using IdP- or SP-initiated login?

Hi @mostafa,
The user doesn't exist in my database before the login and the results are the same for both IdP- and SP-initiated login.
Could it be that if there is both a username and an email in the SAML response, only the username is set?

It all depends on your Django settings. AFAIK, you need to use a custom user model to map username and email correctly.