Closed cheslijones closed 3 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.
OK, thanks for confirming. Is there built in functionality I might be overlooking in the documentation to verify the access_token
from Django/DRF ?
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.
OK, reread the docs this morning and gave it a shot. These are the parts I read:
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]
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.
I managed to configure the following scenario: angular application + backend application (django rest framework).
The explanation is in the issue 114.
Thanks, I'll take a look when circle around to this project again in like a week.
@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.
How is your scenario? Frontend and backend? Are you using django rest ou only django?
@marlonpatrick I set it wrong while trying to integrate it with just one application (Web only). Finally found issue and get over it. Thanks
Closing in favor of #81 . Explanation is as previously mentioned in #114, and a PR has been raised (#123) to fix documentation.
So in my app, and how I understand this should work in a microservice application, the flow is the following:
https://www.example.com/
which is a ReactJS FE.react-aad
, they are automatically redirected to login using their Azure AD credentials for our tenant ID.id_token
andaccess_token
.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
andpassword
. Again, it is my understandinguser
andpassword
are not being sent from the ReactjS FE client, just theaccess_token
and the API is supposed to verify it. This example seems to contradict that.Can you clarify?