Skip to main content

Overview

Access control ensures users can only access resources they’re authorized to view or modify. This guide demonstrates authorization best practices based on the secure implementation.

Authentication vs Authorization

Authentication

Who are you?Verifying user identity through credentials (login).

Authorization

What can you do?Determining what resources and actions a user can access.
Authentication confirms identity; authorization controls access. Both are required for secure applications.

Route Protection

Basic Authentication Check

Always verify user authentication before allowing access to protected resources:
secure/app.py
@app.route('/dashboard')
def dashboard():
    # Check if user is authenticated
    if 'user_id' not in session:
        flash('Debes iniciar sesión', 'warning')
        return redirect('/login')
    
    return render_template('dashboard.html', 
                         username=session.get('username'),
                         role=session.get('role'))
1

Check session

Verify the user has an active session:
if 'user_id' not in session:
    return redirect('/login')
2

Redirect if unauthorized

Send unauthenticated users to login:
flash('Debes iniciar sesión', 'warning')
return redirect('/login')
3

Process authorized request

Allow authenticated users to proceed:
return render_template('dashboard.html')

Preventing IDOR Attacks

Insecure Direct Object Reference (IDOR) occurs when attackers access resources by manipulating IDs. Always validate ownership.

IDOR Prevention Example

secure/app.py
@app.route('/profile')
def profile():
    # Check authentication
    if 'user_id' not in session:
        flash('Debes iniciar sesión', 'warning')
        return redirect('/login')
    
    # Get requested user ID
    requested_id = request.args.get('id', session['user_id'])
    
    # Validate type
    try:
        requested_id = int(requested_id)
    except ValueError:
        flash('ID inválido', 'danger')
        return redirect('/dashboard')
    
    # Authorization check: Only allow viewing own profile
    # Unless user is an admin
    if requested_id != session['user_id'] and session.get('role') != 'admin':
        flash('No tienes permiso para ver este perfil', 'danger')
        return redirect('/dashboard')
    
    # User is authorized, fetch profile
    connection = create_connection()
    cursor = connection.cursor()
    
    query = "SELECT id, username, email, role, created_at FROM users WHERE id = %s"
    cursor.execute(query, (requested_id,))
    user = cursor.fetchone()
    
    return render_template('profile.html', user=user)
This implementation prevents users from viewing other users’ profiles by modifying the ?id= parameter, unless they have admin role.

IDOR Protection Steps

1

Validate input type

Ensure the ID is valid:
try:
    requested_id = int(request.args.get('id'))
except ValueError:
    return error_response('Invalid ID')
2

Check ownership or permissions

Verify user owns the resource or has admin rights:
if requested_id != session['user_id'] and session.get('role') != 'admin':
    return error_response('Unauthorized')
3

Fetch with user context

Query with ownership validation:
query = "SELECT * FROM resources WHERE id = %s AND user_id = %s"
cursor.execute(query, (resource_id, session['user_id']))

Role-Based Access Control (RBAC)

Defining Roles

Store user roles in the database and session:
secure/database.py
cursor.execute("""
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT NOT NULL UNIQUE,
        password TEXT NOT NULL,
        email TEXT,
        role TEXT DEFAULT 'user',
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
""")
secure/app.py
# Store role in session during login
if user and check_password_hash(user['password'], password):
    session['user_id'] = user['id']
    session['username'] = user['username']
    session['role'] = user['role']  # Store role

Role-Based Authorization

def admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user_id' not in session:
            flash('Please login', 'warning')
            return redirect('/login')
        
        if session.get('role') != 'admin':
            flash('Admin access required', 'danger')
            return redirect('/dashboard')
        
        return f(*args, **kwargs)
    return decorated_function

@app.route('/admin/users')
@admin_required
def admin_users():
    # Only admins can access this route
    return render_template('admin_users.html')

User Role

  • View own profile
  • Edit own data
  • Access user dashboard

Admin Role

  • View all profiles
  • Edit any user data
  • Access admin panel
  • Manage system settings

Session-Based Authorization

Storing Authorization Data

secure/app.py
# During login, store minimal necessary data
session['user_id'] = user['id']
session['username'] = user['username']
session['role'] = user['role']
session.permanent = True
Only store essential authorization data in sessions. Never store passwords or sensitive information.

Checking Authorization

secure/app.py
@app.route('/profile')
def profile():
    # Authentication check
    if 'user_id' not in session:
        return redirect('/login')
    
    requested_id = request.args.get('id', session['user_id'])
    
    # Authorization check based on role
    if requested_id != session['user_id'] and session.get('role') != 'admin':
        flash('No tienes permiso para ver este perfil', 'danger')
        return redirect('/dashboard')

Sensitive Data Protection

Never Return Sensitive Fields

Exclude sensitive data from queries and responses:
secure/app.py
# SECURE: Exclude password from SELECT
query = "SELECT id, username, email, role, created_at FROM users WHERE id = %s"
cursor.execute(query, (requested_id,))
user = cursor.fetchone()

Include

  • User ID
  • Username
  • Email
  • Role
  • Creation date

Exclude

  • Password (hashed or not)
  • Password reset tokens
  • API keys
  • Secret keys
  • Internal identifiers

Authorization Best Practices

1

Verify authentication first

Always check if user is logged in before checking permissions:
if 'user_id' not in session:
    return redirect('/login')
2

Validate resource ownership

Check if user owns the resource they’re trying to access:
if resource.user_id != session['user_id']:
    return abort(403)
3

Check role permissions

Verify user role has required permissions:
if session.get('role') not in ['admin', 'moderator']:
    return abort(403)
4

Use deny-by-default

Explicitly grant access rather than blocking access:
# Good: Explicitly allow
if user_has_permission:
    return resource
return abort(403)

Common Authorization Patterns

Pattern 1: Own Resource Only

@app.route('/edit-post/<int:post_id>')
def edit_post(post_id):
    if 'user_id' not in session:
        return redirect('/login')
    
    # Fetch post with ownership check
    query = "SELECT * FROM posts WHERE id = %s AND user_id = %s"
    cursor.execute(query, (post_id, session['user_id']))
    post = cursor.fetchone()
    
    if not post:
        flash('Post not found or unauthorized', 'danger')
        return redirect('/dashboard')
    
    return render_template('edit_post.html', post=post)

Pattern 2: Own Resource or Admin

@app.route('/delete-post/<int:post_id>')
def delete_post(post_id):
    if 'user_id' not in session:
        return redirect('/login')
    
    cursor.execute("SELECT * FROM posts WHERE id = %s", (post_id,))
    post = cursor.fetchone()
    
    # Allow if owner or admin
    if post['user_id'] != session['user_id'] and session.get('role') != 'admin':
        return abort(403)
    
    cursor.execute("DELETE FROM posts WHERE id = %s", (post_id,))
    return redirect('/posts')

Pattern 3: Role-Based

@app.route('/admin/settings')
def admin_settings():
    if 'user_id' not in session:
        return redirect('/login')
    
    if session.get('role') != 'admin':
        flash('Admin access required', 'danger')
        return redirect('/dashboard')
    
    return render_template('admin_settings.html')

Database-Level Security

Enforce authorization at the database level when possible:
-- Always include user_id in WHERE clause for user resources
SELECT * FROM posts WHERE id = ? AND user_id = ?

-- Use database constraints
CREATE TABLE posts (
    id INTEGER PRIMARY KEY,
    user_id INTEGER NOT NULL,
    content TEXT,
    FOREIGN KEY (user_id) REFERENCES users(id)
);
Adding user_id to WHERE clauses ensures users can only access their own data, even if application-level checks fail.

Testing Authorization

Test authorization logic thoroughly:
def test_idor_protection():
    # Login as user 1
    client.post('/login', data={'username': 'user1', 'password': 'pass'})
    
    # Try to access user 2's profile
    response = client.get('/profile?id=2')
    
    # Should be denied
    assert response.status_code == 302  # Redirect
    assert b'No tienes permiso' in response.data

def test_admin_access():
    # Login as regular user
    client.post('/login', data={'username': 'user', 'password': 'pass'})
    
    # Try to access admin page
    response = client.get('/admin/users')
    
    # Should be denied
    assert response.status_code == 403

Authorization Checklist

Authentication

  • Verify user is logged in
  • Check session is valid
  • Ensure session not expired

Ownership

  • Validate resource belongs to user
  • Check user_id matches
  • Include ownership in queries

Role Permissions

  • Verify user has required role
  • Check specific permissions
  • Use role-based decorators

Data Protection

  • Exclude sensitive fields
  • Don’t return passwords
  • Limit data exposure

Next Steps

Secure Authentication

Learn about secure login and session management

Security Headers

Configure security headers for defense in depth

Build docs developers (and LLMs) love