In AI programming, there is one category of code that easily “grows and grows messier”:

if user.role != "admin":
    raise HTTPException(status_code=403)

Switch to another endpoint, and the AI writes:

if "user:delete" not in user.permissions:
    raise ForbiddenError()

Yet another endpoint:

if current_user.id != owner_id and not current_user.is_admin:
    raise HTTPException(status_code=403)

Each snippet looks defensible on its own. But once dozens of variations of these ifs appear across the project, the permission system has effectively gone out of control.

The mature solution is not “make the AI write its if-statements more carefully”, but to establish a unified:

Authentication + permission model + permission enforcement mechanism

The most common permission model here is RBAC (Role-Based Access Control).


📌 Technology Card

Authentication

Answers the question:

“Who are you?”

For example, verifying the login account, or whether a JWT token is valid.


Authorization

Answers the question:

“What are you allowed to do?”

For example, whether a user can delete an Agent, modify a knowledge base, or manage users.


RBAC / Role-Based Access Control

Instead of attaching a pile of permissions to each user directly, it establishes a unified grant relationship through:

User → Role → Permission

For example:

Zhang San
System Administrator
user:list
user:create
user:update
user:delete

Rather than checking separately in dozens of endpoints:

if username == "zhangsan":
    ...

or:

if role == "admin":
    ...

💡 An Intuitive Way to Understand It: Access Control Is Like a Company Badge System

Imagine a company where employees swipe their badge cards when entering the building.

The system first needs to determine:

Is this card genuine?
Has it expired?
Who does it belong to?
Is this employee still on staff?

That is:

Authentication

Once identity is confirmed, further checks continue as the employee enters different areas:

Can a regular employee enter the office area?
Yes.

Can a regular employee enter the server room?
No.

Can ops staff enter the server room?
Yes.

Can finance staff open the finance system?
Yes.

That is:

Authorization

If designed sensibly, the company doesn’t tape a note next to every door saying:

If Zhang San comes, open the door
If Li Si comes, open it too
If Wang Wu comes, don't open
If the boss comes, open everything

Because once there are many employees, that rule set immediately spirals out of control.

The more sensible approach is:

Employee
Role
Permission
Resource

For example:

Zhang San → Ops staff → server:manage
Li Si → Finance staff → finance:view
Wang Wu → Regular employee → office:access

That is RBAC.


1. Why Does AI Botch Permission Systems So Easily?

Because AI is very good at solving the problem right in front of it.

You tell it:

Add a permission check to the delete-user endpoint.

It most easily generates:

if not current_user.is_admin:
    raise HTTPException(status_code=403)

Feature done.

But next time you ask it:

Add a permission check to the delete-Agent endpoint.

It may generate:

if "agent:delete" not in user.permissions:
    raise HTTPException(status_code=403)

Then ask it to protect the knowledge base:

if user.role not in ["admin", "manager"]:
    raise HTTPException(status_code=403)

Eventually the project may simultaneously contain:

is_admin
role == "admin"
role in [...]
permissions
permission_codes
user_type
is_super

six or seven different permission-checking approaches.

This is the classic case of:

Functionally correct, architecturally out of control.


2. Architectural Rules

1. First, Separate Authentication from Authorization

A unified auth system must begin with a very important boundary:

Authentication
Auth: who are you?
Authorization
Auth: what can you do?

Don’t blend the two into one giant function. A clean architecture looks like:

Client Request
Bearer Token
JWT Verification
Resolve User
Check User Status
Authenticated User ID
Permission Resolution
Permission Check
Router
Service

Where:

Bearer Token

is usually carried in the HTTP request header:

Authorization: Bearer xxxxx

And:

JWT (JSON Web Token)

is responsible for proving:

Who issued this token?
Has it been tampered with?
Has it expired?
Which user does it correspond to?

RBAC, meanwhile, is responsible for:

Which roles does this user hold?
Which permissions do those roles hold?
Which permission does the current endpoint require?

The two have completely different responsibilities.


2. JWT Issuance Must Have Exactly One Standard Implementation

Another common problem in AI projects is that token issuance logic gets implemented repeatedly.

For example:

payload = {
    "username": username,
    "exp": datetime.utcnow() + timedelta(hours=1)
}

token = jwt.encode(payload, SECRET)

Somewhere else:

payload = {
    "sub": user.id,
    "expire": ...
}

And a third place:

jwt.encode(
    {"user": username},
    key,
    algorithm="HS256"
)

Before long you end up with:

Different fields
Different expiration times
Different algorithms
Different ways of reading the signing key
Different verification logic

So you should explicitly require the AI to follow:

Token creation, parsing, and verification must go through the unified JWT component only.

In miniagent, this responsibility is centralized in:

app/core/security/jwt_auth.py

JWTAuth.create_token() uniformly generates the payload:

payload = {
    "sub": username,
    "exp": expire,
    "iat": datetime.now(timezone.utc),
    "type": token_type
}

Where:

  • sub: Subject, the token principal;
  • exp: Expiration Time;
  • iat: Issued At;
  • type: the token type.

It then uniformly calls:

jwt.encode(
    payload,
    self.secret_key,
    algorithm=self.algorithm
)

to complete issuance.

This means miniagent never needs the Login Router, Admin Router, or User Router each to figure out JWT generation on their own.


3. Token Verification Must Also Be Centralized

Unifying issuance is not enough — verification must not be scattered either.

The wrong way is:

try:
    payload = jwt.decode(...)
except:
    ...

written separately in dozens of endpoints.

Because token verification involves at least:

Is the signature valid?
Has it expired?
Which algorithm is used?
What is the subject field?
How are exceptions handled?

In miniagent, JWTAuth.verify_token() uniformly calls:

payload = jwt.decode(
    token,
    self.secret_key,
    algorithms=[self.algorithm],
    options={
        "verify_exp": True,
        "verify_signature": True
    }
)

After verification passes, it uniformly reads:

username = payload.get("sub")

If the token is expired or invalid, a failure result is returned uniformly.

So the whole project only needs to acknowledge one fact:

JWTAuth
  ├─ create_token()
  └─ verify_token()

instead of:

Router A → decodes on its own
Router B → decodes on its own
Service C → decodes on its own
Middleware D → writes decode yet again

4. Successful Authentication Does Not Mean Having Permission

This is a point beginners easily confuse. A successful JWT verification only proves:

This request corresponds to a legitimate identity.

It does not prove:

This user can do anything.

For example, a regular user logs in successfully:

JWT ✅

But tries to delete another user:

user:delete ❌

So after authentication, the authorization phase still follows.

In miniagent, this part is handled centrally by:

AuthPermission

Its resolve_user_id() flow is:

Token
JWT verify
username
Look up the user
Check is_active
user_id

If the token is invalid, the user doesn’t exist, or the account has been disabled, the request is uniformly rejected.

This design matters, because authentication must not trust the token alone.

For example:

User logged in yesterday
Obtained a valid token
Admin disabled the account today
The old token has not yet expired

If the system only verified the JWT signature, that user could theoretically keep accessing the system.

miniagent additionally reads the user’s status from the database, so disabling an account takes real effect.


5. The Core of RBAC: Never Write Permission Checks Directly Against Users

The simplest RBAC relationship is:

User
Role
Permission

For example:

User: Alice
Role: admin
Permissions:
   user:list
   user:create
   user:update
   user:delete

Another user:

User: Bob
Role: viewer
Permissions:
   user:list

So the Router no longer cares about:

Is Alice an administrator?
What role does Bob have?
How many roles does Carol belong to?

The Router only cares about:

Which Permission does this endpoint require?

For example:

system:user:delete

And then lets the unified permission system answer:

Does the current user have this permission?

6. How Does miniagent Obtain User Permissions?

In miniagent’s AsyncMenuDatabase, user permissions are not hard-coded in Routers, but obtained through relationship queries:

User
Role
Menu / Permission

Corresponding to this relational query in the code:

select(Menu.name)
    .select_from(User)
    .join(User.roles)
    .join(Role.menus)
    .where(
        User.id == user_id,
        Menu.is_active.is_(True)
    )

Then converted into:

set(result.scalars().all())

that is, a permission set. This already forms the classic RBAC pattern:

User
Role
Resource Code

Business endpoints don’t need to know how the role table, the user-role mapping table, or the role-menu mapping table are actually queried.


7. Even Super Admins Must Not Lead to if is_admin Everywhere

Super administrators are another spot where permission systems easily get messy. The most dangerous approach is:

if user.username == "admin":
    return True

or:

if user.role == "superadmin":
    ...

scattered across dozens of business files.

miniagent instead uses the unified:

SUPER_PERMISSION

If the user belongs to the super role:

return {SUPER_PERMISSION}

Regular users get their actual permission set returned. In the end, all permission checks converge into:

if SUPER_PERMISSION in perms or required in perms:
    return

Otherwise access is denied.

This way the super-admin rule has exactly one authoritative implementation, instead of:

if is_super:

popping up all over the project.


8. Routers Should “Declare Permissions”, Not “Implement Permissions”

This is the single most important sentence in the whole design.

The wrong Router:

@router.delete("/users/{user_id}")
async def delete_user(
    user_id: int,
    request: Request
):
    token = request.headers.get("Authorization")
    username = jwt_auth.verify_token(token)
    user = await user_db.get_user(username)

    permissions = await menu_db.get_user_resource_codes(
        user.id
    )

    if (
        "system:user:delete" not in permissions
        and "*" not in permissions
    ):
        raise HTTPException(status_code=403)

    return await user_service.delete(user_id)

Here the Router is simultaneously doing:

Parse the header
Verify the JWT
Look up the user
Look up permissions
Check super admin
Enforce the permission check
Execute the business

The Router is no longer a Router.

The more reasonable version:

@router.delete("/users/{user_id}")
async def delete_user(
    user_id: int,
    current_user_id: int = Depends(
        Permission("system:user:delete")
    )
):
    return await user_service.delete(user_id)

Now the business layer expresses only:

Deleting a user requires system:user:delete.

As for:

How is the token extracted?
How is the JWT verified?
How is the user looked up?
How are permissions queried?
How is caching handled?
How is a super admin determined?
How is the 403 raised?

All of that is the unified auth system’s responsibility.


9. The Permission Design in miniagent

AuthPermission.Permission in miniagent does exactly this.

The core flow is very clear:

async def __call__(
    self,
    request: Request,
    credentials: HTTPAuthorizationCredentials = Depends(
        _bearer_scheme
    ),
) -> int:

    auth = request.app.state.container.auth

    user_id = await auth.resolve_user_id(
        credentials.credentials
    )

    await auth.check(
        user_id,
        self._code
    )

    return user_id

That is:

Bearer Token
resolve_user_id()
JWT verification
User identity
check()
Permission set
required permission
Allow / Deny

This logic is executed by the unified dependency, not implemented by each business Router itself.


10. Permission Queries Also Need Caching

After unifying authentication, a practical problem appears.

If every API request executes:

JWT verification
Query User
Query Role
Query Permission

this adds database load under high concurrency, so the permission set is a great fit for caching.

In miniagent:

CACHE_TTL_SECONDS = 3600.0

that is, one hour of caching by default.

The lookup logic is:

flowchart TB A[Request permissions] --> B{Cache exists?} B -->|Yes| C[Return directly] B -->|No| D[Query database] D --> E[Write to cache] E --> C

In the code:

cached = self._cache.mget_ttl(
    [self._cache_key(user_id)]
)[0]

if cached is not None:
    return self._decode(cached)

return await self._load_permissions(user_id)

When permissions change, the cache is proactively invalidated via:

invalidate(user_id)

So unified authentication not only makes the code cleaner, it also leaves a unified entry point for performance optimization.


11. Authentication, Authorization, and Business Should Form Clear Boundaries

The overall architecture can ultimately be understood as:

miniagent unified authentication and authorization architecture

miniagent’s JWTAuth handles unified token issuance and verification.

miniagent’s AuthPermission handles resolving the user from the token, checking user status, loading and caching permissions, and performing the final permission decision.

miniagent’s permission data is read through the User → Role → Menu relationships; the super role is uniformly mapped to SUPER_PERMISSION, rather than repeatedly checking admin status in business code.

In short:

flowchart TB A[Request] --> B[Bearer Token] B --> C["JWTAuth
Token issuance
Signature verification
Expiration check"] C --> D["AuthPermission
User resolution
User status
Permission loading
Permission caching
Permission check"] D --> E[Permission] E --> F[Router] F --> G[Service] G --> H[Repository]

The biggest value here is not saving a few lines of code.

It is the crispness of the boundaries:

JWTAuth
owns the "token"

AuthPermission
owns "authentication + authorization"

Permission
owns "declaring endpoint permissions"

Router
owns "request orchestration"

Service
owns "business logic"

As long as the AI respects these boundaries, it won’t easily scatter permission logic around.


3. What Are the Benefits of Unified Authentication?

1. Security Rules Have Exactly One Implementation

Suppose the JWT algorithm changes later.

If the project has 30 copies of:

jwt.decode(...)

you have to review 30 places.

If everything is centralized in:

JWTAuth.verify_token()

you change exactly one place.


2. Permission Logic Is Genuinely Consistent

All endpoints uniformly pass through:

resolve_user_id()
get_permissions()
check()

So:

How disabled users are handled
How super admins are handled
How missing permissions are handled
How 403 is returned
How caching is handled

is consistent across the board.


3. Routers Become Easy to Understand

Seeing:

Permission("system:user:delete")

a developer immediately knows:

This endpoint requires the delete-user permission.

No need to read 20 lines of ifs to figure out who can actually access it.


4. Permissions Can Be Centrally Audited

Once all authorization flows through the unified:

AuthPermission.check()

adding any of the following later:

Permission-denied logging
Security auditing
Access statistics
Anomalous behavior analysis

only requires touching one entry point.


5. It Suits AI Programming Better

What AI fears most is:

The project has five ways of doing it,
but nobody tells it which one to use.

Unified authentication effectively tells the AI:

Do not design your own permission system.

Need a token:
go to JWTAuth.

Need the current user:
go to CurrentUser.

Need permissions:
go to Permission.

Need a permission check:
go to AuthPermission.

The AI’s freedom is constrained to exactly the right places.


4. Write the Architecture Rules Down Clearly for the AI

You can add the following directly to your project-level AI Rules:

## Authentication and Authorization

Authentication and permission control must be implemented in a unified way.

### JWT

- It is forbidden to create or verify JWTs inside Routers, Services, or Repositories;
- All JWT issuance and verification must reuse the project's existing JWTAuth;
- It is forbidden to design another payload, algorithm, or expiration policy.

### Permissions

- The permission model uses RBAC;
- Hard-coded role checks in business code are forbidden;
- Scattered permission if-checks are forbidden;
- Routers declare permissions via the existing Permission / AuthPermission;
- Services must not re-implement endpoint-level permission checks;
- The super-admin bypass rule must be implemented centrally.

### Responsibilities

JWTAuth:
owns the Token.

AuthPermission:
owns authentication, permission loading, caching, and permission checks.

Router:
only declares which permission is required.

Service:
only handles business logic.

Putting It into Prompts: Don’t Ask the AI to “Add Some Auth”

Bad prompt:

Add admin permissions to this endpoint.

The problem with this sentence is:

the AI has no idea how “admin permissions” should actually be implemented in your project.

So it will very likely write:

if user.role != "admin":

A more sensible prompt:

Please add permission control to this endpoint.

It must follow the current project's unified auth architecture:

1. Do not manually parse the Authorization Header in the Router;
2. Do not call jwt.decode directly;
3. Do not check role == "admin" in the Router or Service;
4. Do not add scattered permission if-checks;
5. Reuse the existing AuthPermission / Permission;
6. The Router only declares the required permission;
7. Continue using the existing JWTAuth for JWT;
8. Continue using the existing SUPER_PERMISSION for super admins;
9. Do not change the existing RBAC data model;
10. Do not re-implement the existing permission caching logic.

With constraints like these, the AI no longer needs to “design a permission system”.

It only needs to:

Plug into the existing permission system.


When Developing New Features, Ask the AI One Question First

From now on, when asking the AI to add an endpoint, you can first require:

Before modifying any code, please check:

1. Whether the project already has a unified JWT implementation;
2. Whether the project already has an authentication dependency;
3. Whether the project already has a permission model;
4. Whether the project already has a way to declare permissions;
5. Which existing mechanism the current endpoint should reuse.

If an existing mechanism exists, re-implementation is forbidden.

This sentence is crucial:

If an existing mechanism exists, re-implementation is forbidden.

Because one of the biggest hidden risks of AI programming is:

The old code already solved it once,
and the AI very diligently solves it a second time.

Summary

The core of unified authentication and RBAC is not:

writing more security checks.

Quite the opposite — it demands:

Don’t let security checks scatter across business code.

A healthy permission architecture should be crystal clear:

JWT
handles identity credentials

Authentication
answers "who are you"

RBAC
answers "which permissions you hold"

Authorization
answers "may you do this"

Router
declares which permission is required

Service
executes the actual business

And in the age of AI programming, one more hard rule should be added:

The AI may call the permission system, but it is not allowed to reinvent the permission system.

Don’t let the project end up as:

if user.role == "admin":
if user.is_super:
if permission in permissions:
if username == "root":

scattered everywhere. What we really want to see is:

Permission("system:user:delete")

with all the complex authentication and authorization machinery working behind a unified architecture.

That is RBAC’s greatest value for AI programming:

Turning “who can do what” into a unified rule set, instead of letting the AI guess it anew in every business file.


Open Source Code


🪐 Good luck 🪐