Django, mozilla-django-oidc and admin - django

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.

Related

Django - URL problem for user profile view and edit profile view

In my Django project, specifically in my accounts app, I have 2 simple views, one allows users to view their profile (or other users' profiles), and the other one allows users to edit their own profile.
My account.urls looks like this
from django.urls import path
from accounts.views import ProfileView, EditProfileView
app_name = 'accounts'
urlpatterns = [
path('<slug:username>/', ProfileView.as_view(), name='profile'),
path('edit/', EditProfileView.as_view(), name='edit'),
]
Everything works great, but if an user creates an accounts with the word edit as their username, they will NEVER be able to edit their profile, since the first URL pattern will always be matched.
What's the best way of solving this problem in Django?
I know I could change my URLs to something like:
urlpatterns = [
path('<slug:username>/', ProfileView.as_view(), name='profile'),
path('<slug:username>/edit/', EditProfileView.as_view(), name='edit'),
]
But I prefer to NOT edit my URLs. I was wondering if there is another solution I don't know.

why does <myproject>/accounts/profile/ show the <myproject>/profile/ page

Using django-allauth, after a successful login a user is redirected to http://<myproject>/accounts/profile/... However this URL doesn't exists, but yet it still successfully shows view from http://<myproject>/profile/
settings.py
urlpatterns = [
path('', include('pages.urls')),
path('admin/', admin.site.urls),
url(r'^accounts/', include('allauth.urls')),
url('album/', include('albums.urls')),
url('profile/', include('user_profile.urls')),
]
user_profile\urls.py
urlpatterns = [
path('', views.profile, name='profile'),
]
using show_urls I don't see anything for /accounts/* which would call the view.profile view
/accounts/confirm-email/ allauth.account.views.EmailVerificationSentView account_email_verification_sent
/accounts/confirm-email/<key>/ allauth.account.views.ConfirmEmailView account_confirm_email
/accounts/email/ allauth.account.views.EmailView account_email
/accounts/inactive/ allauth.account.views.AccountInactiveView account_inactive
/accounts/login/ allauth.account.views.LoginView account_login
/accounts/logout/ allauth.account.views.LogoutView account_logout
/accounts/password/change/ allauth.account.views.PasswordChangeView account_change_password
/accounts/password/reset/ allauth.account.views.PasswordResetView account_reset_password
/accounts/password/reset/done/ allauth.account.views.PasswordResetDoneView account_reset_password_done
/accounts/password/reset/key/<uidb36>-<key>/ allauth.account.views.PasswordResetFromKeyView account_reset_password_from_key
/accounts/password/reset/key/done/ allauth.account.views.PasswordResetFromKeyDoneView account_reset_password_from_key_done
/accounts/password/set/ allauth.account.views.PasswordSetView account_set_password
/accounts/signup/ allauth.account.views.SignupView account_signup
/accounts/social/connections/ allauth.socialaccount.views.ConnectionsView socialaccount_connections
/accounts/social/login/cancelled/ allauth.socialaccount.views.LoginCancelledView socialaccount_login_cancelled
/accounts/social/login/error/ allauth.socialaccount.views.LoginErrorView socialaccount_login_error
/accounts/social/signup/ allauth.socialaccount.views.SignupView socialaccount_signup
only the actual /profile/ url...
/profile/ user_profile.views.profile profile
help me to understand why /accounts/profile/ is showing the /profile/ view...
Actually redirecting to /accounts/profile/ is default behavior in django. In django global settings, it is defined as LOGIN_REDIRECT_URL. Django expects you to implement this page/url. Django django-allauth also uses same setting to redirect user after login.
If you want to redirect to any other page like /profile/, override this setting in your project settings.
LOGIN_REDIRECT_URL = '/profile/'
Or if you want django-allauth not to redirect at all set LOGIN_REDIRECT_URL = False in your settings as described here.
Your profile url path is not being limited to match only the start of the url:
url('profile/', include('user_profile.urls')),
This will match anything like gibberishprofile/ for example, including accounts/profile/. If you change the url config to
url('^profile/', include('user_profile.urls')), # note the ^ before profile
Then only profile/ will match.

How to set custom admin login URL in Django Admin on session timeout?

I wrote a Django app which has an external authentication system reacheable at some URL (say, https://.../myproject/login). This is working well.
However, when the session expires, the user gets redirected to the default login url which is https://.../myproject/admin). I'd like to change the behavior of the app so if the session expires, the user should be redirected to https://.../myproject/login and only use the /admin login when explicitly opened.
Is there a built-in way to do this in Django?
Django admin redirects the users to /admin/login when the session is expired or session is missing.
There are several ways to redirect users to https://.../myproject/login instead of https://.../myproject/admin/login.
Approach 1:
Override the view of myproject/admin/login URL with the view of myproject/login.
Let's say that myproject/login uses LoginView to render external system's login page, then add url(r'^admin/login/?$', LoginView.as_view(), name='admin:login') just above url(r'^admin/', include(admin.site.urls)) in myproject/myproject/urls.py
urlpatterns = [
url(r'^admin/login/?$', LoginView.as_view(), name='admin:login'),
url(r'^admin/', include(admin.site.urls)),
]
Pros:
Render the external system's login page instead of default Django admin login page on /myproject/admin/login
Cons:
The URL still points to myproject/admin/login
No way to access the default admin login page
Approach 2:
Override the view of myproject/admin/login url and redirect the user to myproject/login
Lets create a new view AdminLoginView then add url(r'^admin/login/?$', AdminLoginView.as_view(), name='admin:login') just above url(r'^admin/', include(admin.site.urls)) in myproject/myproject/urls.py
from django.core.urlresolvers import reverse
class AdminLoginView(TemplateView):
def get(self, request, *args, **kwargs):
"""
Assuming the name of the external system's login url is "login"
"""
return HttpResponseRedirect(reverse('login'))
urlpatterns = [
url(r'^admin/login/?$', AdminLoginView.as_view(), name='admin:login'),
url(r'^admin/default-login/?$', admin.site.login, name='default-admin-login'),
url(r'^admin/', include(admin.site.urls)),
]
Pros:
The URL changes to myproject/login
Cons:
You have to add extra code for the default login page.
I would recommend approach 2 to solve the problem mentioned in the question.
Thanks.
You can use LOGIN_URL and LOGOUT_REDIRECT_URL
https://docs.djangoproject.com/en/2.2/ref/settings/#login-url
Redirect to myproject/login for login (Default redirects to /accounts/login/)
LOGIN_URL = '/myproject/login/'
Redirect to the login page after log out (Default to None).
LOGOUT_REDIRECT_URL = '/myproject/login/'
IMO the best possible solution is to override the function for login view.
To do this add these lines of code in your urls.py containing the 'admin/'
# urls.py
def login(request):
if request.user and request.user.is_authenticated and request.user.is_active and request.user.is_staff:
return HttpResponseRedirect(reverse('admin:index', current_app='admin'))
else:
return HttpResponseRedirect(reverse('index', current_app='your_app_name'))
admin.site.login = login
# The lines below probably exist
# urlpatterns = [
# path('admin/', admin.site.urls),
# path('', include('your_app.urls')),
# ]

how can I prevent user to go to login page after successful authentication?

I am adding settings.py, root url and views.py. After login user is redirected to respective dashboard. In this situation, if user is pressing back button or changing url to accounts/login, then also it should remain on the dashboard page only. I am using django-registration-redux
settings.py
REGISTRATION_OPEN = True
ACCOUNT_ACTIVATION_DAYS = 7
REGISTRATION_AUTO_LOGIN = False
REGISTRATION_FORM = 'signin.forms.MyRegForm'
LOGIN_REDIRECT_URL = '/signin/user_sign/'
views.py
def user_sign(request):
obj = UserSelection.objects.get(user=request.user)
if obj.user_type == 'candidate':
return redirect('candidate:cand_dash')
else:
return redirect('employer:employer_dash')
urls.py
from django.conf.urls import url, include
from django.contrib import admin
from django.conf import settings
from signin.regbackend import MyRegistrationView
from django.contrib.auth import views as auth_views
urlpatterns = [
url(r'^$', auth_views.LoginView.as_view(template_name='registration/login.html'), name='home'),
url(r'^accounts/register/$', MyRegistrationView.as_view(), name='registration_register'),
url(r'^accounts/', include('registration.backends.default.urls')),
url(r'^candidate/', include('candidate.urls')),
url(r'^employer/', include('employer.urls')),
url(r'^signin/', include('signin.urls')),
]
You could use a Boolean variable authenticated.
Then you should need to set it as False before the user Authentication.
def registration(request):
authenticated = False
...
Then after the user's authentication just change the var as authenticated = True
Finally every time you need to know if user is authenticated just use if user.authenticated
Also, if you need to use authenticated a lot take a look at custom decorators (https://docs.djangoproject.com/en/2.0/topics/http/decorators/) maybe they could help you.

how to change a default django admin login view to generate token on login to admin site

my site.py:
from django.contrib.admin import AdminSite
class OptiAdminSite(AdminSite):
def get_urls(self):
from django.conf.urls import patterns, url, include
from core import views
from django.contrib.contenttypes import views as contenttype_views
urlpatterns = patterns('',
#url(r'^$', wrap(self.index), name='index'),
url(r'^login/$', views.login, name='login'),
url(r'^logout/$', views.logout, name='logout'),
)
return urlpatterns
opti_site = OptiAdminSite()
I'm developing an authentication API. When user logs in to my API it generates a code which get destroyed once user hit logout.
My problem is that whenever I'm running my API and django admin site in same browser, then if I login into admin-site It automatically login me in my API too with out any token. When I try to logout in that case from my API it generates an error - 'Token does not exist'. I want to generate token when admin user login to admin-site.
I've tried to do it with above trick as in official documentation but didn't find the right way to do it.
Please suggest me the correct way to do it. Is it necessary to make a separate app for it?
Thanks! in advance.
This solution is almost complete... Almost, because you're simply creating your own admin site in opti_site variable, but probably not using it anywhere.
To make it work, you can monkey-patch default admin site with your site, using:
from django.contrib import admin
admin.sites.site = opti_site
admin.site = admin.sites.site
Remember that you must do it before root urlpatterns definition (especially before defining urls to your admin site).
Another approach is to change default admin to your admin in include of url patterns:
url(r'^admin/', include(opti_site.urls)),