SAML2 with Django stuck on infinite loop - django

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),
],

Related

How to add password and username pop up for Django Swagger?

I am using drf-yasg library for the Django Swagger. I need to add the authentication on username and password level. There are three security schemes available in this library "basic", "apiKey" or "oauth2".
Is there any way I can set my credentials for swagger in my django project settings and authenticate the swagger apidocs based on that?
To have the popup for authentication in 'DRF' and also in 'SWAGGER' panel, simply add these lines of code which I arrowed to your settings.py:
'DRF' implementation
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
# the link you can read about
# https://stackoverflow.com/questions/51906745/django-rest-framework-logout-not-working-after-token-authentication
'rest_framework.authentication.BasicAuthentication', # <<--
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
}
In REST_FRAMEWORK, inside the DEFAULT_AUTHENTICATION_CLASSES, (which is a list) add the
rest_framework.authentication.BasicAuthentication.
It tells the djagno to authenticate using the default authentication that djagno provides.
'SWAGGER' implementation
If you want to use it in 'SWAGGER' as well, do the below:
In SWAGGER_SETTINGS, inside the SECURITY_DEFINITIONS which is a dict, add these lines of code to implement that:
'basic': {
'type': 'basic'
},
Default 'swagger' settings would be like this:
SWAGGER_SETTINGS = {
'DOC_EXPANSION': 'list',
'APIS_SORTER': 'alpha',
'USE_SESSION_AUTH': False,
'SECURITY_DEFINITIONS': {
'Bearer': { # <<-- is for JWT access token
'type': 'apiKey',
'name': 'Authorization',
'in': 'header'
},
'basic': { # <<-- is for djagno authentication
'type': 'basic'
},
},
}
Attention that Bearer is for JWT access token. basic is for djagno authentication.
Thant you for reading!

AllowAny CreateAPIView returns 401 for unauthenticated users

I use DRF with djangorestframework-simplejwt package. In my settings.py:
INSTALLED_APPS = [
...
'rest_framework',
...
]
...
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTTokenUserAuthentication',
),
}
SWAGGER_SETTINGS = {
'SECURITY_DEFINITIONS': {
'Bearer': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header',
'description': 'E.g. \'Bearer jwt.token.here\''
}
}
}
And in my apps' views.py:
...
class PublicCreateView(generics.CreateAPIView):
"""
Should be available for unauthenticated users
"""
serializer_class = PublicThingSerializer
def post(self, request, *args, **kwargs):
return Response("It works!", 200)
...
Yet for some reason this view returns 401 response for unauthenticated users. I tried a lot of things, the best I got was noticing that when I remove the REST_FRAMEWORK config from my settings.py completely, the response code changes to 403 forbidden. Any ideas?
As MojixCoder mentinoned in a comment, cleaning the cookies might have solved the issue but this time, after a few hours of debugging it turned out that my problem was actually related to urls.py which declared (simplified):
from . import views
urlpatterns = [
path("something/", views.SomethingViewSet()),
...
path("something/more/", views.PublicCreateView.as_view())
]
And the issue was that while routing a request to url /something/more/ Django actually used the first matching rule (perfectly understandable and expected behavior) which had rest_framework.permissions.IsAuthenticated set in permission_classes. This behavior is described in Django documentation on URL dispatcher under How Django processes a request section, point 3:
Django runs through each URL pattern, in order, and stops at the first one that matches the requested URL, matching against path_info.
Hope it saves someone's time. Since the API only returned a generic 401 answer that was surprisingly hard to figure out.

Django saml2 login missing session variables

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'

Django, mozilla-django-oidc and admin

i am trying to connect Okta with a custom Django (v.3.0.2) app i am coding, using the mozilla-django-oidc library. So far the initial user authentication and account creation (using Django's user model) works, but i don't understand what i need to do to have the Django AdminSite work.
The Adminsite, before introducing mozilla-django-oidc worked as expected. I created an admin user, named "admin" and the user was able to login.
To integrate the mozilla-django-oidc library i followed the instructions here: https://mozilla-django-oidc.readthedocs.io/en/stable/installation.html. The instructions do not have any specific mention of the AdminSite.
When i access the AdminSite after the library integration, i have the following:
The AdminSite uses the default template - my assumption was that it
would also use Okta to authenticate.
The admin account "admin" that used to be able to login into the AdminSite does not work anymore
My goal is to be able to access the AdminSite. I don't mind if it will be over Okta or over the vanilla interface as long as i can access it.
Below are the relevant segments from the files (in order to integrate):
urls.py
urlpatterns = [
path('', static_site.site_index, name='site_index'),
path('admin/', admin.site.urls),
path('review/', include('review.urls')),
path('oidc/', include('mozilla_django_oidc.urls')),
]
settings.py
# OICD
AUTHENTICATION_BACKENDS = (
'mozilla_django_oidc.auth.OIDCAuthenticationBackend',
)
OIDC_RP_CLIENT_ID = 'xxxxx'
OIDC_RP_CLIENT_SECRET = 'xxxx'
OIDC_RP_SIGN_ALGO = 'RS256'
OIDC_OP_JWKS_ENDPOINT = 'https://dev-xxx.okta.com/oauth2/default/v1/keys'
OIDC_RP_SCOPES = 'openid email profile'
OIDC_OP_AUTHORIZATION_ENDPOINT = 'https://dev-xxx.okta.com/oauth2/default/v1/authorize'
OIDC_OP_TOKEN_ENDPOINT = 'https://dev-xxx.okta.com/oauth2/default/v1/token'
OIDC_OP_USER_ENDPOINT = 'https://dev-xxx.okta.com/oauth2/default/v1/userinfo'
# Provided by mozilla-django-oidc
LOGIN_URL = reverse_lazy('oidc_authentication_callback')
# App urls
LOGIN_REDIRECT_URL = reverse_lazy('review:dashboard')
LOGOUT_REDIRECT_URL = reverse_lazy('site_index')
Any ideas or pointers welcomed!
The goal was achieved by adding the default auth backend to the settings:
settings.py
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'mozilla_django_oidc.auth.OIDCAuthenticationBackend',
]
I don't get Okta auth for the admin, but since i am happy just to have the admin running, i will stop here.
I've come up with a solution for using the mozilla-django-oidc login with the django admin. It's a little hacky but it's a lot less intimidating to redirect the admin login page than to override AdminSite.
In my top-level urls.py I have
class CustomLogin(View):
def get(self, request, **kwargs):
return HttpResponseRedirect(
reverse('oidc_authentication_init') + (
'?next={}'.format(request.GET['next']) if 'next' in request.GET else ''
)
)
urlpatterns = [
path('oidc/', include("mozilla_django_oidc.urls")),
path('admin/login/', CustomLogin.as_view()),
path('admin/', admin.site.urls),
# the rest of my urls...
]
If you don't care about passing the ?next= value correctly you can skip the CustomLogin class and do the following instead
urlpatterns = [
path('oidc/', include("mozilla_django_oidc.urls")),
]
# This only works if you break up urlpatterns so the reverse below can find what it needs
urlpatterns += [
path('admin/login/', RedirectView.as_view(
url=reverse('oidc_authentication_init') + ?next=/admin/,
permanent=False
)),
path('admin/', admin.site.urls),
# the rest of my urls...
]
I added ?next=/admin/ because by default once you log in you will be redirected to settings.LOGIN_REDIRECT_URL which I'm already using for something else
If you're using the default primary identifier, "email", you can create a superuser with that same email which will give SU privileges to that SSO user. So for example, if you have an SSOuser with email testuser#example.com, you can then run python manage.py createsuperuser and when prompted, set the email to testuser#example.com; the username and password don't matter since you're not actually using them for authentication (if you remove 'django.contrib.auth.backends.ModelBackend' from AUTHENTICATION_BACKENDS). I currently have this working, although I am extending the mozilla backend with the steps recommended in https://mozilla-django-oidc.readthedocs.io/en/stable/installation.html#connecting-oidc-user-identities-to-django-users to prevent users from being created on the fly.

Django REST Swagger with OAuth2 authentication

I have a REST API created with Django rest framework. To authenticate my API calls I am using OAuth2 tokens. My question is how can I enable standard username/password authentication in docs generated by Django rest swagger.
Right now i am gettings
401 : {"detail":"Authentication credentials were not provided."} http://127.0.0.1:8000/docs/?format=openapi
settings
REST_FRAMEWORK = {
# Don't perform any authentication on API calls so we don't have any CSRF problems
# :PRODUCTION: Put back authentication for production version when not testing on same server?
'DEFAULT_AUTHENTICATION_CLASSES': [
'oauth2_provider.ext.rest_framework.OAuth2Authentication',
'rest_framework_social_oauth2.authentication.SocialAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'PAGE_SIZE': 1000, # Max number of results returned from a list API call
'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',),
# Use JSONRender so the Web API interface is not shown. This is needed when testing the app on the same server
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
)
}
SWAGGER_SETTINGS = {
'SECURITY_DEFINITIONS': {
'veeu': {
'type': 'oauth2',
'flow': 'password',
'tokenUrl': 'http://localhost:8000/auth/token/',
'scopes': {
'write:all': 'Write all',
'read:all': 'Read all',
}
}
},
}
LOGIN_URL = 'http://localhost:8000/admin/'
When I click Django login it takes me to admin login page. And after I log in, this message is still the same. If I add header Authorization: Bearer TokenHere it works. However, the point is to enable username/password login.
To access Swagger documentation, you need SessionAuth with the following in the settings.py :
# API VERSIONING
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated'
],
}
This allows you to access Swagger generated documentation. Problem is, whatever endpoint you have protected with OAuth2 won't be visible via Swagger, at least if you are generating OAuth via "application". The following code does not work at all and I'm linking the thread discussed asking for anyone to work on that feature:
# TODO Swagger implementation is not working for password since
# it sends client_id and client_secret as query strings and not as
# user separated with "::"
# The "application" flow setting also that does work
#
SWAGGER_SETTINGS = {
'SUPPORTED_SUBMIT_METHODS': [], # Due to bug described above
'SECURITY_DEFINITIONS': {
"customers_auth": {
"type": "oauth2",
"tokenUrl": "/o/token/",
"flow": "password",
"scopes": {
"read": "Read scope",
"write": "Write scope"
}
}
},
}