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
Copy
class UnauthorizedException extends SoException {
constructor(
public message = "Unauthorized",
public payload?: unknown,
);
}
Copy
@api()
async getMyProfile(ctx: Context) {
if (!ctx.user) {
throw new UnauthorizedException("Login required");
}
return this.findById(ctx.user.id);
}
Practical Examples
Basic Authentication Check
Copy
@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 });
}
Using Guards for Authentication (Recommended)
For simple login checks, using Guards is cleaner:Copy
// 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
Copy
@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
Copy
@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
Copy
@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
Copy
@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
Copy
@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
Copy
@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
Copy
@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
Using Guards (Recommended)
Copy
// 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)
Copy
@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
Copy
throw new UnauthorizedException("Login required", {
loginUrl: "/auth/login",
returnUrl: ctx.request.url
});
Permission Upgrade Prompt
Copy
throw new UnauthorizedException(
"Premium feature",
{
requiredPlan: "premium",
currentPlan: "basic",
upgradeUrl: "/subscription/upgrade",
features: ["Advanced analytics", "Unlimited projects", "Priority support"]
}
);
Expiration Information
Copy
throw new UnauthorizedException(
"Session has expired",
{
expiredAt: session.expiresAt,
refreshUrl: "/auth/refresh"
}
);
Client Response Examples
Basic Response
Copy
{
"statusCode": 401,
"message": "Login required"
}
Response with payload
Copy
{
"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
UnauthorizedException handles both, but you can create a custom ForbiddenException if needed.
Copy
// 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);
}