AgentSkillsCN

dual-auth-rbac

双层认证系统(会话+JWT),带有基于角色的访问控制(RBAC),适用于多租户应用。当跨Web UI、API和移动客户端实现安全认证,具有特许/租户范围权限时使用。支持多种语言:PHP、Node.js、Python、Go等。

SKILL.md
--- frontmatter
name: dual-auth-rbac
description: "Dual authentication system (Session + JWT) with role-based access control (RBAC) for multi-tenant applications. Use when implementing secure authentication across web UI and API/mobile clients, with franchise/tenant-scoped permissions. Works across languages: PHP, Node.js, Python, Go, etc."

Dual Authentication with RBAC

Implement production-grade dual authentication combining session-based (stateful) and JWT-based (stateless) auth with comprehensive RBAC and multi-tenant isolation.

Core Principle: Different clients need different auth strategies. Web UIs benefit from sessions; APIs/mobile need stateless tokens. RBAC must work seamlessly across both.

See references/ for: schema.sql (complete database design with 9 tables)

When to Use

✅ Multi-tenant SaaS with web + API access ✅ Web UI + mobile apps authentication ✅ Role-based permissions with tenant isolation ✅ Token revocation capability required ✅ Multiple device sessions per user

❌ Simple single-tenant apps (overkill) ❌ Read-only public APIs ❌ Internal tools (simpler auth suffices)

Architecture

code
Web UI → Session Cookie + CSRF
Mobile → JWT Access + Refresh
API    → JWT Access + Refresh
         ↓
    Auth Layer → RBAC Engine → Database

Database Schema Essentials

Core tables (see references/schema.sql):

  1. tbl_users - Accounts with tenant scope
  2. tbl_global_roles - Reusable roles
  3. tbl_permissions - Atomic permissions
  4. tbl_global_role_permissions - Role → Permission
  5. tbl_user_roles - User → Role (tenant-scoped)
  6. tbl_user_permissions - Direct grants/denials
  7. tbl_franchise_role_overrides - Tenant overrides
  8. tbl_api_refresh_tokens - JWT revocation
  9. tbl_login_attempts - Security monitoring

Key indexes: (tenant_id, user_id), (jti), (username, attempt_time)

Password Security

Algorithm: Argon2ID + Salt + Pepper

code
Hash Flow:
1. Random salt (16 bytes)
2. Combine password + salt
3. HMAC with pepper (env secret)
4. Argon2ID hash
5. Store: salt + hash

Parameters:
  memory_cost: 65536 KB
  time_cost: 4
  threads: 3
  salt: 16 bytes
  pepper: env variable

Requirements: Min 8 chars, uppercase + lowercase + numbers + special

JWT Architecture

Access Token (15 min)

json
{
  "sub": 12345,          // user_id
  "fid": 67,             // franchise_id
  "ut": "staff",         // user_type
  "did": "device-123",   // device_id
  "jti": "unique-id",    // for revocation
  "exp": 1706000900,
  "type": "access"
}

Refresh Token (30 days)

json
{
  "sub": 12345,
  "fid": 67,
  "ut": "staff",
  "did": "device-123",
  "jti": "unique-id",
  "exp": 1708592000,
  "type": "refresh"
}

Security:

  • HS256 signing (RS256 for distributed)
  • Timing-safe comparison
  • Unique JTI per token
  • Token rotation on refresh
  • Revocation table

Session Management

Config:

code
HttpOnly: true
Secure: true
SameSite: Strict
Lifetime: 30 minutes

Session data:

code
{
  user_id, franchise_id, user_type,
  username, last_activity, csrf_token
}

Security:

  • Regenerate ID on login
  • Timeout check (30 min idle)
  • Complete destruction on logout
  • CSRF validation on mutations

RBAC Permission Resolution

Priority (highest to lowest):

code
1. User Denial → DENIES even if role grants
2. User Grant → GRANTS even if not in role
3. Franchise Override → Tenant enables/disables
4. Role Permission → Default from role
5. Super Admin → ALL permissions

Algorithm:

code
hasPermission(userId, franchiseId, permissionCode):
  if user.type == 'super_admin': return true
  if userPermission.denied(...): return false
  if userPermission.granted(...): return true

  for role in getUserRoles(userId, franchiseId):
    override = getFranchiseOverride(...)
    if override and override.disabled: continue
    if permissionCode in getRolePermissions(role): return true

  return false

Caching: 15 min TTL, invalidate on role/permission changes

Authentication Flows

Web Login (Session)

code
1. POST /auth/login {username, password, csrf_token}
2. Validate CSRF
3. Find user (check status, not locked)
4. Verify password
5. Create session (regenerate ID, store context)
6. Return redirect

API Login (JWT)

code
1. POST /api/v1/auth/login {username, password, device_id}
2. Find user, verify password
3. Generate access + refresh tokens
4. Persist refresh token (revocation table)
5. Return {access_token, refresh_token, expires_in}

Token Refresh

code
1. POST /api/v1/auth/refresh {refresh_token}
2. Verify token, check revocation
3. Revoke old token (rotation)
4. Generate new token pair
5. Persist new refresh token
6. Return new tokens

Multi-Tenant Isolation

Session-based:

code
franchiseId = session.franchise_id
SELECT * FROM orders WHERE franchise_id = ?

JWT-based:

code
token = verifyAccessToken(bearerToken)
franchiseId = token.fid
SELECT * FROM orders WHERE franchise_id = ?

Cross-tenant protection:

code
if user.type != 'super_admin' and user.franchise_id != requestedFranchiseId:
  throw ForbiddenError("Cross-tenant access denied")

Security Checklist

Password

  • Argon2ID (memory_cost ≥ 65536 KB)
  • Random salt (16+ bytes)
  • Application pepper (env var)
  • Complexity validation
  • Rehash check on login

JWT

  • Access ≤ 15 min
  • Refresh rotation
  • Unique JTI
  • Secure secret (32+ bytes)
  • Audience + issuer validation
  • Timing-safe comparison

Session

  • HttpOnly, Secure, SameSite
  • Regeneration on login
  • 30 min timeout
  • Complete destruction
  • CSRF validation

Account Protection

  • Lock after 5 failures
  • Login attempt logging
  • Rate limiting
  • Password reset limits
  • Force change on first login

Multi-Tenant

  • Tenant ID in session/token
  • Tenant filter on EVERY query
  • Tenant-scoped permissions
  • Cross-tenant validation

RBAC

  • Permission caching (15 min)
  • Super admin bypass
  • Protected resource checks
  • Franchise overrides

Middleware Pattern

Session (Web):

code
requireAuth():
  if not session.user_id: redirect("/login")
  if timeout: logout(), redirect("/login")
  session.last_activity = now()
  return session

requirePermission(code):
  session = requireAuth()
  if not hasPermission(...): return 403
  return session

JWT (API):

code
authenticateJWT():
  token = extractBearerToken()
  if not token: return 401
  payload = verifyAccessToken(token)
  return {user_id, franchise_id, user_type}

requirePermission(code):
  context = authenticateJWT()
  if not hasPermission(...): return 403
  return context

Environment Variables

bash
JWT_SECRET=<openssl rand -hex 32>
JWT_ACCESS_TTL=900
JWT_REFRESH_TTL=2592000
PASSWORD_PEPPER=<openssl rand -hex 32>
SESSION_LIFETIME=1800
MAX_LOGIN_ATTEMPTS=5

Common Endpoints

Web: /auth/login, /auth/logout, /auth/password/reset API: /api/v1/auth/login, /api/v1/auth/refresh, /api/v1/auth/logout

Language-Specific

PHP: password_hash(PASSWORD_ARGON2ID), session_start(), firebase/php-jwt Node: argon2 package, express-session, jsonwebtoken Python: argon2-cffi, Flask-Session/Django, PyJWT Go: golang.org/x/crypto/argon2, gorilla/sessions, golang-jwt/jwt

Testing

  • Valid login returns session/tokens
  • Invalid password → 401
  • Locked account → 403
  • Token refresh works
  • Logout revokes
  • Super admin bypasses
  • Role permissions resolve
  • User overrides work
  • Tenant isolation enforced
  • Missing permission → 403
  • Expired tokens rejected
  • Revoked tokens rejected
  • Invalid signatures rejected
  • CSRF works
  • Session timeout enforced

Implementation Steps

  1. Create database schema (references/schema.sql)
  2. Implement password helper (Argon2ID)
  3. Implement JWT service (sign, verify, refresh)
  4. Implement session management
  5. Implement permission service (RBAC + caching)
  6. Create auth endpoints
  7. Create middleware (validators, permission checks)
  8. Add security (rate limiting, locking, CSRF)
  9. Write tests
  10. Configure environment

Remember: Security is not optional. Follow checklist completely.