Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ lms/envs/private.py
cms/envs/private.py
.venv/
CLAUDE.md
.claude/
AGENTS.md
# end-noclean

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
Testing SAML Authentication Locally with MockSAML
==================================================

This guide walks through setting up and testing SAML authentication in a local Open edX devstack environment using MockSAML.com as a test Identity Provider (IdP).

Overview
--------

SAML (Security Assertion Markup Language) authentication in Open edX requires three configuration objects to work together:

1. **SAMLConfiguration**: Configures the Service Provider (SP) metadata - entity ID, keys, and organization info
2. **SAMLProviderConfig**: Configures a specific Identity Provider (IdP) connection with metadata URL and attribute mappings
3. **SAMLProviderData**: Stores the IdP's metadata (SSO URL, public key) fetched from the IdP's metadata endpoint

**Critical Requirement**: The SAMLConfiguration object MUST have the slug "default" because this value is hardcoded in the authentication execution path at ``common/djangoapps/third_party_auth/models.py:906``.

Prerequisites
-------------

* Local Open edX devstack running
* Access to Django admin at http://localhost:18000/admin/
* MockSAML.com account (free service for SAML testing)

Step 1: Configure SAMLConfiguration
------------------------------------

The SAMLConfiguration defines your Open edX instance as a SAML Service Provider (SP).

1. Navigate to Django Admin → Third Party Auth → SAML Configurations
2. Click "Add SAML Configuration"
3. Configure with these **required** values:

============ ===================================================
Field Value
============ ===================================================
Site localhost:18000
**Slug** **default** (MUST be "default" - hardcoded in code)
Entity ID https://saml.example.com/entityid
Enabled ✓ (checked)
============ ===================================================

4. For local testing with MockSAML, you can leave the keys blank.

5. Optionally configure Organization Info (use default or customize):

.. code-block:: json

{
"en-US": {
"url": "http://localhost:18000",
"displayname": "Local Open edX",
"name": "localhost"
}
}

6. Click "Save"

Step 2: Configure SAMLProviderConfig
-------------------------------------

The SAMLProviderConfig connects to a specific SAML Identity Provider (MockSAML in this case).

1. Navigate to Django Admin → Third Party Auth → Provider Configuration (SAML IdPs)
2. Click "Add Provider Configuration (SAML IdP)"
3. Configure with these values:

========================= ===================================================
Field Value
========================= ===================================================
Name Test Localhost (or any descriptive name)
Slug default (to match test URLs)
Backend Name tpa-saml
Entity ID https://saml.example.com/entityid
Metadata Source https://mocksaml.com/api/saml/metadata
Site localhost:18000
SAML Configuration Select the SAMLConfiguration created in Step 1
Enabled ✓ (checked)
Visible ☐ (unchecked for testing)
Skip hinted login dialog ✓ (checked - recommended)
Skip registration form ✓ (checked - recommended)
Skip email verification ✓ (checked - recommended)
Send to registration first ✓ (checked - recommended)
========================= ===================================================

4. Leave all attribute mappings (User ID, Email, Full Name, etc.) blank to use defaults
5. Click "Save"

**Important**: The Entity ID in SAMLProviderConfig MUST match the Entity ID in SAMLConfiguration.

Step 3: Set IdP Data
--------------------

The SAMLProviderData stores metadata from the Identity Provider (MockSAML), create a record with

* **Entity ID**: https://saml.example.com/entityid
* **SSO URL**: https://mocksaml.com/api/saml/sso
* **Public Key**: The IdP's signing certificate
* **Expires At**: Set to 1 year from fetch time


Step 4: Test SAML Authentication
---------------------------------

1. Navigate to: http://localhost:18000/auth/idp_redirect/saml-default
2. You should be redirected to MockSAML.com
3. Complete the authentication on MockSAML - just click "Sign In" with whatever is in the form.
4. You should be redirected back to Open edX
5. If this is a new user, you'll see the registration form
6. After registration, you should be logged in

Expected Behavior
^^^^^^^^^^^^^^^^^

1. Initial redirect to MockSAML (https://mocksaml.com/api/saml/sso)
2. MockSAML displays the login page
3. After authentication, MockSAML POSTs the SAML assertion back to Open edX
4. Open edX validates the assertion and creates/logs in the user
5. User is redirected to the dashboard or registration form (if new user)

Reference Configuration
-----------------------

Here's a summary of a working test configuration:

**SAMLConfiguration** (id=6):

* Site: localhost:18000
* Slug: **default**
* Entity ID: https://saml.example.com/entityid
* Enabled: True

**SAMLProviderConfig** (id=11):

* Name: Test Localhost
* Slug: default
* Entity ID: https://saml.example.com/entityid
* Metadata Source: https://mocksaml.com/api/saml/metadata
* Backend Name: tpa-saml
* Site: localhost:18000
* SAML Configuration: → SAMLConfiguration (id=6)
* Enabled: True

**SAMLProviderData** (id=3):

* Entity ID: https://saml.example.com/entityid
* SSO URL: https://mocksaml.com/api/saml/sso
* Public Key: (certificate from MockSAML metadata)
* Fetched At: 2026-02-27 18:05:40+00:00
* Expires At: 2027-02-27 18:05:41+00:00
* Valid: True

**MockSAML Configuration**:

* SP Entity ID: https://saml.example.com/entityid
* ACS URL: http://localhost:18000/auth/complete/tpa-saml/
* Test User Attributes: email, firstName, lastName, uid
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated migration for adding optional checkbox skip configuration field

from django.db import migrations, models
import django.utils.translation


class Migration(migrations.Migration):

dependencies = [
('third_party_auth', '0013_default_site_id_wrapper_function'),
]

operations = [
migrations.AddField(
model_name='samlproviderconfig',
name='skip_registration_optional_checkboxes',
field=models.BooleanField(
default=False,
help_text=django.utils.translation.gettext_lazy(
"If enabled, optional checkboxes (marketing emails opt-in, etc.) will not be rendered "
"on the registration form for users registering via this provider. When these checkboxes "
"are skipped, their values are inferred as False (opted out)."
),
),
),
]
8 changes: 8 additions & 0 deletions common/djangoapps/third_party_auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,14 @@ class SAMLProviderConfig(ProviderConfig):
"immediately after authenticating with the third party instead of the login page."
),
)
skip_registration_optional_checkboxes = models.BooleanField(
default=False,
help_text=_(
"If enabled, optional checkboxes (marketing emails opt-in, etc.) will not be rendered "
"on the registration form for users registering via this provider. When these checkboxes "
"are skipped, their values are inferred as False (opted out)."
),
)
other_settings = models.TextField(
verbose_name="Advanced settings", blank=True,
help_text=(
Expand Down
2 changes: 2 additions & 0 deletions lms/static/js/student_account/views/RegisterView.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
);
this.currentProvider = data.thirdPartyAuth.currentProvider || '';
this.syncLearnerProfileData = data.thirdPartyAuth.syncLearnerProfileData || false;
this.skipRegistrationOptionalCheckboxes = data.thirdPartyAuth.skipRegistrationOptionalCheckboxes || false;
this.errorMessage = data.thirdPartyAuth.errorMessage || '';
this.platformName = data.platformName;
this.autoSubmit = data.thirdPartyAuth.autoSubmitRegForm;
Expand Down Expand Up @@ -156,6 +157,7 @@
fields: fields,
currentProvider: this.currentProvider,
syncLearnerProfileData: this.syncLearnerProfileData,
skipRegistrationOptionalCheckboxes: this.skipRegistrationOptionalCheckboxes,
providers: this.providers,
hasSecondaryProviders: this.hasSecondaryProviders,
platformName: this.platformName,
Expand Down
18 changes: 10 additions & 8 deletions lms/templates/student_account/register.underscore
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,16 @@
<div class="form-fields <% if (context.is_require_third_party_auth_enabled) { %>hidden<% } %>">
<%= context.fields /* xss-lint: disable=underscore-not-escaped */ %>

<div class="form-field checkbox-optional_fields_toggle">
<input type="checkbox" id="toggle_optional_fields" class="input-block checkbox"">
<label for="toggle_optional_fields">
<span class="label-text-small">
<%- gettext("Support education research by providing additional information") %>
</span>
</label>
</div>
<% if (!context.skipRegistrationOptionalCheckboxes) { %>
<div class="form-field checkbox-optional_fields_toggle">
<input type="checkbox" id="toggle_optional_fields" class="input-block checkbox"">
<label for="toggle_optional_fields">
<span class="label-text-small">
<%- gettext("Support education research by providing additional information") %>
</span>
</label>
</div>
<% } %>

<button type="submit" class="action action-primary action-update js-register register-button">
<% if ( context.registerFormSubmitButtonText ) { %><%- context.registerFormSubmitButtonText %><% } else { %><%- gettext("Create Account") %><% } %>
Expand Down
Loading
Loading