Skip to main content
UnauthorizedException is used when authentication is required or permissions are insufficient. It returns HTTP 401 status code and is used for situations like login required, session expired, or insufficient permissions.

Basic Usage

class UnauthorizedException extends SoException {
  constructor(
    public message = "Unauthorized",
    public payload?: unknown,
  );
}
Simple Example:
@api()
async getMyProfile(ctx: Context) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  return this.findById(ctx.user.id);
}

Practical Examples

Basic Authentication Check

@api()
async updateMyProfile(
  ctx: Context,
  name: string,
  bio: string
) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  return this.update(ctx.user.id, { name, bio });
}
For simple login checks, using Guards is cleaner:
// Using Guards (recommended)
@api({ guards: ["user"] })
async getMyData(ctx: Context) {
  // ctx.user is guaranteed
  return this.findById(ctx.user!.id);
}

// Guard handling in sonamu.config.ts
export default {
  server: {
    apiConfig: {
      guardHandler: (guard, request, api) => {
        if (guard === "user" && !request.user) {
          throw new UnauthorizedException("Login required");
        }
        return true;
      }
    }
  }
} satisfies SonamuConfig;

Resource Ownership Validation

@api()
async deletePost(ctx: Context, postId: number) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  const post = await this.findById(postId);

  if (!post) {
    throw new NotFoundException("Post not found");
  }

  // Only the author can delete
  if (post.authorId !== ctx.user.id) {
    throw new UnauthorizedException(
      "You can only delete your own posts",
      { postId, authorId: post.authorId }
    );
  }

  return this.delete(postId);
}

Role-Based Permission Validation

@api()
async deleteUser(ctx: Context, userId: number) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  // Check admin permission
  if (!ctx.user.isAdmin) {
    throw new UnauthorizedException(
      "Only administrators can delete users",
      { requiredRole: "admin", currentRole: ctx.user.role }
    );
  }

  // Cannot delete yourself
  if (ctx.user.id === userId) {
    throw new BadRequestException("Cannot delete yourself");
  }

  return this.delete(userId);
}

Complex Permission Validation

@api()
async publishPost(ctx: Context, postId: number) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  const post = await this.findById(postId);

  if (!post) {
    throw new NotFoundException("Post not found");
  }

  // Must be author or have editor permissions
  const isAuthor = post.authorId === ctx.user.id;
  const isEditor = ctx.user.role === "editor" || ctx.user.role === "admin";

  if (!isAuthor && !isEditor) {
    throw new UnauthorizedException(
      "You don't have permission to publish this post",
      {
        postId,
        authorId: post.authorId,
        currentUserId: ctx.user.id,
        currentRole: ctx.user.role,
        requiredCondition: "author or editor/admin role"
      }
    );
  }

  return this.update(postId, { status: "published" });
}

Organization/Team Permission Validation

@api()
async addTeamMember(
  ctx: Context,
  teamId: number,
  newMemberId: number
) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  const team = await TeamModel.findById(teamId);

  if (!team) {
    throw new NotFoundException("Team not found");
  }

  // Only team admins can add members
  const membership = await TeamMemberModel.findOne({
    teamId,
    userId: ctx.user.id
  });

  if (!membership) {
    throw new UnauthorizedException(
      "You are not a team member",
      { teamId }
    );
  }

  if (membership.role !== "admin" && membership.role !== "owner") {
    throw new UnauthorizedException(
      "Only team administrators can add members",
      {
        teamId,
        currentRole: membership.role,
        requiredRole: "admin or owner"
      }
    );
  }

  return TeamMemberModel.create({
    teamId,
    userId: newMemberId,
    role: "member"
  });
}

API Key Authentication

@api()
async getApiData(ctx: Context) {
  const apiKey = ctx.headers["x-api-key"];

  if (!apiKey) {
    throw new UnauthorizedException(
      "API key is required",
      { header: "x-api-key" }
    );
  }

  const validKey = await ApiKeyModel.findByKey(apiKey);

  if (!validKey) {
    throw new UnauthorizedException(
      "Invalid API key"
    );
  }

  if (validKey.expiresAt && validKey.expiresAt < new Date()) {
    throw new UnauthorizedException(
      "Expired API key",
      { expiresAt: validKey.expiresAt }
    );
  }

  if (!validKey.isActive) {
    throw new UnauthorizedException(
      "Inactive API key",
      { keyId: validKey.id }
    );
  }

  // Return API data
  return this.getDataForApiKey(validKey.id);
}

Time-Based Access Control

@api()
async accessRestrictedResource(ctx: Context, resourceId: number) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  const subscription = await SubscriptionModel.findByUserId(ctx.user.id);

  if (!subscription) {
    throw new UnauthorizedException(
      "Subscription required for this resource",
      { resourceId }
    );
  }

  const now = new Date();

  // Check subscription expiration
  if (subscription.expiresAt < now) {
    throw new UnauthorizedException(
      "Subscription has expired",
      {
        expiresAt: subscription.expiresAt,
        renewUrl: "/subscription/renew"
      }
    );
  }

  // Access restrictions by subscription plan
  const resource = await ResourceModel.findById(resourceId);

  if (resource.requiredPlan === "premium" && subscription.plan === "basic") {
    throw new UnauthorizedException(
      "Premium plan required for this resource",
      {
        requiredPlan: "premium",
        currentPlan: "basic",
        upgradeUrl: "/subscription/upgrade"
      }
    );
  }

  return resource;
}

IP-Based Access Control

@api()
async adminOnlyEndpoint(ctx: Context) {
  if (!ctx.user?.isAdmin) {
    throw new UnauthorizedException("Admin permission required");
  }

  // Allow access only from specific IP ranges
  const allowedIPs = ["192.168.1.0/24", "10.0.0.0/8"];
  const clientIP = ctx.request.ip;

  if (!this.isIPAllowed(clientIP, allowedIPs)) {
    throw new UnauthorizedException(
      "IP address not allowed",
      {
        clientIP,
        allowedRanges: allowedIPs
      }
    );
  }

  return this.getAdminData();
}

private isIPAllowed(ip: string, allowedRanges: string[]): boolean {
  // IP range check logic
  return true; // Actual implementation needed
}

Guards vs Manual Check Comparison

// sonamu.config.ts
guardHandler: (guard, request, api) => {
  if (guard === "user" && !request.user) {
    throw new UnauthorizedException("Login required");
  }

  if (guard === "admin" && !request.user?.isAdmin) {
    throw new UnauthorizedException("Admin permission required");
  }

  return true;
}

// API method
@api({ guards: ["user"] })
async simpleUserEndpoint(ctx: Context) {
  // Authentication check automatically completed
  return this.getData(ctx.user!.id);
}

@api({ guards: ["admin"] })
async adminEndpoint(ctx: Context) {
  // Admin permission check automatically completed
  return this.getAdminData();
}

Manual Check (Complex Logic)

@api()
async complexAuthEndpoint(ctx: Context, postId: number) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  const post = await PostModel.findById(postId);

  // Complex permission logic: author, editor, or same team member
  const isAuthor = post.authorId === ctx.user.id;
  const isEditor = ctx.user.role === "editor";
  const isSameTeam = await TeamModel.areInSameTeam(
    ctx.user.id,
    post.authorId
  );

  if (!isAuthor && !isEditor && !isSameTeam) {
    throw new UnauthorizedException("Access denied");
  }

  return post;
}

payload Usage Patterns

Login Prompt

throw new UnauthorizedException("Login required", {
  loginUrl: "/auth/login",
  returnUrl: ctx.request.url
});

Permission Upgrade Prompt

throw new UnauthorizedException(
  "Premium feature",
  {
    requiredPlan: "premium",
    currentPlan: "basic",
    upgradeUrl: "/subscription/upgrade",
    features: ["Advanced analytics", "Unlimited projects", "Priority support"]
  }
);

Expiration Information

throw new UnauthorizedException(
  "Session has expired",
  {
    expiredAt: session.expiresAt,
    refreshUrl: "/auth/refresh"
  }
);

Client Response Examples

Basic Response

{
  "statusCode": 401,
  "message": "Login required"
}

Response with payload

{
  "statusCode": 401,
  "message": "Premium plan required for this resource",
  "payload": {
    "requiredPlan": "premium",
    "currentPlan": "basic",
    "upgradeUrl": "/subscription/upgrade"
  }
}

401 vs 403

  • 401 Unauthorized: When authentication itself fails or permissions are insufficient
  • 403 Forbidden: When authenticated but explicitly lacks permission to access the resource
In Sonamu, UnauthorizedException handles both, but you can create a custom ForbiddenException if needed.
// Custom 403 exception
export class ForbiddenException extends SoException {
  constructor(
    public message = "Forbidden",
    public payload?: unknown,
  ) {
    super(403, message, payload);
  }
}

// Usage
@api()
async deleteUser(ctx: Context, userId: number) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  if (!ctx.user.isAdmin) {
    throw new ForbiddenException(
      "You don't have permission to perform this action",
      { requiredRole: "admin" }
    );
  }

  return this.delete(userId);
}