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
52 changes: 0 additions & 52 deletions packages/managed-auth-react/src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,55 +198,3 @@ export const SpinnerIcon = (p: IconProps) => (
/>
</svg>
);

// SSO provider marks
export const GoogleMark = (p: IconProps) => (
<svg viewBox="0 0 24 24" aria-hidden="true" {...p}>
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
</svg>
);

export const GitHubMark = (p: IconProps) => (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...p}>
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg>
);

export const MicrosoftMark = (p: IconProps) => (
<svg viewBox="0 0 24 24" aria-hidden="true" {...p}>
<path d="M1 1h10.5v10.5H1V1z" fill="#F25022" />
<path d="M12.5 1H23v10.5H12.5V1z" fill="#7FBA00" />
<path d="M1 12.5h10.5V23H1V12.5z" fill="#00A4EF" />
<path d="M12.5 12.5H23V23H12.5V12.5z" fill="#FFB900" />
</svg>
);

export const FacebookMark = (p: IconProps) => (
<svg viewBox="0 0 24 24" aria-hidden="true" {...p}>
<path
d="M24 12c0-6.627-5.373-12-12-12S0 5.373 0 12c0 5.99 4.388 10.954 10.125 11.854V15.47H7.078V12h3.047V9.356c0-3.007 1.792-4.668 4.533-4.668 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874V12h3.328l-.532 3.47h-2.796v8.384C19.612 22.954 24 17.99 24 12z"
fill="#1877F2"
/>
</svg>
);

export const AppleMark = (p: IconProps) => (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...p}>
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
</svg>
);
82 changes: 50 additions & 32 deletions packages/managed-auth-react/src/components/sso-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,58 @@
import type { ReactNode } from "react";
import {
AppleMark,
BuildingIcon,
FacebookMark,
GitHubMark,
GoogleMark,
KeyIcon,
MicrosoftMark,
} from "./icons";
import { useState, type ReactNode } from "react";
import { BuildingIcon, KeyIcon } from "./icons";

export interface SSOProviderInfo {
label: string;
icon: ReactNode;
}

const NON_BRAND_ICONS: Record<string, { label: string; icon: ReactNode }> = {
passkey: { label: "Passkey", icon: <KeyIcon className="kma-sso-icon" /> },
sso: { label: "SSO", icon: <BuildingIcon className="kma-sso-icon" /> },
saml: { label: "SSO", icon: <BuildingIcon className="kma-sso-icon" /> },
};

function slugify(provider: string): string {
return provider.toLowerCase().replace(/[^a-z0-9]/g, "");
}

function titleCase(provider: string): string {
return provider
.split(/[\s_-]+/)
.filter(Boolean)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join(" ");
}

function SSOProviderIcon({ provider }: { provider: string }) {
const [errored, setErrored] = useState(false);
const slug = slugify(provider);
const letter = provider.trim().charAt(0).toUpperCase() || "?";

if (!slug || errored) {
return (
<span className="kma-sso-icon kma-sso-icon--letter" aria-hidden="true">
{letter}
</span>
);
}

return (
<img
src={`https://cdn.simpleicons.org/${slug}`}
alt=""
className="kma-sso-icon"
onError={() => setErrored(true)}
/>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale error state persists across provider prop changes

Low Severity

SSOProviderIcon stores CDN load failure in errored state via useState, but never resets it when the provider prop changes. If a CDN request fails for one provider and then the component receives a different provider (e.g., ssoProvider changes in the parent while the tree position stays mounted), the stale errored = true causes the new provider to immediately render the letter-avatar fallback instead of attempting its own CDN fetch.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0c8c226. Configure here.


export function getSSOProviderInfo(provider: string): SSOProviderInfo {
const p = provider.toLowerCase();
if (p.includes("google"))
return { label: "Google", icon: <GoogleMark className="kma-sso-icon" /> };
if (p.includes("github"))
return { label: "GitHub", icon: <GitHubMark className="kma-sso-icon" /> };
if (p.includes("microsoft") || p.includes("azure"))
return {
label: "Microsoft",
icon: <MicrosoftMark className="kma-sso-icon" />,
};
if (p.includes("facebook"))
return {
label: "Facebook",
icon: <FacebookMark className="kma-sso-icon" />,
};
if (p.includes("apple"))
return { label: "Apple", icon: <AppleMark className="kma-sso-icon" /> };
if (p.includes("saml") || p.includes("sso"))
return { label: "SSO", icon: <BuildingIcon className="kma-sso-icon" /> };
if (p.includes("passkey"))
return { label: "Passkey", icon: <KeyIcon className="kma-sso-icon" /> };
return { label: provider, icon: null };
const key = slugify(provider);
const nonBrand = NON_BRAND_ICONS[key];
if (nonBrand) return nonBrand;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-brand providers lose substring matching, breaking compound names

Medium Severity

The old code used p.includes("saml") || p.includes("sso") to match provider strings, so compound names like "enterprise-sso" or "saml-provider" would correctly get the building icon. The new code slugifys the provider (yielding e.g. "enterprisesso") and does an exact dictionary lookup against NON_BRAND_ICONS, which only has keys "passkey", "sso", and "saml". These compound names now miss the non-brand check entirely, hit the CDN path, fail to load, and degrade to a letter avatar instead of the intended BuildingIcon or KeyIcon.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0c8c226. Configure here.

return {
label: titleCase(provider),
icon: <SSOProviderIcon provider={provider} />,
};
}
12 changes: 12 additions & 0 deletions packages/managed-auth-react/src/styles/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,18 @@
height: 1.25rem;
}

.kma-sso-icon--letter {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--kma-color-muted, #6b7280);
color: #fff;
font-size: 0.75rem;
font-weight: 600;
line-height: 1;
}

/* ---------- Form / Input ---------- */

.kma-form {
Expand Down
Loading