Skip to main content

Command Palette

Search for a command to run...

ACAI — Chapter 38: Authentication, Sessions, Authorization & Access Control

Updated
14 min readView as Markdown
B
Musfiqur Rahim | Founder & CEO at Black Shadow Team | Ethical Hacker & Security Researcher | Passionate about building secure digital infrastructure and pushing the boundaries of cybersecurity.
Post cover

38.1 Introduction

Chapter 37 transformed the database architecture into a working PostgreSQL and Prisma implementation.

The next requirement is identity.

A database can store users, projects, files, conversations, and audit events, but the application must know:

  • Who is making a request?

  • Is that user authenticated?

  • Is the session still valid?

  • What resources does the user own?

  • What actions is the user permitted to perform?

  • Does the user have an administrator or ordinary-user role?

  • Should a particular operation require additional approval?

Therefore, authentication and authorization become a critical security boundary.

The fundamental architecture is:

Client
  │
  ▼
Authentication
  │
  ▼
Session
  │
  ▼
Identity
  │
  ▼
Authorization
  │
  ▼
Resource Ownership
  │
  ▼
Business Operation

Authentication answers:

"Who are you?"

Authorization answers:

"What are you allowed to do?"

These are different security functions and should not be confused.


38.2 Authentication vs Authorization

Authentication verifies identity.

Examples:

Email + Password
OAuth
Passkey
Magic Link
Multi-Factor Authentication

Authorization determines permissions.

Examples:

User
Admin
Project Owner
Project Member
Reviewer
System Operator

A user can be successfully authenticated and still be unauthorized to access a resource.

For example:

User A → authenticated
Project B → owned by User B

Authentication succeeds.

Authorization must reject the operation.


38.3 Security Model

The application should follow:

REQUEST
   │
   ▼
Is request structurally valid?
   │
   ▼
Is user authenticated?
   │
   ▼
Does user have permission?
   │
   ▼
Does user own/access resource?
   │
   ▼
Is operation allowed?
   │
   ▼
Execute

A failure at any security boundary should stop the operation.


38.4 Extending the User Model

The user model can be extended with a role.

Example:

enum UserRole {
  USER
  ADMIN
}

model User {
  id           String     @id @default(uuid())
  email        String     @unique
  name         String?
  status       UserStatus @default(ACTIVE)
  role         UserRole   @default(USER)

  createdAt    DateTime   @default(now())
  updatedAt    DateTime   @updatedAt
}

A migration is then created:

npx prisma migrate dev --name add_user_role

The migration should be reviewed before being applied to important environments.


38.5 Session Model

A production application needs a controlled session mechanism.

A conceptual session table can contain:

Session
 ├── id
 ├── userId
 ├── tokenHash
 ├── expiresAt
 ├── createdAt
 ├── lastUsedAt
 └── revokedAt

Example Prisma model:

model Session {
  id         String    @id @default(uuid())

  userId     String
  user       User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  tokenHash  String    @unique

  expiresAt  DateTime
  lastUsedAt DateTime?
  revokedAt  DateTime?

  createdAt  DateTime  @default(now())

  @@index([userId])
  @@index([expiresAt])
  @@index([revokedAt])
}

The raw session token should not be stored unnecessarily.

A safer pattern is:

Random session token
        │
        ▼
Hash
        │
        ▼
Database

The server can compare a presented token against the stored representation.


38.6 Session Lifecycle

A session follows:

LOGIN
  │
  ▼
Credentials verified
  │
  ▼
Session created
  │
  ▼
Secure cookie issued
  │
  ▼
Authenticated requests
  │
  ▼
Session validation
  │
  ├── Valid → continue
  │
  └── Expired/revoked → reject

Logout:

LOGOUT
  │
  ▼
Session revoked
  │
  ▼
Cookie removed

38.7 Password Storage

Passwords must never be stored as plaintext.

Bad:

password = "mypassword123"

The database should instead contain a password hash.

Conceptually:

Password
   │
   ▼
Password Hashing Function
   │
   ▼
Password Hash
   │
   ▼
Database

A modern password hashing algorithm designed for password storage should be used.

The application should never attempt to decrypt a password.

Password verification works by comparing a supplied password against the stored password hash.


38.8 Login Flow

A login request should conceptually follow:

POST /api/auth/login
          │
          ▼
Validate input
          │
          ▼
Find user
          │
          ▼
Check account status
          │
          ▼
Verify password
          │
          ▼
Create session
          │
          ▼
Set secure cookie
          │
          ▼
Return safe user information

The response should not expose:

passwordHash
sessionToken
private keys
internal database information

38.9 Secure Cookie Principles

For browser sessions, cookies should normally be configured with security properties such as:

HttpOnly
Secure
SameSite
appropriate expiration

The purpose is to reduce unnecessary exposure of authentication material to client-side scripts and cross-site request scenarios.

The exact configuration should depend on the application's deployment architecture.


38.10 Current User Resolution

Create a server-side helper such as:

src/lib/auth.ts

Conceptually:

export async function getCurrentUser() {
  // Read session information
  // Validate session
  // Load user
  // Return user or null
}

The helper becomes the central identity-resolution mechanism.

Example usage:

const user = await getCurrentUser();

if (!user) {
  throw new Error("Unauthorized");
}

The application should avoid duplicating authentication logic across every API route.


38.11 Authorization Helper

Authentication alone is insufficient.

A helper can enforce authorization:

export async function requireUser() {
  const user = await getCurrentUser();

  if (!user) {
    throw new Error("Unauthorized");
  }

  return user;
}

For administrative operations:

export async function requireAdmin() {
  const user = await requireUser();

  if (user.role !== "ADMIN") {
    throw new Error("Forbidden");
  }

  return user;
}

The distinction is:

Unauthorized = identity not established

Forbidden = identity established but permission denied

38.12 Resource Ownership

Consider:

GET /api/projects/abc123

The existence of project abc123 does not automatically authorize access.

The server should evaluate:

Current User
      │
      ▼
Project
      │
      ▼
Ownership / Membership
      │
      ▼
Permission

Example repository query:

const project = await prisma.project.findFirst({
  where: {
    id: projectId,
    ownerId: userId,
  },
});

If no record is returned, the server should not disclose whether a protected resource exists unless the application's policy explicitly permits such disclosure.


38.13 Ownership Check Pattern

A safe service operation might look like:

export async function getUserProject(
  userId: string,
  projectId: string
) {
  const project = await prisma.project.findFirst({
    where: {
      id: projectId,
      ownerId: userId,
    },
  });

  if (!project) {
    throw new Error("Project not found");
  }

  return project;
}

The important property is that the authorization condition is included in the database query itself.


38.14 Role-Based Access Control

The first role system can be simple:

USER
ADMIN

Later it can become more granular:

USER
PROJECT_MEMBER
PROJECT_OWNER
REVIEWER
ADMIN
SYSTEM_OPERATOR

However, roles should not be created merely because they sound useful.

Every role should have a clearly documented permission set.

Example:

Role View Own Data Manage Own Projects Manage Users
USER Yes Yes No
ADMIN Yes Yes Yes

38.15 Permission-Based Design

For larger systems, permissions can be represented explicitly:

project.read
project.create
project.update
project.delete

file.read
file.upload
file.delete

conversation.read
conversation.create

admin.users.read
admin.users.update

Then roles can map to permissions.

Conceptually:

Role
  │
  ├── Permission A
  ├── Permission B
  └── Permission C

This is more flexible than embedding dozens of role-specific conditions throughout application code.


38.16 API Protection

Every protected API endpoint should have an explicit security boundary.

Example:

export async function GET() {
  const user = await requireUser();

  // Continue only after authentication.
}

For a project operation:

export async function PATCH(
  request: Request,
  context: {
    params: {
      id: string;
    };
  }
) {
  const user = await requireUser();

  const project =
    await getUserProject(
      user.id,
      context.params.id
    );

  // Authorized operation continues here.
}

38.17 Never Trust Client-Supplied Identity

A dangerous pattern is:

{
  "userId": "some-id",
  "name": "new name"
}

followed by:

await updateUser(body.userId, ...)

The client should not be trusted to determine which authenticated identity is making the request.

Instead:

Authenticated Session
       │
       ▼
Server determines userId
       │
       ▼
Operation

If the request body contains a user ID, it should not override the authenticated identity.


38.18 Authentication Rate Limiting

Login endpoints are security-sensitive.

A system should consider controls such as:

Rate limiting
Credential attempt monitoring
Temporary throttling
Account protection
Suspicious activity detection

The objective is to reduce automated abuse without unnecessarily blocking legitimate users.


38.19 Session Expiration

Sessions should have expiration policies.

Example conceptual lifecycle:

Session created
      │
      ▼
Active
      │
      ├── User logs out → Revoked
      │
      ├── Admin revokes → Revoked
      │
      └── Expiration reached → Expired

The server should validate expiration before granting access.


38.20 Session Revocation

A security-conscious system should be able to revoke sessions.

Examples:

User logs out
Password changed
Account compromised
Administrator disables account
Suspicious session detected

The system can mark:

revokedAt = current time

rather than relying only on client-side cookie removal.


38.21 Multiple Sessions

Users may have multiple active devices:

Laptop
   │
   └── Session A

Phone
   │
   └── Session B

Tablet
   │
   └── Session C

A session-management interface can eventually allow the user to see and revoke individual sessions.


38.22 Account Status

Authentication should also respect account state.

For example:

ACTIVE
SUSPENDED
DELETED

A suspended account may have valid credentials but should still be denied access.

Therefore:

Valid credentials
        +
ACTIVE account
        +
Valid session
        =
Authenticated request

38.23 Admin Protection

Administrative functionality requires additional safeguards.

A basic model:

Normal User
    ↓
Standard API

Admin
    ↓
Admin API
    ↓
Permission Check
    ↓
Audit Log

Administrative actions should generate audit events.

Example:

ADMIN_USER_SUSPENDED
ADMIN_USER_RESTORED
ADMIN_PROJECT_REVIEWED
ADMIN_CONFIGURATION_CHANGED

38.24 Audit Integration

Authentication events should connect to the existing audit system.

Examples:

LOGIN_SUCCESS
LOGIN_FAILURE
LOGOUT
SESSION_CREATED
SESSION_REVOKED
PASSWORD_CHANGED
ACCOUNT_SUSPENDED
ROLE_CHANGED

The audit system should avoid storing sensitive credentials.


38.25 Security Event Data

A useful audit record may include:

userId
action
resource
resourceId
timestamp
request identifier
result

Potentially useful metadata:

authentication method
application version
device category

Only collect information that is justified by the security and operational requirements.


38.26 Authentication Error Messages

Avoid revealing unnecessary account information.

For example, overly specific responses can disclose whether an account exists.

A safer external response can be intentionally generic:

Authentication failed.

Detailed diagnostics remain server-side.


38.27 Database Constraints

Authentication security should not depend only on application code.

The database should enforce constraints such as:

email UNIQUE
session token hash UNIQUE
foreign-key integrity
non-null required fields

Application validation and database constraints complement one another.


38.28 Security Testing

Authentication should be tested systematically.

Minimum test categories:

Valid login
Invalid password
Unknown account
Expired session
Revoked session
Suspended account
Unauthenticated request
Unauthorized resource access
Admin-only operation
Logout
Multiple sessions

Example:

Test:
User A requests User B's project.

Expected:
Access denied.

Another:

Test:
Normal user calls admin endpoint.

Expected:
Forbidden.

38.29 Authorization Test Matrix

A useful matrix is:

Operation Anonymous User Owner Admin
Public landing page
Create own project
Read own project
Modify another user's project Policy-dependent
User administration

The actual matrix must be defined by the application's requirements.


38.30 Defense-in-Depth

Authentication should not be the only security mechanism.

The system should combine:

Input Validation
        +
Authentication
        +
Authorization
        +
Ownership Checks
        +
Database Constraints
        +
Rate Limiting
        +
Audit Logging
        +
Monitoring

If one control fails, other controls can still reduce the impact.


38.31 Zero-Trust Request Handling

A useful principle is:

Every request should be treated as untrusted until the server validates it.

That means:

Client-provided ID
Client-provided role
Client-provided ownership
Client-provided permission

should never be accepted as authoritative.

The server determines identity and authorization.


38.32 Authentication Architecture

The resulting architecture becomes:

                   Request
                      │
                      ▼
               Input Validation
                      │
                      ▼
               Session Resolver
                      │
                      ▼
                Current User
                      │
                      ▼
              Authorization
                      │
          ┌───────────┴───────────┐
          │                       │
       Allowed                 Denied
          │                       │
          ▼                       ▼
    Service Layer              403/401
          │
          ▼
     Repository
          │
          ▼
       Prisma
          │
          ▼
     PostgreSQL

38.33 Recommended Authentication Boundaries

The application should have centralized functions for:

getCurrentUser()
requireUser()
requireAdmin()
requireProjectOwner()
requirePermission()
createSession()
revokeSession()

Centralization reduces inconsistent security logic.


38.34 Future Authentication Extensions

The architecture can later support:

Email verification
Password reset
Multi-factor authentication
Passkeys
OAuth providers
Device management
Session management
Security notifications
Recovery codes
Adaptive authentication

These should be introduced incrementally.

A simple, auditable authentication system is preferable to a complicated system that is difficult to verify.


38.35 Security Research Perspective

Authentication and authorization are particularly important in AI platforms because AI functionality often operates on valuable resources:

Uploaded documents
Private conversations
Generated media
API usage
Model configurations
Stored memories
Agent tasks
Connected tools

A failure in access control can therefore expose more than ordinary profile information.

For an AI platform, authorization should be applied to the resources used by the AI system itself.


38.36 AI-Specific Authorization

An AI assistant should not automatically inherit unrestricted access to every resource belonging to a user.

Instead:

User
 │
 ▼
Authorized Context
 │
 ├── Project A
 ├── Selected Files
 ├── Selected Conversations
 └── Approved Tools

The model receives only the context required for the task.

This creates an additional security boundary between:

User permissions

and

AI execution permissions

38.37 Agent Permission Boundary

For future agent functionality:

Agent
 │
 ▼
Requested Action
 │
 ▼
Policy Check
 │
 ├── Allowed → Execute
 │
 └── Approval Required → Pause

The model should not be treated as an authority that can grant itself permissions.

Authorization belongs to the application.


38.38 Human Approval

Sensitive operations can use:

REQUEST
  ↓
POLICY CHECK
  ↓
APPROVAL REQUIRED
  ↓
HUMAN REVIEW
  ↓
APPROVED / REJECTED

This becomes particularly useful for actions that have external side effects.

The approval decision should be auditable.


38.39 Chapter 38 Security Checklist

[ ] Passwords are never stored in plaintext
[ ] Session data is protected
[ ] Sessions can expire
[ ] Sessions can be revoked
[ ] Cookies use appropriate security settings
[ ] Authentication is server-side
[ ] Authorization is server-side
[ ] Client-supplied user IDs are not trusted
[ ] Ownership is verified
[ ] Admin operations require authorization
[ ] Sensitive actions are audited
[ ] Account status is checked
[ ] Login abuse controls exist
[ ] Protected APIs require authentication
[ ] AI resources have authorization boundaries
[ ] Agent actions have explicit permissions

38.40 Conclusion

Chapter 38 established the identity and access-control layer.

The resulting security model is:

Identity
   ↓
Session
   ↓
Authentication
   ↓
Authorization
   ↓
Ownership
   ↓
Permission
   ↓
Business Operation

This prevents a major architectural mistake: treating authentication as the entire security system.

Authentication establishes identity.

Authorization establishes permission.

Ownership establishes resource boundaries.

Audit logging establishes accountability.

Together, these form the foundation for a secure multi-user AI platform.

The next chapter will move into API architecture and request processing, including route organization, validation, response contracts, error handling, rate limiting, request IDs, service boundaries, and secure communication between the frontend and backend.

END OF CHAPTER 38