ns-auth
v0.1.0
Published
Discord-based authentication for Network School applications
Maintainers
Readme
ns-auth
Lightweight, self-contained Discord OAuth authentication for Network School applications. Drop it in with minimal config — no database, no session store required.
Features
- One-step Discord OAuth + NS guild role verification
- JWT issued as an
httpOnly,SameSite=Strictcookie (never JS-accessible) - Roles read live from Discord on every sign-in (revoked members can't reuse old sessions)
- Express adapter + Next.js App Router adapter
- React hooks and a drop-in sign-in button
- CSRF protection via
stateparameter
Install
npm install ns-authPeer dependencies (only needed for client-side usage):
npm install react react-domRequires Node.js ≥ 18 (uses built-in fetch and crypto.randomUUID).
Quick start — Express
import express from 'express'
import cookieParser from 'cookie-parser'
import { createNSAuth, expressAdapter } from 'ns-auth'
const app = express()
app.use(cookieParser())
app.use(express.json())
const nsAuth = createNSAuth({
discord: {
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
redirectUri: 'https://yourapp.com/auth/callback',
guildId: process.env.NS_GUILD_ID!,
memberRoleId: process.env.NS_MEMBER_ROLE_ID!,
longtermerRoleId: process.env.NS_LONGTERMER_ROLE_ID, // optional
},
session: {
secret: process.env.SESSION_SECRET!,
expiresIn: '7d',
},
})
// Mount auth routes at /auth
app.use('/auth', expressAdapter(nsAuth))
// Protect routes
app.get('/api/proposals', nsAuth.requireMember, (req, res) => {
res.json({ user: req.user })
})
app.post('/api/proposals/:id/endorse', nsAuth.requireLongtermer, (req, res) => {
res.json({ ok: true })
})Quick start — Next.js App Router
// app/api/auth/[...ns]/route.ts
import { createNSAuth, nextjsAdapter } from 'ns-auth'
const nsAuth = createNSAuth({ ...config })
export const { GET, POST } = nextjsAdapter(nsAuth)// app/api/member-area/route.ts
import { requireMemberNS } from 'ns-auth'
import { nsAuth } from '@/lib/auth'
export async function GET(request: Request) {
const user = requireMemberNS(request, nsAuth.config)
if (user instanceof Response) return user // 401 or 403
return Response.json({ message: `Hello ${user.username}` })
}Configuration
createNSAuth({
discord: {
clientId: string, // Discord app client ID
clientSecret: string, // Discord app client secret
redirectUri: string, // Must match Discord OAuth2 redirect URI setting
guildId: string, // NS Discord server ID
memberRoleId: string, // Role ID for ns-member
longtermerRoleId?: string, // Role ID for ns-longtermer (optional)
},
session: {
secret: string, // HS256 signing secret (≥32 random bytes recommended)
expiresIn: string, // JWT expiry, e.g. '7d', '24h', '1h'
},
})Auth endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | /auth/discord | Redirects to Discord OAuth |
| GET | /auth/callback | Exchanges code, verifies role, issues JWT cookie |
| POST | /auth/signout | Clears session cookie |
| GET | /auth/me | Returns current user from JWT |
Pass ?redirect_to=/your-page on the initial /auth/discord request to return the user to a specific page after sign-in.
Route guards (Express)
// 401 if no session, 403 if not ns-member
app.get('/protected', nsAuth.requireMember, handler)
// 401/403 if not ns-longtermer
app.post('/longtermer-only', nsAuth.requireLongtermer, handler)
// Attaches req.user if session exists, never blocks
app.get('/public-with-user', nsAuth.optional, handler)JWT payload
{
sub: string, // Discord user ID
username: string, // Display name (guild nick → global name → username)
avatar: string, // Full Discord avatar URL
roles: {
isMember: boolean, // holds ns-member role
isLongtermer: boolean,
},
iat: number,
exp: number,
}Client — React
// Wrap your app
import { NSAuthProvider } from 'ns-auth/client'
export default function App({ children }) {
return <NSAuthProvider apiBase="/auth">{children}</NSAuthProvider>
}// Drop-in sign-in button
import { NSAuthButton } from 'ns-auth/client'
<NSAuthButton successRedirect="/dashboard" />// Hook
import { useNSUser } from 'ns-auth/client'
function MyComponent() {
const { user, isAuthenticated, isMember, isLongtermer, signOut } = useNSUser()
if (!isAuthenticated) return <NSAuthButton />
return <p>Welcome, {user.username}</p>
}// Full hook with signIn helper
import { useNSAuth } from 'ns-auth/client'
const { signIn, signOut, user, isAuthenticated } = useNSAuth()
signIn({ redirectTo: '/dashboard' })Security notes
- The
stateCSRF parameter is validated on every callback. - The JWT is stored in an
httpOnly,SameSite=Strictcookie — not readable by JavaScript. - Discord roles are checked on every sign-in, never cached.
- In production, set
NODE_ENV=productionto enable theSecureflag on cookies.
Example app
A full reference implementation with Express + SQLite (server) and React/Vite (client) is in example/.
cd example
cp .env.example .env
# fill in your Discord credentials
npm install
npm run dev