@salesforce/ui-bundle-template-feature-angular-authentication 12.9.0 → 12.11.0
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/UIBundleSocialLoginConfig.cls +145 -0
- package/dist/force-app/main/default/classes/UIBundleSocialLoginConfig.cls-meta.xml +5 -0
- package/dist/force-app/main/default/classes/UIBundleSocialLoginConfig_Test.cls +181 -0
- package/dist/force-app/main/default/classes/UIBundleSocialLoginConfig_Test.cls-meta.xml +5 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-authentication/package.json +3 -3
- package/dist/force-app/main/default/uiBundles/feature-angular-authentication/src/app/features/authentication/social-login/social-login.html +39 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-authentication/src/app/features/authentication/social-login/social-login.ts +107 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-authentication/src/app/pages/login/login-page.html +2 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-authentication/src/app/pages/login/login-page.ts +2 -1
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
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
|
+
## [12.11.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v12.10.0...v12.11.0) (2026-10-07)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [12.10.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v12.9.0...v12.10.0) (2026-10-06)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
## [12.9.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v12.8.0...v12.9.0) (2026-09-29)
|
|
7
23
|
|
|
8
24
|
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST endpoint for Social Login configuration on React Experience Cloud sites.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint: GET /services/apexrest/auth/social-login-config?startURL=/
|
|
5
|
+
*
|
|
6
|
+
* This replicates the behavior of the OOTB community_login/socialLogin LWC:
|
|
7
|
+
* - applauncher.SocialLoginController.getAuthProviders()
|
|
8
|
+
* - applauncher.SocialLoginController.getSamlProviders()
|
|
9
|
+
* - applauncher.SocialLoginController.getCommunityDomainSsoUrl()
|
|
10
|
+
* - applauncher.SocialLoginController.getSamlSsoUrl()
|
|
11
|
+
* - applauncher.SocialLoginController.handleIdp()
|
|
12
|
+
*
|
|
13
|
+
* The OOTB LWC does not render on React (Site Container) sites due to a platform
|
|
14
|
+
* restriction (isSiteContainer check in node-loginsettings.ftl).
|
|
15
|
+
* This endpoint enables a custom React component to achieve the same functionality.
|
|
16
|
+
*
|
|
17
|
+
* @see https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_class_Auth_AuthConfiguration.htm
|
|
18
|
+
*/
|
|
19
|
+
@RestResource(urlMapping='/auth/social-login-config')
|
|
20
|
+
global with sharing class UIBundleSocialLoginConfig {
|
|
21
|
+
|
|
22
|
+
@HttpGet
|
|
23
|
+
global static SocialLoginConfigResponse getConfig() {
|
|
24
|
+
try {
|
|
25
|
+
// Resolve the post-authentication redirect path, sanitized against
|
|
26
|
+
// open-redirect attacks (CWE-601). See resolveStartUrl.
|
|
27
|
+
String startUrl = resolveStartUrl();
|
|
28
|
+
|
|
29
|
+
String communityUrl = Site.getBaseUrl();
|
|
30
|
+
Auth.AuthConfiguration authConfig = new Auth.AuthConfiguration(communityUrl, startUrl);
|
|
31
|
+
|
|
32
|
+
List<AuthProviderInfo> authProviders = new List<AuthProviderInfo>();
|
|
33
|
+
List<SamlProviderInfo> samlProviders = new List<SamlProviderInfo>();
|
|
34
|
+
String autoRedirectUrl = null;
|
|
35
|
+
|
|
36
|
+
// Fetch OAuth Auth Providers (Google, Facebook, etc.)
|
|
37
|
+
List<AuthProvider> rawAuthProviders = authConfig.getAuthProviders();
|
|
38
|
+
for (AuthProvider ap : rawAuthProviders) {
|
|
39
|
+
String ssoUrl = Auth.AuthConfiguration.getAuthProviderSsoDomainUrl(
|
|
40
|
+
communityUrl, startUrl, ap.DeveloperName
|
|
41
|
+
);
|
|
42
|
+
authProviders.add(new AuthProviderInfo(
|
|
43
|
+
ap.DeveloperName,
|
|
44
|
+
ap.FriendlyName,
|
|
45
|
+
ap.IconUrl,
|
|
46
|
+
ssoUrl
|
|
47
|
+
));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fetch SAML SSO Providers
|
|
51
|
+
List<SamlSsoConfig> rawSamlProviders = authConfig.getSamlProviders();
|
|
52
|
+
for (SamlSsoConfig saml : rawSamlProviders) {
|
|
53
|
+
String ssoUrl = Auth.AuthConfiguration.getSamlSsoUrl(
|
|
54
|
+
communityUrl, startUrl, saml.Id
|
|
55
|
+
);
|
|
56
|
+
samlProviders.add(new SamlProviderInfo(
|
|
57
|
+
saml.Id,
|
|
58
|
+
saml.MasterLabel,
|
|
59
|
+
ssoUrl
|
|
60
|
+
));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// handleIdp behavior: auto-redirect if exactly one IDP and no username/password login
|
|
64
|
+
Integer totalProviders = rawAuthProviders.size() + rawSamlProviders.size();
|
|
65
|
+
if (totalProviders == 1 && !authConfig.getUsernamePasswordEnabled()) {
|
|
66
|
+
if (!authProviders.isEmpty()) {
|
|
67
|
+
autoRedirectUrl = authProviders[0].ssoUrl;
|
|
68
|
+
} else if (!samlProviders.isEmpty()) {
|
|
69
|
+
autoRedirectUrl = samlProviders[0].ssoUrl;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return new SocialLoginConfigResponse(authProviders, samlProviders, autoRedirectUrl);
|
|
74
|
+
} catch (Exception ex) {
|
|
75
|
+
System.debug(LoggingLevel.ERROR, 'UIBundleSocialLoginConfig error: ' + ex.getMessage());
|
|
76
|
+
return new SocialLoginConfigResponse(
|
|
77
|
+
new List<AuthProviderInfo>(),
|
|
78
|
+
new List<SamlProviderInfo>(),
|
|
79
|
+
null
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
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
|
+
|
|
104
|
+
global class SocialLoginConfigResponse {
|
|
105
|
+
global List<AuthProviderInfo> authProviders;
|
|
106
|
+
global List<SamlProviderInfo> samlProviders;
|
|
107
|
+
global String autoRedirectUrl;
|
|
108
|
+
|
|
109
|
+
global SocialLoginConfigResponse(
|
|
110
|
+
List<AuthProviderInfo> authProviders,
|
|
111
|
+
List<SamlProviderInfo> samlProviders,
|
|
112
|
+
String autoRedirectUrl
|
|
113
|
+
) {
|
|
114
|
+
this.authProviders = authProviders;
|
|
115
|
+
this.samlProviders = samlProviders;
|
|
116
|
+
this.autoRedirectUrl = autoRedirectUrl;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
global class AuthProviderInfo {
|
|
121
|
+
global String developerName;
|
|
122
|
+
global String friendlyName;
|
|
123
|
+
global String iconUrl;
|
|
124
|
+
global String ssoUrl;
|
|
125
|
+
|
|
126
|
+
global AuthProviderInfo(String developerName, String friendlyName, String iconUrl, String ssoUrl) {
|
|
127
|
+
this.developerName = developerName;
|
|
128
|
+
this.friendlyName = friendlyName;
|
|
129
|
+
this.iconUrl = iconUrl;
|
|
130
|
+
this.ssoUrl = ssoUrl;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
global class SamlProviderInfo {
|
|
135
|
+
global String id;
|
|
136
|
+
global String masterLabel;
|
|
137
|
+
global String ssoUrl;
|
|
138
|
+
|
|
139
|
+
global SamlProviderInfo(String id, String masterLabel, String ssoUrl) {
|
|
140
|
+
this.id = id;
|
|
141
|
+
this.masterLabel = masterLabel;
|
|
142
|
+
this.ssoUrl = ssoUrl;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test class for UIBundleSocialLoginConfig REST endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Auth.AuthConfiguration requires a valid community URL so these tests
|
|
5
|
+
* exercise the error-handling path and verify the response wrapper structure.
|
|
6
|
+
* Integration-level testing of actual Auth Provider/SAML discovery happens
|
|
7
|
+
* in the target org via the React component E2E flow.
|
|
8
|
+
*/
|
|
9
|
+
@isTest
|
|
10
|
+
private class UIBundleSocialLoginConfig_Test {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Test that the endpoint returns an empty config when no community context
|
|
14
|
+
* is available (covers the catch block graceful fallback).
|
|
15
|
+
*/
|
|
16
|
+
@isTest
|
|
17
|
+
static void testGetConfigNoSiteContext() {
|
|
18
|
+
RestRequest req = new RestRequest();
|
|
19
|
+
req.requestURI = '/services/apexrest/auth/social-login-config';
|
|
20
|
+
req.httpMethod = 'GET';
|
|
21
|
+
req.params.put('startURL', '/');
|
|
22
|
+
RestContext.request = req;
|
|
23
|
+
RestContext.response = new RestResponse();
|
|
24
|
+
|
|
25
|
+
Test.startTest();
|
|
26
|
+
UIBundleSocialLoginConfig.SocialLoginConfigResponse result =
|
|
27
|
+
UIBundleSocialLoginConfig.getConfig();
|
|
28
|
+
Test.stopTest();
|
|
29
|
+
|
|
30
|
+
// Should return empty lists (graceful fallback), not null
|
|
31
|
+
System.assertNotEquals(null, result, 'Response should not be null');
|
|
32
|
+
System.assertNotEquals(null, result.authProviders, 'authProviders list should not be null');
|
|
33
|
+
System.assertNotEquals(null, result.samlProviders, 'samlProviders list should not be null');
|
|
34
|
+
System.assertEquals(0, result.authProviders.size(), 'authProviders should be empty without community context');
|
|
35
|
+
System.assertEquals(0, result.samlProviders.size(), 'samlProviders should be empty without community context');
|
|
36
|
+
System.assertEquals(null, result.autoRedirectUrl, 'autoRedirectUrl should be null without providers');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Test that the endpoint handles a missing startURL parameter gracefully
|
|
41
|
+
* (defaults to "/").
|
|
42
|
+
*/
|
|
43
|
+
@isTest
|
|
44
|
+
static void testGetConfigNoStartUrl() {
|
|
45
|
+
RestRequest req = new RestRequest();
|
|
46
|
+
req.requestURI = '/services/apexrest/auth/social-login-config';
|
|
47
|
+
req.httpMethod = 'GET';
|
|
48
|
+
// Intentionally not setting startURL param
|
|
49
|
+
RestContext.request = req;
|
|
50
|
+
RestContext.response = new RestResponse();
|
|
51
|
+
|
|
52
|
+
Test.startTest();
|
|
53
|
+
UIBundleSocialLoginConfig.SocialLoginConfigResponse result =
|
|
54
|
+
UIBundleSocialLoginConfig.getConfig();
|
|
55
|
+
Test.stopTest();
|
|
56
|
+
|
|
57
|
+
System.assertNotEquals(null, result, 'Response should not be null even without startURL');
|
|
58
|
+
System.assertNotEquals(null, result.authProviders, 'authProviders should not be null');
|
|
59
|
+
System.assertNotEquals(null, result.samlProviders, 'samlProviders should not be null');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Test the inner class constructors for code coverage.
|
|
64
|
+
*/
|
|
65
|
+
@isTest
|
|
66
|
+
static void testInnerClassConstructors() {
|
|
67
|
+
Test.startTest();
|
|
68
|
+
|
|
69
|
+
UIBundleSocialLoginConfig.AuthProviderInfo authInfo =
|
|
70
|
+
new UIBundleSocialLoginConfig.AuthProviderInfo(
|
|
71
|
+
'TestProvider', 'Test Provider', 'https://example.com/icon.png', 'https://example.com/sso'
|
|
72
|
+
);
|
|
73
|
+
System.assertEquals('TestProvider', authInfo.developerName);
|
|
74
|
+
System.assertEquals('Test Provider', authInfo.friendlyName);
|
|
75
|
+
System.assertEquals('https://example.com/icon.png', authInfo.iconUrl);
|
|
76
|
+
System.assertEquals('https://example.com/sso', authInfo.ssoUrl);
|
|
77
|
+
|
|
78
|
+
UIBundleSocialLoginConfig.SamlProviderInfo samlInfo =
|
|
79
|
+
new UIBundleSocialLoginConfig.SamlProviderInfo(
|
|
80
|
+
'001000000000001', 'Test SAML', 'https://example.com/saml/sso'
|
|
81
|
+
);
|
|
82
|
+
System.assertEquals('001000000000001', samlInfo.id);
|
|
83
|
+
System.assertEquals('Test SAML', samlInfo.masterLabel);
|
|
84
|
+
System.assertEquals('https://example.com/saml/sso', samlInfo.ssoUrl);
|
|
85
|
+
|
|
86
|
+
List<UIBundleSocialLoginConfig.AuthProviderInfo> authList =
|
|
87
|
+
new List<UIBundleSocialLoginConfig.AuthProviderInfo>{ authInfo };
|
|
88
|
+
List<UIBundleSocialLoginConfig.SamlProviderInfo> samlList =
|
|
89
|
+
new List<UIBundleSocialLoginConfig.SamlProviderInfo>{ samlInfo };
|
|
90
|
+
|
|
91
|
+
UIBundleSocialLoginConfig.SocialLoginConfigResponse resp =
|
|
92
|
+
new UIBundleSocialLoginConfig.SocialLoginConfigResponse(
|
|
93
|
+
authList, samlList, 'https://example.com/redirect'
|
|
94
|
+
);
|
|
95
|
+
System.assertEquals(1, resp.authProviders.size());
|
|
96
|
+
System.assertEquals(1, resp.samlProviders.size());
|
|
97
|
+
System.assertEquals('https://example.com/redirect', resp.autoRedirectUrl);
|
|
98
|
+
|
|
99
|
+
Test.stopTest();
|
|
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
|
+
}
|
|
181
|
+
}
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"@angular/router": "^21.2.0",
|
|
29
29
|
"@ng-icons/core": "^32.2.0",
|
|
30
30
|
"@ng-icons/lucide": "^32.2.0",
|
|
31
|
-
"@salesforce/angular-plugin-ui-bundle": "^12.
|
|
32
|
-
"@salesforce/platform-sdk": "^12.
|
|
31
|
+
"@salesforce/angular-plugin-ui-bundle": "^12.11.0",
|
|
32
|
+
"@salesforce/platform-sdk": "^12.11.0",
|
|
33
33
|
"@spartan-ng/brain": "^1.3.1",
|
|
34
34
|
"class-variance-authority": "^0.7.1",
|
|
35
35
|
"clsx": "^2.1.1",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"typescript": "~5.9.2",
|
|
62
62
|
"typescript-eslint": "^8.0.0",
|
|
63
63
|
"vitest": "^4.1.11",
|
|
64
|
-
"@salesforce/graphiti": "^12.
|
|
64
|
+
"@salesforce/graphiti": "^12.11.0"
|
|
65
65
|
},
|
|
66
66
|
"overrides": {
|
|
67
67
|
"undici": "^7.28.0"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
@if (hasProviders()) {
|
|
2
|
+
<div class="mt-6">
|
|
3
|
+
@if (showHeader()) {
|
|
4
|
+
<div class="relative">
|
|
5
|
+
<div class="absolute inset-0 flex items-center">
|
|
6
|
+
<span class="w-full border-t"></span>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="relative flex justify-center text-xs uppercase">
|
|
9
|
+
<span class="bg-background px-2 text-muted-foreground">{{ headerText() }}</span>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
<div class="mt-4 grid gap-2">
|
|
15
|
+
@for (provider of authProviders(); track provider.developerName) {
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
(click)="redirectTo(provider.ssoUrl)"
|
|
19
|
+
[attr.aria-label]="'Sign in with ' + provider.friendlyName"
|
|
20
|
+
class="inline-flex w-full items-center justify-center gap-2 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
21
|
+
>
|
|
22
|
+
@if (provider.iconUrl) {
|
|
23
|
+
<img [src]="provider.iconUrl" alt="" class="h-4 w-4" aria-hidden="true" />
|
|
24
|
+
}
|
|
25
|
+
<span>{{ provider.friendlyName }}</span>
|
|
26
|
+
</button>
|
|
27
|
+
} @for (provider of samlProviders(); track provider.id) {
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
(click)="redirectTo(provider.ssoUrl)"
|
|
31
|
+
[attr.aria-label]="'Sign in with ' + provider.masterLabel"
|
|
32
|
+
class="inline-flex w-full items-center justify-center gap-2 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
33
|
+
>
|
|
34
|
+
<span>{{ provider.masterLabel }}</span>
|
|
35
|
+
</button>
|
|
36
|
+
}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
ChangeDetectionStrategy,
|
|
8
|
+
Component,
|
|
9
|
+
type OnInit,
|
|
10
|
+
computed,
|
|
11
|
+
inject,
|
|
12
|
+
input,
|
|
13
|
+
signal,
|
|
14
|
+
} from "@angular/core";
|
|
15
|
+
import { ActivatedRoute } from "@angular/router";
|
|
16
|
+
import { DataClient } from "../../../api/data-client.service";
|
|
17
|
+
import { getStartUrl } from "../utils/auth-helpers";
|
|
18
|
+
|
|
19
|
+
interface AuthProviderInfo {
|
|
20
|
+
developerName: string;
|
|
21
|
+
friendlyName: string;
|
|
22
|
+
iconUrl: string | null;
|
|
23
|
+
ssoUrl: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SamlProviderInfo {
|
|
27
|
+
id: string;
|
|
28
|
+
masterLabel: string;
|
|
29
|
+
ssoUrl: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SocialLoginConfigResponse {
|
|
33
|
+
authProviders: AuthProviderInfo[];
|
|
34
|
+
samlProviders: SamlProviderInfo[];
|
|
35
|
+
autoRedirectUrl: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Social Login (IDP) component — Angular port of the React `SocialLogin`.
|
|
40
|
+
*
|
|
41
|
+
* Replicates the OOTB community_login/socialLogin LWC behavior which does not
|
|
42
|
+
* render on Site Container sites due to a platform restriction (isSiteContainer
|
|
43
|
+
* check in node-loginsettings.ftl).
|
|
44
|
+
*
|
|
45
|
+
* Behavior:
|
|
46
|
+
* - On init, fetches configured Auth Providers + SAML Providers
|
|
47
|
+
* - If only one IDP is configured and username/password is disabled, auto-redirects
|
|
48
|
+
* - Renders a button per provider with icon (Auth Providers) or without (SAML)
|
|
49
|
+
* - On click, redirects to the SSO URL to initiate the OAuth/SAML flow
|
|
50
|
+
*
|
|
51
|
+
* Backend: UIBundleSocialLoginConfig.cls (Apex REST endpoint), copied verbatim
|
|
52
|
+
* from the React feature. Uses the Auth.AuthConfiguration API.
|
|
53
|
+
*/
|
|
54
|
+
@Component({
|
|
55
|
+
selector: "app-social-login",
|
|
56
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
57
|
+
templateUrl: "./social-login.html",
|
|
58
|
+
})
|
|
59
|
+
export class SocialLoginComponent implements OnInit {
|
|
60
|
+
private readonly route = inject(ActivatedRoute);
|
|
61
|
+
private readonly dataClient = inject(DataClient);
|
|
62
|
+
|
|
63
|
+
/** Whether to show the header text above the buttons. Default: true */
|
|
64
|
+
readonly showHeader = input(true);
|
|
65
|
+
|
|
66
|
+
/** Header text. Default: "Or log in using:" */
|
|
67
|
+
readonly headerText = input("Or log in using:");
|
|
68
|
+
|
|
69
|
+
protected readonly authProviders = signal<AuthProviderInfo[]>([]);
|
|
70
|
+
protected readonly samlProviders = signal<SamlProviderInfo[]>([]);
|
|
71
|
+
protected readonly hasProviders = computed(
|
|
72
|
+
() => this.authProviders().length > 0 || this.samlProviders().length > 0,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
async ngOnInit(): Promise<void> {
|
|
76
|
+
const startUrl = getStartUrl(this.route.snapshot.queryParamMap);
|
|
77
|
+
const config = await this.fetchConfig(startUrl);
|
|
78
|
+
if (!config) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (config.autoRedirectUrl) {
|
|
82
|
+
window.location.assign(config.autoRedirectUrl);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.authProviders.set(config.authProviders ?? []);
|
|
86
|
+
this.samlProviders.set(config.samlProviders ?? []);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
protected redirectTo(ssoUrl: string): void {
|
|
90
|
+
window.location.assign(ssoUrl);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async fetchConfig(startUrl: string): Promise<SocialLoginConfigResponse | null> {
|
|
94
|
+
try {
|
|
95
|
+
const response = await this.dataClient.fetch(
|
|
96
|
+
`/services/apexrest/auth/social-login-config?startURL=${encodeURIComponent(startUrl)}`,
|
|
97
|
+
{ method: "GET", headers: { Accept: "application/json" } },
|
|
98
|
+
);
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return (await response.json()) as SocialLoginConfigResponse;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -8,6 +8,7 @@ import { FormBuilder } from "@angular/forms";
|
|
|
8
8
|
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
|
|
9
9
|
import { DataClient } from "../../api/data-client.service";
|
|
10
10
|
import { AuthFormComponent } from "../../features/authentication/auth-form/auth-form";
|
|
11
|
+
import { SocialLoginComponent } from "../../features/authentication/social-login/social-login";
|
|
11
12
|
import { ReactiveFormPageBase } from "../../features/authentication/reactive-form-page.base";
|
|
12
13
|
import { FieldComponent } from "../../components/ui/field/field";
|
|
13
14
|
import { InputComponent } from "../../components/ui/input/input";
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
@Component({
|
|
33
34
|
selector: "app-login-page",
|
|
34
35
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
35
|
-
imports: [RouterLink, AuthFormComponent, FieldComponent, InputComponent],
|
|
36
|
+
imports: [RouterLink, AuthFormComponent, FieldComponent, InputComponent, SocialLoginComponent],
|
|
36
37
|
templateUrl: "./login-page.html",
|
|
37
38
|
})
|
|
38
39
|
export class LoginPageComponent extends ReactiveFormPageBase {
|
package/dist/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/ui-bundle-template-base-sfdx-project",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.11.0",
|
|
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": "12.
|
|
9
|
+
"version": "12.11.0",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"fast-xml-parser": "^5.9.3",
|
package/dist/package.json
CHANGED