Django-OIDC with keycloak - OIDC callback state not found in session oidc_states

Viewed 1534

I am trying to implement Keycloak SSO with Django API using OIDC. Getting below error message after requesting the call to Keyclock as a response

SuspiciousOperation at /^oidc/callback/
OIDC callback state not found in session oidc_states!

The same thing I did in the Flask framework and it's working as expected.

Please find the below details.

Django Log:

[20/Mar/2021 15:30:57] "GET / HTTP/1.1" 404 2229
[20/Mar/2021 15:31:03] "GET /hi HTTP/1.1" 302 0
[20/Mar/2021 15:31:03] "GET /oidcauthenticate/?next=/hi HTTP/1.1" 302 0
failed to get or create user: Claims verification failed
[20/Mar/2021 15:31:11] "GET /oidccallback/?state=UziLDF2ZcE9p2WrUIihUahgUsWdQ8zYQ&code=22d5a22b-16a6-42e3-81ba-9b569a3f1c81.2cd52631-8c4c-4f2e-a718-c908611336a3.81db1495-24af-402d-8244-a82f32bf4355 HTTP/1.1" 302 0
Not Found: /
[20/Mar/2021 15:31:11] "GET / HTTP/1.1" 404 2229

Settings:

Django settings for keycloakexample project.

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

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

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/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/3.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '_vg@)t*rri()^^wm7yl*i&$%gk2h1h%!xk$xms19ceb3*2z&g^'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []

# Application definition

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

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.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'mozilla_django_oidc.middleware.SessionRefresh',
]

ROOT_URLCONF = 'keycloakexample.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 = 'keycloakexample.wsgi.application'

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

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

# 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 = 'UTC'

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/'
from keycloak_oidc.default_settings import *


OIDC_OP_AUTHORIZATION_ENDPOINT = 'http://localhost:8080/auth/realms/aaa/protocol/openid-connect/auth'
OIDC_OP_TOKEN_ENDPOINT = 'http://localhost:8080/auth/realms/aaa/protocol/openid-connect/token'
OIDC_OP_USER_ENDPOINT = 'http://localhost:8080/auth/realms/aaa/protocol/openid-connect/userinfo'
OIDC_OP_JWKS_ENDPOINT = 'http://localhost:8080/auth/realms/aaa/protocol/openid-connect/certs'
OIDC_OP_LOGOUT_ENDPOINT = 'http://localhost:8080/auth/realms/aaa/protocol/openid-connect/logout'


AUTHENTICATION_BACKENDS = (
    'keycloak_oidc.auth.OIDCAuthenticationBackend',
)


OIDC_RP_CLIENT_ID = 'test'
OIDC_RP_CLIENT_SECRET = 'd6e007e-1098-4146-a217-04e4b1d1ae9b'

# REST_FRAMEWORK = dict(
#     DEFAULT_AUTHENTICATION_CLASSES=(
#         'mozilla_django_oidc.contrib.drf.OIDCAuthentication',
#         'rest_framework.authentication.SessionAuthentication'
#     )
# )

View:

from django.http import HttpResponse
from django.contrib.auth.decorators import login_required


@login_required()
def hello(request):
    print('123')
    return HttpResponse('hello')


@login_required
def hi(request):
    print('123')
    return HttpResponse('hi')

urls:

from django.urls import path, include


urlpatterns = [
    path('oidc', include('keycloak_oidc.urls')),
    path('', include('demo.urls')),

]

3 Answers

Thanks for the support.

The issue has been resolved by recreating the user in keycloak with full details.

Please finds the screenshot.Keycloak user creation

Cheers SG

The key issue is here that keycloak_oidc is used, specifically notice the AUTHENTICATION_BACKENDS. It is a project that synchronizes the attributes found in OIDC with the local Django installation, unlike mozilla-django-oidc which merely does authentication. The answer above is the key to solving this issue, once the email address (plus first and lastname) are not provided it cannot be added to Django. It is likely that this fails to with any OIDC provider which does not add these attributes. I have added an issue upstream.

You needs add class of authentication backend and orverride create_user method to add is_staff or is_superuser to True. Add AUTHENTICATION_BACKENDS in settings.py refering to you custom class backent Example:

# auth_backends.py

from mozilla_django_oidc.auth import OIDCAuthenticationBackend


class KeycloakOIDCAuthenticationBackend(OIDCAuthenticationBackend):

    def create_user(self, claims):
        """ Overrides Authentication Backend so that Django users are
            created with the keycloak preferred_username.
            If nothing found matching the email, then try the username.
        """
        user = super(KeycloakOIDCAuthenticationBackend, self).create_user(claims)
        user.first_name = claims.get('given_name', '')
        user.last_name = claims.get('family_name', '')
        user.email = claims.get('email')
        user.is_staff = True #Here the fix that error
        user.username = claims.get('preferred_username')
        user.save()
        return user

    def filter_users_by_claims(self, claims):
        """ Return all users matching the specified email.
            If nothing found matching the email, then try the username
        """
        email = claims.get('email')

        if not email:
            return self.UserModel.objects.none()
        users = self.UserModel.objects.filter(email__iexact=email)
        return users

    def update_user(self, user, claims):
        user.first_name = claims.get('given_name', '')
        user.last_name = claims.get('family_name', '')
        user.email = claims.get('email')
        user.save()
        return user

In settings.py

#settings.py
AUTHENTICATION_BACKENDS = (
    'my_app.auth_backends.KeycloakOIDCAuthenticationBackend',
)

Related