@loop8id/auth-spa-js
v1.0.0
Published
Auth SDK for Single Page Applications using OpenID Connect with https://auth.l8p8.com
Downloads
148
Readme
@loop8id/auth-spa-js
OpenID Connect / OAuth 2.0 authentication SDK for Single Page Applications
Authority: https://auth.l8p8.com
Features
- 🔐 PKCE flow — Authorization Code + PKCE (S256), the most secure SPA auth flow
- 🔄 Token refresh — Refresh token rotation or silent iframe renewal
- 🪟 Popup login —
loginWithPopup()for popup-based auth - 💾 Flexible caching — In-memory (default) or
localStorage - 📜 ID token validation — iss / aud / exp / nonce checks
- 🔌 OIDC discovery — Auto-fetches and caches
/.well-known/openid-configuration - 🧩 TypeScript-first — Full type definitions included
- 🪶 Zero dependencies — Uses only native browser APIs (Web Crypto, fetch)
Installation
npm install @loop8id/auth-spa-js
# or
yarn add @loop8id/auth-spa-js
# or
pnpm add @loop8id/auth-spa-jsOr via CDN:
<script src="https://unpkg.com/@loop8id/auth-spa-js/dist/loop8id-auth-spa-js.production.js"></script>
<!-- window.l8p8Auth is now available -->Quick Start (Vanilla JS)
1. Register your application
Go to https://developer.l8p8.com/projects, create a Single Page Application, and configure:
| Setting | Value |
|---|---|
| Allowed Callback URLs | http://localhost:5173 |
| Allowed Logout URLs | http://localhost:5173 |
| Allowed Web Origins | http://localhost:5173 |
2. Create the client
import { createLoop8IdClient } from '@loop8id/auth-spa-js';
const client = await createLoop8IdClient({
clientId: 'YOUR_CLIENT_ID',
// authority defaults to 'https://auth.l8p8.com'
});3. Handle the redirect callback
// On page load — check if we're returning from the login page
if (client.isRedirectCallback()) {
const { appState } = await client.handleRedirectCallback();
// Redirect to the original page (if stored in appState)
window.history.replaceState({}, '', appState?.returnTo || '/');
}4. Login
// Redirect to the login page
await client.loginWithRedirect({
appState: { returnTo: window.location.pathname },
});
// --- or via popup ---
await client.loginWithPopup();5. Check auth state & get user
const isAuthenticated = await client.isAuthenticated();
if (isAuthenticated) {
const user = await client.getUser();
console.log(user.name, user.email, user.picture);
}6. Get an id token
const itToken = await client.getIdToken();
### 7. Logout
```js
await client.logout({
logoutParams: { returnTo: window.location.origin },
});API Reference
createLoop8IdClient(options) → Promise<Loop8IdClient>
Factory function — creates a client and pre-fetches the OIDC discovery document.
const client = await createLoop8IdClient({
clientId: string; // required
authority?: string; // default: 'https://auth.l8p8.com'
redirectUri?: string; // default: window.location.origin
scope?: string; // default: 'openid profile email'
audience?: string; // API identifier for the id token
cacheLocation?: 'memory' | 'localstorage'; // default: 'memory'
useRefreshTokens?: boolean; // default: false
leeway?: number; // clock skew tolerance in seconds, default: 60
});client.loginWithRedirect(options?)
Redirects to the OIDC login page. After authentication, the user is redirected back to redirectUri.
await client.loginWithRedirect({
authorizationParams: {
prompt: 'login', // force re-authentication
scope: 'openid',
},
appState: { returnTo: '/dashboard' }, // passed back in handleRedirectCallback
});client.handleRedirectCallback(url?)
Call after the user is redirected back. Exchanges the authorization code for tokens.
const { appState } = await client.handleRedirectCallback();
// Cleans up ?code=&state= from the URL automaticallyclient.isAuthenticated() → Promise<boolean>
Returns true if the user has a valid id token (automatically attempts silent renewal if expired).
client.getUser<T>() → Promise<T | undefined>
Returns the user's profile. Uses ID token claims if available, otherwise calls the UserInfo endpoint.
client.getIdTokenClaims() → Promise<IdTokenClaims | undefined>
Returns all decoded claims from the ID token.
client.logout(options?)
Clears the session and redirects to the end_session_endpoint.
await client.logout({
logoutParams: { returnTo: 'https://myapp.com' },
localOnly: false, // set true to skip redirect, only clear local session
});client.isRedirectCallback(url?) → boolean
Returns true if the URL contains code and state parameters.
Silent Authentication Callback
For silent renewal via iframes, serve this page at a registered redirect URI:
<!-- /silent-callback.html -->
<!DOCTYPE html>
<html>
<body>
<script type="module">
import { handleSilentCallback } from '@loop8id/auth-spa-js';
handleSilentCallback();
</script>
</body>
</html>Popup Callback
For popup-based login, serve this page at the redirect URI used for popups:
<!-- /popup-callback.html -->
<!DOCTYPE html>
<html>
<body>
<script type="module">
import { handlePopupCallback } from '@loop8id/auth-spa-js';
handlePopupCallback();
</script>
</body>
</html>Framework Examples
React
import { createLoop8IdClient } from '@loop8id/auth-spa-js';
import { createContext, useContext, useEffect, useState } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [client, setClient] = useState(null);
const [user, setUser] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
const c = await createLoop8IdClient({ clientId: 'YOUR_CLIENT_ID' });
if (c.isRedirectCallback()) {
await c.handleRedirectCallback();
window.history.replaceState({}, '', '/');
}
const auth = await c.isAuthenticated();
if (auth) setUser(await c.getUser());
setIsAuthenticated(auth);
setClient(c);
setLoading(false);
})();
}, []);
return (
<AuthContext.Provider value={{ client, user, isAuthenticated, loading }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);Vue 3 (Composable)
// composables/useAuth.js
import { createLoop8IdClient } from '@loop8id/auth-spa-js';
import { ref, readonly } from 'vue';
const client = ref(null);
const user = ref(null);
const isAuthenticated = ref(false);
const loading = ref(true);
export function useAuth() {
async function init() {
client.value = await createLoop8IdClient({ clientId: 'YOUR_CLIENT_ID' });
if (client.value.isRedirectCallback()) {
await client.value.handleRedirectCallback();
window.history.replaceState({}, '', '/');
}
isAuthenticated.value = await client.value.isAuthenticated();
if (isAuthenticated.value) user.value = await client.value.getUser();
loading.value = false;
}
return {
client: readonly(client),
user: readonly(user),
isAuthenticated: readonly(isAuthenticated),
loading: readonly(loading),
init,
login: (opts) => client.value?.loginWithRedirect(opts),
logout: (opts) => client.value?.logout(opts),
getToken: (opts) => client.value?.getIdToken(opts),
};
}Migrating from @auth0/auth0-spa-js
@loop8id/auth-spa-js mirrors the Auth0 SPA SDK API. For a drop-in migration:
- import { createAuth0Client } from '@auth0/auth0-spa-js';
+ import { createLoop8IdClient as createAuth0Client } from '@loop8id/auth-spa-js';
const client = await createAuth0Client({
- domain: 'YOUR_AUTH0_DOMAIN',
+ clientId: 'YOUR_CLIENT_ID',
- clientId: 'YOUR_CLIENT_ID',
});The createAuth0Client alias is also exported directly:
import { createAuth0Client } from '@loop8id/auth-spa-js';
// Works as a drop-in replacementError Types
| Class | error code | When thrown |
|---|---|---|
| L8P8Error | various | Base error class |
| OAuthError | from server | Server returned an OAuth error |
| MissingRefreshTokenError | missing_refresh_token | Silent renewal attempted with no refresh token |
| TimeoutError | timeout | Silent iframe timed out |
| PopupCancelledError | popup_cancelled | User closed the popup |
| PopupTimeoutError | popup_timeout | Popup timed out |
Security Considerations
- PKCE is always used — authorization code interception attacks are prevented
- In-memory token storage (
cacheLocation: 'memory') is the default and most secure option — tokens are not accessible to other scripts and are cleared on page refresh cacheLocation: 'localstorage'survives page refreshes but is accessible to any JavaScript on the page — only use if your site has a strong Content Security Policy- ID token validation checks
iss,aud,exp,iat, andnonceon every token exchange - Always register exact Allowed Callback URLs and Allowed Web Origins in your dashboard
License
MIT © L8P8
