Studio (the Open edX CMS) has no token API for authoring: every authoring
endpoint is guarded by a Django session cookie + CSRF token. This skill
captures that session from a browser the user signs into, verifies it against
Studio and the LMS (the LMS side shares the same login), and stores it
in studio.env. Run it once per host; sessions last about two weeks.
Preflight — run this before any other Studio skill
node .claude/skills/iblai-api-studio-auth/scripts/studio-login.mjs --check && grep -q '^STUDIO_ORG=.\+' studio.env && echo "STUDIO_ORG set" || echo "MISSING: run /iblai-api-studio-auth and set STUDIO_ORG in studio.env"
--check prints studio: ok · lms: ok and exits 0 when both sessions work,
exits 2 when the file is missing or a session expired. Every other
/iblai-api-studio-* skill starts with this line; when it fails, come back
here instead of retrying calls.
What gets written
| Key | Value | Used by |
|---|---|---|
STUDIO_URL |
https://studio.learn.iblai.app (default) — test server https://studio.learn.iblai.org |
every /iblai-api-studio-* skill |
LMS_URL |
https://learn.iblai.app (derived: strip studio.) |
/iblai-api-studio-lms, verification links |
LMS_APP_URL |
https://lms.ibl.ai (test: https://lms.iblai.org) — the app users open |
course URLs in reports |
STUDIO_ORG |
org key used as the org when creating courses (set by hand once; the script preserves it) |
/iblai-api-studio-course-create |
STUDIO_USERNAME |
the signed-in username (read from GET $LMS_URL/api/user/v1/me) |
LMS calls that need username |
STUDIO_SESSION / STUDIO_CSRF |
Studio cookies studio_session_id / csrftoken |
Studio -b cookie + X-CSRFToken |
LMS_SESSION / LMS_CSRF |
LMS cookies sessionid / csrftoken |
LMS -b cookie + X-CSRFToken |
STUDIO_SESSION_EXPIRES |
cookie expiry (ISO) | "is it still valid?" without a request |
The file is chmod 600 and the script adds it to .gitignore. Never print a
session value — the script prints **** + last four only.
Reading the file from a script (not just the shell)
Session values are single-quoted because Django session ids contain |
and :. set -a; . ./studio.env; set +a strips the quotes; a naive parser
does not, and Studio answers a malformed cookie with HTTP 500 and an HTML
"Studio Server Error" page, not 401/403. Strip them:
import re
env = {m[1]: m[2].strip().strip("'") for m in (re.match(r"^([A-Z_]+)=(.*)$", l) for l in open("studio.env")) if m}
const env = Object.fromEntries([...fs.readFileSync("studio.env","utf8").matchAll(/^([A-Z_]+)=(.*)$/gm)].map(([,k,v]) => [k, v.trim().replace(/^'(.*)'$/, "$1")]));
Sanity check: len(env["STUDIO_SESSION"]) is the cookie length (typically 187), with no quote characters.
Steps
-
Run the capture script from the project directory (needs Node 18+; uses Google Chrome / Edge if installed, else Playwright's Chromium; installs
playwright-coreinto~/.cache/iblai-studio/once if no Playwright is resolvable — no browser download):node .claude/skills/iblai-api-studio-auth/scripts/studio-login.mjs # production node .claude/skills/iblai-api-studio-auth/scripts/studio-login.mjs --studio https://studio.learn.iblai.org # test serverA browser window opens on
$STUDIO_URL/home. Tell the user to sign in there themselves (Studio redirects to the LMS login, then back). Never type credentials for them. The script polls once a second for thestudio_session_id+sessionidcookies, verifies both, writes the file, closes the browser, and prints one line:connected: studio.learn.iblai.app as <username> · session ****ab12 · expires 2026-09-28 → /path/studio.envFlags:
--out <file>(default./studio.env),--lms <url>,--lms-app <url>,--profile <dir>(browser profile, default~/.cache/iblai-studio/profile-<host>— re-runs skip the login while the browser session lasts),--timeout <s>(default 300). Exit 1 = timed out waiting for a sign-in. -
Set the org key (once):
STUDIO_ORG=<org key>instudio.env. Course creation needs the key of the organization the user authors in (the path segment after/platform/on os.ibl.ai, orPLATFORMiniblai.env). If you do not know it, list the user's roles —org-instructorrows carry it:set -a; . ./studio.env; set +a curl -s -b "studio_session_id=$STUDIO_SESSION; csrftoken=$STUDIO_CSRF" -H "Accept: application/json" \ "$STUDIO_URL/api/ibl/users/manage/roles/?username=$STUDIO_USERNAME" # [{"role":"org-instructor","org":"acme","course":""}, …] -
Verify with the preflight line above.
No browser available?
Have the user copy the cookies from DevTools (Application → Cookies) on a
signed-in Studio tab: studio_session_id, csrftoken; and on a signed-in LMS
tab: sessionid. Write studio.env yourself with the keys above (single-quote
the values), chmod 600, add it to .gitignore, then run --check. Ask for
cookies in a file or env var, never pasted into the chat when avoidable.
How every Studio call is made (the snippet other skills reuse)
set -a; . ./studio.env; set +a
S=(-s -b "studio_session_id=$STUDIO_SESSION; csrftoken=$STUDIO_CSRF" -H "X-CSRFToken: $STUDIO_CSRF" \
-H "Origin: $STUDIO_URL" -H "Referer: $STUDIO_URL/" -H "Accept: application/json" -H "Content-Type: application/json")
curl "${S[@]}" "$STUDIO_URL/api/contentstore/v2/home/courses"
Accept: application/jsonis mandatory — the same URLs return HTML pages without it.X-CSRFToken+Origin/Refereron the Studio origin are required on every POST/PUT/PATCH/DELETE (Django CSRF checksOriginon HTTPS).- LMS calls use
-b "sessionid=$LMS_SESSION; csrftoken=$LMS_CSRF" -H "X-CSRFToken: $LMS_CSRF" -H "Origin: $LMS_URL" -H "Referer: $LMS_URL/".
Reading failures
| You see | It means | Do |
|---|---|---|
302 → /login/… (Studio), 401 (LMS API) |
session expired or missing | re-run step 1 |
403 mentioning CSRF |
X-CSRFToken/Origin missing or stale csrftoken |
resend with the snippet; re-run step 1 if it persists |
403 {"error":"Permission denied"} / "You do not have permission…" |
signed in, but not staff/instructor on that org or course | /iblai-api-studio-team, /iblai-api-management |
500 + HTML "Studio Server Error" on a documented call |
almost always a malformed cookie (quotes, truncation, wrong file) — or a parameter the skill flags as broken | run --check; compare cookie length; re-read the skill's notes |
HTML page instead of JSON with 200 |
missing Accept: application/json |
add the header |
Notes
- One
studio.env= one host. Switching between production and the test server means re-running with--studio(or keeping two files and--out). - The script never sees or stores a password; the browser profile directory
under
~/.cache/iblai-studio/holds the browser's own cookies — delete it to force a fresh login. - Studio's session is created by an OAuth round trip through the LMS
(
/login/edx-oauth2/→ LMS login →/complete/edx-oauth2/), which is why the LMSsessionidis always available too and why both are captured. - Headless/CI: there is no service-account path for Studio authoring. For
org-level user and role administration use
/iblai-api-management(Api-Token); for catalog and enrollment data use/iblai-api-catalog. - Family index and build order:
/iblai-api-studio.