Agent skill

SaaS Auth Patterns

by vibeeval in vibeeval/vibecosystem

SaaS authentication and authorization patterns including JWT vs session strategies, multi-tenant isolation, RBAC, API key management, passwordless flows, MFA, and secure session handling.

MITAuto-check passedBackend & APIs

Install SaaS Auth Patterns

skills CLI
$ npx skills add vibeeval/vibecosystem --skill saas-auth-patterns -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install vibeeval/vibecosystem saas-auth-patterns --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/vibeeval/vibecosystem.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/saas-auth-patterns .claude/skills/saas-auth-patterns && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
saas-auth-patterns
GitHub stars
531
Token cost
~3.2k tokens
SKILL.md length
160 words
Files
1
Skills in repo
144
Repo updated
First seen
Licence
MIT

At a glance

SaaS authentication and authorization patterns including JWT vs session strategies, multi-tenant isolation, RBAC, API key management, passwordless flows, MFA, and secure session handling.

  • Tasks that involve Authentication
  • SKILL.md covers Auth Strategy Decision Matrix, Multi-Tenant Auth, Account Linking (Email +… and Role-Based Access Control (RBAC), plus 5 more sections
  • Instructions only: no scripts, shell commands, URLs or credentials in SKILL.md
  • Tasks that involve Authorization and RBAC

What it does

SaaS Auth Patterns is an agent skill from vibeeval/vibecosystem. SaaS authentication and authorization patterns including JWT vs session strategies, multi-tenant isolation, RBAC, API key management, passwordless flows, MFA, and secure session handling.

Its SKILL.md is about 3.2k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Backend & APIs, covering Authentication, Authorization and RBAC and Multi-tenancy. The repository describes itself as: AI software team for Claude Code - 138 agents, 295 skills, 73 hooks. Self-learning, multi-agent swarm, autonomous skill evolution. The licence is MIT.

When your agent uses it

  • Tasks that involve Authentication
  • Tasks that involve Authorization and RBAC
  • Tasks that involve Multi-tenancy

Example prompts

  • “/saas-auth-patterns”

What it can do on your machine

Read from SKILL.md and the folder at commit 3b763b1. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    No scripts in the folder and no shell commands in SKILL.md (its code samples are typescript).

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

SaaS Auth Patterns loads about 3.2k tokens when it runs. Until then it costs about 52 tokens; SKILL.md has 160 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~52
When it runs · the whole SKILL.md, loaded when a task matches
~3.2k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from vibeeval/vibecosystem at commit 3b763b1, republished under its MIT licence (© vibeeval). 160 words, ~3,193 tokens.

Download SKILL.mdSave it as .claude/skills/saas-auth-patterns/SKILL.md (or your agent's skills folder).
name
saas-auth-patterns
description
SaaS authentication and authorization patterns including JWT vs session strategies, multi-tenant isolation, RBAC, API key management, passwordless flows, MFA, and secure session handling.

SaaS Auth Patterns

Authentication and authorization patterns for multi-tenant SaaS applications.

Auth Strategy Decision Matrix

StrategyStatelessScalableRevocableBest For
JWT + RefreshYesHighHard (needs blocklist)API-first, mobile clients
Session (server)NoMedium (sticky/shared store)InstantTraditional web apps
OAuth 2.0 + PKCEYesHighVia providerThird-party login, SSO

Pick JWT when you control both client and server and need horizontal scaling. Pick sessions when you need instant revocation and serve server-rendered pages. Pick OAuth when users expect "Sign in with Google/GitHub" or you federate identity.

Multi-Tenant Auth

Tenant Isolation Middleware
typescript
interface TenantContext {
  tenantId: string
  userId: string
  role: string
}

// Extract tenant from JWT claims or subdomain
function resolveTenant(req: Request): TenantContext {
  const token = req.headers.get('authorization')?.replace('Bearer ', '')
  if (!token) throw new AuthError('Missing token')

  const payload = verifyJwt(token)
  return {
    tenantId: payload.tenantId,
    userId: payload.sub,
    role: payload.role,
  }
}

// Every DB query scoped to tenant - no cross-tenant leakage
async function getTenantUsers(ctx: TenantContext): Promise<User[]> {
  return db.users.findMany({
    where: { tenantId: ctx.tenantId },
  })
}
Shared DB vs Isolated DB
typescript
// Shared DB (row-level isolation) - simpler ops, lower cost
// Every table has tenant_id column + RLS policy
// SQL: CREATE POLICY tenant_isolation ON users
//        USING (tenant_id = current_setting('app.tenant_id'))

async function withTenantScope<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
  await db.$executeRaw`SELECT set_config('app.tenant_id', ${tenantId}, true)`
  return fn()
}

// Isolated DB (schema-per-tenant) - stronger isolation, harder ops
// Use when: compliance requires it, tenants have wildly different data volumes
function getTenantConnection(tenantId: string): PrismaClient {
  // SECURITY: Validate tenantId to prevent schema injection
  if (!/^[a-zA-Z0-9_-]+$/.test(tenantId)) {
    throw new Error('Invalid tenant ID format')
  }
  const schema = `tenant_${tenantId}`
  // Note: Cache PrismaClient instances per tenant to avoid connection leaks
  return new PrismaClient({ datasources: { db: { url: `${DB_URL}?schema=${schema}` } } })
}

Account Linking (Email + Social Merge)

typescript
async function linkOrCreateAccount(provider: string, profile: OAuthProfile): Promise<User> {
  // Step 1: Check if social account already linked
  const existing = await db.socialAccounts.findUnique({
    where: { provider_providerAccountId: { provider, providerAccountId: profile.id } },
    include: { user: true },
  })
  if (existing) return existing.user

  // Step 2: Check if email matches an existing user
  // SECURITY: Only auto-link if provider verified the email
  if (!profile.email_verified) {
    return db.users.create({
      data: {
        email: null, name: profile.name,
        socialAccounts: { create: { provider, providerAccountId: profile.id } },
      },
    })
  }

  const emailUser = await db.users.findUnique({
    where: { email: profile.email },
  })

  if (emailUser) {
    // Link social account to existing user (merge)
    await db.socialAccounts.create({
      data: { userId: emailUser.id, provider, providerAccountId: profile.id },
    })
    return emailUser
  }

  // Step 3: Brand new user - create both records
  return db.users.create({
    data: {
      email: profile.email,
      name: profile.name,
      socialAccounts: {
        create: { provider, providerAccountId: profile.id },
      },
    },
  })
}

Role-Based Access Control (RBAC)

typescript
type Permission = 'read' | 'write' | 'delete' | 'manage_users' | 'billing'

const ROLE_PERMISSIONS: Record<string, Permission[]> = {
  owner:  ['read', 'write', 'delete', 'manage_users', 'billing'],
  admin:  ['read', 'write', 'delete', 'manage_users'],
  member: ['read', 'write'],
  viewer: ['read'],
}

function authorize(role: string, required: Permission): boolean {
  const permissions = ROLE_PERMISSIONS[role]
  if (!permissions) return false
  return permissions.includes(required)
}

// Middleware factory - attach to any route
function requirePermission(permission: Permission) {
  return async (req: Request): Promise<void> => {
    const ctx = resolveTenant(req)
    if (!authorize(ctx.role, permission)) {
      throw new AuthError('Insufficient permissions')
    }
  }
}

// Usage
// await requirePermission('manage_users')(req)
// await requirePermission('billing')(req)

API Key Management

typescript
import { randomBytes, createHash } from 'crypto'

// Generate: show full key once, store only the hash
function generateApiKey(): { fullKey: string; hashedKey: string; prefix: string } {
  const raw = randomBytes(32).toString('base64url')
  const prefix = raw.slice(0, 8)
  const fullKey = `sk_live_${raw}`
  const hashedKey = createHash('sha256').update(fullKey).digest('hex')
  return { fullKey, hashedKey, prefix }
}

// Store key with scopes and expiry
async function createApiKey(tenantId: string, name: string, scopes: string[]): Promise<string> {
  const { fullKey, hashedKey, prefix } = generateApiKey()
  await db.apiKeys.create({
    data: { tenantId, name, hashedKey, prefix, scopes, expiresAt: addDays(new Date(), 90) },
  })
  return fullKey  // Return ONCE - never stored in plaintext
}

// Validate incoming API key
async function validateApiKey(key: string): Promise<{ tenantId: string; scopes: string[] }> {
  const hashedKey = createHash('sha256').update(key).digest('hex')
  const record = await db.apiKeys.findUnique({ where: { hashedKey } })

  if (!record) throw new AuthError('Invalid API key')
  if (record.expiresAt < new Date()) throw new AuthError('API key expired')
  if (record.revokedAt) throw new AuthError('API key revoked')

  await db.apiKeys.update({ where: { id: record.id }, data: { lastUsedAt: new Date() } })
  return { tenantId: record.tenantId, scopes: record.scopes }
}

// Rotation: create new key, mark old as deprecated, revoke after grace period
async function rotateApiKey(oldKeyId: string, tenantId: string): Promise<string> {
  const oldKey = await db.apiKeys.findUnique({ where: { id: oldKeyId } })
  if (!oldKey) throw new Error('Key not found')

  const newFullKey = await createApiKey(tenantId, `${oldKey.name} (rotated)`, oldKey.scopes)
  await db.apiKeys.update({ where: { id: oldKeyId }, data: { revokedAt: addDays(new Date(), 7) } })
  return newFullKey
}
typescript
async function sendMagicLink(email: string): Promise<void> {
  const token = randomBytes(32).toString('base64url')
  const hashedToken = createHash('sha256').update(token).digest('hex')

  await db.magicLinks.create({
    data: { email, hashedToken, expiresAt: new Date(Date.now() + 15 * 60 * 1000) },  // 15 min
  })

  const link = `${process.env.APP_URL}/auth/verify?token=${token}`
  await sendEmail(email, 'Sign in', `Click to sign in: ${link}`)
}

async function verifyMagicLink(token: string): Promise<{ userId: string; sessionToken: string }> {
  const hashedToken = createHash('sha256').update(token).digest('hex')

  // Atomic: mark as used only if not already used (prevents TOCTOU race)
  const result = await db.magicLinks.updateMany({
    where: { hashedToken, usedAt: null, expiresAt: { gt: new Date() } },
    data: { usedAt: new Date() },
  })
  if (result.count === 0) throw new AuthError('Invalid, expired, or already used link')

  const record = await db.magicLinks.findUnique({ where: { hashedToken } })

  const user = await findOrCreateUser(record.email)
  const sessionToken = await createSession(user.id)
  return { userId: user.id, sessionToken }
}

MFA Integration

typescript
import { authenticator } from 'otplib'

// Enrollment: generate secret, user scans QR code
async function enrollMfa(userId: string): Promise<{ secret: string; qrUri: string }> {
  const secret = authenticator.generateSecret()
  // SECURITY: Encrypt secret at rest in production (AES-256-GCM)
  await db.mfaSecrets.create({ data: { userId, secret, verified: false } })

  const qrUri = authenticator.keyuri(userId, process.env.APP_NAME ?? 'My App', secret)
  return { secret, qrUri }
}

// Verify first code to activate MFA
async function activateMfa(userId: string, code: string): Promise<void> {
  const record = await db.mfaSecrets.findUnique({ where: { userId } })
  if (!record) throw new AuthError('MFA not enrolled')

  if (!authenticator.check(code, record.secret)) {
    throw new AuthError('Invalid MFA code')
  }

  await db.mfaSecrets.update({ where: { userId }, data: { verified: true } })
}

// Login: after password check, require MFA if enabled
async function loginWithMfa(email: string, password: string, mfaCode?: string): Promise<string> {
  const user = await verifyPassword(email, password)

  const mfa = await db.mfaSecrets.findUnique({ where: { userId: user.id, verified: true } })
  if (mfa) {
    if (!mfaCode) throw new MfaRequiredError('MFA code required')
    if (!authenticator.check(mfaCode, mfa.secret)) throw new AuthError('Invalid MFA code')
  }

  return createSession(user.id)
}

Session Management

typescript
// Session with refresh token rotation
async function createSession(userId: string): Promise<{ accessToken: string; refreshToken: string }> {
  const accessToken = signJwt({ sub: userId }, { expiresIn: '15m' })
  const refreshToken = randomBytes(32).toString('base64url')
  const hashedRefresh = createHash('sha256').update(refreshToken).digest('hex')

  await db.sessions.create({
    data: { userId, hashedRefreshToken: hashedRefresh, expiresAt: addDays(new Date(), 30) },
  })

  return { accessToken, refreshToken }
}

// Refresh: issue new pair, invalidate old refresh token (rotation)
async function refreshSession(oldRefreshToken: string): Promise<{ accessToken: string; refreshToken: string }> {
  const hashed = createHash('sha256').update(oldRefreshToken).digest('hex')
  const session = await db.sessions.findUnique({ where: { hashedRefreshToken: hashed } })

  if (!session || session.expiresAt < new Date()) throw new AuthError('Session expired')
  if (session.revokedAt) {
    // Refresh token reuse detected - revoke ALL sessions for this user
    await db.sessions.updateMany({ where: { userId: session.userId }, data: { revokedAt: new Date() } })
    throw new AuthError('Token reuse detected, all sessions revoked')
  }

  // Revoke old, issue new
  await db.sessions.update({ where: { id: session.id }, data: { revokedAt: new Date() } })
  return createSession(session.userId)
}

// Concurrent session limit
async function enforceSessionLimit(userId: string, maxSessions: number): Promise<void> {
  const activeSessions = await db.sessions.findMany({
    where: { userId, revokedAt: null, expiresAt: { gt: new Date() } },
    orderBy: { createdAt: 'asc' },
  })

  if (activeSessions.length >= maxSessions) {
    const oldest = activeSessions[0]
    await db.sessions.update({ where: { id: oldest.id }, data: { revokedAt: new Date() } })
  }
}

Token Storage: GOOD vs BAD

typescript
// BAD: localStorage is accessible to any JS on the page (XSS = full account takeover)
localStorage.setItem('token', accessToken)
fetch('/api/data', { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } })

// GOOD: httpOnly cookie - JS cannot read it, browser sends it automatically
// Server sets the cookie on login response:
function setAuthCookie(res: Response, accessToken: string): void {
  res.headers.set('Set-Cookie', [
    `access_token=${accessToken}`,
    'HttpOnly',        // JS cannot access
    'Secure',          // HTTPS only
    'SameSite=Lax',    // CSRF protection
    'Path=/',
    'Max-Age=900',     // 15 minutes
  ].join('; '))
}

// Server reads from cookie, not from Authorization header:
function getTokenFromCookie(req: Request): string {
  const cookies = req.headers.get('cookie') || ''
  const match = cookies.match(/access_token=([^;]+)/)
  if (!match) throw new AuthError('No session cookie')
  return match[1]
}

Core rule: Store tokens in httpOnly cookies, hash secrets before persisting, rotate keys on a schedule, and treat refresh token reuse as a breach signal. Auth is the one system where "good enough" is never good enough.

© vibeeval, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in skills/saas-auth-patterns of vibeeval/vibecosystem.

Open the folder on GitHubat commit 3b763b1

Compare with similar skills

SaaS Auth Patterns next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

SaaS Auth Patterns compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
SaaS Auth Patterns this skillvibeeval/vibecosystem531—~3.2kAutomated safety check: PassMIT
Supercheck Security Authsupercheck-io/supercheck215—~1.2kAutomated safety check: PassAGPL-3.0
Clerk Orgsgrowupanand/ConvoForm101—~5.4kAutomated safety check: PassMIT
Reviewing Security Architecturebitwarden/ai-plugins154—~2.2kAutomated safety check: PassCustom licence
Clerk Orgsgeekskai/blog103—~4.8kAutomated safety check: PassMIT
Cometchat Securitycometchat/cometchat-skills1291 repos~1.9kAutomated safety check: PassMIT

Similar skills

  • Supercheck Security Auth

    supercheck-io/supercheck

    Work on Supercheck authentication, RBAC, tenant isolation, sessions, API and trigger keys, invitations, project membership, project variables, OAuth, super-admin behavior, SSRF, or…

    215 GitHub stars~1.2k tokensUpdated today
    Backend & APIsAuto-check passed
  • Clerk Orgs

    growupanand/ConvoForm

    Clerk Organizations for B2B and multi-tenant apps - org switching, roles and permissions, verified domains, and enterprise SSO.

    101 GitHub stars~5.4k tokensUpdated yesterday
    Backend & APIsAuto-check passed
  • Official

    This skill should be used when the user asks to "review the security architecture", "check authentication patterns", "evaluate trust boundaries", "review encryption implementation", "assess…

    154 GitHub stars~2.2k tokensUpdated yesterday
    Backend & APIsAuto-check passed
  • Clerk Orgs

    geekskai/blog

    Clerk Organizations for B2B SaaS - create multi-tenant apps with org switching, role-based access, verified domains, and enterprise SSO.

    103 GitHub stars~4.8k tokensUpdated yesterday
    Backend & APIsAuto-check passed
  • Cometchat Security

    cometchat/cometchat-skills

    Enterprise auth & access control for CometChat — SSO/OIDC/SAML via your own IdP, server-minted auth tokens, token revocation & session control, and role-based access (RBAC app-wide roles + group…

    129 GitHub starsUsed in 1 repo~1.9k tokens
    Backend & APIsAuto-check passed
  • Security Protocol

    NoobyGains/godmode

    A skill your agent uses when writing code that processes user input, manages authentication or authorization, constructs database queries, handles file operations, interacts with external data…

    107 GitHub stars~2.4k tokensUpdated 7 mo ago
    Backend & APIsAuto-check: notes

More from vibeeval/vibecosystem

All 144 skills in this repo
  • Agent Benchmark

    vibeeval/vibecosystem

    Framework for measuring and tracking agent response quality over time.

    531 GitHub stars~2.9k tokensUpdated 2 mo ago
    Auto-check passed
  • Differential Review

    vibeeval/vibecosystem

    Security-focused differential code review with blast radius analysis, risk-adaptive depth (DEEP/FOCUSED/SURGICAL), git history correlation, and structured finding format.

    531 GitHub stars~1.6k tokensUpdated 2 mo ago
    Auto-check passed
  • Factcheck Guard

    vibeeval/vibecosystem

    A skill your agent uses when making any factual claim about the codebase — existence, absence, or behavior.

    531 GitHub stars~2.2k tokensUpdated 2 mo ago
    Auto-check passed
  • Fp Check

    vibeeval/vibecosystem

    Systematic false positive verification for security findings.

    531 GitHub stars~1.6k tokensUpdated 2 mo ago
    Auto-check passed
  • N8n Workflows

    vibeeval/vibecosystem

    n8n otomasyon workflow'lari. An agent skill from vibeeval/vibecosystem.

    531 GitHub stars~3.3k tokensUpdated 2 mo ago
    Auto-check passed
  • Notepad System

    vibeeval/vibecosystem

    A skill your agent uses when context compression is imminent, when resuming a session, or when preserving critical decisions across long tasks.

    531 GitHub stars~1.7k tokensUpdated 2 mo ago
    Auto-check passed

Categories

Questions about SaaS Auth Patterns

What does SaaS Auth Patterns do?

SaaS authentication and authorization patterns including JWT vs session strategies, multi-tenant isolation, RBAC, API key management, passwordless flows, MFA, and secure session handling. SaaS Auth Patterns is an agent skill from vibeeval/vibecosystem. SaaS authentication and authorization patterns including JWT vs session strategies, multi-tenant isolation, RBAC, API key management, passwordless flows, MFA, and secure session handling.

When should I use SaaS Auth Patterns?

SaaS Auth Patterns fits situations like: tasks that involve Authentication; tasks that involve Authorization and RBAC; tasks that involve Multi-tenancy.

How do I install SaaS Auth Patterns in Claude Code?

Run `npx skills add vibeeval/vibecosystem --skill saas-auth-patterns -a claude-code`. Or copy the skill folder (skills/saas-auth-patterns in vibeeval/vibecosystem) into .claude/skills/saas-auth-patterns in your project. Claude Code loads it when a task matches its description.

How do I install SaaS Auth Patterns in Codex?

Run `npx skills add vibeeval/vibecosystem --skill saas-auth-patterns -a codex`. Or copy the skill folder (skills/saas-auth-patterns in vibeeval/vibecosystem) into .agents/skills/saas-auth-patterns in your project. Codex loads it when a task matches its description.

Can I use SaaS Auth Patterns in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add vibeeval/vibecosystem --skill saas-auth-patterns -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/saas-auth-patterns, .gemini/skills/saas-auth-patterns, .github/skills/saas-auth-patterns and .opencode/skills/saas-auth-patterns in your project.

What does SaaS Auth Patterns need to run?

SKILL.md names no scripts, command-line tools or credentials: SaaS Auth Patterns is instructions for the agent only.

Does SaaS Auth Patterns access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is SaaS Auth Patterns safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does SaaS Auth Patterns use?

SaaS Auth Patterns is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does SaaS Auth Patterns use?

About 3.2k tokens (SKILL.md is roughly 13k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to SaaS Auth Patterns?

Skills that share tags, products or a category with SaaS Auth Patterns: Supercheck Security Auth (supercheck-io/supercheck, 215 stars), Clerk Orgs (growupanand/ConvoForm, 101 stars), Reviewing Security Architecture (bitwarden/ai-plugins, 154 stars) and Clerk Orgs (geekskai/blog, 103 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains SaaS Auth Patterns?

vibeeval (a GitHub user) maintains it in vibeeval/vibecosystem, which has 531 GitHub stars. The repository holds 144 skills in this directory. The repository was last updated on August 8, 2026.

Source: vibeeval/vibecosystem on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.