30.3. Role-Based Authorization
Table of Contents
Understanding Role-Based Authorization
Role-based authorization controls what a user is allowed to do in your system based on their assigned roles, such as user, admin, or moderator. In this chapter, you will see how to design and implement role-based authorization for a backend authentication system.
We focus on how roles interact with authentication, sessions, and tokens, not on basic authorization theory, which is covered in the general Authorization section.
Key idea:
Authentication answers: “Who are you?”
Role-based authorization answers: “Given who you are, what are you allowed to do?”
Defining Roles in an Authentication System
Before you can authorize anything, you must define which roles exist and what they mean.
Typical roles in a simple authentication system might be:
| Role | Description | Example Permissions |
|---|---|---|
user | Regular logged-in user | Manage own profile, create tasks/orders |
admin | System administrator | Manage all users, change system settings |
moderator | Content or user management without full power | Approve content, suspend users (limited) |
guest | Not logged in, or very limited account | Read public content only |
You do not want to hardcode too many roles too early. Start small, for example:
useradmin
You can extend the list later if needed.
Storing Roles
You must store a user’s role(s) somewhere persistent so that the backend can check them on each request.
Single-role vs multi-role
Two design choices:
- Single role per user
- Each user has exactly one role, such as
useroradmin. - Simple to implement and reason about.
- Multiple roles per user
- A user can have roles like
["user", "moderator"]. - More flexible, but slightly more complex.
For many beginner projects, start with a single role per user, then extend if needed.
Example user table / model
If you use a relational database and a simple single-role setup:
| Column | Type | Example Value |
|---|---|---|
| id | integer | 23 |
| text | "alice@example.com" | |
| password | text (hash) | "$2b$..." |
| role | text | "user" or "admin" |
In Python with an ORM, a user model might look like:
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String, unique=True, index=True, nullable=False)
password_hash = Column(String, nullable=False)
role = Column(String, nullable=False, default="user")If you use multi-role, you typically have a separate table, for example:
rolestable: (id,name)user_rolestable: (user_id,role_id)
But for this chapter, we will mostly talk in terms of a single role field.
Assigning Roles During Registration
Role-based authorization depends on consistent rules for how roles are assigned.
Default role on registration
In most authentication systems, new users get a default, minimal role such as user.
Example logic during registration:
- User sends
email,passwordto a/registerendpoint. - Backend hashes the password.
- Backend assigns
role="user"for all new users. - Backend saves the user in the database.
Example pseudo-code:
def register_user(email: str, password: str) -> User:
hashed = hash_password(password)
user = User(
email=email,
password_hash=hashed,
role="user" # default role
)
db.add(user)
db.commit()
db.refresh(user)
return user
Rule: Never let a registration endpoint accept the role from the client.
If the client can send role="admin" during registration, anyone can create an admin account.
If you need to create admins, you can:
- Insert them manually in the database, or
- Provide a secure admin-only endpoint to change roles, or
- Use a one-time setup script or "first user is admin" logic for development only.
Including Roles in Tokens or Sessions
After a user logs in, you need to carry the user’s role with their authenticated identity so that each request can be authorized correctly.
For token-based authentication (JWT)
If you use JWT (JSON Web Tokens) as described in this project, you can embed the role in the token claims.
When you authenticate a user and issue an access token:
- Load user from database.
- Read user’s
idandrole. - Put them into the JWT payload.
Example FastAPI-style snippet:
def create_access_token(user: User) -> str:
payload = {
"sub": str(user.id),
"role": user.role,
"exp": datetime.utcnow() + timedelta(minutes=15)
}
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
return tokenOn each request:
- Extract the token from the
Authorizationheader. - Decode the JWT.
- Read the
rolefrom the payload. - Use this role to check if the user may perform the requested action.
Rule: Treat the role claim in a JWT as trusted only if:
- The JWT is signed with a secret or private key you control, and
- You verify the signature on every request.
Never accept client-provided role data without verification.
For session-based authentication
If you use sessions:
- Store the user’s
idandroleinside the server-side session store, for example in Redis, or - Use a signed session cookie that includes role.
Example session data:
{
"user_id": 23,
"role": "admin"
}Then on each request, look up the session and get the role from there.
Enforcing Role-Based Access on Endpoints
Once you can identify the user and their role during a request, you can protect endpoints based on role.
Basic pattern
On each protected route:
- Authenticate the user.
- Get user’s role.
- Check if the role is in the set of allowed roles.
- If yes, continue; if not, return a 403 Forbidden response.
HTTP status codes for authorization:
401 Unauthorizedis for unauthenticated (no valid credentials).403 Forbiddenis for authenticated but not allowed (role not sufficient).
Example: decorator-style check (language-agnostic idea)
Imagine a simple require_role helper:
def require_role(allowed_roles):
def decorator(handler):
def wrapper(request, *args, **kwargs):
user = authenticate_request(request)
if user is None:
return Response(status_code=401)
if user.role not in allowed_roles:
return Response(status_code=403)
return handler(request, *args, **kwargs)
return wrapper
return decoratorThen you can use it like this:
@require_role(["admin"])
def delete_user(request, user_id):
# only admins get here
...Example: endpoint rules table
Consider these API endpoints:
| Endpoint | Action | Allowed Roles |
|---|---|---|
GET /me | Get current user profile | user, admin |
PUT /me | Update own profile | user, admin |
GET /users | List all users | admin |
DELETE /users/{id} | Delete any user | admin |
POST /auth/login | Log in | all (no auth required) |
Some examples of decisions:
- Regular
usercan view and update only their own profile. - Only
admincan list or delete other users.
For a more advanced design, you may combine role-based rules with resource ownership, which is covered in the Authorization section.
Example: Role-Based Authorization Flow with JWT
Let us put the pieces together in a simple flow for a JWT-based auth system:
- Registration
- User sends email and password.
- Backend creates user with
role="user". - Admin creation
- You manually update a record in the database:
UPDATE users SET role = 'admin' WHERE email = 'admin@example.com';- Or use an admin-only endpoint like
POST /admin/users/{id}/promote.
- Login
- User sends email and password to
/auth/login. - Backend verifies password.
- Backend generates a JWT that includes:
{
"sub": "23",
"role": "admin",
"exp": 1699999999
}- Authorized request
- Client sends
Authorization: Bearer <token>header. - Backend decodes JWT and reads
subandrole. - Backend loads user if needed, or trusts the role claim if token is valid.
- Backend checks:
- Is user authenticated? If not,
401. - Is user role in allowed roles for this endpoint? If not,
403. - If allowed, executes business logic.
Fine-Grained Role Rules
Different parts of your authentication system may require different roles.
Example: Role-based rules for auth endpoints
| Endpoint | Description | Required Role |
|---|---|---|
POST /auth/register | Register new account | Public (no auth) |
POST /auth/login | Log in and get access token | Public (no auth) |
POST /auth/logout | Invalidate session or token | user or admin |
GET /auth/me | Get current user info | user or admin |
PATCH /auth/me | Update own info | user or admin |
GET /admin/users | List all users | admin |
PATCH /admin/users/{id}/role | Change user’s role | admin |
DELETE /admin/users/{id} | Delete any user | admin |
Specific examples:
- A regular user should not be able to change their role to admin.
- Only an admin can promote or demote other users.
Example check inside an endpoint:
def change_user_role(current_user: User, target_user_id: int, new_role: str):
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Admins only")
# now admin can change roles
...Avoiding Common Pitfalls
Role-based authorization is simple in theory, but there are some frequent mistakes.
1. Relying only on the frontend
You might hide admin buttons in the frontend, but this is not enough.
- Attackers can still send requests directly with tools like Postman or curl.
- Never trust the UI to enforce permissions.
Rule: Every permission check must be enforced in the backend, not only in the frontend.
2. Storing role in an unsigned client-side token
If you store the role in a JSON object on the client without signing it:
{
"user_id": 23,
"role": "admin"
}The client can simply modify it to:
{
"user_id": 23,
"role": "admin"
}and gain unauthorized admin access.
You must:
- Use signed JWTs or
- Use server-side sessions where role is stored on the server.
3. Not checking both role and ownership
Sometimes you need both:
- Role check, for example users must be at least
userto access a resource. - Ownership check, for example users can only change their own data.
Example:
def update_user(current_user: User, user_id: int, data):
if current_user.role != "admin" and current_user.id != user_id:
# not admin and not the owner
raise HTTPException(status_code=403, detail="Not allowed")
# user is admin or owner
...4. Hardcoding role names everywhere
If you write strings like "admin" or "user" all over your code, it becomes hard to change later.
Instead:
- Define constants, or
- Use an enum.
Example constants:
ROLE_USER = "user"
ROLE_ADMIN = "admin"
ALLOWED_ADMIN_ROLES = {ROLE_ADMIN}You can then use them consistently.
Evolving Beyond Simple Roles
In this chapter we focus on role-based authorization: simple sets of roles with fixed meanings.
As your project grows, you may need more flexible strategies that are covered in the Authorization section, for example:
- Role-Based Access Control (RBAC) with multiple roles per user.
- Permission-based access control, where you have fine-grained permissions such as
can_delete_user,can_update_order. - Attribute-based access control, which uses attributes of the user and resource.
- Combining roles with scope-based tokens (for example,
scope: "read:users").
For this authentication project, a clean and simple role-based system with at least user and admin is usually enough.
Putting It All Together in Your Auth System
When you integrate role-based authorization in this project, keep these steps in mind:
- Design your roles
- Start with at least
userandadmin. - Decide whether users can have one or many roles.
- Store roles
- Add a
rolefield to your user model or a user-roles relation. - Assign roles
- Registration sets
role="user"automatically. - Admin creation is controlled and not open to the public.
- Include roles in identity
- JWT: put
roleinto the token payload. - Sessions: store
rolein session data. - Protect endpoints
- Decide allowed roles for each route.
- Implement role checks in middleware, decorators, or route dependencies.
- Use
401for unauthenticated and403for unauthorized. - Test your authorization
- Test that
usercannot access admin endpoints. - Test that
admincan access both user and admin endpoints. - Test that changing frontend code does not bypass backend checks.
Once you have these pieces, your authentication system will not only know who the user is, but also reliably control what they are allowed to do.
Views: 5
KAHIBARO