feat(web): add same-origin SPA authentication
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { useEffect, useState, type ReactElement } from 'react';
|
||||
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { api } from '@/lib/api';
|
||||
import { resolveAuthCallbackURL } from '@/lib/auth-redirect';
|
||||
import { signIn } from '@/lib/auth-client';
|
||||
import type { SsoProviderDiscovery } from '@/lib/sso';
|
||||
|
||||
export function SsoCallbackPage(): ReactElement {
|
||||
const { provider: providerId = '' } = useParams<'provider'>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const requestedCallbackURL = searchParams.get('callbackURL');
|
||||
const [providerName, setProviderName] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function redirectToProvider(): Promise<void> {
|
||||
try {
|
||||
const callbackURL = resolveAuthCallbackURL(requestedCallbackURL, window.location.origin);
|
||||
const providers = await api<SsoProviderDiscovery[]>('/api/sso/providers');
|
||||
if (cancelled) return;
|
||||
|
||||
const provider = providers.find((candidate) => candidate.id === providerId);
|
||||
if (!provider) {
|
||||
setError('Unknown SSO provider.');
|
||||
return;
|
||||
}
|
||||
|
||||
setProviderName(provider.name);
|
||||
if (!provider.configured) {
|
||||
setError(`${provider.name} is not enabled in this deployment.`);
|
||||
return;
|
||||
}
|
||||
if (provider.loginMode !== 'oidc') {
|
||||
setError(`${provider.name} is not available for OIDC sign in.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await signIn.oauth2({
|
||||
providerId: provider.id,
|
||||
callbackURL,
|
||||
});
|
||||
|
||||
if (!cancelled && result?.error) {
|
||||
setError(result.error.message ?? `${provider.name} sign in failed.`);
|
||||
}
|
||||
} catch (caught: unknown) {
|
||||
if (!cancelled) {
|
||||
setError(caught instanceof Error ? caught.message : 'Unable to start single sign-on.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void redirectToProvider();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [providerId, requestedCallbackURL]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex min-h-[50vh] max-w-md flex-col justify-center">
|
||||
<h1 className="text-2xl font-semibold text-text-primary">Single sign-on</h1>
|
||||
<p className="mt-2 text-sm text-text-secondary">
|
||||
{providerName
|
||||
? `Redirecting you to ${providerName}...`
|
||||
: 'Preparing your sign-in request...'}
|
||||
</p>
|
||||
|
||||
{error ? (
|
||||
<div
|
||||
role="alert"
|
||||
className="mt-6 rounded-lg border border-error/30 bg-error/10 px-4 py-3 text-sm text-error"
|
||||
>
|
||||
<p>{error}</p>
|
||||
<Link
|
||||
to="/login"
|
||||
className="mt-3 inline-block font-medium text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
Return to login
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 rounded-lg border border-surface-border bg-surface-elevated px-4 py-3 text-sm text-text-secondary">
|
||||
If the redirect does not start automatically, return to the login page and try again.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user