@smarthivelabs-devs/auth-server
v1.1.1
Published
SmartHive Auth middleware for Express and Next.js App Router — JWT verification via JWKS and social auth proxy router
Readme
@smarthivelabs-devs/auth-server
SmartHive Auth server-side middleware for Express and Next.js. Verifies JWTs using JWKS — no shared secrets required.
Installation
npm install @smarthivelabs-devs/auth-server
# or
pnpm add @smarthivelabs-devs/auth-server
# or
yarn add @smarthivelabs-devs/auth-serverFor Express projects, also install the peer dependency:
npm install express
expressis optional — Next.js users do not need it.
How It Works
The server SDK verifies Authorization: Bearer <token> JWTs by fetching your SmartHive Auth project's public JSON Web Key Set (JWKS) at:
https://<your-issuer>/api/auth/jwksNo secrets are shared between your server and SmartHive Auth — only public keys are used for verification. Keys are fetched automatically and cached by jose.
Configuration
interface ServerAuthConfig {
/** Your SmartHive Auth issuer URL — MUST be https:// */
issuer: string;
/** Optional: restrict accepted tokens to a specific audience */
audience?: string;
/** Optional: restrict accepted tokens to a specific project */
projectId?: string;
}The verified token is decoded into an AuthContext:
interface AuthContext {
userId: string; // token subject (sub claim)
projectId?: string; // project_id claim from the token
email?: string; // email claim from the token
raw: JWTPayload; // full decoded JWT payload
}Express Middleware
requireAuth(config)
An Express middleware that verifies the Authorization: Bearer <token> header. Attaches the decoded AuthContext to req.auth on success. Returns 401 on missing or invalid tokens.
import express from "express";
import { requireAuth } from "@smarthivelabs-devs/auth-server";
const app = express();
const auth = requireAuth({
issuer: process.env.AUTH_ISSUER!, // e.g. "https://auth.myapp.com"
projectId: process.env.AUTH_PROJECT_ID, // optional — restricts to your project
});
// Apply to a single route
app.get("/api/profile", auth, (req, res) => {
res.json({ userId: req.auth!.userId, email: req.auth!.email });
});
// Apply to an entire router
const apiRouter = express.Router();
apiRouter.use(auth);
apiRouter.get("/me", (req, res) => {
res.json(req.auth);
});
apiRouter.post("/posts", (req, res) => {
const userId = req.auth!.userId;
// create post for userId...
res.json({ ok: true });
});
app.use("/api", apiRouter);
app.listen(3000);TypeScript: Typed Request
The req.auth field is added by the middleware. Use SmartHiveAuthRequest for type-safe access:
import type { SmartHiveAuthRequest } from "@smarthivelabs-devs/auth-server";
import type { Response, NextFunction } from "express";
function myHandler(req: SmartHiveAuthRequest, res: Response) {
const { userId, email, projectId, raw } = req.auth!;
res.json({ userId, email });
}Full Express App Example
import express from "express";
import { requireAuth, type SmartHiveAuthRequest } from "@smarthivelabs-devs/auth-server";
const app = express();
app.use(express.json());
const authMiddleware = requireAuth({
issuer: process.env.AUTH_ISSUER!,
projectId: process.env.AUTH_PROJECT_ID,
});
// Public route
app.get("/health", (_req, res) => res.json({ status: "ok" }));
// Protected route
app.get("/api/me", authMiddleware, (req: SmartHiveAuthRequest, res) => {
res.json({
userId: req.auth!.userId,
email: req.auth!.email,
});
});
app.put("/api/profile", authMiddleware, express.json(), async (req: SmartHiveAuthRequest, res) => {
const userId = req.auth!.userId;
const { displayName } = req.body;
// update profile in DB...
res.json({ ok: true });
});
// Error responses from requireAuth:
// 401 { error: "unauthorized" } — no token
// 401 { error: "invalid_token" } — bad/expired token
app.listen(3000, () => console.log("Server running on :3000"));Next.js App Router
createNextAuthMiddleware(config)
Returns an async function you can call inside your Next.js middleware.ts (Edge runtime) or route handlers. Accepts a Next.js-style request object and returns { auth, error }.
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { createNextAuthMiddleware } from "@smarthivelabs-devs/auth-server";
const verifyAuth = createNextAuthMiddleware({
issuer: process.env.AUTH_ISSUER!,
projectId: process.env.AUTH_PROJECT_ID,
});
export async function middleware(request: NextRequest) {
const { auth, error } = await verifyAuth(request);
if (error) {
// Redirect browser requests to login, reject API requests with 401
if (request.nextUrl.pathname.startsWith("/api/")) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
}
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("redirect", request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
// auth.userId is available here
// Optionally forward user info to route handlers via headers
const res = NextResponse.next();
res.headers.set("x-user-id", auth!.userId);
return res;
}
export const config = {
matcher: ["/dashboard/:path*", "/api/protected/:path*"],
};In Route Handlers (App Router)
// app/api/profile/route.ts
import { createNextAuthMiddleware } from "@smarthivelabs-devs/auth-server";
import { NextRequest, NextResponse } from "next/server";
const verifyAuth = createNextAuthMiddleware({
issuer: process.env.AUTH_ISSUER!,
});
export async function GET(request: NextRequest) {
const { auth, error } = await verifyAuth(request);
if (error) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
}
return NextResponse.json({
userId: auth!.userId,
email: auth!.email,
});
}Next.js Pages Router (API Routes)
// pages/api/profile.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { verifyAccessToken } from "@smarthivelabs-devs/auth-server";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const token = req.headers.authorization?.slice("Bearer ".length);
if (!token) return res.status(401).json({ error: "unauthorized" });
try {
const auth = await verifyAccessToken(token, {
issuer: process.env.AUTH_ISSUER!,
});
res.json({ userId: auth.userId, email: auth.email });
} catch {
res.status(401).json({ error: "invalid_token" });
}
}Standalone: verifyAccessToken
Use this directly when you need full control over the verification flow.
import { verifyAccessToken } from "@smarthivelabs-devs/auth-server";
const auth = await verifyAccessToken(token, {
issuer: "https://auth.myapp.com",
audience: "https://api.myapp.com", // optional
projectId: "proj_abc123", // optional
});
console.log(auth.userId); // e.g. "user_abc123"
console.log(auth.email); // e.g. "[email protected]"
console.log(auth.raw); // full JWT payload objectThrows Error if:
issuerdoes not start withhttps://- JWKS fetch fails
- Token signature is invalid
- Token is expired
audiencedoesn't match (if provided)projectIddoesn't match the token'sproject_idclaim (if provided)
decodeClaims(token)
Decodes a JWT without verifying the signature. Use only for reading non-sensitive claims before verification (e.g. extracting the projectId to look up the correct issuer).
import { decodeClaims } from "@smarthivelabs-devs/auth-server";
const claims = decodeClaims(token);
console.log(claims.sub); // user ID
console.log(claims.project_id); // project ID
console.log(claims.exp); // expiry timestampNever use
decodeClaimsalone for authorization decisions — the signature is not verified.
Multi-tenant Setup
If you serve multiple projects from one server, use decodeClaims to extract the project ID, then look up the correct issuer:
import { decodeClaims, verifyAccessToken } from "@smarthivelabs-devs/auth-server";
const projectIssuers: Record<string, string> = {
proj_abc: "https://auth.tenant-a.com",
proj_xyz: "https://auth.tenant-b.com",
};
async function verifyMultiTenantToken(token: string) {
const claims = decodeClaims(token);
const projectId = String(claims.project_id ?? "");
const issuer = projectIssuers[projectId];
if (!issuer) throw new Error("Unknown project");
return verifyAccessToken(token, { issuer, projectId });
}Social Auth Proxy (White-label OAuth Domain)
By default, when users tap "Sign in with Google" the iOS prompt shows SmartHive's domain and the Google consent screen says "to continue to smarthivelabs.dev". createSocialProxyRouter fixes this by routing OAuth through your own backend — Google and Apple only ever see your domain.
How It Works
App → your-api.com/api/auth/social/google?redirect_uri=myapp://callback
→ (your backend) redirects to SmartHive with proxy_callback=your-api.com/...
→ (SmartHive) sends redirect_uri=your-api.com/... to Google
→ Google consent screen shows "to continue to your-app.com" ✓
→ iOS shows "wants to use your-api.com" ✓
→ Google → your-api.com/api/auth/social/google/callback?code=...
→ (your backend) forwards to SmartHive → SmartHive issues tokens
→ myapp://callback?access_token=... (app gets token) ✓Setup
Step 1 — Mount the proxy router in your Express backend:
import express from "express";
import { createSocialProxyRouter } from "@smarthivelabs-devs/auth-server";
const app = express();
app.use("/api/auth/social", await createSocialProxyRouter({
smarthiveBaseUrl: process.env.SMARTHIVE_AUTH_BASE_URL!, // https://authcore.smarthivelabs.dev
environment: process.env.SMARTHIVE_AUTH_ENVIRONMENT!, // prod | staging | dev
projectId: process.env.SMARTHIVE_PROJECT_ID!,
appBaseUrl: process.env.APP_BASE_URL!, // https://api.yourapp.com
}));Step 2 — Add env vars:
SMARTHIVE_AUTH_BASE_URL=https://authcore.smarthivelabs.dev
SMARTHIVE_AUTH_ENVIRONMENT=prod
SMARTHIVE_PROJECT_ID=your-project-id
APP_BASE_URL=https://api.yourapp.comStep 3 — Register your backend callback URL with Google/Apple:
- Google Cloud Console → OAuth 2.0 → Authorized redirect URIs:
https://api.yourapp.com/api/auth/social/google/callback - Apple Developer → Service ID → Return URLs:
https://api.yourapp.com/api/auth/social/apple/callback
Step 4 — Point your SDK at the proxy:
// Expo / React Native
<SmartHiveAuthProvider
publishableKey="pk_..."
projectId="proj_..."
baseUrl="https://authcore.smarthivelabs.dev"
redirectUri="myapp://auth/callback"
socialProxyUrl="https://api.yourapp.com" // ← add this
>
// React web
<SmartHiveAuthProvider
publishableKey="pk_..."
projectId="proj_..."
baseUrl="https://authcore.smarthivelabs.dev"
socialProxyUrl="https://api.yourapp.com" // ← add this
>Config Reference
interface SocialProxyConfig {
/** SmartHive Auth service base URL, e.g. "https://authcore.smarthivelabs.dev" */
smarthiveBaseUrl: string;
/** Environment: "dev" | "staging" | "prod" */
environment: string;
/** Your SmartHive project ID */
projectId: string;
/**
* Your backend's public base URL, e.g. "https://api.myapp.com".
* This is the domain that will appear on Google/Apple consent screens.
*/
appBaseUrl: string;
}The router creates two routes automatically:
GET /:provider— initiates the OAuth flow, redirects to SmartHive withproxy_callbacksetGET /:provider/callback— receives the provider redirect, forwards to SmartHive for token exchange
Fastify Integration
import Fastify from "fastify";
import { verifyAccessToken } from "@smarthivelabs-devs/auth-server";
const app = Fastify();
app.addHook("onRequest", async (request, reply) => {
const authHeader = request.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
reply.code(401).send({ error: "unauthorized" });
return;
}
try {
(request as any).auth = await verifyAccessToken(authHeader.slice(7), {
issuer: process.env.AUTH_ISSUER!,
});
} catch {
reply.code(401).send({ error: "invalid_token" });
}
});
app.get("/api/me", async (request) => {
return (request as any).auth;
});
app.listen({ port: 3000 });tRPC Integration
import { initTRPC, TRPCError } from "@trpc/server";
import { verifyAccessToken } from "@smarthivelabs-devs/auth-server";
import type { AuthContext } from "@smarthivelabs-devs/auth-server";
type Context = { auth: AuthContext | null };
export const t = initTRPC.context<Context>().create();
// Middleware that requires auth
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.auth) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({ ctx: { auth: ctx.auth } });
});
// In your context factory (createContext)
export async function createContext({ req }: { req: Request }): Promise<Context> {
const token = req.headers.get("authorization")?.slice("Bearer ".length);
if (!token) return { auth: null };
try {
const auth = await verifyAccessToken(token, {
issuer: process.env.AUTH_ISSUER!,
});
return { auth };
} catch {
return { auth: null };
}
}Environment Variables
# .env
AUTH_ISSUER=https://auth.myapp.com
AUTH_PROJECT_ID=proj_abc123
# Optional — if your tokens have an audience claim
AUTH_AUDIENCE=https://api.myapp.comSecurity Notes
- HTTPS required: The
issuermust start withhttps://. The SDK enforces this and will throw if you pass anhttp://URL. - No shared secrets: Verification uses only public JWKS keys — your server never receives or stores any private keys or client secrets.
- Key rotation: JWKS keys are fetched and cached by
jose. Key rotation on the SmartHive side is handled transparently. - Audience validation: Pass
audienceto restrict tokens to your specific API service, preventing tokens issued for other services from being accepted. - Project isolation: Pass
projectIdto reject tokens from other SmartHive projects.
TypeScript Types
import type {
ServerAuthConfig,
AuthContext,
SmartHiveAuthRequest, // extends Express Request
NextRequest, // minimal Next.js-compatible request type
} from "@smarthivelabs-devs/auth-server";Related Packages
| Package | Use case |
|---|---|
| @smarthivelabs-devs/auth-sdk | Core SDK — framework-agnostic client |
| @smarthivelabs-devs/auth-react | React web apps — provider and hooks |
| @smarthivelabs-devs/auth-expo | React Native / Expo apps |
License
MIT © SmartHive Labs
