Authenticate your users once and seamlessly pass their identity into every Legion widget — no extra logins required.
Get Credentials
Create an SSO client and receive your Client ID & Secret
Authenticate
Exchange user identity for a short-lived JWT token
Integrate
Pass the token to widgets via React SDK, iframe, or web component
Secure
Server-side secret, origin validation, auto-expiring tokens
Legion SSO lets your existing authentication system issue short-lived JWT tokens that Legion widgets accept automatically. Your users never see a second login screen.
Important: The SSO clientSecret must never be exposed in client-side code. Always call the SSO endpoint from your server.
React SDK Prop
Pass authToken as a prop to any <Legion*> component.
URL Parameter
Append ?token=JWT&origin=… to any widget iframe URL.
Web Component Attribute
Set auth-token on any <legion-*> custom element.
Before you can authenticate users, you need an SSO client with a unique Client ID and Client Secret.
Contact Legion Hand Technologies at [email protected] to request SSO credentials for your organization. Provide your production domain(s) and a contact email.
| Field | Example | Description |
|---|---|---|
clientId | client-a1b2c3d4-… | Public identifier for your SSO integration |
clientSecret | e4f5a6b7-… | Secret key — keep server-side only |
allowedOrigins | ["https://mysite.com"] | Domains authorized to use this client |
tokenExpiryHours | 24 | How long issued JWTs remain valid |
Store your credentials as environment variables — never hard-code them.
1# .env (server-side only — do NOT expose to the browser)
2LEGION_SSO_CLIENT_ID=client-a1b2c3d4-e5f6-7890-abcd-ef1234567890
3LEGION_SSO_CLIENT_SECRET=e4f5a6b7-c8d9-0123-4567-89abcdef0123
4LEGION_BASE_URL=https://your-legion-domain.comVerify everything works with a quick curl call:
1curl -X POST https://your-legion-domain.com/api/auth/sso \
2 -H "Content-Type: application/json" \
3 -d '{
4 "clientId": "YOUR_CLIENT_ID",
5 "clientSecret": "YOUR_CLIENT_SECRET",
6 "userIdentifier": "[email protected]",
7 "userData": {
8 "firstName": "Test",
9 "lastName": "User"
10 }
11 }'
12
13# ✅ Expected response:
14# {
15# "token": "eyJhbGciOiJIUzI1NiIs…",
16# "userId": "uuid-of-user",
17# "expiresIn": "24h"
18# }Use the @legionhandtech/lht-react-widgets NPM package for first-class React support with typed props and SSO built in.
npm install @legionhandtech/lht-react-widgetsThis keeps your clientSecret on the server. Works with both the App Router and Pages Router.
1// app/api/legion-token/route.ts (App Router)
2import { NextResponse } from "next/server";
3
4export async function POST(request: Request) {
5 const { userEmail, firstName, lastName } = await request.json();
6
7 const res = await fetch(
8 `${process.env.LEGION_BASE_URL}/api/auth/sso`,
9 {
10 method: "POST",
11 headers: { "Content-Type": "application/json" },
12 body: JSON.stringify({
13 clientId: process.env.LEGION_SSO_CLIENT_ID,
14 clientSecret: process.env.LEGION_SSO_CLIENT_SECRET,
15 userIdentifier: userEmail,
16 userData: { firstName, lastName, email: userEmail },
17 }),
18 }
19 );
20
21 if (!res.ok) {
22 return NextResponse.json(
23 { error: "SSO authentication failed" },
24 { status: res.status }
25 );
26 }
27
28 const data = await res.json();
29 return NextResponse.json({ token: data.token, expiresIn: data.expiresIn });
30}1// hooks/useLegionToken.ts
2import { useState, useEffect, useCallback } from "react";
3
4export function useLegionToken(user: { email: string; firstName?: string; lastName?: string } | null) {
5 const [token, setToken] = useState<string | null>(null);
6 const [loading, setLoading] = useState(false);
7
8 const fetchToken = useCallback(async () => {
9 if (!user) return;
10 setLoading(true);
11 try {
12 const res = await fetch("/api/legion-token", {
13 method: "POST",
14 headers: { "Content-Type": "application/json" },
15 body: JSON.stringify({
16 userEmail: user.email,
17 firstName: user.firstName,
18 lastName: user.lastName,
19 }),
20 });
21 const data = await res.json();
22 if (data.token) setToken(data.token);
23 } catch (err) {
24 console.error("Failed to get Legion token:", err);
25 } finally {
26 setLoading(false);
27 }
28 }, [user]);
29
30 useEffect(() => { fetchToken(); }, [fetchToken]);
31
32 return { token, loading, refresh: fetchToken };
33}1import { LegionProfile, LegionOpportunities, LegionWallet } from "@legionhandtech/lht-react-widgets";
2import { useLegionToken } from "@/hooks/useLegionToken";
3
4export default function Dashboard() {
5 const user = useCurrentUser(); // your auth hook
6 const { token, loading } = useLegionToken(user);
7
8 if (loading || !token) return <p>Loading widgets…</p>;
9
10 return (
11 <div className="grid grid-cols-2 gap-6">
12 <LegionProfile
13 baseUrl={process.env.NEXT_PUBLIC_LEGION_URL!}
14 authToken={token}
15 theme="light"
16 />
17 <LegionWallet
18 baseUrl={process.env.NEXT_PUBLIC_LEGION_URL!}
19 authToken={token}
20 theme="light"
21 />
22 <LegionOpportunities
23 baseUrl={process.env.NEXT_PUBLIC_LEGION_URL!}
24 authToken={token}
25 count={6}
26 layout="horizontal"
27 theme="light"
28 />
29 </div>
30 );
31}Tip: Store NEXT_PUBLIC_LEGION_URL (the base URL without the secret) in your public env vars so the React components can reference it directly.
A common pattern: WordPress handles content & user accounts via the REST API, while a React (or Next.js) frontend renders the UI and Legion widgets.
Architecture: WordPress REST API → your Node/Next.js backend (proxies SSO) → React frontend with Legion widgets. The clientSecret lives on your Node server, never in WordPress or the browser.
Use the built-in /wp-json/wp/v2/users/me endpoint (requires authentication via JWT or application passwords) or a custom endpoint.
1// functions.php — optional custom endpoint
2add_action('rest_api_init', function () {
3 register_rest_route('legion/v1', '/me', [
4 'methods' => 'GET',
5 'callback' => function (WP_REST_Request $request) {
6 $user = wp_get_current_user();
7 return [
8 'email' => $user->user_email,
9 'firstName' => $user->first_name,
10 'lastName' => $user->last_name,
11 ];
12 },
13 'permission_callback' => function () {
14 return is_user_logged_in();
15 },
16 ]);
17});1// server/legion-sso.ts (Express or Next.js API route)
2import express from "express";
3const router = express.Router();
4
5router.post("/api/legion-token", async (req, res) => {
6 // 1. Verify the WordPress session (cookie or JWT)
7 const wpUser = await verifyWordPressSession(req);
8 if (!wpUser) return res.status(401).json({ error: "Not authenticated" });
9
10 // 2. Request a Legion token
11 const ssoRes = await fetch(
12 `${process.env.LEGION_BASE_URL}/api/auth/sso`,
13 {
14 method: "POST",
15 headers: { "Content-Type": "application/json" },
16 body: JSON.stringify({
17 clientId: process.env.LEGION_SSO_CLIENT_ID,
18 clientSecret: process.env.LEGION_SSO_CLIENT_SECRET,
19 userIdentifier: wpUser.email,
20 userData: {
21 firstName: wpUser.firstName,
22 lastName: wpUser.lastName,
23 email: wpUser.email,
24 },
25 }),
26 }
27 );
28
29 const data = await ssoRes.json();
30 res.json({ token: data.token, expiresIn: data.expiresIn });
31});1import { LegionProfile, LegionOpportunities } from "@legionhandtech/lht-react-widgets";
2import { useEffect, useState } from "react";
3
4function LegionDashboard() {
5 const [token, setToken] = useState<string | null>(null);
6
7 useEffect(() => {
8 fetch("/api/legion-token", { method: "POST", credentials: "include" })
9 .then(r => r.json())
10 .then(d => setToken(d.token))
11 .catch(console.error);
12 }, []);
13
14 if (!token) return <p>Authenticating…</p>;
15
16 return (
17 <>
18 <LegionProfile
19 baseUrl="https://your-legion-domain.com"
20 authToken={token}
21 theme="light"
22 />
23 <LegionOpportunities
24 baseUrl="https://your-legion-domain.com"
25 authToken={token}
26 count={4}
27 layout="vertical"
28 theme="light"
29 />
30 </>
31 );
32}No framework required. Use web components or iframes with a simple fetch call to your backend for the token.
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <title>My Site</title>
5 <script src="https://your-legion-domain.com/js/profile-widget.js"></script>
6 <script src="https://your-legion-domain.com/js/opportunities-widget.js"></script>
7</head>
8<body>
9 <legion-profile id="profile" base-url="https://your-legion-domain.com" theme="light"></legion-profile>
10 <legion-opportunities id="opps" base-url="https://your-legion-domain.com" count="4" theme="light"></legion-opportunities>
11
12 <script>
13 // Fetch token from YOUR server (keeps clientSecret safe)
14 fetch("/api/legion-token", { method: "POST", credentials: "include" })
15 .then(r => r.json())
16 .then(({ token }) => {
17 document.getElementById("profile").setAttribute("auth-token", token);
18 document.getElementById("opps").setAttribute("auth-token", token);
19 });
20 </script>
21</body>
22</html>1<div id="widget-container"></div>
2
3<script>
4 async function loadWidget() {
5 const res = await fetch("/api/legion-token", {
6 method: "POST",
7 credentials: "include",
8 });
9 const { token } = await res.json();
10
11 const origin = encodeURIComponent(window.location.origin);
12 const iframe = document.createElement("iframe");
13 iframe.src = `https://your-legion-domain.com/widget/profile?token=${token}&origin=${origin}`;
14 iframe.width = "100%";
15 iframe.height = "400";
16 iframe.style.border = "none";
17 iframe.style.borderRadius = "12px";
18 document.getElementById("widget-container").appendChild(iframe);
19 }
20
21 loadWidget();
22</script>Load the iframe first, then send the token after it's ready. Useful when the token arrives asynchronously.
1const iframe = document.querySelector("#legion-iframe");
2
3iframe.addEventListener("load", async () => {
4 const res = await fetch("/api/legion-token", {
5 method: "POST",
6 credentials: "include",
7 });
8 const { token } = await res.json();
9
10 iframe.contentWindow.postMessage(
11 { type: "AUTH_TOKEN", token },
12 "https://your-legion-domain.com"
13 );
14});Your backend is the only place that should know the clientSecret. Here are examples for popular server frameworks.
1import express from "express";
2
3const app = express();
4app.use(express.json());
5
6app.post("/api/legion-token", async (req, res) => {
7 // 1. Verify the user is authenticated in YOUR system
8 const user = await getAuthenticatedUser(req); // your auth middleware
9 if (!user) return res.status(401).json({ error: "Unauthorized" });
10
11 // 2. Exchange for a Legion token
12 const response = await fetch(
13 `${process.env.LEGION_BASE_URL}/api/auth/sso`,
14 {
15 method: "POST",
16 headers: { "Content-Type": "application/json" },
17 body: JSON.stringify({
18 clientId: process.env.LEGION_SSO_CLIENT_ID,
19 clientSecret: process.env.LEGION_SSO_CLIENT_SECRET,
20 userIdentifier: user.email,
21 userData: {
22 firstName: user.firstName,
23 lastName: user.lastName,
24 email: user.email,
25 phone: user.phone,
26 },
27 }),
28 }
29 );
30
31 const data = await response.json();
32 if (!response.ok) return res.status(response.status).json(data);
33
34 res.json({ token: data.token, expiresIn: data.expiresIn });
35});1import os, requests
2from flask import Flask, request, jsonify
3
4app = Flask(__name__)
5
6@app.route("/api/legion-token", methods=["POST"])
7def get_legion_token():
8 user = get_authenticated_user(request) # your auth check
9 if not user:
10 return jsonify(error="Unauthorized"), 401
11
12 resp = requests.post(
13 f"{os.environ['LEGION_BASE_URL']}/api/auth/sso",
14 json={
15 "clientId": os.environ["LEGION_SSO_CLIENT_ID"],
16 "clientSecret": os.environ["LEGION_SSO_CLIENT_SECRET"],
17 "userIdentifier": user["email"],
18 "userData": {
19 "firstName": user.get("first_name"),
20 "lastName": user.get("last_name"),
21 "email": user["email"],
22 },
23 },
24 )
25
26 data = resp.json()
27 if not resp.ok:
28 return jsonify(data), resp.status_code
29
30 return jsonify(token=data["token"], expiresIn=data["expiresIn"])The single endpoint that exchanges your SSO credentials + user identity for a JWT token.
| Field | Type | Required | Description |
|---|---|---|---|
clientId | string | Required | Your SSO client ID |
clientSecret | string | Required | Your SSO client secret |
userIdentifier | string | Required | User's email address or phone number |
userData | object | Optional | Additional user profile data (see below) |
userData.firstName | string | Optional | User's first name |
userData.lastName | string | Optional | User's last name |
userData.email | string | Optional | Secondary email (if identifier is phone) |
userData.phone | string | Optional | Secondary phone (if identifier is email) |
1{
2 "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0…",
3 "userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
4 "expiresIn": "24h"
5}The decoded JWT contains:
1{
2 "userId": "a1b2c3d4-…",
3 "sub": "[email protected]",
4 "iss": "client-a1b2c3d4-…",
5 "aud": "legion-widgets",
6 "iat": 1708900000,
7 "exp": 1708986400
8}| Status | Body | Cause |
|---|---|---|
400 | {"error":"clientId, clientSecret, and userIdentifier are required"} | Missing required fields |
401 | {"error":"Invalid client credentials"} | Wrong clientId or clientSecret |
401 | {"error":"SSO client not found or inactive"} | Client deactivated or deleted |
500 | {"error":"Internal server error"} | Server-side issue — contact support |
If the userIdentifier doesn't match an existing user, Legion automatically creates a new account using the provided userData. On subsequent calls, ifuserData is provided and the user already exists, missing fields (like a phone number) will be backfilled — existing data is never overwritten.
If widgets show "Authentication Required" or API calls return 401, work through these checks in order.
Symptoms: 401 Invalid client credentials from/api/auth/sso.
Fix: Double-check clientId and clientSecret in your environment variables. Ensure the SSO client is marked active. Contact support if you need new credentials.
Symptoms: Token generation works, but widgets still show "Authentication Required". Browser console shows CORS errors.
Fix: Your page's origin (window.location.origin) must exactly match one of the allowedOrigins in your SSO client config. Common mismatches: http vs https, trailing slashes,www vs non-www, different ports.
// Debug: check your actual origin
console.log("My origin:", window.location.origin);
// Must match exactly: "https://mysite.com" (no trailing slash)Symptoms: Token is generated successfully but the widget doesn't receive it.
Checklist:
authToken prop is not null or undefinedtoken and origin URL params are present and URL-encodedauth-token attribute is set after the element is in the DOMSymptoms: Widget works initially but fails after some time. API calls return 401.
Fix: Tokens expire based on your SSO client's tokenExpiryHours setting. Implement token refresh logic:
1// Decode and check expiry
2const payload = JSON.parse(atob(token.split(".")[1]));
3const expiresAt = new Date(payload.exp * 1000);
4const isExpired = expiresAt < new Date();
5
6if (isExpired) {
7 // Re-fetch from your /api/legion-token endpoint
8 const { token: newToken } = await refreshLegionToken();
9 // Update widgets with new token
10}Symptoms: You see your clientSecret in browser dev tools (Network tab or source code).
Fix: Never call /api/auth/sso directly from client-side code. Always proxy through your own backend. If your secret has been exposed, contact support immediately to rotate it.
Cause: Your SSO client's allowedOrigins only includes localhost URLs.
Fix: Contact Legion Hand Technologies to add your production domain(s) to the allowed origins list:
1{
2 "allowedOrigins": [
3 "https://mysite.com",
4 "https://www.mysite.com",
5 "http://localhost:3000"
6 ]
7}allowedOriginscurl test returns a valid token (3-part JWT)aud: "legion-widgets" and a valid userIdexp claim is in the future)/api/user/profile returns 200 (not 401) in the Network tabContact Legion Hand Technologies for SSO client setup, origin configuration, or integration support.