Skip to content

Commit a5951f0

Browse files
authored
feat(experience): harden logto signature against css overrides (#7920)
1 parent 90acd30 commit a5951f0

File tree

1 file changed

+158
-7
lines changed
  • packages/experience/src/components/LogtoSignature

1 file changed

+158
-7
lines changed

packages/experience/src/components/LogtoSignature/index.tsx

Lines changed: 158 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Theme } from '@logto/schemas';
2-
import { useContext } from 'react';
2+
import { useContext, useEffect, useRef } from 'react';
33

44
import PageContext from '@/Providers/PageContextProvider/PageContext';
55
import LogtoLogtoDark from '@/assets/icons/logto-logo-dark.svg?react';
@@ -13,6 +13,68 @@ const logtoUrl = `https://logto.io/?${new URLSearchParams({
1313
utm_medium: 'powered_by',
1414
}).toString()}`;
1515

16+
const guardStyleSelector = 'style[data-logto-signature-guard="true"]';
17+
18+
const signatureGuardStyle = `
19+
[data-logto-signature-container="secured"][data-logto-signature-container="secured"] {
20+
display: block !important;
21+
visibility: visible !important;
22+
opacity: 1 !important;
23+
}
24+
25+
[data-logto-signature="secured"][data-logto-signature="secured"] {
26+
display: flex !important;
27+
align-items: center !important;
28+
justify-content: flex-start !important;
29+
font: var(--font-label-2) !important;
30+
font-weight: normal !important;
31+
color: var(--color-neutral-variant-60) !important;
32+
padding: 4px 8px !important;
33+
text-decoration: none !important;
34+
opacity: 75% !important;
35+
direction: ltr !important;
36+
position: relative !important;
37+
inset: auto !important;
38+
left: auto !important;
39+
right: auto !important;
40+
top: auto !important;
41+
bottom: auto !important;
42+
transform: none !important;
43+
pointer-events: auto !important;
44+
}
45+
46+
[data-logto-signature="secured"][data-logto-signature="secured"]:is(:hover, :active, :focus-visible) {
47+
opacity: 100% !important;
48+
}
49+
50+
[data-logto-signature="secured"][data-logto-signature="secured"] [data-logto-signature-icon="static"] {
51+
display: block !important;
52+
}
53+
54+
[data-logto-signature="secured"][data-logto-signature="secured"] [data-logto-signature-icon="highlight"] {
55+
display: none !important;
56+
}
57+
58+
[data-logto-signature="secured"][data-logto-signature="secured"]:is(:hover, :active, :focus-visible)
59+
[data-logto-signature-icon="static"] {
60+
display: none !important;
61+
}
62+
63+
[data-logto-signature="secured"][data-logto-signature="secured"]:is(:hover, :active, :focus-visible)
64+
[data-logto-signature-icon="highlight"] {
65+
display: block !important;
66+
}
67+
68+
[data-logto-signature-text] {
69+
margin-inline-end: 4px !important;
70+
}
71+
72+
body.mobile [data-logto-signature="secured"][data-logto-signature="secured"] {
73+
color: var(--color-neutral-variant-80) !important;
74+
font: var(--font-label-3) !important;
75+
}
76+
`;
77+
1678
type Props = {
1779
readonly className?: string;
1880
};
@@ -21,18 +83,107 @@ const LogtoSignature = ({ className }: Props) => {
2183
const { theme } = useContext(PageContext);
2284
const LogtoLogo = theme === Theme.Light ? LogtoLogoLight : LogtoLogtoDark;
2385

86+
const containerRef = useRef<HTMLDivElement>(null);
87+
const anchorRef = useRef<HTMLAnchorElement>(null);
88+
89+
useEffect(() => {
90+
if (typeof document === 'undefined') {
91+
return;
92+
}
93+
94+
const { current: container } = containerRef;
95+
const { current: anchor } = anchorRef;
96+
97+
if (!anchor) {
98+
return;
99+
}
100+
101+
const ensureGuardStyle = (): { created: boolean; element: HTMLStyleElement } => {
102+
const existing = document.head.querySelector<HTMLStyleElement>(guardStyleSelector);
103+
104+
if (existing) {
105+
return { created: false, element: existing };
106+
}
107+
108+
const createdElement = document.createElement('style');
109+
Reflect.set(createdElement.dataset, 'logtoSignatureGuard', 'true');
110+
createdElement.append(signatureGuardStyle);
111+
document.head.append(createdElement);
112+
113+
return { created: true, element: createdElement };
114+
};
115+
116+
const { created, element: guardStyleElement } = ensureGuardStyle();
117+
118+
const enforceIntegrity = () => {
119+
if (container) {
120+
container.removeAttribute('hidden');
121+
container.style.setProperty('display', 'block', 'important');
122+
container.style.setProperty('visibility', 'visible', 'important');
123+
container.style.setProperty('opacity', '1', 'important');
124+
container.style.setProperty('position', 'static', 'important');
125+
}
126+
127+
anchor.removeAttribute('hidden');
128+
129+
if (styles.signature && !anchor.classList.contains(styles.signature)) {
130+
anchor.classList.add(styles.signature);
131+
}
132+
133+
anchor.style.removeProperty('display');
134+
anchor.style.removeProperty('visibility');
135+
anchor.style.removeProperty('opacity');
136+
anchor.style.removeProperty('position');
137+
anchor.style.removeProperty('left');
138+
anchor.style.removeProperty('right');
139+
anchor.style.removeProperty('top');
140+
anchor.style.removeProperty('bottom');
141+
anchor.style.removeProperty('transform');
142+
};
143+
144+
enforceIntegrity();
145+
146+
const observer = new MutationObserver(() => {
147+
enforceIntegrity();
148+
});
149+
150+
observer.observe(anchor, { attributes: true, attributeFilter: ['class', 'style', 'hidden'] });
151+
152+
if (container) {
153+
observer.observe(container, {
154+
attributes: true,
155+
attributeFilter: ['class', 'style', 'hidden'],
156+
});
157+
}
158+
159+
const intervalId = window.setInterval(enforceIntegrity, 2000);
160+
161+
return () => {
162+
observer.disconnect();
163+
window.clearInterval(intervalId);
164+
165+
if (created) {
166+
guardStyleElement.remove();
167+
}
168+
};
169+
}, [className]);
170+
24171
return (
25-
<div className={className}>
172+
<div ref={containerRef} className={className} data-logto-signature-container="secured">
26173
<a
27-
className={styles.signature}
174+
ref={anchorRef}
28175
aria-label="Powered By Logto"
176+
className={styles.signature}
177+
data-logto-signature="secured"
29178
href={logtoUrl.toString()}
30-
target="_blank"
31179
rel="noopener"
180+
target="_blank"
32181
>
33-
<span className={styles.text}>Powered by</span>
34-
<LogtoLogoShadow className={styles.staticIcon} />
35-
<LogtoLogo className={styles.highlightIcon} />
182+
<span data-logto-signature-text className={styles.text}>
183+
Powered by
184+
</span>
185+
<LogtoLogoShadow data-logto-signature-icon="static" className={styles.staticIcon} />
186+
<LogtoLogo data-logto-signature-icon="highlight" className={styles.highlightIcon} />
36187
</a>
37188
</div>
38189
);

0 commit comments

Comments
 (0)