Implement "Sign in with SSO" in React SPA applications using SSOJet OIDC with PKCE.
This expert AI assistant guide walks you through integrating "Sign in with SSO" functionality into an existing login page in a React application using SSOJet as an OIDC identity provider. The goal is to modify the existing login flow to add SSO support without disrupting the current traditional login functionality (e.g., email/password).
oidc-client-ts, react-oidc-context.http://localhost:5173/auth/callback).Note: For SPAs, the recommended flow is Authorization Code with PKCE (no Client Secret required on the front-end).
Run the following command to install the required libraries:
npm install oidc-client-ts react-oidc-context react-router-dom
Create a dedicated configuration file (e.g., src/lib/oidcConfig.ts):
// src/lib/oidcConfig.ts
import { WebStorageStateStore } from 'oidc-client-ts';
export const oidcConfig = {
authority: 'https://auth.ssojet.com', // Your SSOJet Issuer URL
client_id: 'your_client_id',
redirect_uri: 'http://localhost:5173/auth/callback',
post_logout_redirect_uri: 'http://localhost:5173/login',
response_type: 'code',
scope: 'openid profile email',
userStore: new WebStorageStateStore({ store: window.localStorage }),
};
Update your src/main.tsx to wrap the application with the OIDC provider:
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from 'react-oidc-context';
import { oidcConfig } from './lib/oidcConfig';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AuthProvider {...oidcConfig}>
<BrowserRouter>
<App />
</BrowserRouter>
</AuthProvider>
</React.StrictMode>
);
Modify your existing login component (e.g., src/pages/Login.tsx) to include the "Sign in with SSO" toggle:
// src/pages/Login.tsx
import { useState } from 'react';
import { useAuth } from 'react-oidc-context';
export default function LoginPage() {
const [isSSO, setIsSSO] = useState(false);
const [email, setEmail] = useState('');
const auth = useAuth();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (isSSO) {
// Trigger SSO login via the OIDC library with login_hint
auth.signinRedirect({
extraQueryParams: { login_hint: email },
});
} else {
// Existing password login logic here
console.log('Processing traditional login...');
}
};
return (
<div className="login-container">
<h1>Sign In</h1>
<form onSubmit={handleLogin}>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
{!isSSO && (
<div>
<label htmlFor="password">Password</label>
<input type="password" id="password" required />
</div>
)}
<button type="submit">
{isSSO ? 'Continue with SSO' : 'Sign In'}
</button>
</form>
<button type="button" onClick={() => setIsSSO(!isSSO)}>
{isSSO ? 'Back to Password Login' : 'Sign in with SSO'}
</button>
</div>
);
}
Create a callback component to handle the OIDC redirect (e.g., src/pages/AuthCallback.tsx):
// src/pages/AuthCallback.tsx
import { useEffect } from 'react';
import { useAuth } from 'react-oidc-context';
import { useNavigate } from 'react-router-dom';
export default function AuthCallback() {
const auth = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (auth.isAuthenticated) {
console.log('Authenticated User:', auth.user?.profile);
navigate('/dashboard');
}
if (auth.error) {
console.error('OIDC Callback Error:', auth.error);
navigate('/login?error=oidc_failed');
}
}, [auth.isAuthenticated, auth.error, navigate]);
if (auth.isLoading) {
return <p>Authenticating...</p>;
}
return null;
}
Update your src/App.tsx to include the callback route:
// src/App.tsx
import { Routes, Route } from 'react-router-dom';
import LoginPage from './pages/Login';
import AuthCallback from './pages/AuthCallback';
import Dashboard from './pages/Dashboard';
export default function App() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/auth/callback" element={<AuthCallback />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/" element={<LoginPage />} />
</Routes>
);
}
Dashboard Page (src/pages/Dashboard.tsx):
// src/pages/Dashboard.tsx
import { useAuth } from 'react-oidc-context';
import { Navigate } from 'react-router-dom';
export default function Dashboard() {
const auth = useAuth();
if (!auth.isAuthenticated) {
return <Navigate to="/login" />;
}
return (
<div>
<h1>Dashboard</h1>
<pre>{JSON.stringify(auth.user?.profile, null, 2)}</pre>
<button onClick={() => auth.signoutRedirect()}>Logout</button>
</div>
);
}
npm run dev.http://localhost:5173/login)./auth/callback and then to /dashboard.auth.error from the useAuth hook to display OIDC-specific errors in the UI.oidc-client-ts. Never store a Client Secret in the front-end.auth.isAuthenticated before rendering child routes.npx skills add ssojet/oidc-hosted-page-react下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer