Skip to content

Commit fdd2ea0

Browse files
committed
feat: make marketing email and research opt-in checkboxs selectively ignorable
We want to support a flow for SSO-enabled Enterprise customers who have agreed off-platform that none of their learners will opt-in to marketing emails or sharing research data. This change proposes to do so by adding an optional field that, when enabled, disables the presence of the two checkboxes on this registration form and sets their values to false. ENT-11401
1 parent 85e81b3 commit fdd2ea0

10 files changed

Lines changed: 673 additions & 28 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ lms/envs/private.py
1313
cms/envs/private.py
1414
.venv/
1515
CLAUDE.md
16+
.claude/
1617
AGENTS.md
1718
# end-noclean
1819

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
Testing SAML Authentication Locally with MockSAML
2+
==================================================
3+
4+
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).
5+
6+
Overview
7+
--------
8+
9+
SAML (Security Assertion Markup Language) authentication in Open edX requires three configuration objects to work together:
10+
11+
1. **SAMLConfiguration**: Configures the Service Provider (SP) metadata - entity ID, keys, and organization info
12+
2. **SAMLProviderConfig**: Configures a specific Identity Provider (IdP) connection with metadata URL and attribute mappings
13+
3. **SAMLProviderData**: Stores the IdP's metadata (SSO URL, public key) fetched from the IdP's metadata endpoint
14+
15+
**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``.
16+
17+
Prerequisites
18+
-------------
19+
20+
* Local Open edX devstack running
21+
* Access to Django admin at http://localhost:18000/admin/
22+
* MockSAML.com account (free service for SAML testing)
23+
24+
Step 1: Configure SAMLConfiguration
25+
------------------------------------
26+
27+
The SAMLConfiguration defines your Open edX instance as a SAML Service Provider (SP).
28+
29+
1. Navigate to Django Admin → Third Party Auth → SAML Configurations
30+
2. Click "Add SAML Configuration"
31+
3. Configure with these **required** values:
32+
33+
============ ===================================================
34+
Field Value
35+
============ ===================================================
36+
Site localhost:18000
37+
**Slug** **default** (MUST be "default" - hardcoded in code)
38+
Entity ID https://saml.example.com/entityid
39+
Enabled ✓ (checked)
40+
============ ===================================================
41+
42+
4. For local testing with MockSAML, you can leave the keys blank.
43+
44+
5. Optionally configure Organization Info (use default or customize):
45+
46+
.. code-block:: json
47+
48+
{
49+
"en-US": {
50+
"url": "http://localhost:18000",
51+
"displayname": "Local Open edX",
52+
"name": "localhost"
53+
}
54+
}
55+
56+
6. Click "Save"
57+
58+
Step 2: Configure SAMLProviderConfig
59+
-------------------------------------
60+
61+
The SAMLProviderConfig connects to a specific SAML Identity Provider (MockSAML in this case).
62+
63+
1. Navigate to Django Admin → Third Party Auth → Provider Configuration (SAML IdPs)
64+
2. Click "Add Provider Configuration (SAML IdP)"
65+
3. Configure with these values:
66+
67+
========================= ===================================================
68+
Field Value
69+
========================= ===================================================
70+
Name Test Localhost (or any descriptive name)
71+
Slug default (to match test URLs)
72+
Backend Name tpa-saml
73+
Entity ID https://saml.example.com/entityid
74+
Metadata Source https://mocksaml.com/api/saml/metadata
75+
Site localhost:18000
76+
SAML Configuration Select the SAMLConfiguration created in Step 1
77+
Enabled ✓ (checked)
78+
Visible ☐ (unchecked for testing)
79+
Skip hinted login dialog ✓ (checked - recommended)
80+
Skip registration form ✓ (checked - recommended)
81+
Skip email verification ✓ (checked - recommended)
82+
Send to registration first ✓ (checked - recommended)
83+
========================= ===================================================
84+
85+
4. Leave all attribute mappings (User ID, Email, Full Name, etc.) blank to use defaults
86+
5. Click "Save"
87+
88+
**Important**: The Entity ID in SAMLProviderConfig MUST match the Entity ID in SAMLConfiguration.
89+
90+
Step 3: Set IdP Data
91+
--------------------
92+
93+
The SAMLProviderData stores metadata from the Identity Provider (MockSAML), create a record with
94+
95+
* **Entity ID**: https://saml.example.com/entityid
96+
* **SSO URL**: https://mocksaml.com/api/saml/sso
97+
* **Public Key**: The IdP's signing certificate
98+
* **Expires At**: Set to 1 year from fetch time
99+
100+
101+
Step 4: Test SAML Authentication
102+
---------------------------------
103+
104+
1. Navigate to: http://localhost:18000/auth/idp_redirect/saml-default
105+
2. You should be redirected to MockSAML.com
106+
3. Complete the authentication on MockSAML - just click "Sign In" with whatever is in the form.
107+
4. You should be redirected back to Open edX
108+
5. If this is a new user, you'll see the registration form
109+
6. After registration, you should be logged in
110+
111+
Expected Behavior
112+
^^^^^^^^^^^^^^^^^
113+
114+
1. Initial redirect to MockSAML (https://mocksaml.com/api/saml/sso)
115+
2. MockSAML displays the login page
116+
3. After authentication, MockSAML POSTs the SAML assertion back to Open edX
117+
4. Open edX validates the assertion and creates/logs in the user
118+
5. User is redirected to the dashboard or registration form (if new user)
119+
120+
Reference Configuration
121+
-----------------------
122+
123+
Here's a summary of a working test configuration:
124+
125+
**SAMLConfiguration** (id=6):
126+
127+
* Site: localhost:18000
128+
* Slug: **default**
129+
* Entity ID: https://saml.example.com/entityid
130+
* Enabled: True
131+
132+
**SAMLProviderConfig** (id=11):
133+
134+
* Name: Test Localhost
135+
* Slug: default
136+
* Entity ID: https://saml.example.com/entityid
137+
* Metadata Source: https://mocksaml.com/api/saml/metadata
138+
* Backend Name: tpa-saml
139+
* Site: localhost:18000
140+
* SAML Configuration: → SAMLConfiguration (id=6)
141+
* Enabled: True
142+
143+
**SAMLProviderData** (id=3):
144+
145+
* Entity ID: https://saml.example.com/entityid
146+
* SSO URL: https://mocksaml.com/api/saml/sso
147+
* Public Key: (certificate from MockSAML metadata)
148+
* Fetched At: 2026-02-27 18:05:40+00:00
149+
* Expires At: 2027-02-27 18:05:41+00:00
150+
* Valid: True
151+
152+
**MockSAML Configuration**:
153+
154+
* SP Entity ID: https://saml.example.com/entityid
155+
* ACS URL: http://localhost:18000/auth/complete/tpa-saml/
156+
* Test User Attributes: email, firstName, lastName, uid
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated migration for adding optional checkbox skip configuration field
2+
3+
from django.db import migrations, models
4+
import django.utils.translation
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('third_party_auth', '0013_default_site_id_wrapper_function'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='samlproviderconfig',
16+
name='skip_registration_optional_checkboxes',
17+
field=models.BooleanField(
18+
default=False,
19+
help_text=django.utils.translation.gettext_lazy(
20+
"If enabled, optional checkboxes (marketing emails opt-in, etc.) will not be rendered "
21+
"on the registration form for users registering via this provider. When these checkboxes "
22+
"are skipped, their values are inferred as False (opted out)."
23+
),
24+
),
25+
),
26+
]

common/djangoapps/third_party_auth/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,14 @@ class SAMLProviderConfig(ProviderConfig):
745745
"immediately after authenticating with the third party instead of the login page."
746746
),
747747
)
748+
skip_registration_optional_checkboxes = models.BooleanField(
749+
default=False,
750+
help_text=_(
751+
"If enabled, optional checkboxes (marketing emails opt-in, etc.) will not be rendered "
752+
"on the registration form for users registering via this provider. When these checkboxes "
753+
"are skipped, their values are inferred as False (opted out)."
754+
),
755+
)
748756
other_settings = models.TextField(
749757
verbose_name="Advanced settings", blank=True,
750758
help_text=(

lms/static/js/student_account/views/RegisterView.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
);
5959
this.currentProvider = data.thirdPartyAuth.currentProvider || '';
6060
this.syncLearnerProfileData = data.thirdPartyAuth.syncLearnerProfileData || false;
61+
this.skipRegistrationOptionalCheckboxes = data.thirdPartyAuth.skipRegistrationOptionalCheckboxes || false;
6162
this.errorMessage = data.thirdPartyAuth.errorMessage || '';
6263
this.platformName = data.platformName;
6364
this.autoSubmit = data.thirdPartyAuth.autoSubmitRegForm;
@@ -156,6 +157,7 @@
156157
fields: fields,
157158
currentProvider: this.currentProvider,
158159
syncLearnerProfileData: this.syncLearnerProfileData,
160+
skipRegistrationOptionalCheckboxes: this.skipRegistrationOptionalCheckboxes,
159161
providers: this.providers,
160162
hasSecondaryProviders: this.hasSecondaryProviders,
161163
platformName: this.platformName,

lms/templates/student_account/register.underscore

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,16 @@
5656
<div class="form-fields <% if (context.is_require_third_party_auth_enabled) { %>hidden<% } %>">
5757
<%= context.fields /* xss-lint: disable=underscore-not-escaped */ %>
5858

59-
<div class="form-field checkbox-optional_fields_toggle">
60-
<input type="checkbox" id="toggle_optional_fields" class="input-block checkbox"">
61-
<label for="toggle_optional_fields">
62-
<span class="label-text-small">
63-
<%- gettext("Support education research by providing additional information") %>
64-
</span>
65-
</label>
66-
</div>
59+
<% if (!context.skipRegistrationOptionalCheckboxes) { %>
60+
<div class="form-field checkbox-optional_fields_toggle">
61+
<input type="checkbox" id="toggle_optional_fields" class="input-block checkbox"">
62+
<label for="toggle_optional_fields">
63+
<span class="label-text-small">
64+
<%- gettext("Support education research by providing additional information") %>
65+
</span>
66+
</label>
67+
</div>
68+
<% } %>
6769

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

0 commit comments

Comments
 (0)