authentication login route across apps

I have 2 apps in a django project.
App1 is from the graph tutorial found here
App2 will allow the users to list data from a DB in this case it will be branch names.

If I try and access a page with @login_required decorator then the url route has /accounts/login/ added and I get the usual cant find error.

Page not found (404)
Request Method:	GET
Request URL:	http://localhost:8000/accounts/login/?next=/calendar

Using the URLconf defined in graph_project.urls, Django tried these URL patterns, in this order:

[name='home']
about [name='about']
signin [name='signin']
signout [name='signout']
calendar [name='calendar']
callback [name='callback']
branches/
admin/
branches/
The current path, accounts/login/, didn't match any of these.

If I am reading the django docs correctly then this is default and I can redirect the login path using the LOGIN_URL in the project settings. When I set that to the signin function created in the tutorial for App1

def sign_in(request):
  # Get the sign-in URL
  sign_in_url, state = get_sign_in_url()
  # Save the expected state so we can validate in the callback
  request.session['auth_state'] = state
  # Redirect to the Azure sign-in page
  return HttpResponseRedirect(sign_in_url)

It will forever cycle the MS OAuth login but never access the requested page once completed. If I leave out the LOGIN_URL from settings it adds the accounts/login/ to the url as that is the default.

What is it that I am not understanding as to have login/logout requests handled by the functions in App1 for any requests made in other Apps when the request is behind a Login_Required decorator? And why does it not check if I am already authenticated when I can see It holds my name/email/calendar calls if I do not have a @Login_Required decorator and move between pages.

Thanks

To try to clarify your base question, it doesn’t matter which app is using the login_required decorator. That decorator essentially works by checking to see if the request.user object passes the is_authenticated test. If they aren’t, it’s going to redirect you to the LOGIN_URL URL, with the next query parameter set to the current URL. The current app has no effect on this.

Your LOGIN_URL view then needs to display whatever login page you need, and then, upon successful authentication, redirect you to the URL specified by that next query parameter.

If you have a valid session, then the login_required decorator will have no effect on that view.

Ken

Thanks for answering @KenWhitesell .
From what you have said then I feel I am reading the Docs correctly and it seems to be the routing that is causing me the confusion.
If I could please impose on your time briefly can you explain this to me as to what I am not understanding. I will do my best to make it as clear as possible.

Project is called Project
urls.py has

urlpatterns = [
    path('', include('auth_graph.urls')),    
    path('cpd/', include('cpd.urls')),
]

settings.py has this at the bottom

LOGIN_URL = '/signin'

App1 is called auth_graph (this holds all the functions for authentication using Microsoft Graph.
urls.py has

urlpatterns = [
    path('', views.home, name='home'),
    path('about/', views.about, name='about'),    
    path('signin', views.sign_in, name='signin'),
    path('signout', views.sign_out, name='signout'),
    path('callback', views.callback, name='callback'),
]

All authorization works fine and if I have no authentication checks I can access any page inside app2.

App2 is called cpd this is the main app my users would see and interact with. (locked to only authenticated users)

urls.py has
urlpatterns = [
    path('', views.home, name='cpd-home'),
    path('about/', views.about, name='cpd-about'),
    path('colleagues/', ColleagueListView.as_view(), name='cpd-colleagues'),
    path('modules/', ModuleListView.as_view(), name='cpd-modules'),
    path('module/<int:pk>/', ModuleDetailView.as_view(), name='module-detail'),
    path('module/new/', ModuleCreateView.as_view(), name='module-create'),
    path('module/<int:pk>/update/', ModuleUpdateView.as_view(), name='module-update'),
    path('completions/', CompletionListView.as_view(), name='cpd-completions'),
    path('branches/', BranchListView.as_view(), name='cpd-branches'),      
]

If i traverse to localhost:8000/cpd/modules
it asks for me to log in as its CBV is using the Mixin

class ModuleListView(LoginRequiredMixin,ListView):  
    paginate_by = 10
    model = Module
    template_name = 'cpd/modules.html'
    context_object_name = 'modules'
    ordering = ['-date_due']

    def get_context(self):
      context = initialize_context(self)
      return context

if I log in it redirects to localhost:8000/cpd which is the current redirect URL in the callback method of auth_graph (App1). However if I then traverse back to cpd/modules it asks me to log in again.

My base template is wrapped in a user.is_authenticated check and when I log in the first time, additional links appear in Nav and the call to Microsoft Graph returns my name, email and test calendar event so I know the authentication is successful.

What I am failing to understand is why it keeps asking me to log in again when I go to cpd/modules or on other LoginRequired pages. If I navigate to any other page the extra Nav links and details from Graph remain so I know I am still authenticated.

I know I am asking a lot but I am really struggling to get my head around this.

Ok, re-reading this more closely, it looks like you’re integrating Azure authentication into your site?

If so, I’d like to see what middleware you’re using and a reference to whatever documentation you’re referencing that shows you how to integrate Azure authentication into your Django site.

Superficially, it looks like you’re probably authenticating ok, but something in the middleware isn’t recognizing it on subsequent pages.

Sorry, just seen your edit. Yes the authentication is against Azure AD.
I’m using standard Django authentication process but the authentication happens via Microsoft Graph.
It was built using a 10 min tutorial here

Middleware - Nop changes have been made from the default.

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.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',
]

I can confirm it is also the standard user model being used.
This is the installed apps

INSTALLED_APPS = [    
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'auth_graph',
    'cpd',
    'branches',
]

This is the signin method

def sign_in(request):
    # Get the sign-in URL
    sign_in_url, state = get_sign_in_url()
    # Save the expected state so we can validate in the callback
    request.session['auth_state'] = state
    # Redirect to the Azure sign-in page
    return HttpResponseRedirect(sign_in_url)

This is the callback

def callback(request):
    # Get the state saved in session
    expected_state = request.session.pop('auth_state', '')
    # Make the token request
    token = get_token_from_code(request.get_full_path(), expected_state)

    # Get the user's profile
    user = get_user(token)

    # Save token and user
    store_token(request, token)
    store_user(request, user)

    return HttpResponseRedirect(reverse('cpd-home'))

If it helps this is the yaml file that the settings are held in (excluding sensitives)

app_id: "####"
app_secret: "####"
redirect: "http://localhost:8000/callback"
scopes: "openid profile offline_access user.read"
authority: "https://login.microsoftonline.com/[TENANT NAME]"
authorize_endpoint: "/oauth2/v2.0/authorize"
token_endpoint: "/oauth2/v2.0/token"

I appreciate the help.

Unfortunately, I can’t see anything immediately wrong. My current guess is that there’s some difference between the LoginRequiredMixin and the login_required decorator that allows their method to work with the latter but causes the former to fail.

If I were trying to diagnose this, I’d try creating just a simple function-based view and use the function decorator to see if that works. If it does, then there is something wrong with how their helper function populates the session, creating an incompatibility with the mixin.

Personally, I’m a bit suspicious with how they’re doing this. I’ve read through the tutorial and it looks to me like they’ve taken some shortcuts with managing the user in the session.

I’d also start looking at packages like https://pypi.org/project/django-azure-ad-auth/, https://django-auth-adfs.readthedocs.io/en/latest/, and https://pypi.org/project/django-microsoft-auth/ that provide that integration in a more appropriate mechanism.

1 Like

Sorry for the slow reply Ken, UK based so needed to call it a night for work today.

Thank you for going so above and beyond there I really appreciate it. If someone as knowledgeable on the framework as yourself says the Graph route is suspicious then I will cease that attempt and use a more tried and tested means.
Thank you for the links I will have a read through of them and implement them in a new app for the project.

Not sure if this site is similar to SO and you can close a topic off. If not hopefully someone in the same position will stumble across it in the future.

Whoa … please, don’t take anything I say as etched in stone. I try not to understate or overstate anything I say here.
When I said this was a guess, that’s a sincerely honest statement. I don’t know that what they’re doing is “wrong”.
What I do know is that it doesn’t pass my “sniff test” - and all that means is that I’ve never seen it done that way before and I don’t quite understand what it’s doing - or how it fits.

I mean, sure, if you want to abandon this, that’s obviously your call. But I wouldn’t drop it without at least verifying my hypothesis.

Cheers!
Ken