@salesforce/ui-bundle-template-feature-react-authentication 11.66.1 → 11.66.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/classes/UIBundleAuthUtils_Test.cls +141 -0
- package/dist/force-app/main/default/classes/UIBundleAuthUtils_Test.cls-meta.xml +5 -0
- package/dist/force-app/main/default/classes/UIBundleSocialLoginConfig.cls +23 -11
- package/dist/force-app/main/default/classes/UIBundleSocialLoginConfig_Test.cls +80 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/package.json +4 -4
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +2 -2
package/dist/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [11.66.3](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.66.2...v11.66.3) (2026-08-20)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [11.66.2](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.66.1...v11.66.2) (2026-08-20)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
## [11.66.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.66.0...v11.66.1) (2026-08-20)
|
|
7
23
|
|
|
8
24
|
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test class for UIBundleAuthUtils.
|
|
3
|
+
*
|
|
4
|
+
* Focuses on getSanitizedStartUrl, the shared open-redirect (CWE-601) guard
|
|
5
|
+
* used by UIBundleLogin, UIBundleRegistration and UIBundleSocialLoginConfig.
|
|
6
|
+
* Every rejection path and the safe-path prefixing
|
|
7
|
+
* behavior are asserted deterministically with an explicit path prefix.
|
|
8
|
+
*/
|
|
9
|
+
@isTest
|
|
10
|
+
private class UIBundleAuthUtils_Test {
|
|
11
|
+
|
|
12
|
+
// Explicit, non-null community path prefix so behavior is deterministic
|
|
13
|
+
// (Site.getPathPrefix() is null/blank in a plain unit-test context).
|
|
14
|
+
private static final String PREFIX = '/community';
|
|
15
|
+
|
|
16
|
+
// ---- Safe / allowed inputs ----------------------------------------------
|
|
17
|
+
|
|
18
|
+
@isTest
|
|
19
|
+
static void allowsSafeRelativePath() {
|
|
20
|
+
System.assertEquals(
|
|
21
|
+
'/community/dashboard',
|
|
22
|
+
UIBundleAuthUtils.getSanitizedStartUrl('/dashboard', PREFIX),
|
|
23
|
+
'A safe community-relative path should be prefixed and preserved'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@isTest
|
|
28
|
+
static void allowsNestedRelativePath() {
|
|
29
|
+
System.assertEquals(
|
|
30
|
+
'/community/safe/relative/path',
|
|
31
|
+
UIBundleAuthUtils.getSanitizedStartUrl('/safe/relative/path', PREFIX),
|
|
32
|
+
'A nested relative path should be prefixed and preserved'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@isTest
|
|
37
|
+
static void blankAndRootFallBackToDefault() {
|
|
38
|
+
System.assertEquals(PREFIX, UIBundleAuthUtils.getSanitizedStartUrl(null, PREFIX), 'null -> default');
|
|
39
|
+
System.assertEquals(PREFIX, UIBundleAuthUtils.getSanitizedStartUrl('', PREFIX), 'empty -> default');
|
|
40
|
+
System.assertEquals(PREFIX, UIBundleAuthUtils.getSanitizedStartUrl(' ', PREFIX), 'whitespace -> default');
|
|
41
|
+
System.assertEquals(PREFIX, UIBundleAuthUtils.getSanitizedStartUrl('/', PREFIX), '"/" -> default');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---- Rejected / malicious inputs (all fall back to the default) ----------
|
|
45
|
+
|
|
46
|
+
@isTest
|
|
47
|
+
static void rejectsAbsoluteHttpUrl() {
|
|
48
|
+
System.assertEquals(
|
|
49
|
+
PREFIX,
|
|
50
|
+
UIBundleAuthUtils.getSanitizedStartUrl('https://evil-attacker.com', PREFIX),
|
|
51
|
+
'Absolute http(s) URL must be rejected'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@isTest
|
|
56
|
+
static void rejectsProtocolRelativeUrl() {
|
|
57
|
+
System.assertEquals(
|
|
58
|
+
PREFIX,
|
|
59
|
+
UIBundleAuthUtils.getSanitizedStartUrl('//evil-attacker.com', PREFIX),
|
|
60
|
+
'Protocol-relative URL must be rejected'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@isTest
|
|
65
|
+
static void rejectsAmbiguousScheme() {
|
|
66
|
+
System.assertEquals(
|
|
67
|
+
PREFIX,
|
|
68
|
+
UIBundleAuthUtils.getSanitizedStartUrl('https:evil-attacker.com', PREFIX),
|
|
69
|
+
'Malformed/ambiguous scheme must be rejected'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@isTest
|
|
74
|
+
static void rejectsBackslashVariant() {
|
|
75
|
+
System.assertEquals(
|
|
76
|
+
PREFIX,
|
|
77
|
+
UIBundleAuthUtils.getSanitizedStartUrl('/\\evil-attacker.com', PREFIX),
|
|
78
|
+
'Backslash normalization trick must be rejected'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@isTest
|
|
83
|
+
static void rejectsAtSign() {
|
|
84
|
+
System.assertEquals(
|
|
85
|
+
PREFIX,
|
|
86
|
+
UIBundleAuthUtils.getSanitizedStartUrl('/x@evil-attacker.com', PREFIX),
|
|
87
|
+
'User-info "@" must be rejected'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@isTest
|
|
92
|
+
static void rejectsColon() {
|
|
93
|
+
System.assertEquals(
|
|
94
|
+
PREFIX,
|
|
95
|
+
UIBundleAuthUtils.getSanitizedStartUrl('/path:8080', PREFIX),
|
|
96
|
+
'Colon (port/scheme manipulation) must be rejected'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@isTest
|
|
101
|
+
static void rejectsEncodedProtocolRelativeBypass() {
|
|
102
|
+
// %2f%2f decodes to // — the decode step must catch this.
|
|
103
|
+
System.assertEquals(
|
|
104
|
+
PREFIX,
|
|
105
|
+
UIBundleAuthUtils.getSanitizedStartUrl('%2f%2fevil-attacker.com', PREFIX),
|
|
106
|
+
'URL-encoded protocol-relative bypass must be rejected'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@isTest
|
|
111
|
+
static void rejectsControlCharacters() {
|
|
112
|
+
String withNewline = '/foo' + String.fromCharArray(new List<Integer>{ 10 }) + 'bar';
|
|
113
|
+
System.assertEquals(
|
|
114
|
+
PREFIX,
|
|
115
|
+
UIBundleAuthUtils.getSanitizedStartUrl(withNewline, PREFIX),
|
|
116
|
+
'Embedded control characters must be rejected'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---- Supporting API coverage --------------------------------------------
|
|
121
|
+
|
|
122
|
+
@isTest
|
|
123
|
+
static void authExceptionCarriesStatusAndMessages() {
|
|
124
|
+
UIBundleAuthUtils.AuthException single = new UIBundleAuthUtils.AuthException(400, 'bad request');
|
|
125
|
+
System.assertEquals(400, single.statusCode, 'statusCode should round-trip');
|
|
126
|
+
System.assertEquals(1, single.messages.size(), 'single-message constructor');
|
|
127
|
+
|
|
128
|
+
UIBundleAuthUtils.AuthException multi =
|
|
129
|
+
new UIBundleAuthUtils.AuthException(500, new List<String>{ 'a', 'b' });
|
|
130
|
+
System.assertEquals(500, multi.statusCode, 'statusCode should round-trip');
|
|
131
|
+
System.assertEquals(2, multi.messages.size(), 'multi-message constructor');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@isTest
|
|
135
|
+
static void debugLogCompletesWithoutError() {
|
|
136
|
+
Test.startTest();
|
|
137
|
+
UIBundleAuthUtils.debugLog(new IllegalArgumentException('boom'), LoggingLevel.ERROR);
|
|
138
|
+
Test.stopTest();
|
|
139
|
+
System.assert(true, 'debugLog should complete without throwing');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -22,17 +22,9 @@ global with sharing class UIBundleSocialLoginConfig {
|
|
|
22
22
|
@HttpGet
|
|
23
23
|
global static SocialLoginConfigResponse getConfig() {
|
|
24
24
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Ensure startURL includes the community path prefix so post-auth
|
|
31
|
-
// redirect lands on the correct community (not the org default).
|
|
32
|
-
String pathPrefix = Site.getPathPrefix();
|
|
33
|
-
if (String.isNotBlank(pathPrefix) && !startUrl.startsWith(pathPrefix)) {
|
|
34
|
-
startUrl = pathPrefix + startUrl;
|
|
35
|
-
}
|
|
25
|
+
// Resolve the post-authentication redirect path, sanitized against
|
|
26
|
+
// open-redirect attacks (CWE-601). See resolveStartUrl.
|
|
27
|
+
String startUrl = resolveStartUrl();
|
|
36
28
|
|
|
37
29
|
String communityUrl = Site.getBaseUrl();
|
|
38
30
|
Auth.AuthConfiguration authConfig = new Auth.AuthConfiguration(communityUrl, startUrl);
|
|
@@ -89,6 +81,26 @@ global with sharing class UIBundleSocialLoginConfig {
|
|
|
89
81
|
}
|
|
90
82
|
}
|
|
91
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Resolves the post-authentication redirect path from the request and
|
|
86
|
+
* sanitizes it to prevent open-redirect attacks (CWE-601).
|
|
87
|
+
*
|
|
88
|
+
* Delegates to the shared UIBundleAuthUtils.getSanitizedStartUrl — the same
|
|
89
|
+
* validation used by UIBundleLogin and UIBundleRegistration — which
|
|
90
|
+
* URL-decodes the value (catching encoded bypasses such as %2f%2f) and then
|
|
91
|
+
* rejects absolute, protocol-relative ("//"), backslash, "@", ":" and
|
|
92
|
+
* control-character inputs, falling back to the community path prefix. Only
|
|
93
|
+
* safe, community-relative paths survive, prefixed with Site.getPathPrefix()
|
|
94
|
+
* so the redirect lands on this community rather than the org default.
|
|
95
|
+
*/
|
|
96
|
+
@TestVisible
|
|
97
|
+
private static String resolveStartUrl() {
|
|
98
|
+
return UIBundleAuthUtils.getSanitizedStartUrl(
|
|
99
|
+
RestContext.request.params.get('startURL'),
|
|
100
|
+
Site.getPathPrefix()
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
global class SocialLoginConfigResponse {
|
|
93
105
|
global List<AuthProviderInfo> authProviders;
|
|
94
106
|
global List<SamlProviderInfo> samlProviders;
|
|
@@ -98,4 +98,84 @@ private class UIBundleSocialLoginConfig_Test {
|
|
|
98
98
|
|
|
99
99
|
Test.stopTest();
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
// ---- Open-redirect sanitization (CWE-601) -------------------------------
|
|
103
|
+
// resolveStartUrl() is the boundary where the endpoint sanitizes the
|
|
104
|
+
// caller-supplied startURL before it is embedded in the SSO redirect URL.
|
|
105
|
+
// These tests assert malicious values never survive that boundary and that
|
|
106
|
+
// legitimate relative paths do.
|
|
107
|
+
|
|
108
|
+
@isTest
|
|
109
|
+
static void resolveStartUrlRejectsAbsoluteUrl() {
|
|
110
|
+
assertNotReflected('https://evil-attacker.com');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@isTest
|
|
114
|
+
static void resolveStartUrlRejectsProtocolRelativeUrl() {
|
|
115
|
+
assertNotReflected('//evil-attacker.com');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@isTest
|
|
119
|
+
static void resolveStartUrlRejectsBackslashVariant() {
|
|
120
|
+
assertNotReflected('/\\evil-attacker.com');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@isTest
|
|
124
|
+
static void resolveStartUrlRejectsEncodedBypass() {
|
|
125
|
+
assertNotReflected('%2f%2fevil-attacker.com');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@isTest
|
|
129
|
+
static void resolveStartUrlAllowsRelativePath() {
|
|
130
|
+
setRequest('/dashboard');
|
|
131
|
+
String resolved = UIBundleSocialLoginConfig.resolveStartUrl();
|
|
132
|
+
System.assertNotEquals(null, resolved, 'A valid relative path should resolve to a non-null value');
|
|
133
|
+
System.assert(
|
|
134
|
+
resolved.endsWith('/dashboard'),
|
|
135
|
+
'A valid relative path must be preserved (optionally prefixed): ' + resolved
|
|
136
|
+
);
|
|
137
|
+
System.assert(!resolved.startsWith('http'), 'Result must not be absolute: ' + resolved);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@isTest
|
|
141
|
+
static void getConfigWithMaliciousStartUrlReturnsGracefulEmpty() {
|
|
142
|
+
// End-to-end: a malicious startURL must not crash the endpoint; without
|
|
143
|
+
// a real site context it returns the graceful empty response (unchanged).
|
|
144
|
+
setRequest('https://evil-attacker.com');
|
|
145
|
+
|
|
146
|
+
Test.startTest();
|
|
147
|
+
UIBundleSocialLoginConfig.SocialLoginConfigResponse result =
|
|
148
|
+
UIBundleSocialLoginConfig.getConfig();
|
|
149
|
+
Test.stopTest();
|
|
150
|
+
|
|
151
|
+
System.assertNotEquals(null, result, 'Response should not be null');
|
|
152
|
+
System.assertEquals(0, result.authProviders.size(), 'No providers without site context');
|
|
153
|
+
System.assertEquals(0, result.samlProviders.size(), 'No SAML providers without site context');
|
|
154
|
+
System.assertEquals(null, result.autoRedirectUrl, 'No auto-redirect without providers');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- helpers ------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
/** Asserts a malicious startURL value is not reflected by resolveStartUrl(). */
|
|
160
|
+
private static void assertNotReflected(String maliciousStartUrl) {
|
|
161
|
+
setRequest(maliciousStartUrl);
|
|
162
|
+
String resolved = UIBundleSocialLoginConfig.resolveStartUrl();
|
|
163
|
+
String safe = resolved == null ? '' : resolved;
|
|
164
|
+
System.assert(!safe.contains('evil-attacker'), 'Malicious host must not be reflected: ' + safe);
|
|
165
|
+
System.assert(!safe.startsWith('http'), 'Result must not be an absolute URL: ' + safe);
|
|
166
|
+
System.assert(!safe.startsWith('//'), 'Result must not be protocol-relative: ' + safe);
|
|
167
|
+
System.assert(!safe.contains('\\'), 'Result must not contain a backslash: ' + safe);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Builds a GET RestContext with the given startURL param (null => param omitted). */
|
|
171
|
+
private static void setRequest(String startUrlValue) {
|
|
172
|
+
RestRequest req = new RestRequest();
|
|
173
|
+
req.requestURI = '/services/apexrest/auth/social-login-config';
|
|
174
|
+
req.httpMethod = 'GET';
|
|
175
|
+
if (startUrlValue != null) {
|
|
176
|
+
req.params.put('startURL', startUrlValue);
|
|
177
|
+
}
|
|
178
|
+
RestContext.request = req;
|
|
179
|
+
RestContext.response = new RestResponse();
|
|
180
|
+
}
|
|
101
181
|
}
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"graphql:schema": "node scripts/get-graphql-schema.mjs"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@salesforce/platform-sdk": "^11.66.
|
|
22
|
-
"@salesforce/ui-bundle": "^11.66.
|
|
21
|
+
"@salesforce/platform-sdk": "^11.66.3",
|
|
22
|
+
"@salesforce/ui-bundle": "^11.66.3",
|
|
23
23
|
"@tailwindcss/vite": "^4.1.17",
|
|
24
24
|
"class-variance-authority": "^0.7.1",
|
|
25
25
|
"clsx": "^2.1.1",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"@graphql-eslint/eslint-plugin": "^4.1.0",
|
|
47
47
|
"@graphql-tools/utils": "^11.0.0",
|
|
48
48
|
"@playwright/test": "^1.49.0",
|
|
49
|
-
"@salesforce/graphiti": "^11.66.
|
|
50
|
-
"@salesforce/vite-plugin-ui-bundle": "^11.66.
|
|
49
|
+
"@salesforce/graphiti": "^11.66.3",
|
|
50
|
+
"@salesforce/vite-plugin-ui-bundle": "^11.66.3",
|
|
51
51
|
"@testing-library/jest-dom": "^6.6.3",
|
|
52
52
|
"@testing-library/react": "^16.1.0",
|
|
53
53
|
"@testing-library/user-event": "^14.5.2",
|
package/dist/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/ui-bundle-template-base-sfdx-project",
|
|
3
|
-
"version": "11.66.
|
|
3
|
+
"version": "11.66.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@salesforce/ui-bundle-template-base-sfdx-project",
|
|
9
|
-
"version": "11.66.
|
|
9
|
+
"version": "11.66.3",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"fast-xml-parser": "^5.9.3",
|
package/dist/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/ui-bundle-template-feature-react-authentication",
|
|
3
|
-
"version": "11.66.
|
|
3
|
+
"version": "11.66.3",
|
|
4
4
|
"description": "Authentication feature for UI Bundles",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"author": "",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"clean": "rm -rf dist"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@salesforce/ui-bundle": "^11.66.
|
|
19
|
+
"@salesforce/ui-bundle": "^11.66.3",
|
|
20
20
|
"@tanstack/react-form": "^1.27.7",
|
|
21
21
|
"@types/react": "^19.2.7",
|
|
22
22
|
"@types/react-dom": "^19.2.3",
|