@lumen-id/auth-sdk
v0.1.0
Published
Official Lumen IDP SDK for web and backend applications
Maintainers
Readme
@lumen/auth-sdk
Official Lumen IDP SDK for web and backend applications. Provides seamless OAuth2/OIDC integration with PKCE flow.
Features
- Framework-agnostic core - Works with any framework or vanilla JS
- React integration - Provider and hook for easy React integration
- Backend support - Express, Fastify, and NestJS middleware/guards
- Automatic token refresh - Keeps sessions alive seamlessly
- Granular data access - Field-level consent flow with
requestDataAccess+getUserData() - TypeScript-first - Full TypeScript support with strict types
- CDN ready - Use directly from unpkg or jsdelivr
Installation
npm install @lumen/auth-sdk
# or
yarn add @lumen/auth-sdk
# or
pnpm add @lumen/auth-sdkCDN Usage
<!-- For core SDK -->
<script src="https://unpkg.com/@lumen/auth-sdk/dist/index.global.js"></script>
<!-- For React -->
<script src="https://unpkg.com/@lumen/auth-sdk/dist/react.global.js"></script>Quick Start
1. Core SDK (Framework-agnostic)
import { LumenAuthClient } from '@lumen/auth-sdk';
const client = new LumenAuthClient({
clientId: 'your-client-id',
issuer: 'https://api.lumen.com/idp',
redirectUri: 'http://localhost:5173/callback',
scopes: ['openid', 'profile', 'email', 'offline_access'],
autoRefresh: true,
});
// Initiate login
await client.login();
// Handle callback (on your callback page)
const session = await client.handleCallback();
console.log('User:', session.user);
// Check authentication status
if (client.isAuthenticated()) {
const user = client.getSession()?.user;
}
// Logout
await client.logout();2. React Integration
import { LumenAuthProvider, useLumenAuth } from '@lumen/auth-sdk/react';
// App setup
function App() {
return (
<LumenAuthProvider
config={{
clientId: 'your-client-id',
issuer: 'https://api.lumen.com/idp',
redirectUri: 'http://localhost:5173/callback',
scopes: ['openid', 'profile', 'email'],
}}
>
<YourApp />
</LumenAuthProvider>
);
}
// Component usage
function UserProfile() {
const { isAuthenticated, user, login, logout, isLoading } = useLumenAuth();
if (isLoading) return <div>Loading...</div>;
if (!isAuthenticated) {
return <button onClick={login}>Login with Lumen</button>;
}
return (
<div>
<h1>Welcome, {user?.name}</h1>
<p>Email: {user?.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}3. Backend Integration
Express
import express from 'express';
import { createLumenAuthMiddleware } from '@lumen/auth-sdk/backend';
const app = express();
app.use(createLumenAuthMiddleware({
issuer: 'https://api.lumen.com/idp',
publicRoutes: ['/health', '/public'],
}));
app.get('/protected', (req, res) => {
res.json({
message: 'Hello, authenticated user!',
user: req.user
});
});Fastify
import Fastify from 'fastify';
import lumenAuth from '@lumen/auth-sdk/backend/fastify';
const app = Fastify();
app.register(lumenAuth, {
issuer: 'https://api.lumen.com/idp',
publicRoutes: ['/health'],
});
app.get('/protected', async (request, reply) => {
return { user: request.user };
});NestJS
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { createLumenAuthGuard, CurrentUser, Public } from '@lumen/auth-sdk/backend';
const LumenAuthGuard = createLumenAuthGuard({
issuer: 'https://api.lumen.com/idp',
});
@Module({
providers: [
{
provide: APP_GUARD,
useClass: LumenAuthGuard,
},
],
})
export class AppModule {}
// Controller
@Controller('api')
export class ApiController {
@Public()
@Get('health')
health() {
return { status: 'ok' };
}
@Get('profile')
getProfile(@CurrentUser() user: UserInfo) {
return user;
}
}Configuration
LumenAuthConfig
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| clientId | string | Yes | - | OAuth client ID |
| issuer | string | Yes | - | Lumen IDP issuer URL |
| redirectUri | string | Yes | - | Callback URL after login |
| apiBaseUrl | string | No | derived from issuer | Override API base URL for getUserData() |
| scopes | string[] | No | ['openid', 'profile', 'email'] | Requested scopes |
| requestDataAccess | boolean | { fields?: string[] } | No | - | Request field-level user data access (see below) |
| autoRefresh | boolean | No | true | Auto-refresh tokens before expiry |
| refreshBufferSeconds | number | No | 60 | Seconds before expiry to refresh |
Scopes
openid- Required for OIDCprofile- Access to name, preferred_usernameemail- Access to email, email_verifiedoffline_access- Get refresh_token for offline accessdata- Required for reading user data fields (set automatically viarequestDataAccess)
Data Access
Lumen lets users grant apps read access to specific fields in their data store. The user reviews the fields individually on the IdP consent screen before any code is issued — your app gets exactly the fields the user ticked.
Requesting access
Set requestDataAccess in the client config. The SDK automatically appends the data scope to the authorization URL and the Lumen IdP shows the field-consent screen before redirecting back.
const client = new LumenAuthClient({
clientId: 'my-app',
issuer: 'https://api.lumen.com/idp',
redirectUri: 'https://myapp.com/callback',
// Append 'data' scope → IdP shows field-consent screen
requestDataAccess: true,
// Or hint which fields you need (user still decides individually):
// requestDataAccess: { fields: ['email', 'first_name'] },
});Reading a consented field
After the user has logged in and consented, call getUserData(key) to read a field.
import type { AuthError } from '@lumen/auth-sdk';
try {
const item = await client.getUserData('email');
console.log(item.value); // "[email protected]"
} catch (err) {
const e = err as AuthError;
if (e.code === 'access_not_granted') {
// User did not consent to this field, or later revoked it
}
}getUserData throws typed AuthError with these codes:
| Code | Cause |
|------|-------|
| unauthenticated | No active session |
| access_not_granted | No active grant covering this field (HTTP 403) |
| user_data_fetch_failed | Other API error |
React usage
import { LumenAuthProvider, useLumenAuth } from '@lumen/auth-sdk/react';
import type { AuthError } from '@lumen/auth-sdk';
function App() {
return (
<LumenAuthProvider
config={{
clientId: 'my-app',
issuer: 'https://api.lumen.com/idp',
redirectUri: window.location.origin + '/callback',
requestDataAccess: { fields: ['email', 'first_name'] },
}}
>
<Profile />
</LumenAuthProvider>
);
}
function Profile() {
const { client, isAuthenticated, login } = useLumenAuth();
const [email, setEmail] = useState<string | null>(null);
useEffect(() => {
if (!isAuthenticated) return;
client
.getUserData('email')
.then((item) => setEmail(item.value as string))
.catch((err) => {
if ((err as AuthError).code === 'access_not_granted') {
setEmail('(not shared)');
}
});
}, [client, isAuthenticated]);
if (!isAuthenticated) return <button onClick={login}>Login</button>;
return <p>Email: {email ?? '…'}</p>;
}How it works
- Your app calls
client.login()with thedatascope included - The user authenticates on the Lumen IdP
- If no grant exists for your app, the IdP shows the field-consent screen — a list of the user's data fields with checkboxes
- The user ticks the fields they want to share and submits
- The IdP creates a
DataAccessGrantwith the selected fields and issues the authorization code - Your app exchanges the code for tokens and can now call
getUserData(key)for any consented field - The user can view, adjust, and revoke access at any time on the Connected Services page in their Lumen account
Every read is verified server-side: the API checks the grant on each request, so revocations take effect immediately without waiting for a token to expire.
API Reference
Core Client
LumenAuthClient
Main SDK client class.
const client = new LumenAuthClient(config, storage?);Methods:
login()- Redirect to Lumen IDP for authenticationhandleCallback(url?)- Handle OAuth callback, exchange code for tokenslogout()- Revoke tokens and clear sessiongetSession()- Get current sessionisAuthenticated()- Check if user is authenticatedrefreshTokens()- Manually refresh access tokengetUserData(key, userId?)- Read a consented user data field (requiresrequestDataAccess)
React Hook
useLumenAuth()
React hook for accessing auth state and methods.
const {
session, // Current session
user, // User info
status, // 'unauthenticated' | 'authorizing' | 'authenticated' | 'error'
isAuthenticated, // Boolean
isLoading, // Boolean
isError, // Boolean
login, // () => Promise<void>
logout, // () => Promise<void>
handleCallback, // (url?) => Promise<Session>
refresh, // () => Promise<Session | null>
} = useLumenAuth();Error Handling
All errors are instances of AuthError with a code property:
import { AuthError } from '@lumen/auth-sdk';
try {
await client.handleCallback();
} catch (error) {
if (error instanceof AuthError) {
console.log(error.code); // 'state_mismatch', 'token_exchange_failed', etc.
console.log(error.message);
}
}Common error codes:
| Code | Source | Meaning |
|------|--------|---------|
| state_mismatch | handleCallback | CSRF protection triggered |
| missing_code | handleCallback | No authorization code in callback URL |
| token_exchange_failed | handleCallback | Token endpoint rejected the code |
| no_refresh_token | refreshTokens | Session has no refresh token |
| refresh_failed | refreshTokens | Refresh token was revoked or expired |
| unauthenticated | getUserData | No active session |
| access_not_granted | getUserData | No active grant covering this field |
| user_data_fetch_failed | getUserData | Unexpected API error |
Storage
By default, the SDK uses localStorage for session persistence. You can provide a custom storage adapter:
import { LumenAuthClient, createMemoryStorageAdapter } from '@lumen/auth-sdk';
const memoryStorage = createMemoryStorageAdapter();
const client = new LumenAuthClient(config, memoryStorage);Development
# Install dependencies
npm install
# Run in watch mode
npm run dev
# Build
npm run build
# Run tests
npm test
# Type check
npm run typecheck
# Lint
npm run lintLicense
MIT © Lumen Team
