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

Azure AD Authentication with django_auth_adfs middleware applied (302 - redirect) #77

Closed alinjie closed 5 years ago

alinjie commented 5 years ago

Hi,

I've set up Azure AD authentication by following the guide provided in the docs. It seems to be working fine, as claims are resolved etc when requesting a DRF endpoint with the access token in the header.

When I apply the django_auth_adfs.middleware.LoginRequiredMiddleware middleware, every request is redirected with a status code of 302, indicating that the request is not authenticated even though the access token is in the header and the request should be authenticated.

Configuration

settings.py

AUTHENTICATION_BACKENDS = (
    'django_auth_adfs.backend.AdfsAccessTokenBackend',
    'django_auth_adfs.backend.AdfsAuthCodeBackend'
)
AUTH_ADFS = {
    "TENANT_ID": "<my_tenant_id",
    "CLIENT_ID": "<client_id_of_native_app>",
    "RELYING_PARTY_ID": "https://intility.onmicrosoft.com/<client_id_of_web_app>",
    "AUDIENCE": "<scope_uri_of_native_app>",
    "CLAIM_MAPPING": {"first_name": "given_name",
                      "last_name": "family_name",
                      "email": "email"},
    "GROUPS_CLAIM": "groups",
    "MIRROR_GROUPS": True,
    "USERNAME_CLAIM": "upn",
}

Debug console output (without middleware applied)

INFO 2019-06-06 12:57:00,500 django_auth_adfs django_auth_adfs loaded settings from ADFS server.
INFO 2019-06-06 12:57:00,506 django_auth_adfs operating mode:         openid_connect
INFO 2019-06-06 12:57:00,511 django_auth_adfs authorization endpoint: https://login.microsoftonline.com/_TENANT_ID_/oauth2/authorize
INFO 2019-06-06 12:57:00,512 django_auth_adfs token endpoint:         https://login.microsoftonline.com/_TENANT_ID_/oauth2/token
INFO 2019-06-06 12:57:00,517 django_auth_adfs end session endpoint:   https://login.microsoftonline.com/_TENANT_ID_/oauth2/logout
INFO 2019-06-06 12:57:00,535 django_auth_adfs issuer:                 https://sts.windows.net/_TENANT_ID_/
DEBUG 2019-06-06 12:57:00,538 django_auth_adfs Received access token: _ACCESS_TOKEN_
DEBUG 2019-06-06 12:57:00,560 django_auth_adfs Attribute 'first_name' for user '_USER_PRINCIPAL_NAME_' was set to '_FIRST_NAME_'.
DEBUG 2019-06-06 12:57:00,562 django_auth_adfs Attribute 'last_name' for user '_USER_PRINCIPAL_NAME_' was set to '_LAST_NAME_'.
was set to 'Njie'.
WARNING 2019-06-06 12:57:00,564 django_auth_adfs Claim 'email' for user field 'email' was not found in the access token for user '_USER_PRINCIPAL_NAME_'. Field is not required and will be left empty
DEBUG 2019-06-06 12:57:00,571 django_auth_adfs The configured groups claim 'groups' was not found in
the access token
DEBUG 2019-06-06 12:57:00,579 django_auth_adfs The configured group claim was not found in the access token
[06/Jun/2019 12:57:00] "GET /api/users/ HTTP/1.1" 200 17

Console Output (With middleware applied):

System check identified no issues (0 silenced).
June 06, 2019 - 12:59:57
Django version 2.2.1, using settings 'compliancedash.settings'
Starting development server at http://127.0.0.1:8080/
Quit the server with CTRL-BREAK.
[06/Jun/2019 13:00:01] "GET /api/users/ HTTP/1.1" 302 0

Both requests was made in the same session. The only difference is that the middleware was commented out

Do you have any ideas as to what is causing this? Any help would be appriciated!

alinjie commented 5 years ago

All my middlewares, in case that is relevant:

MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django_auth_adfs.middleware.LoginRequiredMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'corsheaders.middleware.CorsMiddleware',
]
jobec commented 5 years ago

The LoginRequiredMiddleware is meant for vanilla Django pages. Not for Django Rest Framework.

DRF uses a different system for authenticating.

If you want to mix regular Django views with DRF, then you should exclude the API path from the LoginRequiredMiddleware config. See https://django-auth-adfs.readthedocs.io/en/latest/settings_ref.html#login-exempt-urls

alinjie commented 5 years ago

I see! I'll create my own custom middleware then.

Thanks for the quick reply! :)

alinjie commented 5 years ago

Related to this issue, I've encountered another problem.

I've made the following middleware to ensure that the request is authenticated:

def require_auth(get_response):
    # One-time configuration and initialization.

    def middleware(request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.

        if not request.user.is_authenticated and request.method != "OPTIONS":
            if not request.path in settings.EXCLUDED_PAHTS:
                return HttpResponse("Unauthorized", 401)

        response = get_response(request)

        # Code to be executed for each request/response after
        # the view is called.

        return response

    return middleware

The middlewares are applied in the following order (my custom middleware last):

MIDDLEWARE = [
    "elasticapm.contrib.django.middleware.TracingMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "compliancedash.custom_middleware.require_auth.require_auth",
]

When inspecting the request, the user.is_authenticated variable is set to false, even though the access token is included in the headers. It seems to me like the authentication process happens after my custom middleware is applied. When inspecting the request, without the middleware applied, in a DRF view, the user.is_authenticated variable is true and the authenticated user is resolved correctly.

Am I missing something? Is there any way to "manually" authenticate the user, perhaps?

alinjie commented 5 years ago

My bad. I added this, which solves my issue:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}