djangocurldjango-rest-frameworkazure-active-directorypostman

How to get authorized in Microsoft AD using curl?


I have a django project which uses Azure Active directory for the authentication and I have set up its API using rest_framework. At the moment, I am trying to access this API using curl. I am able to generate an access token, but when I send a request using it, I get the Microsoft Login Page, as if I do not have any access token.

I generated an access token using the following code:

curl -X POST https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token -H "Content-Type: application/x-www-form-urlencoded" -d "client_id={client_id}" -d "client_secret={client_secret}" -d "grant_type=client_credentials" -d "redirect_uri=https://{my_domain}/oauth2/callback" -d "scope=api://{client_id}/.default openid profile email" 

Then, using the jwt token that I recieved, I did:

curl -X GET -L https://<my_domain>/api/benches/1/status/ -H "Authorization: Bearer <token>"

And what I get, is the Microsoft login page, which its first few lines look like this:

<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html dir="ltr" class="" lang="en">
<head>
    <title>Sign in to your account</title>
.
.
.

Also, the result of the -v (verbose) looks like this:

Note: Unnecessary use of -X or --request, GET is already inferred.
* Host <my_domain>:443 was resolved.
* IPv6: (none)
* IPv4: ip
*   Trying ip:443...
* Connected to <my_domain> (ip) port 443
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
* using HTTP/1.x
> GET /api/benches/1/status/ HTTP/1.1
> Host: <my_domain>
> User-Agent: curl/8.7.1
> Accept: */*
> Authorization: Bearer <token>
>
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
< HTTP/1.1 302 Found
< Server: nginx/1.14.1
< Date: Wed, 17 Jul 2024 16:08:34 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 0
< Connection: keep-alive
< Location: /oauth2/login?next=/api/benches/1/status/
< X-Frame-Options: DENY
< Vary: Cookie
< X-Content-Type-Options: nosniff
< Referrer-Policy: same-origin
<
* Request completely sent off
* Connection #0 to host <my_domain> left intact

Also, I am doing this for my organization, is it possible that there is a missing permission that I might need in my Azure application?

Here is my Django settings.py for my REST configuration:

"""
Django settings for core project.

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

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

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

from pathlib import Path

import os
from dotenv import load_dotenv
load_dotenv()

# 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/5.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')

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

ALLOWED_HOSTS = ['my_domain']

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

# Application definition

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    # Installed apps
    'django_auth_adfs',
    'bench',
    'api',
    'rest_framework',
]

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

    # Other Middlewares
    'django_auth_adfs.middleware.LoginRequiredMiddleware',
]

ROOT_URLCONF = "core.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        'DIRS': [
            BASE_DIR / 'static/templates',
        ],
        "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 = "core.wsgi.application"


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

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


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


# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "GB"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/

STATIC_URL = '/static/'
MEDIA_URL = '/media/'

MEDIA_ROOT = BASE_DIR / 'media'

STATIC_ROOT = '/root/to/static'

STATICFILES_DIRS = [
    BASE_DIR / 'static',
]

# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

LOGIN_URL = 'django_auth_adfs:login'
LOGOUT_URL = 'django_auth_adfs:logout'
LOGIN_REDIRECT_URL = 'https://my_domain/oauth2/callback'


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

client_id = os.getenv('client_id')
client_secret = os.getenv('client_secret')
tenant_id = os.getenv('tenant_id')


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': 'roles',
    'MIRROR_GROUPS': True,
    'USERNAME_CLAIM': 'email',
    'TENANT_ID': tenant_id,
    'RELYING_PARTY_ID': client_id,
    'LOGIN_EXEMPT_URLS': [
        '^api',  # Assuming you API is available at /api
    ],
}

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'django_auth_adfs.rest_framework.AdfsAccessTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
}

Here is a screenshot of my permissions blade. I am using Benches.Change.All which is under my application name.

enter image description here


Solution

  • Note that: Client credential flow does not support delegated permissions type. If you are making use of client credential flow to generate the token, then you need to grant application type Api permissions to the Microsoft Entra ID application.

    You are getting the error as you granted delegated API permissions and making use of client credential flow to generate the token.

    To resolve the error. generate the token using Authorization code flow and then call the API.

    I granted API permissions to the Application:

    enter image description here

    Configure redirect URL as web with https://oauth.pstmn.io/v1/callback' in the application. You can also pass your custom redirect url.

    Now generate the access token by selecting Authorization code flow:

    Grant type: Authorization code 
    
    Callback URL: https://oauth.pstmn.io/v1/callback
    Auth URL:  https://login.microsoftonline.com/TenantId/oauth2/v2.0/authorize
    Token URL : https://login.microsoftonline.com/TenantId/oauth2/v2.0/token
    Client ID : ClientId
    Client Secret : ClientSecret
    Scope: api://ClientID/Benches.Change.All
    

    enter image description here

    And click on Generate access token and you will be redirected to browser to sign-in:

    enter image description here

    The access token will be generated:

    enter image description here

    Decode the access token in jwt.ms: Welcome! and make sure the scp claim have value Benches.Change.All

    enter image description here

    Now use this access token to call the API and it will be successful without any error.