snok / django-auth-adfs

A Django authentication backend for Microsoft ADFS and AzureAD
http://django-auth-adfs.readthedocs.io/
BSD 2-Clause "Simplified" License
270 stars 100 forks source link

Clarification in documentation for DRF and verifying access_token #113

Closed cheslijones closed 3 years ago

cheslijones commented 4 years ago

So in my app, and how I understand this should work in a microservice application, the flow is the following:

  1. User navigates to https://www.example.com/ which is a ReactJS FE.
  2. Using react-aad, they are automatically redirected to login using their Azure AD credentials for our tenant ID.
  3. This gives them the id_token and access_token.
  4. I need to send this access_token to our Django/DRF API where it needs to be verified as being authentic and thus granting client/API communication.

This is my understanding of how social authentication for microservices should work: client gets access_token, sends to API, API verifies it is authentic. This is where your library comes in and your documentation seems to verify this flow.

What I'm confused by, is in the DRF Integration section regarding the access token, the example is showing user and password. Again, it is my understanding user and password are not being sent from the ReactjS FE client, just the access_token and the API is supposed to verify it. This example seems to contradict that.

Can you clarify?

JonasKs commented 4 years ago

You’re right. The access_token is fetched by your frontend (through e.g. react-aad) and is used for authentication. There is at no point sent username or password to the Django app.

The example you link to describes how to get an access token through Python, if you want to use the API through a script. This code, using username and password is sent to AzureAD/ADFS(not Django), and then returns an access token.
This token can then be used to authenticate to the DRF API.

cheslijones commented 4 years ago

OK, thanks for confirming. Is there built in functionality I might be overlooking in the documentation to verify the access_token from Django/DRF ?

JonasKs commented 4 years ago

Add the authentication middleware to your restframework settings. It’s all described in the docs. You can set that up after the configuration is done.

cheslijones commented 4 years ago

OK, reread the docs this morning and gave it a shot. These are the parts I read:

  1. https://django-auth-adfs.readthedocs.io/en/latest/install.html
  2. https://django-auth-adfs.readthedocs.io/en/latest/azure_ad_config_guide.html
  3. https://django-auth-adfs.readthedocs.io/en/latest/rest_framework.html

And it occurred to me that in the "Requesting and access token" portion the only thing that was relevant to me was from the # Make a request towards this API on down... ReactJS is doing all of the proceeding in my use case.

Furthermore, if I'm understanding correctly, the middleware is just looking for the Authorization: Bearer <token> coming from the FE in the headers for any of my /api routes, and I don't have to refer to a specific route to validate the access_token (e.g., /api/oauth/validate or something).

At any rate, I'm running into a django_auth_adfs namespace error.

This is what I ended up with:

// Authentication.js
...
  const testApiAuthentication = async () => {
    let token = await authProvider.getAccessToken();
    // console.log(token.accessToken);
    setAccessToken(token.accessToken);
    if (token.accessToken) {
      console.log(token.accessToken);
      setAuthenticatingToken(true);
      axios({
        method: 'get',
        url: '/api/test/',
        headers: {
          'Accept': 'application/json',
          'Authorization': 'Bearer ' + token.accessToken
        }
      })
        .then((response) => {
          console.log(response);
        })
        .catch((error) => {
          console.log(error);
        });
    }
  };
...
# settings.py

import os
from datetime import timedelta

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ['SECRET_KEY']

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ['DEBUG'] == 'True'

ALLOWED_HOSTS = [
    os.environ['DOMAIN'],
    'companyapp.local',
    '.company.com'
]

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # Third-party packages
    'channels',
    'rest_framework',
    'django_auth_adfs',
]

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',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django_auth_adfs.middleware.LoginRequiredMiddleware',
]

ROOT_URLCONF = 'config.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'config.wsgi.application'
ASGI_APPLICATION = "config.routing.application"

# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ['PGDATABASE'],
        'USER': os.environ['PGUSER'],
        'PASSWORD': os.environ['PGPASSWORD'],
        'HOST': os.environ['PGHOST'],
        'PORT': 5432
    }
}

# REST Framework settings
# https://www.django-rest-framework.org/
REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
        'django_auth_adfs.rest_framework.AdfsAccessTokenAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
}

# SIMPLE_JWT Settings
# https://github.com/davesque/django-rest-framework-simplejwt
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(days=3),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'SIGNING_KEY': os.environ['SECRET_KEY'],
}

AUTHENTICATION_BACKENDS = (
    'django_auth_adfs.backend.AdfsAuthCodeBackend',
    'django_auth_adfs.backend.AdfsAccessTokenBackend',
    'django.contrib.auth.backends.ModelBackend',
)

AUTH_ADFS = {
    # 'LOGIN_EXEMPT_URLS': [
    #     'api/',  # Assuming you API is available at /api
    # ],
    'CLIENT_ID': '<client_id>',
    'TENANT_ID': '<tenant_id>',
    'RELYING_PARTY_ID': 'https://login.microsoftonline.com/<tenant_id/',
    'AUDIENCE': 'https://login.microsoftonline.com/<tenant_id>/',
}

# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'America/Los_Angeles'

USE_I18N = True

USE_L10N = True

USE_TZ = True

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/

STATIC_URL = '/static/'

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
# STATIC_URL = '/static/'
STATIC_URL = '/api/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')

# Define the upload and download directories
DOWNLOAD_ROOT = '/mnt/company-files/client-downloads/'
MEDIA_ROOT = '/mnt/company-files/client-submissions/'
# urls.py
from django.contrib import admin
from django.urls import path, include

from users.views import TestView 

urlpatterns = [
    path('api/admin/', admin.site.urls),
    path('api/test/', TestView.as_view())
]
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework.authentication import TokenAuthentication

# Create your views here.
class TestView(APIView):
    authentication_classes = [TokenAuthentication]
    permission_classes = [AllowAny]

    def get(self, request, *args, **kwargs):
        return Response('Hello World')

I'm running into an error however:

[api] Watching for file changes with StatReloader
[api] Performing system checks...
[api] 
[api] System check identified no issues (0 silenced).
[api] November 09, 2020 - 11:10:49
[api] Django version 3.1.2, using settings 'config.settings'
[api] Starting ASGI/Channels version 2.4.0 development server at http://0.0.0.0:5000/
[api] Quit the server with CONTROL-C.
[api] Traceback (most recent call last):
[api]   File "/usr/local/lib/python3.8/site-packages/django/urls/base.py", line 72, in reverse
[api]     extra, resolver = resolver.namespace_dict[ns]
[api] KeyError: 'django_auth_adfs'
[api] 
[api] During handling of the above exception, another exception occurred:
[api] 
[api] Traceback (most recent call last):
[api]   File "/usr/local/lib/python3.8/site-packages/daphne/http_protocol.py", line 163, in process
[api]     self.application_queue = yield maybeDeferred(
[api] django.urls.exceptions.NoReverseMatch: 'django_auth_adfs' is not a registered namespace
[api] 
[api] HTTP GET /api/user/ 500 [0.06, 172.17.0.8:36832]
marlonpatrick commented 3 years ago

hey @cheslijones are you sure about that settings?

'RELYING_PARTY_ID': 'https://login.microsoftonline.com/<tenant_id/', 'AUDIENCE': 'https://login.microsoftonline.com//',

I'm trying to find out the correct values for settings RELYING_PARTY_ID and AUDIENCE but I can't. If you are right, this will help me a lot.

marlonpatrick commented 3 years ago

I managed to configure the following scenario: angular application + backend application (django rest framework).

The explanation is in the issue 114.

cheslijones commented 3 years ago

Thanks, I'll take a look when circle around to this project again in like a week.

mihnsen commented 3 years ago

@marlonpatrick @cheslijones still not sure which value I should put to the 'RELYING_PARTY_ID' and 'AUDIENCE' The values from manifest give me error: is requesting a token for itself. This scenario is supported only if resource is specified using the GUID based App Identifier.

marlonpatrick commented 3 years ago

How is your scenario? Frontend and backend? Are you using django rest ou only django?

mihnsen commented 3 years ago

@marlonpatrick I set it wrong while trying to integrate it with just one application (Web only). Finally found issue and get over it. Thanks

JonasKs commented 3 years ago

Closing in favor of #81 . Explanation is as previously mentioned in #114, and a PR has been raised (#123) to fix documentation.