Skip to content

Commit d6e7a84

Browse files
os-zhuangclaude
andauthored
feat(auth): add "Sign in with SSO" entry to LoginForm (#2000)
Adds an SSO sign-in button to the login page that routes the entered email to its configured IdP (better-auth @better-auth/sso, by email domain) via POST /api/v1/auth/sign-in/sso and redirects to the provider's authorization endpoint. i18n label (labels.ssoButton); inline error when no provider matches the domain. UI half (PR-2) of the per-env external-IdP SSO mechanism; backend lands in framework #2322, design in ADR-0024. Verified end-to-end in a browser: button -> IdP login -> consent -> JIT federated session. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d23db5c commit d6e7a84

1 file changed

Lines changed: 39 additions & 0 deletions

File tree

packages/auth/src/LoginForm.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export interface LoginFormLabels {
3434
signUpText?: string;
3535
/** Divider label between social sign-in and email/password (defaults to "or") */
3636
orText?: string;
37+
/** Label for the SSO sign-in button (defaults to "Sign in with SSO") */
38+
ssoButton?: string;
3739
}
3840

3941
export interface LoginFormProps {
@@ -130,6 +132,7 @@ export function LoginForm({
130132
noAccountText: labels.noAccountText ?? "Don't have an account?",
131133
signUpText: labels.signUpText ?? 'Sign up',
132134
orText: labels.orText ?? 'or',
135+
ssoButton: labels.ssoButton ?? 'Sign in with SSO',
133136
};
134137

135138
const handleSubmit = async (e: React.FormEvent) => {
@@ -147,6 +150,33 @@ export function LoginForm({
147150
}
148151
};
149152

153+
/**
154+
* Federated SSO sign-in: routes the entered email to its configured IdP
155+
* (better-auth `@better-auth/sso`, by email domain) and redirects the
156+
* browser to the provider's authorization endpoint. Falls back to an inline
157+
* error when no provider matches the domain.
158+
*/
159+
const handleSso = async () => {
160+
setError(null);
161+
try {
162+
const base = window.location.pathname.replace(/\/login(?:\/.*)?$/, '');
163+
const res = await fetch('/api/v1/auth/sign-in/sso', {
164+
method: 'POST',
165+
headers: { 'content-type': 'application/json' },
166+
body: JSON.stringify({ email, callbackURL: base + '/home' }),
167+
credentials: 'include',
168+
});
169+
const data = (await res.json().catch(() => ({}))) as { url?: string; message?: string };
170+
if (res.ok && typeof data.url === 'string') {
171+
window.location.href = data.url;
172+
return;
173+
}
174+
setError(data.message || 'No SSO provider is configured for this email domain.');
175+
} catch (err) {
176+
setError(err instanceof Error ? err.message : String(err));
177+
}
178+
};
179+
150180
return (
151181
<div className="mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]">
152182
<AuthFormHeader
@@ -215,6 +245,15 @@ export function LoginForm({
215245
{isLoading && <AuthSpinner />}
216246
{isLoading ? l.submittingButton : l.submitButton}
217247
</button>
248+
249+
<button
250+
type="button"
251+
onClick={handleSso}
252+
disabled={isLoading}
253+
className="flex w-full items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50"
254+
>
255+
{l.ssoButton}
256+
</button>
218257
</form>
219258
</div>
220259

0 commit comments

Comments
 (0)