KAHIBARO
Discord Login Register

30.3. Role-Based Authorization

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:

RoleDescriptionExample Permissions
userRegular logged-in userManage own profile, create tasks/orders
adminSystem administratorManage all users, change system settings
moderatorContent or user management without full powerApprove content, suspend users (limited)
guestNot logged in, or very limited accountRead public content only

You do not want to hardcode too many roles too early. Start small, for example:

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:

  1. Single role per user
    • Each user has exactly one role, such as user or admin.
    • Simple to implement and reason about.
  2. 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:

ColumnTypeExample Value
idinteger23
emailtext"alice@example.com"
passwordtext (hash)"$2b$..."
roletext"user" or "admin"

In Python with an ORM, a user model might look like:

python
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:

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:

  1. User sends email, password to a /register endpoint.
  2. Backend hashes the password.
  3. Backend assigns role="user" for all new users.
  4. Backend saves the user in the database.

Example pseudo-code:

python
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:

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:

  1. Load user from database.
  2. Read user’s id and role.
  3. Put them into the JWT payload.

Example FastAPI-style snippet:

python
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 token

On each request:

  1. Extract the token from the Authorization header.
  2. Decode the JWT.
  3. Read the role from the payload.
  4. 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:

Example session data:

json
{
  "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:

  1. Authenticate the user.
  2. Get user’s role.
  3. Check if the role is in the set of allowed roles.
  4. If yes, continue; if not, return a 403 Forbidden response.

HTTP status codes for authorization:

  • 401 Unauthorized is for unauthenticated (no valid credentials).
  • 403 Forbidden is for authenticated but not allowed (role not sufficient).

Example: decorator-style check (language-agnostic idea)

Imagine a simple require_role helper:

python
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 decorator

Then you can use it like this:

python
@require_role(["admin"])
def delete_user(request, user_id):
    # only admins get here
    ...

Example: endpoint rules table

Consider these API endpoints:

EndpointActionAllowed Roles
GET /meGet current user profileuser, admin
PUT /meUpdate own profileuser, admin
GET /usersList all usersadmin
DELETE /users/{id}Delete any useradmin
POST /auth/loginLog inall (no auth required)

Some examples of decisions:

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:

  1. Registration
    • User sends email and password.
    • Backend creates user with role="user".
  2. Admin creation
    • You manually update a record in the database:
sql
     UPDATE users SET role = 'admin' WHERE email = 'admin@example.com';
  1. Login
    • User sends email and password to /auth/login.
    • Backend verifies password.
    • Backend generates a JWT that includes:
json
     {
       "sub": "23",
       "role": "admin",
       "exp": 1699999999
     }
  1. Authorized request
    • Client sends Authorization: Bearer <token> header.
    • Backend decodes JWT and reads sub and role.
    • 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

EndpointDescriptionRequired Role
POST /auth/registerRegister new accountPublic (no auth)
POST /auth/loginLog in and get access tokenPublic (no auth)
POST /auth/logoutInvalidate session or tokenuser or admin
GET /auth/meGet current user infouser or admin
PATCH /auth/meUpdate own infouser or admin
GET /admin/usersList all usersadmin
PATCH /admin/users/{id}/roleChange user’s roleadmin
DELETE /admin/users/{id}Delete any useradmin

Specific examples:

Example check inside an endpoint:

python
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.

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:

json
{
  "user_id": 23,
  "role": "admin"
}

The client can simply modify it to:

json
{
  "user_id": 23,
  "role": "admin"
}

and gain unauthorized admin access.

You must:

3. Not checking both role and ownership

Sometimes you need both:

Example:

python
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:

Example constants:

python
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:

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:

  1. Design your roles
    • Start with at least user and admin.
    • Decide whether users can have one or many roles.
  2. Store roles
    • Add a role field to your user model or a user-roles relation.
  3. Assign roles
    • Registration sets role="user" automatically.
    • Admin creation is controlled and not open to the public.
  4. Include roles in identity
    • JWT: put role into the token payload.
    • Sessions: store role in session data.
  5. Protect endpoints
    • Decide allowed roles for each route.
    • Implement role checks in middleware, decorators, or route dependencies.
    • Use 401 for unauthenticated and 403 for unauthorized.
  6. Test your authorization
    • Test that user cannot access admin endpoints.
    • Test that admin can 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

Comments

Please login to add a comment.

Don't have an account? Register now!