@idmission/auth-modal
v3.3.0
Published
Shared login modal for IDMission Web SDK products.
Readme
@idmission/auth-modal
Shared login modal for IDMission Web SDK products. Drop-in <AuthDialog /> and <AuthButton /> with full session lifecycle management, environment/region selection, and pluggable persistence.
Install
npm install @idmission/auth-modal
# or pnpm add / yarn addQuick start (Next.js)
1. Mount the catch-all auth route:
// app/api/idmission-auth/[...slug]/route.ts
import { createAuthRouteHandler } from "@idmission/auth-modal/next"
export const { GET, POST } = createAuthRouteHandler()2. Wrap your app with the provider and import the styles:
// app/layout.tsx
import { AuthProvider } from "@idmission/auth-modal"
import "@idmission/auth-modal/styles.css"
export default function RootLayout({ children }) {
return (
<html><body>
<AuthProvider>{children}</AuthProvider>
</body></html>
)
}3. Drop the dialog, env switcher, and button anywhere:
import { AuthDialog, AuthButton, AuthEnvSwitcher, useAuth } from "@idmission/auth-modal"
function App() {
return (
<>
<header>
<AuthEnvSwitcher />
</header>
<AuthDialog />
<AuthButton />
</>
)
}
function YourComponent() {
const auth = useAuth()
if (!auth.isAuthenticated) return null
return (
<div>
Active env: {auth.activeEnvUrl}
<br />
Cached envs: {auth.envs.length}
<br />
Last verified: {new Date(auth.lastValidatedAt!).toLocaleString()}
</div>
)
}4. Set the encryption key for credential persistence:
Generate a 32-byte key (64 hex chars) and add it to your environment as AUTH_ENCRYPTION_KEY:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"If AUTH_ENCRYPTION_KEY is unset, the package gracefully degrades to "no persistence" — sign-in still works, but credentials don't survive page reloads.
Storybook / Vite consumer
Use the localStorageAuthAdapter and configure your Vite proxy to forward to the IDMission API:
import { AuthProvider, AuthDialog, AuthEnvSwitcher, useAuth, localStorageAuthAdapter } from "@idmission/auth-modal"
import "@idmission/auth-modal/styles.css"
const adapter = localStorageAuthAdapter({
sessionsApiProxy: "/api/idmission-portal",
})
function App() {
return (
<AuthProvider adapter={adapter}>
<header>
<AuthEnvSwitcher />
</header>
<main>
<YourApp />
<AuthDialog />
</main>
</AuthProvider>
)
}
function YourApp() {
const auth = useAuth()
if (!auth.isAuthenticated) return null
return (
<div>
Active env: {auth.activeEnvUrl}
<br />
Last verified: {new Date(auth.lastValidatedAt!).toLocaleString()}
</div>
)
}Configure your Vite proxy to forward to the IDMission API:
// vite.config.ts
export default {
server: {
proxy: {
"/api/idmission-portal": {
target: "https://portal-api-dev.idmission.com",
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api\/idmission-portal/, ""),
},
},
},
}Security note: the localStorage adapter stores the API key secret in cleartext in the browser. Use only in dev/demo contexts.
Migration to 3.0
Automatic storage migration
v2 stored data is automatically migrated to v3 on first load. No consumer action required.
Breaking changes
useAuth().sessionremoved. Credentials are no longer held as an in-memory session. UseuseAuth().activeEnvUrlto check the active environment anduseAuth().lastValidatedAtfor the freshness of the last validation. To validate credentials, calluseAuth().revalidateActive().useSessionTimeRemaininghook removed. Session lifecycle is no longer tracked. If you need to detect when credentials expire, polluseAuth().lastValidatedAtor listen to theAuthProvider's state via callback.AuthAdapterinterface redesigned. Custom adapters must update:- Old:
remember(creds),getRemembered({ checkOnly }),forget(),createSession(input) → Session - New:
loadStore(): Promise<StoredAuthV3 | null>→ returns the persisted v3 blob (or null);saveStore(store): Promise<void>→ persists the blob;clearStore(): Promise<void>→ wipes storage. Seesrc/client/adaptersfor built-in implementations. createSession({ sessionsServiceUrl, apiKeyId, apiKeySecret }): Promise<void>— validates credentials against the sessions service. Throws on non-2xx with a human-readable message; the returned token is intentionally discarded (sessions are decorative in v3).
- Old:
Next.js route handler endpoints renamed (if using custom fetch calls to the auth API):
/api/idmission-auth/remember→/api/idmission-auth/save/api/idmission-auth/remembered→/api/idmission-auth/load/api/idmission-auth/forget→/api/idmission-auth/clear- Consumers using
createAuthRouteHandler()are unaffected — the factory handles these paths transparently.
New API
<AuthEnvSwitcher />— Standalone component for switching between cached environments. Place it in a toolbar, header, or menu outside the dialog.useAuth().envs— Array of cached environment objects.useAuth().activeEnvUrl— Currently active environment URL.useAuth().lastValidatedAt— Timestamp of the last successful credential validation. Use this to decide when to revalidate.useAuth().switchEnv(url)— Switch to an existing cached environment.useAuth().removeEnv(url)— Remove an environment from the cache.useAuth().forgetAllEnvs()— Clear all cached environments and sign out.useAuth().revalidateActive()— Validate the active environment's credentials against the API.useAuth().signOutActive()— Sign out of the active environment without clearing the cache.
Migration to 3.1
Security fix: apiKeySecret no longer in client state for Next.js consumers
In 3.0, useAuth().apiKeySecret was non-null in the browser for httpAuthAdapter consumers — /api/idmission-auth/load returned the entire stored blob, including the secret. This was a regression vs. v2's behavior, where the secret was kept server-only in the encrypted cookie.
3.1 restores the server-only privilege boundary:
/api/idmission-auth/loadreturns the v3 blob withapiKeySecret: ""for every entry. The cookie itself still holds the secret encrypted server-side.- After a successful sign-in, the provider scrubs
apiKeySecretfrom in-memory state oncesaveStoreresolves. useAuth().apiKeySecretis the empty string forhttpAuthAdapterconsumers after the same-session sign-in form submit.
localStorageAuthAdapter (Storybook / Vite consumers) is unchanged — there's no server, so the secret necessarily stays in client storage.
New: POST /api/idmission-auth/mint-session
For consumers that need a real session id (e.g. embedding it in iframes, hand-off to other services), the Next.js route handler now exposes a server-side mint endpoint:
const res = await fetch("/api/idmission-auth/mint-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionsServiceUrl: auth.sessionsServiceUrl }),
})
const { session } = await res.json()
// session.id is the upstream session tokenThe route decrypts the encrypted cookie, looks up the entry by sessionsServiceUrl, calls upstream, and returns the session payload. Avoid the temptation to mint sessions client-side using useAuth().apiKeySecret — that secret is empty after page reload in 3.1.
Custom AuthAdapter implementations
If you have a hand-rolled adapter, add the keepsSecretsClientSide capability flag:
const myAdapter: AuthAdapter = {
keepsSecretsClientSide: true, // or false if you have a server-side store
// ... existing methods
}The provider uses this to decide whether to scrub state.envs[*].apiKeySecret after a successful saveStore. TypeScript will surface the omission at compile time.
API reference
See src/client/types.ts for the AuthAdapter interface and src/client/provider/AuthProvider.tsx for AuthProviderProps.
License
UNLICENSED — internal use only.
