@stacknet/userutils
v0.4.1
Published
Reusable auth, billing, and security utilities for StackNet stacks and applications
Maintainers
Readme
@stacknet/userutils
Authentication, session management, and billing for StackNet apps.
Google One Tap, OAuth (Google, Discord, Telegram, X), wallet login (Solana, Ethereum), cross-domain SSO via auth bridge, CSRF protection, and billing/subscription hooks.
Install
pnpm add @stacknet/userutilsQuick Start
1. Set up environment variables
# .env.local
AUTH_SECRET=your-hmac-secret-min-32-chars
NEXT_PUBLIC_STACK_ID=stk_your_stack_id
NEXT_PUBLIC_STACKNET_URL=https://stacknet.magma-rpc.com2. Create API routes
Three server-side routes are required. These handle JWT signing, session validation, and logout — keeping secrets off the client.
// app/api/auth/callback/route.ts
import { createAuthCallback } from '@stacknet/userutils/server'
const handler = createAuthCallback({
authSecret: process.env.AUTH_SECRET!,
stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL || 'https://stacknet.magma-rpc.com',
stackId: process.env.NEXT_PUBLIC_STACK_ID!,
secureCookies: process.env.NODE_ENV === 'production',
})
export async function POST(request: Request) {
return handler(request)
}// app/api/auth/session/route.ts
import { createSessionHandler } from '@stacknet/userutils/server'
const handler = createSessionHandler({
authSecret: process.env.AUTH_SECRET!,
secureCookies: process.env.NODE_ENV === 'production',
})
export async function GET(request: Request) {
return handler(request)
}// app/api/auth/logout/route.ts
import { createLogoutHandler } from '@stacknet/userutils/server'
const handler = createLogoutHandler({
stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL || 'https://stacknet.magma-rpc.com',
secureCookies: process.env.NODE_ENV === 'production',
})
export async function POST(request: Request) {
return handler(request)
}3. Wrap your app with the provider
// components/auth-provider.tsx
'use client'
import { UserUtilsProvider } from '@stacknet/userutils/components'
const config = {
apiBaseUrl: '', // same-origin — hooks call your /api/auth/* routes
stackId: process.env.NEXT_PUBLIC_STACK_ID,
stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL,
theme: 'dark',
}
export function AuthProvider({ children }) {
return (
<UserUtilsProvider config={config}>
{children}
</UserUtilsProvider>
)
}// app/layout.tsx
import { AuthProvider } from '../components/auth-provider'
export default function RootLayout({ children }) {
return (
<html>
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
)
}4. Add a connect page
// app/connect/page.tsx
'use client'
import { ConnectWidget } from '@stacknet/userutils/components'
export default function ConnectPage() {
return (
<ConnectWidget
config={{
apiBaseUrl: '',
stackId: process.env.NEXT_PUBLIC_STACK_ID,
stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL,
}}
onSuccess={() => { window.location.href = '/' }}
showWallets={['phantom', 'metamask']}
showOTP={false}
/>
)
}5. Show auth state in your header
// components/header.tsx
'use client'
import { useSession, useStackAuth } from '@stacknet/userutils/hooks'
import Link from 'next/link'
export function Header() {
const { isAuthenticated, loading } = useSession()
const { session, logout, wallet } = useStackAuth()
const displayAddress = wallet.address
? `${wallet.address.slice(0, 4)}...${wallet.address.slice(-4)}`
: session?.userId?.slice(0, 8) ?? ''
if (loading) return <div>Loading...</div>
if (isAuthenticated) {
return (
<div>
<span>{displayAddress}</span>
<button onClick={logout}>Logout</button>
</div>
)
}
return <Link href="/connect">Log in</Link>
}The JWT auto-refreshes when /api/auth/session is called and the token is within 5 minutes of expiry.
Hooks
useSession()
Reads the public session cookie. No network calls.
const { session, loading, isAuthenticated, refresh, readSession } = useSession()
// session: { userId, address, chain, expiresAt, planId, authMethod } | null
// isAuthenticated: boolean (session exists and not expired)useStackAuth(config?)
Full auth engine — wallet connection, challenge/sign/verify, logout, bridge.
const {
session, // PublicSession | null
isAuthenticated, // boolean
wallet, // { connected, address, chain, provider }
loading, // boolean (auth in progress)
error, // string | null
authenticateSolana, // (provider?: 'phantom' | 'solflare') => Promise<boolean>
authenticateEVM, // () => Promise<boolean>
authenticateOTP, // (code: string) => Promise<boolean>
logout, // () => Promise<void>
refresh, // () => Promise<any>
stackId, // effective stack ID
bridge, // { ready, known, identity, identityCount, resolvedStackId }
} = useStackAuth({
apiBaseUrl: '',
stackId: 'stk_...',
stacknetUrl: 'https://stacknet.magma-rpc.com',
autoConnect: false, // true = auto-login if bridge has known identity
})Components
ConnectWidget
Drop-in wallet connect UI.
<ConnectWidget
config={config} // UserUtilsConfig (required)
onSuccess={() => {}} // Called after successful auth
title="Connect" // Heading text
showWallets={['phantom', 'metamask']} // Which wallets to show
showOTP={false} // Show access code option
className="" // CSS classes
/>UserUtilsProvider
React context provider. Wrap your app with this.
<UserUtilsProvider config={config} callbacks={callbacks?}>
{children}
</UserUtilsProvider>Security
- HttpOnly cookies prevent XSS token theft
- SameSite=Lax prevents CSRF on state-changing requests
- Secure flag enforces HTTPS in production
- CSRF double-submit validates cookie matches header on mutations
- Short JWT expiry (15 min) with transparent auto-refresh
- Rate limiting on auth callback (10 req/min per IP)
- Constant-time comparison for JWT signature validation
License
MIT
