CORS Issue - fixed - but maybe it needs more attention?

I discussed this shortly on Slack, but it may need more attention. I don’t want to bury a potential issue for future operators on Slack.

When trying to create or modify a course, I was faced with CORS issues, it came from me accessing the CMS with https://apps.openedx.mydomain.com/authoring/home url instead of https://studio.openedx.mydomain.com/. The second one redirect to the first and set a cookie it seems.

The natural thing to do, is to put the first URL in favorite on the browser. And later when skipping the studio subdomain, I am not faced with a login screen on the studio url (authoring/home) but everything is displayed as if I am correctly connected, I can for example see every course, but others actions will fail an show a CORS error in the console.

So the fix is to always visit and put in favorite https://studio.openedx.mydomain.com/ instead of https://apps.openedx.mydomain.com/authoring/home. And additionally removing all cookies once (for chrome: chrome://settings/content/all?search=cookie).

Any opinion on that?

I’m not sure who to tag in this one. Maybe @Tim_McCormack or @feanil ?

Not my area of expertise. It does sound like a pretty annoying bug. Maybe course authoring MFE or studio is making some overly limiting assumptions about the domain structure of deployments.

@AmbroiseRabier if you hit them in the wrong order, what URLs fail with CORS errors? And what are the HTTP verbs involved?

CORS issue suggest that the authoring UI is trying to hit the studio backend APIs and the backend is rejecting the calls. The fix is probably to update the studio settings to allow requests from the apps subdomain if it isn’t already doing that but it may be that there are requests being made that should be made differently also so more information would be useful.

I mostly tried with creating a course. Also on the course creation form, the existing organizations are loaded in the organization input. When URLs are hit in wrong order, the existing organizations won’t be loaded and the suggestions stays blank.
I would bet that all update/create/delete operation would be faced with CORS, and being able to connect through on the CMS and view the list of courses is probably the exception.

The preflight OPTIONS request returns 200 but does not include the required CORS headers (Access-Control-Allow-Origin), so the browser blocks the request.

Request URL
https://<OPENEDX_DOMAIN>/oauth2/authorize?client_id=cms-sso&redirect_uri=https%3A%2F%2F<STUDIO_DOMAIN>%2Fcomplete%2Fedx-oauth2%2F%3Fredirect_state%3D<STATE>&state=<STATE>&response_type=code&scope=user_id+profile+email

Request Method
OPTIONS
Status Code
200 OK
Remote Address
<IP_ADDRESS>:443
Referrer Policy
strict-origin-when-cross-origin
alt-svc

------ RESPONSE HEADERS
h3=":443"; ma=2592000
content-length
0
content-type
text/html; charset=utf-8
date
Tue, 17 Mar 2026 12:44:28 GMT
server
Caddy
vary
origin, Cookie

------ REQUEST HEADERS
:authority
<OPENEDX_DOMAIN>
:method
OPTIONS
:path
/oauth2/authorize?client_id=cms-sso&redirect_uri=https%3A%2F%2F<STUDIO_DOMAIN>%2Fcomplete%2Fedx-oauth2%2F%3Fredirect_state%3D<STATE>&state=<STATE>&response_type=code&scope=user_id+profile+email
:scheme
https
accept
*/*
accept-encoding
gzip, deflate, br, zstd
accept-language
fr-FR,fr;q=0.9
access-control-request-headers
use-jwt-cookie,x-csrftoken
access-control-request-method
GET
origin
null
priority
u=1, i
referer
https://apps.<OPENEDX_DOMAIN>/
sec-fetch-dest
empty
sec-fetch-mode
cors
sec-fetch-site
same-site
user-agent
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36

CORS is managed via the GitHub - adamchainz/django-cors-headers: Django app for handling the server headers required for Cross-Origin Resource Sharing (CORS) · GitHub tool. We still set the historicCORS_ORIGIN_WHITELIST setting. Are your frontend domains listed there in your config? This older setting takes precedence over newer settings if you’re just setting the newer setting, it may be that it’s being ignored because the openedx-platform settings sets this old value to an empty list by default.

I had used a new instance to reduce the chances that this bug was related to something I did. It should be reproducible on any new Open edx / Tutor instance.

Nevertheless, on my current prod I have:

/home/user/.local/share/tutor/env/apps/openedx/settings/cms/production.py:365:CORS_ORIGIN_WHITELIST.append("https://studio.openedx.mydomain.com")
/home/user/.local/share/tutor/env/apps/openedx/settings/cms/production.py:377:CORS_ORIGIN_WHITELIST.append("https://apps.openedx.mydomain.com")

/home/user/.local/share/tutor/env/apps/openedx/settings/lms/production.py:400:CORS_ORIGIN_WHITELIST.append("https://openedx.mydomain.com")
/home/user/.local/share/tutor/env/apps/openedx/settings/lms/production.py:503:CORS_ORIGIN_WHITELIST.append("https://apps.openedx.mydomain.com")

From my point of view this seems correct.

Hi everyone,

We recently ran into this exact same issue (CORS errors and 302 redirects when trying to create a new course via the Authoring MFE) and spent some time debugging the network traces. I wanted to share our findings and the definitive solution, as this is essentially an architectural clash between modern JWT authentication and legacy Studio cookie-based authentication.

The Problem & Root Cause

The Authoring MFE communicates with the backend in two different ways:

  1. Modern APIs (JWT): Most of the MFE uses modern API endpoints (like /api/contentstore/v1/home) which authenticate seamlessly using the JWT token provided by the LMS. These calls work perfectly.

  2. Legacy Endpoints (Cookies): Certain actions, such as clicking “New Course”, trigger background XHR/Fetch requests to legacy Django views in Studio (specifically the /organizations endpoint to populate the dropdown). These legacy endpoints do not accept JWT; they strictly require the studio_session_id cookie.

The Breaking Point: If your MFE’s LOGIN_URL is configured to point directly to the LMS (e.g., https://lms.yourdomain.com/login), the user authenticates, gets a JWT, and is redirected straight back to the MFE. Because the user bypassed the Studio backend during login, the studio_session_id cookie is never initialized.

When the MFE makes an XHR request to /organizations without that cookie, Studio treats the user as anonymous and returns an HTTP 302 Redirect to the SSO login. Since this is an AJAX request, the browser attempts to follow the redirect silently, hits the SSO server, and gets blocked by CORS policies.

Steps to Reproduce

  1. In your mfe_config, ensure the Authoring MFE’s LOGIN_URL is pointing directly to the LMS.

  2. Open a new incognito window (or clear your cookies) and navigate directly to the Authoring MFE (https://apps.yourdomain.com/authoring/home).

  3. Log in using your credentials. (Notice that the MFE loads your courses successfully because it uses JWT).

  4. Click the “New course” button.

  5. Nothing happens in the UI, but if you check the Network tab/Console, you will see a CORS error caused by a 302 Found response from the /organizations endpoint redirecting to OAuth.

The Solution

To fix this, you need to ensure the user passes through the Studio backend so the session cookie is correctly stamped.

You must update the LOGIN_URL configuration for the Authoring MFE so that it points to Studio’s login rather than the LMS directly.

  • Change LOGIN_URL to: https://studio.yourdomain.com/login/

By doing this, when an unauthenticated user accesses the MFE, they are routed through Studio’s login endpoint. Studio will handle the OAuth2 flow with the LMS, properly execute the callback (/complete/edx-oauth2/), initialize the studio_session_id cookie, and then land the user on the MFE.

Once that cookie is present in the browser, the XHR calls to /organizations return a 200 OK, and the course creation form opens without any CORS errors.

I have attached a video demonstrating both scenarios to make this crystal clear: first, accessing directly from the Authoring MFE path (/authoring/home) to show how it fails, and second, accessing from the base Studio URL (without any path) to show how it successfully initializes the session and works perfectly.

Hope this helps save some debugging time for future operators!