@canxjs/citadel
v1.0.0
Published
CanxJS Citadel - Secure API Token Authentication
Readme
@canxjs/citadel
✨ Features
- 🔐 Personal Access Tokens - Issue API tokens with custom abilities/scopes
- ⚡ Lightweight - Minimal overhead, maximum security
- 🎯 Fine-grained Permissions - Control what each token can do
- 🔄 Token Revocation - Easily revoke compromised tokens
- 📦 Zero Config - Works out of the box with CanxJS
📦 Installation
npm install @canxjs/citadel
# or
bun add @canxjs/citadel🚀 Quick Start
1. Register the Service Provider
// src/providers.ts
import { CitadelServiceProvider } from "@canxjs/citadel";
export const providers = [
// ... other providers
CitadelServiceProvider,
];2. Run the Install Command
This will publish the necessary migration files:
node canx citadel:install
node canx migrate3. Add Mixin to User Model
import { Model } from "canxjs";
import { HasApiTokens } from "@canxjs/citadel";
class User extends HasApiTokens(Model) {
static tableName = "users";
id!: number;
email!: string;
// ... other fields
}📖 Usage
Issuing Tokens
const user = await User.find(1);
// Create a token with all abilities
const { plainTextToken } = await user.createToken("my-app-token");
// Create a token with specific abilities
const { plainTextToken } = await user.createToken("limited-token", [
"read:posts",
"create:posts",
]);
// Create a token with expiration
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
const { plainTextToken } = await user.createToken(
"temp-token",
["*"],
expiresAt,
);
// Return the plain text token to the client (only visible once!)
return response.json({ token: plainTextToken });Checking Token Abilities
// In your controller or middleware
if (user.tokenCan("create:posts")) {
// User's current token has this ability
}
// Check multiple abilities
const canManagePosts =
user.tokenCan("create:posts") && user.tokenCan("delete:posts");Protecting Routes
import { router } from "canxjs";
// Protect with auth middleware
router
.get("/api/user", (req) => {
return req.user;
})
.middleware("auth");
// Check abilities in route
router
.post("/api/posts", (req) => {
if (!req.user.tokenCan("create:posts")) {
return response.status(403).json({ error: "Insufficient permissions" });
}
// Create post...
})
.middleware("auth");🔧 How It Works
Token Storage
Citadel stores tokens in the personal_access_tokens table:
| Column | Type | Description |
| ---------------- | -------- | ------------------------------- |
| id | integer | Primary key |
| tokenable_type | string | Model class name (e.g., "User") |
| tokenable_id | integer | The user's ID |
| name | string | Token name for identification |
| token | string | SHA-256 hash of the token |
| abilities | json | Array of allowed abilities |
| last_used_at | datetime | Last usage timestamp |
| expires_at | datetime | Optional expiration |
| created_at | datetime | Creation timestamp |
Token Format
Tokens are returned in the format: {id}|{random_string}
- The
ididentifies which token record to look up - The
random_stringis hashed and compared against the stored hash - This prevents timing attacks and ensures tokens can't be guessed
Authentication Flow
- Client sends token in
Authorization: Bearer {token}header - Middleware extracts token and splits by
| - Looks up
PersonalAccessTokenby ID - Hashes the random part and compares with stored hash
- If valid, attaches user and token to request
📚 API Reference
HasApiTokens Mixin
| Method | Description |
| ------------------------------------------- | --------------------------------------- |
| createToken(name, abilities?, expiresAt?) | Creates a new personal access token |
| tokens() | Returns the user's tokens relationship |
| tokenCan(ability) | Checks if current token has the ability |
PersonalAccessToken Model
| Method | Description |
| --------------- | ------------------------------------- |
| can(ability) | Check if token has specific ability |
| cant(ability) | Check if token lacks specific ability |
📄 License
MIT © CanxJS Team
