Overview
Obeya Cloud uses Auth.js v5 (formerly NextAuth.js) for authentication. It supports multiple authentication providers and issues JWT-based sessions for API access.
Authentication Providers
| Provider | Method | Configuration |
|---|
| Email + Password | Credentials | Built-in, always available |
| Google | OAuth 2.0 | Requires Google Cloud Console setup |
| Microsoft | OAuth 2.0 | Requires Azure AD app registration |
| GitHub | OAuth 2.0 | Requires GitHub OAuth app |
| SAML | SAML 2.0 | Enterprise SSO (Business/Enterprise plans) |
Auth Configuration
Auth.js is configured in the @obeya/auth package:
// packages/auth/src/config.ts
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@obeya/db";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: DrizzleAdapter(db),
providers: [
Credentials({
credentials: {
email: { type: "email" },
password: { type: "password" },
},
authorize: async (credentials) => {
const user = await db.query.users.findFirst({
where: eq(users.email, credentials.email),
});
if (!user || !await verifyPassword(credentials.password, user.passwordHash)) {
return null;
}
return { id: user.id, email: user.email, name: user.name };
},
}),
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
GitHub({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
],
session: { strategy: "jwt" },
callbacks: {
jwt: async ({ token, user }) => {
if (user) {
token.userId = user.id;
// Load organization memberships
token.memberships = await loadMemberships(user.id);
}
return token;
},
session: async ({ session, token }) => {
session.user.id = token.userId;
session.user.memberships = token.memberships;
return session;
},
},
});
Session Flow
1. User submits credentials (or OAuth callback)
│
2. Auth.js validates credentials
│
3. JWT token is issued with user ID + memberships
│
4. Token stored in HTTP-only cookie (`authjs.session-token`)
│
5. On each request, middleware reads the JWT
│
6. tRPC context receives the authenticated user
│
7. Procedures enforce authorization checks
tRPC Authentication
The tRPC context extracts the authenticated user from the session:
// packages/api/src/trpc.ts
export const createTRPCContext = async (opts: { headers: Headers }) => {
const session = await auth();
const tenantId = opts.headers.get("x-tenant-id");
return {
db,
session,
tenant: tenantId ? { id: tenantId } : null,
userId: session?.user?.id,
};
};
// Protected procedure - requires authentication
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
...ctx,
session: ctx.session,
userId: ctx.session.user.id,
},
});
});
// Admin procedure - requires admin role
export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
const membership = ctx.session.user.memberships
.find(m => m.organizationId === ctx.tenant?.id);
if (!membership || !["owner", "admin"].includes(membership.role)) {
throw new TRPCError({ code: "FORBIDDEN" });
}
return next({ ctx });
});
API Tokens
For programmatic API access, users can generate personal access tokens:
Generate Token
Go to Settings > API Tokens > Generate New Token.
Set Permissions
Choose which scopes the token has access to (e.g., read-only, read-write, admin).
Copy Token
Copy the token immediately — it will not be shown again.
Use the token in API requests:
curl https://acme.obeya.cloud/api/trpc/items.list \
-H "Authorization: Bearer oby_pat_xxxxxxxxxxxxx" \
-H "Content-Type: application/json"
API tokens are scoped to a single organization and inherit the user’s permissions. Store tokens securely and rotate them regularly.
Password Hashing
Passwords are hashed using Argon2id with the following parameters:
import { hash, verify } from "@node-rs/argon2";
const passwordHash = await hash(password, {
memoryCost: 65536, // 64 MB
timeCost: 3, // 3 iterations
parallelism: 4, // 4 parallel threads
outputLen: 32, // 32-byte hash
});
Rate Limiting
Authentication endpoints are rate-limited to prevent brute-force attacks:
| Endpoint | Limit |
|---|
POST /api/auth/signin | 10 requests per minute per IP |
POST /api/auth/register | 5 requests per minute per IP |
POST /api/auth/forgot-password | 3 requests per minute per email |
| API token authentication | 1000 requests per minute per token |