In my previous blog, Trying to Understand Google Auth, we explored the basics of authentication and how systems like Google Auth handle identity across multiple applications. Building on that understanding, we’re now venturing into designing a custom authentication server specifically for tenant-based applications.
The goal of this series is not to replicate the intricate mechanisms of Google or other large-scale providers but to learn and implement a simple system that achieves Single Sign-On (SSO) for first-party applications. We’ll also explore how to manage user permissions and design for multi-tenant environments.
This blog series will be split into parts, each focusing on a key step in the journey. So, join me as we learn, experiment, and refine this system together!
Defining the System Requirements
Before diving into the design, it’s crucial to define what our authentication server should achieve. By identifying the key requirements, we’ll set a clear roadmap for building a system that supports Single Sign-On (SSO), tenant-based applications, and permission management.
System Requirements
User Registration and Tenant Management
Enable users to register for system access.
Support the creation and management of tenants to organize users and resources.
The root user should be able to assign available products to tenants.
Permissions Assignment
Allow for assigning and managing user permissions.
Enable fine-grained access control for products, modules, or specific resources.
Allowing users to create customs permissions and assign to users.
Login and Logout
Provide a secure login mechanism for user authentication.
Include a logout functionality to end user sessions effectively.
Token Creation and Revocation
Generate secure tokens for managing sessions and enabling API access.
Support revocation of tokens for improved security and control over sessions.
Tenant Management
Organize users and resources under distinct tenants.
Provide administrative control to root users for managing users and assigning permissions within tenants.
With these requirements defined, Let's get started with system design!
First we will be designing the database schema
Requirement 1: User Management
We need a table that stores user information such as name, email, password, and when the user was created or last updated. Additionally, we need to manage user roles and permissions across tenants and products.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(45) NOT NULL,
email VARCHAR(256) UNIQUE NOT NULL,
password VARCHAR(256) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
This table serves as the foundation for user management. The users can be linked to various tenants and products later.
Requirement 2: Tenant Management
We need a mechanism to manage tenants, where a tenant represents a specific organization or unit. Each tenant has its own set of permissions, and users can belong to specific tenants.
CREATE TABLE tenants (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
created_by INT REFERENCES users(id) ON DELETE SET NULL,
updated_by INT REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
With this table, we can track who created each tenant and who updated it. The tenant can also be linked to multiple users (via the user_tenants table), ensuring that users belong to the correct tenant.
Requirement 3: Product Management
We need to track products associated with the tenants. Products can be accessed or managed by users based on permissions.
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(30) NOT NULL,
code VARCHAR(5) UNIQUE NOT NULL,
created_by INT REFERENCES users(id) ON DELETE SET NULL,
updated_by INT REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
This table ensures that products are tracked independently and can be assigned to tenants via the tenants_products table.
Requirement 4: Permissions Management
We need a way to define permissions that can be granted to users. These permissions can be default or custom, and they can be associated with tenants.
CREATE TABLE permissions (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
tenant_id INT REFERENCES tenants(id) ON DELETE CASCADE,
type ENUM('default', 'custom') NOT NULL,
created_by INT REFERENCES users(id) ON DELETE SET NULL,
updated_by INT REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
This table helps manage permission types and ensures that they are specific to a tenant.
Requirement 5: Policy Management
We need to create policies related to products that define how permissions are applied. These policies will control access to various product functionalities.
CREATE TABLE policies (
id SERIAL PRIMARY KEY,
product_id INT REFERENCES products(id) ON DELETE RESTRICT,
slug VARCHAR(30) NOT NULL,
description TEXT,
code VARCHAR(30) UNIQUE NOT NULL,
created_by INT REFERENCES users(id) ON DELETE SET NULL,
updated_by INT REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
This table links policies to specific products and facilitates the creation of access control rules for different product functionalities.
Requirement 6: Linking Permissions and Policies
We need to establish a relationship between permissions and policies so that specific permissions can be applied to products through policies.
CREATE TABLE permissions_policies (
policy_id INT REFERENCES policies(id) ON DELETE CASCADE,
permissions_id INT REFERENCES permissions(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (policy_id, permissions_id)
);
This table links permissions to specific policies, meaning a policy can define what permissions a user has for certain products.
Requirement 7: Assigning Permissions to Users
We need to assign specific permissions to users for different tenants and products. Users should be able to access products and perform actions based on these permissions.
CREATE TABLE users_permissions (
user_id INT REFERENCES users(id) ON DELETE CASCADE,
permissions_id INT REFERENCES permissions(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, permissions_id)
);
This table links users to specific permissions, enabling fine-grained control over what users can do in different tenants and products.
Requirement 8: Mapping Tenants to Products
We need to link tenants with specific products, ensuring that only tenants with access to a particular product can manage or interact with it.
CREATE TABLE tenants_products (
product_id INT REFERENCES products(id) ON DELETE CASCADE,
tenant_id INT REFERENCES tenants(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (product_id, tenant_id)
);
This table links tenants to products, ensuring each tenant has access to a set of products.
Requirement 9: Managing Users in Tenants
We need to manage which users belong to which tenants, enabling role-based access within each tenant.
CREATE TABLE user_tenants (
user_id INT REFERENCES users(id) ON DELETE CASCADE,
tenant_id INT REFERENCES tenants(id) ON DELETE CASCADE,
isActive boolean,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, tenant_id)
);
This table ensures that users are associated with specific tenants, and permissions can be granted accordingly.
Our database schema is ready to meet the requirements, but instead of implementing a role-based model, I decided to directly link permissions to users. Let’s discuss the inspiration behind this decision and why I chose to move away from roles in favor of direct permission assignments.
Inspiration from AWS
In cloud services like AWS IAM (Identity and Access Management), permissions aren’t tied to fixed roles (like Admin, User, or Manager). Instead, they are directly linked to users or groups. This means that each user can have specific permissions that allow them to perform certain actions on resources, such as reading a file or modifying a database entry.
For example, consider two IAM users in AWS:
User A might have a permission that allows them to read data from a specific S3 bucket, but not modify it.
User B might have a permission to write data to the same bucket, but not read from it.
These permissions are directly attached to the users rather than predefined roles, offering more flexibility and granularity. This approach avoids rigid role structures and allows for much more precise control over what users can do.
In a similar way, I chose to link permissions directly to users in our system. This gives us greater flexibility to control user access based on individual needs, rather than having to manage complex roles with broad access levels.
Here’s how the system works: It will have predefined permissions and policies for the products. Policies are fixed and defined by the product owners, meaning they cannot be customized. However, permissions can be created based on the available product policies to suit the specific needs of tenant users. This approach makes the permission-based access control (PBAC) system flexible and scalable for accommodating future roles and requirements.
NOTE: This is not an exact implementation of AWS’s approach; rather, we’re drawing inspiration from it and designing the system to align with our specific requirements.
Here’s a class diagram that represents the given system based on our database schema and requirements.
Additional Requirements:
Email Verification:
Users must verify their email address during the registration process.
A verification email with a unique token should be sent to the user’s registered email.
The system should validate the token before activating the user account.
Password Reset Functionality:
The system must allow users to request a password reset if they forget their password.
A secure reset link containing a unique token should be sent to the user’s registered email.
Users should be able to set a new password after validating the token.
Conclusion
Our system isn’t fully complete just yet. In the next part, we’ll introduce email verification and a reset token link, as well as explore how token generation and verification work within this setup. This will give us deeper insights into the authentication process. Until then, goodbye for now! I’d love to hear your thoughts on this design—feel free to share any suggestions for improvement!