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 98 forks source link

Seems not support in Django-djongo-Mongodb? - ValueError: Field 'id' expected a number but got ObjectId #262

Closed aoiheaven closed 1 year ago

aoiheaven commented 1 year ago

Hi Sir, I got error raised: ValueError: Field 'id' expected a number but got ObjectId(...") from "get_prep_value", when I use mongodb with djongo connector. It worked well in sqlite, but seemed cannot pass validation or data store in user.full_clean()/.save() Can you help about this? I preper Mongodb if possible...

here my log:

Mask security info with "XXXXXXX"


[08/Nov/2022 23:34:47] "GET / HTTP/1.1" 302 0
DEBUG 2022-11-08 23:34:47,029 django_auth_adfs Loading ID Provider configuration.
INFO 2022-11-08 23:34:47,030 django_auth_adfs Trying to get OpenID Connect config from https://login.microsoftonline.com/43083d15-7273-40c1-b7db-39efd9ccc17a/.well-known/openid-configuration?appid=a2a099b9-487c-4c63-a9fe-5a613d1e90e9
DEBUG 2022-11-08 23:34:47,937 django_auth_adfs Loading public key from certificate: XXXXXXX
DEBUG 2022-11-08 23:34:47,943 django_auth_adfs Loading public key from certificate: XXXXXXX
DEBUG 2022-11-08 23:34:47,946 django_auth_adfs Loading public key from certificate: XXXXXXX
DEBUG 2022-11-08 23:34:47,948 django_auth_adfs Loading public key from certificate: XXXXXXX
DEBUG 2022-11-08 23:34:47,951 django_auth_adfs Loading public key from certificate: XXXXXXX
DEBUG 2022-11-08 23:34:47,954 django_auth_adfs Loading public key from certificate: XXXXXXX
INFO 2022-11-08 23:34:47,958 django_auth_adfs Loaded settings from ADFS server.
INFO 2022-11-08 23:34:47,958 django_auth_adfs operating mode:         openid_connect
INFO 2022-11-08 23:34:47,959 django_auth_adfs authorization endpoint: https://login.microsoftonline.com/43083d15-7273-40c1-b7db-39efd9ccc17a/oauth2/authorize
INFO 2022-11-08 23:34:47,959 django_auth_adfs token endpoint:         https://login.microsoftonline.com/43083d15-7273-40c1-b7db-39efd9ccc17a/oauth2/token
INFO 2022-11-08 23:34:47,960 django_auth_adfs end session endpoint:   https://login.microsoftonline.com/43083d15-7273-40c1-b7db-39efd9ccc17a/oauth2/logout
INFO 2022-11-08 23:34:47,960 django_auth_adfs issuer:                 https://sts.windows.net/43083d15-7273-40c1-b7db-39efd9ccc17a/
INFO 2022-11-08 23:34:47,961 django_auth_adfs msgraph endpoint:       graph.microsoft.com
[08/Nov/2022 23:34:47] "GET /oauth2/login?next=/ HTTP/1.1" 302 0
DEBUG 2022-11-08 23:34:48,536 django_auth_adfs Authentication backend was called but no access token was received
DEBUG 2022-11-08 23:34:48,537 django_auth_adfs Received authorization code: XXXXXXX
DEBUG 2022-11-08 23:34:48,539 django_auth_adfs Getting access token at: https://login.microsoftonline.com/43083d15-7273-40c1-b7db-39efd9ccc17a/oauth2/token
DEBUG 2022-11-08 23:34:49,009 django_auth_adfs Received access token: XXXXXXX
DEBUG 2022-11-08 23:34:49,014 django_auth_adfs No group claim has been configured
DEBUG 2022-11-08 23:34:49,788 django_auth_adfs User 'XXXXXXX' has been created.
DEBUG 2022-11-08 23:34:49,789 django_auth_adfs Attribute 'first_name' for instance 'XXXXXXX' was set to 'XXXXXXX'.
DEBUG 2022-11-08 23:34:49,790 django_auth_adfs Attribute 'last_name' for instance 'XXXXXXX' was set to 'XXXXXXX'.    
DEBUG 2022-11-08 23:34:49,791 django_auth_adfs Attribute 'email' for instance 'XXXXXXX' was set to 'XXXXXXX'.
Internal Server Error: /oauth2/callback
Traceback (most recent call last):
File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\fields\__init__.py", line 2018, in get_prep_value
return int(value)
TypeError: int() argument must be a string, a bytes-like object or a number, not 'ObjectId'

The above exception was the direct cause of the following exception:

Traceback (most recent call last): File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\core\handlers\exception.py", line 55, in inner response = get_response(request) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\core\handlers\base.py", line 197, in _get_response response = wrapped_callback(request, *callback_args, callback_kwargs) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\views\generic\base.py", line 103, in view return self.dispatch(request, *args, *kwargs) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\views\generic\base.py", line 142, in dispatch return handler(request, args, kwargs) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django_auth_adfs\views.py", line 40, in get
user = authenticate(request=request, authorization_code=code) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\views\decorators\debug.py", line 42, in sensitive_variables_wrapper return func(func_args, func_kwargs) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\contrib\auth__init__.py", line 77, in authenticate user = backend.authenticate(request, credentials) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django_auth_adfs\backend.py", line 467, in authenticate user = self.process_access_token(access_token, adfs_response) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django_auth_adfs\backend.py", line 204, in process_access_token user.full_clean() File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\base.py", line 1462, in full_clean self.validate_unique(exclude=exclude) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\base.py", line 1207, in validate_unique errors = self._perform_unique_checks(unique_checks) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\base.py", line 1316, in _perform_unique_checks qs = qs.exclude(pk=model_class_pk) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\query.py", line 1428, in exclude return self._filter_or_exclude(True, args, kwargs) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\query.py", line 1438, in _filter_or_exclude clone._filter_or_exclude_inplace(negate, args, kwargs) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\query.py", line 1443, in _filter_or_exclude_inplace self._query.add_q(~Q(args, **kwargs)) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\sql\query.py", line 1532, in addq clause, = self._add_q(q_object, self.used_aliases) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\sql\query.py", line 1562, in _add_q child_clause, needed_inner = self.build_filter( File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\sql\query.py", line 1478, in build_filter condition = self.build_lookup(lookups, col, value) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\sql\query.py", line 1303, in build_lookup lookup = lookup_class(lhs, rhs) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\lookups.py", line 27, in init self.rhs = self.get_prep_lookup() File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\lookups.py", line 341, in get_prep_lookup return super().get_prep_lookup() File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\lookups.py", line 85, in get_prep_lookup return self.lhs.output_field.get_prep_value(self.rhs) File "C:\Users\harryw\web_dev\socd_env\sofa_be_django\venv\lib\site-packages\django\db\models\fields__init.py", line 2020, in get_prep_value raise e.class__( TypeError: Field 'id' expected a number but got ObjectId('636a7719d76033a24e13c120'). [08/Nov/2022 23:34:50] "GET /oauth2/callback?code=XXXXXXX

and find in mongodb corresponding collection (auth_user)

... [ { _id: ObjectId("636a714dfc1ab55037d5d827"), password: '', last_login: null, is_superuser: false, username: 'XXXXXXX', first_name: '', last_name: '', email: '', is_staff: false, is_active: true, date_joined: ISODate("2022-11-08T15:10:04.729Z") } ] ...


My Config: settings.py

""" Django settings for sofa_be_django project.

Generated by 'django-admin startproject' using Django 4.0.6.

For more information on this file, see https://docs.djangoproject.com/en/4.0/topics/settings/

For the full list of settings and their values, see https://docs.djangoproject.com/en/4.0/ref/settings/ """

from pathlib import Path

Build paths inside the project like this: BASE_DIR / 'subdir'.

BASE_DIR = Path(file).resolve().parent.parent

Quick-start development settings - unsuitable for production

See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/

SECURITY WARNING: keep the secret key used in production secret!

SECRET_KEY = "XXXXXXX"

SECURITY WARNING: don't run with debug turned on in production!

DEBUG = True

ALLOWED_HOSTS = ["127.0.0.1", "localhost", "XXXXXXX"]

AUTH SCHEME FOR SSO (AzureAD) - ADFS Authentication for Django

Ref: https://django-auth-adfs.readthedocs.io/en/latest/azure_ad_config_guide.html#step-2-configuring-settings-py

AUTHENTICATION_BACKENDS = { "django_auth_adfs.backend.AdfsAuthCodeBackend", "django_auth_adfs.backend.AdfsAccessTokenBackend",

"django.contrib.auth.backends.ModelBackend",

}

REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "django_auth_adfs.rest_framework.AdfsAccessTokenAuthentication", "rest_framework.authentication.SessionAuthentication", ) }

Application definition

INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "django_filters", "rest_framework", "corsheaders", "django_auth_adfs", # django-auth-adfs -> A Django authentication backend for Microsoft ADFS and Azure AD ]

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",

With this you can force a user to login without using

# the LoginRequiredMixin on every view class
#
# You can specify URLs for which login is not enforced by
# specifying them in the LOGIN_EXEMPT_URLS setting.
"django_auth_adfs.middleware.LoginRequiredMiddleware",

]

ROOT_URLCONF = "sofa_be_django.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 = "sofa_be_django.wsgi.application"

Database

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

DATABASES = {

"default": {

"ENGINE": "django.db.backends.sqlite3",

"NAME": BASE_DIR / "db.sqlite3",

}

}

DATABASES = { "default": { "ENGINE": "djongo", "NAME": "XXXXXXX", "CLIENT": { "host": "mongodb+srv://XXXXXXX" }, } }

Password validation

https://docs.djangoproject.com/en/4.0/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", }, ]

SSO validation

Client secret is not public information. Should store it as an environment variable.

client_id = "a2a099b9-487c-4c63-a9fe-5a613d1e90e9" # 'Your client id here' client_secret = "XXXXXXX" # TODO: 'Your client secret here', need query this value from Environment! tenant_id = "43083d15-7273-40c1-b7db-39efd9ccc17a" # 'Your tenant id here'

AUTH_ADFS = { "AUDIENCE": client_id, "CLIENT_ID": client_id, "CLIENT_SECRET": client_secret, "CLAIM_MAPPING": { "first_name": "given_name", "last_name": "family_name", "email": "upn", }, "GROUPS_CLAIM": None, "MIRROR_GROUPS": False, "USERNAME_CLAIM": "upn", "TENANT_ID": tenant_id, "RELYING_PARTY_ID": client_id, "LOGIN_EXEMPT_URLS": ["^api/", "public/"], # Assuming you API is available at /api }

Configure django to redirect users to the right URL for login

LOGIN_URL = "django_auth_adfs:login" LOGIN_REDIRECT_URL = "/" LOGOUT_REDIRECT_URL = "/"

You can point login failures to a custom Django function based view for customization of the UI

CUSTOM_FAILED_RESPONSE_VIEW = "dot.path.to.custom.views.login_failed"

Internationalization

https://docs.djangoproject.com/en/4.0/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_TZ = True

Static files (CSS, JavaScript, Images)

https://docs.djangoproject.com/en/4.0/howto/static-files/

STATIC_URL = "static/"

Default primary key field type

https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

Record log for Hint

LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "verbose": {"format": "%(levelname)s %(asctime)s %(name)s %(message)s"}, }, "handlers": { "console": {"class": "logging.StreamHandler", "formatter": "verbose"}, }, "loggers": { "django_auth_adfs": { "handlers": ["console"], "level": "DEBUG", }, }, }


urlpatterns :

... path("oauth2/", include("django_auth_adfs.urls")), ...

requirements.txt :

djangorestframework django-filter demjson django-cors-headers djongo pymongo==3.12.3 pymongo[srv] gunicorn jsonfield dnspython == 2.1.0 django-auth-adfs

tim-schilling commented 1 year ago

Please update your requirements to list all versions of the packages.

tim-schilling commented 1 year ago

I suspect this is a djongo issue and not a django-auth-adfs issue. The error is related to the default user model and indicating that the id from the database is an ObjectID and not convertable to an int. Review the djongo docs to see if says anything about changing how settings.AUTH_USER_MODEL should be set.

aoiheaven commented 1 year ago

I suspect this is a djongo issue and not a django-auth-adfs issue. The error is related to the default user model and indicating that the id from the database is an ObjectID and not convertable to an int. Review the djongo docs to see if says anything about changing how settings.AUTH_USER_MODEL should be set.

Thanks for replying!

so that you mean I should create one new app to customize user model to replace AUTH_USER_MODEL and about "id" entry should be configured as "AutoField" to bypass it, right?

tim-schilling commented 1 year ago

I'm honestly not sure. I've never used that library before, but the docs indicate it should automatically convert the user model.

aoiheaven commented 1 year ago

give up and migrate to sql series for extended user model. close it