Django saml2 login missing session variables - django

For my Django application, I am trying to enable SSO using Djangosaml2 and following are the versions I am using
djangosaml2==1.2.0
pysaml2==7.0.0
djangorestframework==3.12.2
Django==3.1.7
python==3.8
My saml2_settings is as follows
from os import path
import saml2
import saml2.saml
from app.local_settings import SERVER_URL
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend',
)
SAML_SESSION_COOKIE_NAME = 'saml_session'
SAML_ATTRIBUTE_MAPPING = {
'username': ('username', ),
'email': ('email', ),
'first_name': ('first_name', ),
'last_name': ('last_name', ),
}
BASEDIR = path.dirname(path.abspath(__file__))
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
LOGIN_URL = '/saml2/login/'
LOGOUT_URL = '/saml2/logout/'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SAML_CREATE_UNKNOWN_USER = True
SAML_SERVER_URL = '10.23.1.114'
SAML_ENABLED = True
# MIDDLEWARE.append('djangosaml2.middleware.SamlSessionMiddleware')
SAML_CONFIG = {
# full path to the xmlsec1 binary programm
'xmlsec_binary': '/usr/bin/xmlsec1',
# your entity id, usually your subdomain plus the url to the metadata view
'entityid': path.join(SAML_SERVER_URL, 'saml2/metadata'),
# directory with attribute mapping
'attribute_map_dir': path.join(BASEDIR, 'attribute_maps'),
# this block states what services we provide
'service': {
# we are just a lonely SP
'sp' : {
'name': 'Dummy app',
'allow_unsolicited': True,
'authn_requests_signed': True,
'force_authn': True,
'want_response_signed': True,
'want_assertions_signed': True,
'logout_requests_signed': True,
'name_id_format_allow_create': False,
'endpoints': {
# url and binding to the assetion consumer service view
# do not change the binding or service name
'assertion_consumer_service': [
(path.join(SAML_SERVER_URL, 'saml2/acs/'),
saml2.BINDING_HTTP_POST),
],
# url and binding to the single logout service view
# do not change the binding or service name
'single_logout_service': [
(path.join(SAML_SERVER_URL, 'saml2/ls/'),
saml2.BINDING_HTTP_REDIRECT),
(path.join(SAML_SERVER_URL, 'saml2/ls/post/'),
saml2.BINDING_HTTP_POST),
],
},
},
},
# where the remote metadata is stored, local, remote or mdq server.
# One metadatastore or many ...
'metadata': {
'local': [path.join(BASEDIR, 'idp_metadata.xml')]
},
# Signing
'key_file': path.join(BASEDIR, 'samlkey.key'), # private part
'cert_file': path.join(BASEDIR, 'samlcert.pem'), # public part
# own metadata settings
'contact_person': [
{'given_name': '--',
'company': '--',
'email_address': '--',
'contact_type': '--'}
],
# you can set multilanguage information here
'organization': {
'name': [('--', 'en')],
'display_name': [('--', 'en')],
'url': [('--', 'en')],
},
"valid_for": 24
}
My middleware is as follows:
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.security.SecurityMiddleware',
'user_sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'app.middleware.APPMiddleware',
'djangosaml2.middleware.SamlSessionMiddleware'
]
With the above mentioned settings I am facing a couple of issues
After successfully authenticating from my SSO server by the time the request reaches my /login Url both the request.session and request.saml_session variables are getting reset and I am getting a complete new session ID. And also I am missing the saml_session attributes due to this issue. I did add a debug point in the djangosaml2 views and in there just before returning the response I could see the attributes present. But for some reason by the time the request reaches my app, its getting reset.
When I try to logout on saml2/logout, I am seeing the following error:
'NoneType' object has no attribute 'name_qualifier'
I cant seem to find what I am missing here. I have tried all I could think of but stuck. Any help is greatly appreciated. Thanks in advance.

I ended up doing the following two things, then it started working for me
Downgraded the djangosaml2 and pysaml version to 0.19.0 and 4.9.0 respectively.
For HTTPS connection, added SESSION_COOKIE_SECURE = True and for dev i.e. run server cases, SESSION_COOKIE_SECURE = False in your settings.py

I was migrating Django from 1.1 to 3.1. Your codebase actually fixed my issue. I added the SESSION_SERIALIZER in my saml/config.py:
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
And added SamlSessionMiddleware in settings.py:
djangosaml2.middleware.SamlSessionMiddleware
My issue was:
'WSGIRequest' object has no attribute 'saml session'

Related

Django doesn't create session cookie in cross-site json request

I want to make cross-site JavaScript call from third-party domain (in this case my desktop/localhost server) to my remote Django server hosted on my_domain.com/ and calling REST WS exposed on my_domain.com/msg/my_service with using session/cookies for storing session state.
But when I call this service (hosted on remote Django server) from my desktop browser or localhost Django server (JS is in index.html), Django doesn't create session cookie and on remote server are doesn't store session state. But when i call this service from Postman or from same localhost JS to localhost instance of same Django service it works and session is created.
My JS script in index.html which make call to WS send_message:
fetch('http://my_domain.com/ws/my_service', {
method:"POST",
credentials: 'include',
body:JSON.stringify(data)
})
.then(res => res.json())
.then(json => {
showResponse(json.message);
})
When I run this script from my desktop browser or my localhost server it runs correctly with cookies and sessions parameters.
Django my_service implementation view
#csrf_exempt
def my_service(request):
if request.method == "POST":
message_bstream= request.body
request.session.set_expiry(0)
message= json.loads(message_bstream)
out,sta=state_machine_response(message["message"],int(request.session["state"]))
request.session["state"] =sta
respo={"message":out}
response = HttpResponse(json.dumps(respo), content_type="application/json")
response.set_cookie(key='name', value='my_value', samesite='None', secure=True)
#return JsonResponse(respo, safe=False, status=200)
return response
else:
return HttpResponseNotFound ("Sorry this methode is not allowed")
Or I try generate response like
return JsonResponse(respo, safe=False, status=200)
My settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_WHITELIST = (
'http://localhost:8000',
)
CORS_ALLOWED_ORIGINS = [
'http://localhost:8000',
]
CSRF_TRUSTED_ORIGINS = [
'http://localhost:8000',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SAMESITE = 'None'
Please do you have any idea?
You can't save cookies from a third-party API call unless you use SameSite=None with the Secure option in the Set-Cookie header. You can achieve this for the sessionid and CSRF cookie with the following settings:
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SAMESITE = 'None'
In this case, you must use the HTTPS protocol scheme.
Another solution would be to use a proxy, which is helpful in a localhost development environment. This is an example using vue.js on the vue.config.js file:
const { defineConfig } = require('#vue/cli-service')
if (process.env.NODE_ENV == "development"){
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
proxy: {
"^/api/": {
target: process.env.VUE_APP_BASE_URL,
changeOrigin: true,
logLevel: "debug",
pathRewrite: { "^/api": "/api" }
}
}
},
})
}
Some useful doc https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure

How to troubleshoot a failing Django LDAP connection

I am using Django Rest with active directory authentication.
I have built a simple app with the following settings:
import ldap
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
# Baseline configuration.
AUTH_LDAP_SERVER_URI = 'ldap://ad_server.com'
AUTH_LDAP_BIND_DN = "CN=binduser,OU=Users,OU=ad_server,DC=ad_server,DC=com"
AUTH_LDAP_BIND_PASSWORD = "somepassword"
# Set up the basic group parameters.
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
"OU=Groups,OU=ad_server,DC=ad_server,DC=com",
ldap.SCOPE_SUBTREE,
"(objectClass=groupOfNames)",
)
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr="cn")
# Simple group restrictions
AUTH_LDAP_REQUIRE_GROUP = "CN=prod,OU=Groups,OU=ad_server,DC=ad_server,DC=com"
AUTH_LDAP_DENY_GROUP = "cn=disabled,ou=django,ou=groups,dc=example,dc=com"
# Populate the Django user from the LDAP directory.
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
'username': 'cn',
"email": "mail",
}
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
"is_active": "CN=prod,OU=Groups,OU=ad_server,DC=ad_server,DC=com",
"is_staff": "CN=prod,OU=Groups,OU=ad_server,DC=ad_server,DC=com",
"is_superuser": "CN=prod,OU=Groups,OU=ad_server,DC=ad_server,DC=com",
}
# This is the default, but I like to be explicit.
AUTH_LDAP_ALWAYS_UPDATE_USER = True
# Use LDAP group membership to calculate group permissions.
AUTH_LDAP_FIND_GROUP_PERMS = True
# Cache distinguished names and group memberships for an hour to minimize
# LDAP traffic.
AUTH_LDAP_CACHE_TIMEOUT = 3600
# Keep ModelBackend around for per-user permissions and maybe a local
# superuser.
AUTHENTICATION_BACKENDS = (
"django_auth_ldap.backend.LDAPBackend",
"django.contrib.auth.backends.ModelBackend",
)
Running an LDAP search from the cli works just fine
However when I try to authenticate with the http://localhost:8000/admin/ I get the following error.
The terminal on the other hand doesn't show anything:
There must be a better way to troubleshoot this, I have been at it all day and I can't figure it out.
add this in settings.py:
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"loggers": {"django_auth_ldap": {"level": "DEBUG", "handlers": ["console"]}},
}
logging

SAML2 with Django stuck on infinite loop

I am trying to use the djangosaml2 1.1.0 module in order to implement SSO login.
After the successful login, it gets stuck in an infinite loop trying to constantly login again.
I have tried to remove the #login_requried decorator, it works on then.
But, I need the #login_required decorator in order to prevent not logged in users to access specific views.
My believe is that django.contrib.auth.backends.ModelBackend is not properly configured with djangosaml2.backends.Saml2Backend
This is my code:
settings.py
SAML_CONFIG = {
# full path to the xmlsec1 binary programm
'xmlsec_binary': '/usr/bin/xmlsec1',
# your entity id, usually your subdomain plus the url to the metadata view
'entityid': 'http://localhost:8000/saml2/metadata/',
# directory with attribute mapping
'attribute_map_dir': path.join(BASEDIR, 'attribute-maps'),
# this block states what services we provide
'service': {
# we are just a lonely SP
'sp' : {
'name': 'Federated Django sample SP',
'name_id_format': saml2.saml.NAMEID_FORMAT_PERSISTENT,
# For Okta add signed logout requets. Enable this:
# "logout_requests_signed": True,
'endpoints': {
# url and binding to the assetion consumer service view
# do not change the binding or service name
'assertion_consumer_service': [
('http://localhost:8000/tacdb/items/',
saml2.BINDING_HTTP_POST),
],
# url and binding to the single logout service view
# do not change the binding or service name
'single_logout_service': [
# Disable next two lines for HTTP_REDIRECT for IDP's that only support HTTP_POST. Ex. Okta:
('http://localhost:8000/saml2/ls/',
saml2.BINDING_HTTP_REDIRECT),
('http://localhost:8000/saml2/ls/post',
saml2.BINDING_HTTP_POST),
],
},
# Mandates that the identity provider MUST authenticate the
# presenter directly rather than rely on a previous security context.
'force_authn': False,
# Enable AllowCreate in NameIDPolicy.
'name_id_format_allow_create': False,
# attributes that this project need to identify a user
'required_attributes': ['username'],
# attributes that may be useful to have but not required
'optional_attributes': ['eduPersonAffiliation'],
# in this section the list of IdPs we talk to are defined
# This is not mandatory! All the IdP available in the metadata will be considered.
'idp': {
# we do not need a WAYF service since there is
# only an IdP defined here. This IdP should be
# present in our metadata
# the keys of this dictionary are entity ids
'https://localhost/simplesaml/saml2/idp/metadata.php': {
'single_sign_on_service': {
saml2.BINDING_HTTP_REDIRECT: 'https://localhost/simplesaml/saml2/idp/SSOService.php',
},
'single_logout_service': {
saml2.BINDING_HTTP_REDIRECT: 'https://localhost/simplesaml/saml2/idp/SingleLogoutService.php',
},
},
},
},
},
LOGIN_URL = '/tacdb/saml2/login/'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
MIDDLEWARE.append('djangosaml2.middleware.SamlSessionMiddleware')
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend',
)
views.py
#csrf_exempt
#login_required
def item_list(request):
....
return render(
request, "items/item_list.html", {"filter": user_filter, "form": form}
)
urls.py
urlpatterns = [
url(
r"^tacdb/",
include(
[
path('', include('tacdashboard.urls')),
path('saml2/', include('djangosaml2.urls')),
]
)
)
]
The problem here was that I did not use the correct 'assertion_consumer_service':
'assertion_consumer_service': [
('http://localhost:8000/tacdb/saml2/acs',
saml2.BINDING_HTTP_POST),
],

Django DRF Getting Missing Auth Headers with JWT, but With Permission and Auth on ReadOnly Views

I'm working with both the latest Django 3.0.2 and DRF 3.11.0 using djangorestframework-simplejwt 4.4.0. I did not have this problem before on a previous Django 2.2.0 project (and older lib versions), but I cannot figure out why I cannot access a view with readonly permissions set and default authentication looks okay in settings. For the simplejwt and firebaseauth library, I'm using the default settings (with correct credentials for firebaseauth, verified it works fine).
If I specify the authentication_classes as empty in the view, then it works fine, but this is just an example. I expected the defaults to work, from settings: unless I'm incorrect and should be explicitly set this way. I have also tried using gunicorn and waitress-serve, and it's the same result.
Here is the view:
class NoteList(generics.ListAPIView):
"""
List all Notes. Not used
"""
# authentication_classes = []
permission_classes = (permissions.AllowAny,)
serializer_class = NoteSerializer
def get_queryset(self):
notes = Note.objects.all()
return notes
Here is my settings:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
REST_FRAMEWORK = {
# When you enable API versioning, the request.version attribute will contain a string
# that corresponds to the version requested in the incoming client request.
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
# Authentication settings
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
'drf_firebase_auth.authentication.FirebaseAuthentication',
],
# Permission settings
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly',
'rest_framework.permissions.IsAuthenticated',
'rest_framework.permissions.IsAdminUser',
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
# For unit test cases
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
# Pagination
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 50,
}
If ordering the above matters, what is the correct order to have them in?
Urls, if it matters:
urlpatterns = [
path('anotes/', NoteList.as_view(), name="lame-notes"),
]
Now, when I visit the URL anonymously, I always get:
GET /v1/anotes/
HTTP 401 Unauthorized
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
WWW-Authenticate: Bearer realm="api"
{
"detail": "Invalid Authorization header format, expecting: JWT <token>."
I've been stuck on this issue for a while and could not find any similar problems to solve it. On my older Django project, I don't get this 401 Unauthorized and am able to have ReadOnly work correctly.
The view or endpoint only works correctly if I'm logged in.
If I'm missing any information please let me know.
Permissions in REST framework are always defined as a list of permission classes.
Before running the main body of the view each permission in the list is checked. If any permission check fails an exceptions.PermissionDenied or exceptions.NotAuthenticated exception will be raised, and the main body of the view will not run.
So an anonymous user can check the first item in your permission list, but fails at second. You should decide what will your default behavior will be and keep that in the list.

Django: SocialApp matching query does not exist

I'm using django-allauth, and I configured localhost:9000/admin/ with the following details:
socialapp .
provider:
Name:
Client id:
App ID, or consumer key
Key:
Secret: etc .
I set SITE_ID = 2 (because I changed the default site example.com to localhost:9000)
In settings.py:
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
'uni_form',
'allauth',
'allauth.account',
'allauth.socialaccount',
'bootstrapform',
# 'allauth.socialaccount.providers.twitter',
# 'allauth.socialaccount.providers.openid',
'allauth.socialaccount.providers.facebook',
)
SOCIALACCOUNT_PROVIDERS = \
{ 'facebook':
{ 'SCOPE': ['email', 'publish_stream'],
'AUTH_PARAMS': { 'auth_type': 'reauthenticate' },
'METHOD': 'oauth2' ,
'LOCALE_FUNC': 'path.to.callable'} }
When I go to:
http://localhost:9000/accounts/facebook/login
I get:
Error : `SocialApp matching query does not exist.
What am I doing wrong?
Using the Django admin you need to create a SocialApp listing your Facebook app credentials. Make sure that this app is attached to the proper site (as in, django.contrib.sites.models.Site).
In your case, there needs to be a django.contrib.sites.models.Site instance with id=2 (check the sites admin) that is listed as a site for the SocialApp.
If either the SocialApp is missing, or if it is created but not attached to a site matching your settings.SITE_ID, then allauth does not know what app to pick, resulting in the error message you listed above.
In my case, on the Add social application page, I forgot to select my site as "Chosen sites" 🤦🏼‍♂️
See screenshot below (on the bottom right is the Chosen Sites list):
For me, it showed this error when I had in settings.py:
SITE_ID = 1
It worked when I changed it to 2:
SITE_ID = 2
Step 1: Go to your admin page.
Find social application
Step 2: now add the lines like the following image.
Give your client id and secret key given by google api and move available site (example.com) to chosen sites
Step 3: Now go to sites in the side navigation of admin page and customize like the following image and save it
Step 4: now, add, SITE_ID = 1 in settings.py file.
As an alternative to putting that into the admin, you can also put it in your settings like so: (At least in 2022 with version django-allauth==0.45.0
SOCIALACCOUNT_PROVIDERS = {
'discord': {
# From https://developer.twitter.com
'APP': {
'client_id': os.environ['TWITTER_API_KEY'],
'secret': os.environ['TWITTER_API_SECRET'],
'key': os.environ['TWITTER_APP_ID'],
}
},
'twitter': {
# From https://developer.twitter.com
'APP': {
'client_id': os.environ['TWITTER_API_KEY'],
'secret': os.environ['TWITTER_API_SECRET'],
'key': os.environ['TWITTER_APP_ID'],
}
},
}
Documentation is at: https://django-allauth.readthedocs.io/en/latest/installation.html and https://django-allauth.readthedocs.io/en/latest/configuration.html
It is possible that Django created an 'example.com' site for you already (if it's not a new project). So if this is the case you will need to delete that entry from the Sites admin page AND change the SITE_ID in settings.py to be the correct ID (probably 2 rather than 1).
Consider playing around with the SITE_ID value. For example: SITE_ID = 3, etc.
FWIW, if configuration via the DB is giving you problems, note that you can also use configuration in settings:
SOCIALACCOUNT_PROVIDERS = {
"google": {
"APP": {
"client_id": os.environ[("GOOGLE_OAUTH_CLIENT_ID"],
"secret": os.environ[("GOOGLE_OAUTH_SECRET"],
},
},
}
#Penners