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

ProviderMethodConfiguration
Email + PasswordCredentialsBuilt-in, always available
GoogleOAuth 2.0Requires Google Cloud Console setup
MicrosoftOAuth 2.0Requires Azure AD app registration
GitHubOAuth 2.0Requires GitHub OAuth app
SAMLSAML 2.0Enterprise 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:
1

Generate Token

Go to Settings > API Tokens > Generate New Token.
2

Set Permissions

Choose which scopes the token has access to (e.g., read-only, read-write, admin).
3

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:
EndpointLimit
POST /api/auth/signin10 requests per minute per IP
POST /api/auth/register5 requests per minute per IP
POST /api/auth/forgot-password3 requests per minute per email
API token authentication1000 requests per minute per token