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

[PEBKAC] Azure AD config forces upn-claim, which is not always present. #73

Closed sergei-maertens closed 5 years ago

sergei-maertens commented 5 years ago

It looks like there's not guarantee the upn claim, which is used to look up or create the user, is not always present.

I am testing this against a free trial of Azure AD, maybe with a bit of an exotic setup (I have zero AD/Azure knowledge!):

Traceback:

Environment:

Request Method: GET
Request URL: http://localhost:8000/oauth2/callback?code=<code>&session_state=1e48fdd5-2100-4371-9b8b-ac39bd7c5813

Django Version: 2.0.13
Python Version: 3.6.8
Installed Applications:
['django.contrib.contenttypes',
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.humanize',
 'django.contrib.postgres',
 'adminsortable2',
 'axes',
 'compat',
 'import_export',
 'privates',
 'rules',
 'solo',
 'treebeard',
 'django_auth_adfs',
 'rest_framework',
 'rest_framework.authtoken',
 'django_filters',
 'isp',
 'isp.accounts',
 'isp.masterdata.apps.MasterdataConfig',
 'isp.dimensions',
 'isp.utils',
 'isp.smartscan',
 'isp.smartscan.pdf2xmlapp',
 'isp.smartscan.masterdata.apps.MasterdataConfig',
 'isp.smartscan.pdfsplitter',
 'isp.smartscan.learningtool',
 'isp.breapi',
 'isp.classification',
 'isp.mapping',
 'isp.budget',
 'isp.elang',
 'isp.workflow',
 'isp.invoice',
 'isp.easyrouting',
 'isp.advancedrouting',
 'isp.outofoffice',
 'isp.config',
 'isp.fieldsetup',
 'isp.validationsetup',
 'isp.setups',
 'isp.erp',
 'isp.matchingtolerance',
 'debug_toolbar',
 'isp.fixtureapp',
 'django_extensions']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.locale.LocaleMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'debug_toolbar.middleware.DebugToolbarMiddleware']

Traceback:

File "/home/bbt/.virtualenvs/ispnext/lib/python3.6/site-packages/django/core/handlers/exception.py" in inner
  35.             response = get_response(request)

File "/home/bbt/.virtualenvs/ispnext/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
  128.                 response = self.process_exception_by_middleware(e, request)

File "/home/bbt/.virtualenvs/ispnext/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
  126.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/home/bbt/.virtualenvs/ispnext/lib/python3.6/site-packages/django/views/generic/base.py" in view
  69.             return self.dispatch(request, *args, **kwargs)

File "/home/bbt/.virtualenvs/ispnext/lib/python3.6/site-packages/django/views/generic/base.py" in dispatch
  89.         return handler(request, *args, **kwargs)

File "/home/bbt/.virtualenvs/ispnext/lib/python3.6/site-packages/django_auth_adfs/views.py" in get
  34.         user = authenticate(request, authorization_code=code)

File "/home/bbt/.virtualenvs/ispnext/lib/python3.6/site-packages/django/contrib/auth/__init__.py" in authenticate
  70.             user = _authenticate_with_backend(backend, backend_path, request, credentials)

File "/home/bbt/.virtualenvs/ispnext/lib/python3.6/site-packages/django/contrib/auth/__init__.py" in _authenticate_with_backend
  116.     return backend.authenticate(*args, **credentials)

File "/home/bbt/.virtualenvs/ispnext/lib/python3.6/site-packages/django_auth_adfs/backend.py" in authenticate
  266.         user = self.process_access_token(access_token, adfs_response)

File "/home/bbt/.virtualenvs/ispnext/lib/python3.6/site-packages/django_auth_adfs/backend.py" in process_access_token
  94.         user = self.create_user(claims)

File "/home/bbt/.virtualenvs/ispnext/lib/python3.6/site-packages/django_auth_adfs/backend.py" in create_user
  124.             usermodel.USERNAME_FIELD: claims[username_claim]

Exception Type: KeyError at /oauth2/callback
Exception Value: 'upn'

I realize this may have something to do with whitelisted domains, but I'm clueless where I could figure this out in AD itself.

Creating a test-user in Azure AD and logging in with that user works fine and completely as expected, as long as the username contains the proper AD domain.

I'd appreciate pointers if this is a known issue or if I just should not try to log in with that account, or somehow be able to force the upn claim for this user.

jobec commented 5 years ago

It doesn't force it, it defaults to it.

This is documented in the FAQ: https://django-auth-adfs.readthedocs.io/en/latest/faq.html#i-m-receiving-an-keyerror-upn-error-when-authenticating-against-azure-ad

Let me know if that helps.

sergei-maertens commented 5 years ago

How clumsy of me that I missed that, I was looking at the code as well but missed the step where own-config overrides defaults again.

Confirmed that using the email claim instead works :+1:

Apologies for the notifications noise because of my failure to read more documentation :smiley: