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

[django_auth_adfs:157] Error decoding signature: Signature verification failed #346

Open a-kuchinski opened 1 month ago

a-kuchinski commented 1 month ago

I've been trying to configure my Django REST Framework app to use django-auth-adfs for Microsoft Entra ID (former Azure AD B2C), but I have an error with signature verification.

Django==5.0.6
djangorestframework==3.15.1
django-auth-adfs==1.14.0

Prior to test oauth2/login page, I tried the example listed in this page

https://django-auth-adfs.readthedocs.io/en/latest/rest_framework.html

with a few additions to make it work with Microsoft Entra:

import os
from pprint import pprint
import requests
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

# User credentials
user = os.getenv('USER_EMAIL')
password = os.getenv('USER_PASSWORD')

# OAuth 2.0 token endpoint
tenant_id = os.getenv('AAD_B2C_TENANT_ID')
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/token"

# Client (application) ID and secret
client_id = os.getenv('AAD_B2C_CLIENT_ID')
client_secret = os.getenv('AAD_B2C_CLIENT_SECRET')

# API scope
api_scope = f"User.Read api://{client_id}/Backend.Read"

# Prepare the payload
payload = {
    "grant_type": "password",
    "response_type": "token",
    "client_id": client_id,
    "client_secret": client_secret,
    "username": user,
    "password": password,
    "resource": client_id,
    "scope": f"openid profile email {api_scope}",
}

# Request an access token
response = requests.post(
    token_url,
    data=payload,
    verify=True  # Ensure SSL certificates are verified
)

# Check for errors
try:
    response.raise_for_status()
    response_data = response.json()
    access_token = response_data['access_token']
    print('Access token retrieved successfully.')
except requests.exceptions.HTTPError as err:
    print('Error retrieving access token:')
    print(response.text)
    raise SystemExit(err)

# Make a request to the API
headers = {
    'Accept': 'application/json',
    'Authorization': f'Bearer {access_token}',
}

api_response = requests.get(
    'http://localhost:8000/api/contract',
    headers=headers,
    verify=True
)

# Check for errors
try:
    api_response.raise_for_status()
    # Print the API response
    pprint(api_response.json())
except requests.exceptions.HTTPError as err:
    print('API request failed:')
    print(api_response.text)
    raise SystemExit(err)

And the auth works in this case, I successfully getting the access_token and using it to call my app endpoint http://localhost:8000/api/contract.

However, when i'm trying to authorize within my api in the Chrome browser via oauth2/login, I keep getting [django_auth_adfs:157] Error decoding signature: Signature verification failed error.

Here is ADFS config in my DRF app settings.py

...

AUTHENTICATION_BACKENDS = (
    'django_auth_adfs.backend.AdfsAccessTokenBackend',
    'django_auth_adfs.backend.AdfsAuthCodeBackend',
)

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'django_auth_adfs.rest_framework.AdfsAccessTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ),
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ],
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 50,  # Default limit if not specified in request
}

AUTH_ADFS = {
    'AUDIENCE': AAD_B2C_CLIENT_ID,
    'CLIENT_ID': AAD_B2C_CLIENT_ID,
    'CLIENT_SECRET': AAD_B2C_CLIENT_SECRET,
    # 'CLAIM_MAPPING': {'first_name': 'given_name',
    #                   'last_name': 'family_name',
    #                   'email': 'upn'},
    'GROUPS_CLAIM': None,
    'MIRROR_GROUPS': False,
    'VERSION': 'v2.0',
    'USERNAME_CLAIM': 'email',
    'DISABLE_SSO': True,
    'TENANT_ID': AAD_B2C_TENANT_ID,
    'RELYING_PARTY_ID': AAD_B2C_CLIENT_ID,
    'CREATE_NEW_USERS': True,
    'LOGIN_EXEMPT_URLS': [
        # '^$',
        '^api',
        # '^admin',
    ],
    'SCOPES': ["openid", "profile", "email", "User.Read", "api://6aeb143f-59f8-4746-ba56-0c489aee6a1f/Backend.Read"],
}

LOGIN_URL = 'django_auth_adfs:login'
LOGOUT_URL = 'django_auth_adfs:logout'
LOGIN_REDIRECT_URL = '/admin/'
REDIRECT_URL = 'django_auth_adfs:callback'

urls.py

"""
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')),
    path('oauth2/', include('django_auth_adfs.urls')),
]

I also tried to change urls to path('oauth2/', include('django_auth_adfs.drf_urls')),as suggested in the docs. But it cause a backend errordjango.urls.exceptions.NoReverseMatch: 'django_auth_adfs' is not a registered namespace`.

Upvote & Fund

Fund with Polar

tim-schilling commented 1 month ago

Have you taken a look at the JWT without verifying the signature to confirm it looks correct? You can do so by disabling the verify_ aspects here: https://github.com/snok/django-auth-adfs/blob/378f14129d774ac035804e09f7b6a1b3f5a3f71f/django_auth_adfs/backend.py#L128

There may be another call you need to make. The goal here isn't to avoid the error by disabling the checks, but to understand what is being sent so you can make changes to get things to work appropriately.