duplxey
duplxey

Reputation: 310

Fetch request from Chrome extension results in 403 Forbidden

Whenever I try to send a POST request from background.js of my Chrome extension I get 403 Forbidden error. If I execute the same code outside my Chrome extension it works normally. My API doesn't require any authentication.

Request code:

let formData = new FormData();
formData.append('message', "This is a test message.");

fetch('https://myapi.com/add', {
    body: formData,
    method: "post"
}).then(r => console.log(r));

Request response:

403 Forbidden response from background.js

I also checked my Apache 2 access.log and everything seems to look normal:

x.x.x.x - - [13/Sep/2020:17:02:30 +0000] "POST add HTTP/1.1" 403 581 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36"

Are there any Chrome extension policies that could block my request?
Do I need to add any special permissions to my manifest.json?
Do I need to make any changes to my API (take this as a reserve, because it works normally outside Chrome extension)?

Upvotes: 3

Views: 4601

Answers (3)

bert
bert

Reputation: 21

I just had the same problem. Thanks so much for posting this. I also work on a Chrome extension with Django backend.

For me deleting 'rest_framework.authentication.SessionAuthentication' from the 'DEFAULT_AUTHENTICATION_CLASSES' did it, but I kept the 'oauth2_provider.contrib.rest_framework.OAuth2Authentication'.

According to this medium article "rest_framework.authenication.SessionAuthentication' is only needed if you want to keep browsable API. I have CSRF and CORS enabled. So I think my setup is very secure.

Below is my settings.py file.

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '*xbmoy2yt4%l=od-dm*w$dxpl+rb(n#rmv0n&0x$a@+io!j+++'

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

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'audio.apps.AudioConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'accounts.apps.AccountsConfig',
    'rest_framework',
    'oauth2_provider',
    #'corsheaders',
]

MIDDLEWARE = [
    #'corsheaders.middleware.CorsMiddleware',
    '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',
    'oauth2_provider.middleware.OAuth2TokenMiddleware'
]

ROOT_URLCONF = 'zeno.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, '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 = 'zeno.wsgi.application'
CORS_ORIGIN_ALLOW_ALL = True


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

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/2.2/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/2.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

CORS_ORIGIN_ALLOW_ALL = False


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

STATIC_URL = '/static/'
LOGIN_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'

PROJECT_ROOT = os.path.normpath(os.path.dirname(__file__))
STATICFILES_DIRS = (
    os.path.join(PROJECT_ROOT, '..', 'static'),
)

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
        #'rest_framework.authentication.SessionAuthentication', # To keep the Browsable API
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    # 'DEFAULT_PAGINATION_CLASS': (
    #     'rest_framework.pagination.PageNumberPagination',
    # ),
    # 'DEFAULT_PERMISSION_CLASSES': (
    #     'rest_framework.permissions.IsAuthenticated',
    # ),
    #'PAGE_SIZE': 10
}


AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend', # To keep the Browsable API
    'oauth2_provider.backends.OAuth2Backend',
)

#STATIC_ROOT = os.path.join(BASE_DIR, "static/")
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'test.com']

AUTH_USER_MODEL = 'accounts.CustomUser'

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
#EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
#EMAIL_FILE_PATH = os.path.join(BASE_DIR, "sent_emails")

Upvotes: 0

duplxey
duplxey

Reputation: 310

The problem was in Django REST framework. Apparently it has a few authentication classes enabled by default. Disabling/setting them to an empty array in settings.py will get rid of the annoying '403 Forbidden' error.

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
    ]
}

EDIT: I don't know how safe this is, but at least it works.

Upvotes: 0

Ryan Wheale
Ryan Wheale

Reputation: 28440

Edit: Read your server logs. The server is probably telling you exactly what the error is. Make sure debugging is enabled.

403 errors are not really emitted from a server without good reason - and that reason is usually specific to your application. A 403 is generally a permission issue: we know who you are, but you're not authorized to do what you're trying to do.

There might be a header present (or missing) which is preventing the request from making any changes (in your case, a POST request). For example, your server might be setting cookies to prevent CSRF attacks. Many times the server will not validate tokens on GET requests, which may explain why GET requests work in your case but not POST requests.

If you can search your server code, I'm willing to be there is something like this pseudocode:

if (requestIsMissingCSRFToken()) {
  throw new Error(STATUS.FORBIDDEN)
}

Edit: Some relevant links

Upvotes: 1

Related Questions