webba-id-sdk
v2.3.4
Published
SDK officiel pour intégrer l'authentification Webba ID dans vos applications
Maintainers
Readme
Webba ID SDK v2.3
Official SDK for OAuth 2.0 + PKCE authentication with Webba ID.
Installation
Copy src/lib/webba-id.ts into your project, or install via npm:
npm install webba-id-sdkRequired Configuration
Get your credentials at https://account.webba-creative.com/platform/applications
# .env
VITE_WEBBA_API_KEY=your_webba_id_supabase_anon_key
VITE_WEBBA_CLIENT_ID=wba_xxx
VITE_WEBBA_CLIENT_SECRET=wbs_xxx
VITE_WEBBA_REDIRECT_URI=http://localhost:5173/auth/callbackUsage
1. Configuration (main.tsx)
import { WebbaID } from '@/lib/webba-id'
WebbaID.configure({
apiKey: import.meta.env.VITE_WEBBA_API_KEY,
clientId: import.meta.env.VITE_WEBBA_CLIENT_ID,
clientSecret: import.meta.env.VITE_WEBBA_CLIENT_SECRET,
redirectUri: import.meta.env.VITE_WEBBA_REDIRECT_URI
})2. Login (redirects to Webba ID)
// In a login button
async function handleLogin() {
await WebbaID.login()
}3. Callback (/auth/callback)
import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { WebbaID } from '@/lib/webba-id'
export default function AuthCallback() {
const navigate = useNavigate()
const [error, setError] = useState<string | null>(null)
const processingRef = useRef(false)
useEffect(() => {
const processCallback = async () => {
// Prevent double execution (React StrictMode)
if (processingRef.current) return
processingRef.current = true
if (!WebbaID.isCallback()) {
setError('Invalid callback page')
return
}
try {
const tokens = await WebbaID.handleCallback()
const user = await WebbaID.getUser(tokens.accessToken)
// Save
WebbaID.saveTokens(tokens)
WebbaID.saveUser(user)
navigate('/dashboard')
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
processingRef.current = false
}
}
processCallback()
}, [navigate])
if (error) return <div>Error: {error}</div>
return <div>Connecting...</div>
}4. Using Tokens
// Get stored user
const user = WebbaID.getStoredUser()
const tokens = WebbaID.getStoredTokens()
// Check if connected
if (user && tokens && !WebbaID.isTokenExpired(tokens)) {
// User is connected
}
// Refresh expired tokens
if (tokens && WebbaID.isTokenExpired(tokens)) {
try {
const newTokens = await WebbaID.refreshTokens(tokens.refreshToken)
WebbaID.saveTokens(newTokens)
} catch {
// Refresh failed, redirect to login
WebbaID.clearTokens()
}
}5. Logout
async function handleLogout() {
const tokens = WebbaID.getStoredTokens()
await WebbaID.logout(tokens?.accessToken)
window.location.href = '/'
}API Reference
Configuration
interface WebbaIDConfig {
apiKey: string // Webba ID Supabase anon key (REQUIRED)
clientId: string // App key wba_xxx (REQUIRED)
clientSecret?: string // App secret wbs_xxx (required for token exchange)
redirectUri?: string // Callback URL (default: origin + '/auth/callback')
baseUrl?: string // Webba ID URL (default: https://account.webba-creative.com)
apiUrl?: string // API URL (default: Supabase Edge Functions)
}Methods
| Method | Description |
|--------|-------------|
| configure(config) | Configure the SDK (call once at startup) |
| isConfigured() | Check if SDK is configured |
| login() | Redirect to Webba ID for authentication |
| handleCallback() | Handle OAuth callback and return tokens |
| getUser(accessToken) | Get user info |
| refreshTokens(refreshToken) | Refresh expired tokens |
| logout(accessToken?) | Revoke token and clear storage |
| isCallback() | Check if on callback page |
| saveTokens(tokens) | Save tokens to localStorage |
| getStoredTokens() | Get stored tokens |
| clearTokens() | Remove stored tokens |
| isTokenExpired(tokens) | Check if tokens are expired |
| saveUser(user) | Save user to localStorage |
| getStoredUser() | Get stored user |
Organizations
| Method | Description |
|--------|-------------|
| getOrganizations(accessToken) | List user's organizations |
| getOrganization(accessToken, orgId) | Get organization details |
| createOrganization(accessToken, data) | Create organization (becomes owner) |
| updateOrganization(accessToken, orgId, data) | Update organization |
| deleteOrganization(accessToken, orgId) | Delete organization |
| getOrganizationMembers(accessToken, orgId) | List members |
| inviteMember(accessToken, orgId, data) | Invite member by email |
| removeMember(accessToken, orgId, memberId) | Remove member |
| updateMemberRole(accessToken, orgId, memberId, role) | Change member role |
App Access
| Method | Description |
|--------|-------------|
| checkAppAccess(accessToken, orgId?) | Check if user has access (returns details) |
| hasAccess(accessToken, orgId?) | Quick boolean check for access |
| enableAppForOrg(accessToken, orgId) | Enable this app for an organization |
| disableAppForOrg(accessToken, orgId) | Disable this app for an organization |
Linked Identities (OAuth Providers)
| Method | Description |
|--------|-------------|
| getLinkedIdentities(accessToken) | Get all linked OAuth providers |
| hasProviderLinked(accessToken, provider) | Check if a specific provider is linked |
| getProviderIdentity(accessToken, provider) | Get identity details for a provider |
| getProviderId(accessToken, provider) | Get just the provider user ID |
| hasCfxreLinked(accessToken) | Check if Cfx.re is linked (shortcut) |
| getCfxreIdentity(accessToken) | Get Cfx.re identity details (shortcut) |
| getCfxreId(accessToken) | Get just the Cfx.re user ID (shortcut) |
Reverse Lookup (Server-side only)
| Method | Description |
|--------|-------------|
| lookupUserByProvider(provider, providerUserId) | Find Webba ID user from provider ID |
| lookupUsersByProvider(provider, providerUserIds[]) | Batch lookup (max 100) |
| getUserIdentitiesByUserId(userId) | Get all linked identities for a user |
| getProviderIdByUserId(userId, provider) | Get specific provider ID for a user |
| getCfxreIdByUserId(userId) | Shortcut: Get Cfx.re ID for a user |
| getDiscordIdByUserId(userId) | Shortcut: Get Discord ID for a user |
Note: These methods require appSecret and are for server-side use only (bots, APIs).
Types
interface AuthTokens {
accessToken: string
refreshToken: string
expiresAt: number
userId: string
}
interface WebbaUser {
id: string
email: string
name: string
firstName?: string
lastName?: string
avatar?: string
}
type OrgRole = 'owner' | 'admin' | 'sso_admin' | 'viewer' | 'member'
interface Organization {
id: string
name: string
slug: string
logo_url: string | null
is_active: boolean
created_at: string
role: OrgRole
members_count?: number
domains_count?: number
}
interface OrganizationMember {
id: string
organization_id: string
user_id: string | null
role: OrgRole
email?: string
profile?: {
first_name?: string
last_name?: string
display_name?: string
avatar_url?: string
}
}
interface CreateOrganizationData {
name: string
slug?: string
logo_url?: string
}
interface InviteMemberData {
email: string
role?: 'admin' | 'sso_admin' | 'viewer' | 'member'
}
interface LinkedIdentity {
provider: string
provider_user_id: string
provider_username: string | null
provider_email: string | null
provider_avatar_url: string | null
linked_at: string
last_used_at: string
}
interface CfxreIdentity {
cfxre_id: string
username: string | null
avatar_url: string | null
linked_at: string
}
interface AppAccessResult {
has_access: boolean
reason: 'subscription' | 'org_access' | 'subscription_and_org' | 'no_access'
user_id: string
app_id: string
subscription: {
id: string
plan: string
status: string
features: Record<string, any> | null
starts_at: string | null
ends_at: string | null
} | null
organizations: Array<{
id: string
name: string
slug: string
role: string
}>
}
interface LookupResult {
provider_user_id: string
user: {
user_id: string
email: string
} | null
}
interface BatchLookupResult {
mapping: Record<string, string> // provider_user_id -> webba_user_id
}
interface UserIdentitiesResult {
user_id: string
identities: LinkedIdentity[]
}OAuth Flow
1. User clicks "Login"
|
2. WebbaID.login() → Redirect to account.webba-creative.com
|
3. User authenticates on Webba ID
|
4. Redirect back to /auth/callback?code=xxx&state=xxx
|
5. WebbaID.handleCallback() → Exchange code for tokens
|
6. WebbaID.getUser() → Get user info
|
7. Save tokens & user → Redirect to dashboardSecurity
PKCE (Proof Key for Code Exchange)
The SDK automatically uses PKCE to protect against interception attacks:
code_verifierrandomly generated (32 bytes)code_challenge= SHA-256(code_verifier) in base64url- Stored in cookie + localStorage (cross-origin safe)
State Parameter
The SDK generates a random state for each login and verifies it on callback (CSRF protection).
Token Storage
- Tokens stored in localStorage (
webba_tokens,webba_user) - PKCE data stored in cookie + localStorage with 10 minute expiration
- Automatic cleanup of old PKCE sessions
Troubleshooting
"Session expired or invalid"
PKCE data has expired or is not accessible. Possible causes:
- More than 10 minutes between login() and callback
- Cookies blocked by browser
- Different origin (e.g., 127.0.0.1 vs localhost)
Solution: Use the same origin (localhost) in development.
"Invalid or expired code"
The authorization code has already been used or expired.
Solution: Add useRef to prevent double execution in React StrictMode (see callback example above).
"clientSecret is required"
The clientSecret was not provided in configuration.
Solution: Add clientSecret: import.meta.env.VITE_WEBBA_CLIENT_SECRET to configure().
Organizations Example
import { WebbaID } from '@/lib/webba-id'
const tokens = WebbaID.getStoredTokens()
// List organizations
const orgs = await WebbaID.getOrganizations(tokens.accessToken)
console.log(orgs) // [{ id: '...', name: 'My Company', role: 'owner', ... }]
// Create organization
const newOrg = await WebbaID.createOrganization(tokens.accessToken, {
name: 'My Company',
slug: 'my-company' // optional
})
// Get organization details
const org = await WebbaID.getOrganization(tokens.accessToken, newOrg.id)
console.log(org.domains, org.members_count)
// Update organization (owner/admin)
await WebbaID.updateOrganization(tokens.accessToken, org.id, {
name: 'New Name'
})
// List members
const members = await WebbaID.getOrganizationMembers(tokens.accessToken, org.id)
// Invite member (owner/admin)
await WebbaID.inviteMember(tokens.accessToken, org.id, {
email: '[email protected]',
role: 'member'
})
// Change member role (owner only)
await WebbaID.updateMemberRole(tokens.accessToken, org.id, memberId, 'admin')
// Remove member (owner/admin)
await WebbaID.removeMember(tokens.accessToken, org.id, memberId)
// Delete organization (owner only)
await WebbaID.deleteOrganization(tokens.accessToken, org.id)App Access Example
Use this to control access to your application based on subscriptions or organization membership.
import { WebbaID } from '@/lib/webba-id'
const tokens = WebbaID.getStoredTokens()
// Full access check with details
const access = await WebbaID.checkAppAccess(tokens.accessToken)
if (!access.has_access) {
// User doesn't have access
console.log('Access denied. Reason:', access.reason)
// Redirect to upgrade page or show error
window.location.href = '/upgrade'
}
// Access granted - check details
if (access.subscription) {
console.log('User plan:', access.subscription.plan)
// access.subscription.features contains plan features
}
if (access.organizations.length > 0) {
console.log('Organizations with access:', access.organizations)
// User has org-level access through these organizations
}
// Quick boolean check (simpler)
if (await WebbaID.hasAccess(tokens.accessToken)) {
// User can use the app
} else {
// No access
}
// Check access for a specific organization
const orgAccess = await WebbaID.checkAppAccess(tokens.accessToken, 'org-uuid')Access is granted if:
- User has an active subscription for this app, OR
- User is member of an organization where the app is enabled
Auto-enable for Organization
Apps can enable themselves for an organization when a user logs in:
import { WebbaID } from '@/lib/webba-id'
const tokens = WebbaID.getStoredTokens()
// Get user's organizations
const orgs = await WebbaID.getOrganizations(tokens.accessToken)
// Enable this app for the first organization
if (orgs.length > 0) {
await WebbaID.enableAppForOrg(tokens.accessToken, orgs[0].id)
}
// Now checkAppAccess will return has_access: true for this org
const access = await WebbaID.checkAppAccess(tokens.accessToken, orgs[0].id)
console.log(access.has_access) // trueProvider Identity Examples
Check if a user has linked specific OAuth providers and retrieve their IDs:
import { WebbaID } from '@/lib/webba-id'
const tokens = WebbaID.getStoredTokens()
// ===================
// Generic methods (any provider)
// ===================
// Check if Discord is linked
if (await WebbaID.hasProviderLinked(tokens.accessToken, 'discord')) {
const discordId = await WebbaID.getProviderId(tokens.accessToken, 'discord')
console.log('Discord ID:', discordId)
// Or get full identity
const discord = await WebbaID.getProviderIdentity(tokens.accessToken, 'discord')
console.log('Username:', discord.provider_username)
}
// Works with any provider: 'discord', 'google', 'github', 'azure', 'cfxre'
const hasGoogle = await WebbaID.hasProviderLinked(tokens.accessToken, 'google')
// ===================
// Cfx.re shortcuts
// ===================
if (await WebbaID.hasCfxreLinked(tokens.accessToken)) {
const cfxreId = await WebbaID.getCfxreId(tokens.accessToken)
console.log('Cfx.re ID:', cfxreId) // "123456"
}
// ===================
// Get all linked providers
// ===================
const identities = await WebbaID.getLinkedIdentities(tokens.accessToken)
// [
// { provider: 'cfxre', provider_user_id: '123456', ... },
// { provider: 'discord', provider_user_id: '789...', ... }
// ]Reverse Lookup (Server-side)
Find Webba ID users from their provider IDs (e.g., Discord ID → Webba ID user).
Requires appSecret - server-side only.
import { WebbaID } from 'webba-id-sdk'
// Server-side configuration (with appSecret)
const webbaId = new WebbaID({
appKey: 'wba_xxx',
appSecret: 'wbs_xxx', // Required for reverse lookup
redirectUri: 'https://example.com/callback'
})
// ===================
// Single lookup
// ===================
// Example: Discord bot finding Webba ID user from Discord user ID
const result = await webbaId.lookupUserByProvider('discord', message.author.id)
if (result.user) {
console.log('Webba ID:', result.user.user_id)
console.log('Email:', result.user.email)
} else {
console.log('This Discord user is not linked to any Webba ID account')
}
// ===================
// Batch lookup (max 100)
// ===================
// Example: Check multiple Discord users at once
const discordIds = ['123456789', '987654321', '111222333']
const batch = await webbaId.lookupUsersByProvider('discord', discordIds)
console.log(batch.mapping)
// { '123456789': 'webba-uuid-1', '987654321': 'webba-uuid-2' }
// Note: '111222333' is absent = not linked
// Check if a specific ID is linked
if (batch.mapping['123456789']) {
console.log('User is linked:', batch.mapping['123456789'])
}
// ===================
// Get identities by user_id
// ===================
// Get all linked identities for a Webba ID user
const result = await webbaId.getUserIdentitiesByUserId('webba-user-uuid')
console.log(result.identities)
// [
// { provider: 'discord', provider_user_id: '123456789', provider_username: 'User#1234', ... },
// { provider: 'cfxre', provider_user_id: '987654', provider_username: 'Player', ... }
// ]
// Get specific provider ID
const cfxreId = await webbaId.getProviderIdByUserId('webba-user-uuid', 'cfxre')
console.log('Cfx.re ID:', cfxreId) // '987654' or null
// Shortcuts for common providers
const cfxre = await webbaId.getCfxreIdByUserId('webba-user-uuid')
const discord = await webbaId.getDiscordIdByUserId('webba-user-uuid')Supported providers: discord, google, github, azure, cfxre
AuthContext Example
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
import { WebbaID, AuthTokens, WebbaUser } from '@/lib/webba-id'
interface AuthContextType {
user: WebbaUser | null
tokens: AuthTokens | null
isLoading: boolean
login: () => Promise<void>
logout: () => Promise<void>
setAuth: (tokens: AuthTokens, user: WebbaUser) => void
}
const AuthContext = createContext<AuthContextType | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<WebbaUser | null>(null)
const [tokens, setTokens] = useState<AuthTokens | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const storedTokens = WebbaID.getStoredTokens()
const storedUser = WebbaID.getStoredUser()
if (storedTokens && storedUser && !WebbaID.isTokenExpired(storedTokens)) {
setTokens(storedTokens)
setUser(storedUser)
}
setIsLoading(false)
}, [])
const login = async () => {
await WebbaID.login()
}
const logout = async () => {
await WebbaID.logout(tokens?.accessToken)
setUser(null)
setTokens(null)
}
const setAuth = (newTokens: AuthTokens, newUser: WebbaUser) => {
WebbaID.saveTokens(newTokens)
WebbaID.saveUser(newUser)
setTokens(newTokens)
setUser(newUser)
}
return (
<AuthContext.Provider value={{ user, tokens, isLoading, login, logout, setAuth }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) throw new Error('useAuth must be used within AuthProvider')
return context
}License
MIT - Webba Creative Technologies 2026
