Building Harbour's API v2: Authentication and Authorization (Part 2)
Part 1 of this series explored the foundational decisions that shaped Harbour's API v2. Now, let's dive into one of the most critical aspects of API design: authentication and authorization. This piece wasn't just about securing our API—it was about creating a flexible system that could grow with our product while maintaining security and usability.
Series Overview:
The Challenge: Beyond Simple Authentication
When we started redesigning our authentication system, we faced several challenges:
Design Considerations
Rather than implementing a one-size-fits-all solution, we adopted a multi-layered approach that could adapt to different use cases while maintaining consistent security standards.
Authentication Layers
We implemented three distinct authentication methods:
API Keys: For all external integrations
OAuth 2.0: For service-to-service communication
Session-based Authentication: For our web application
Authorization with ReBAC
Our move to Relationship-Based Access Control (ReBAC) was the most significant change. This decision was driven by the limitations we encountered with traditional Role-Based Access Control (RBAC).
Traditional RBAC approach:
{
"user": "user123",
"role": "admin",
"organization": "org456"
}
Our ReBAC model:
{
"user": "user123",
"relationships": [
{
"object": "org456",
"relation": "admin"
},
{
"object": "project789",
"relation": "viewer"
}
]
}
ReBAC allows us to:
Implementation Details
API Key Authentication
For API keys, we implemented a hash-based system with prefix support for easy identification:
def verify_api_key(key):
prefix, env, token = parse_key(key)
if not is_valid_prefix(prefix) or not is_valid_env(env):
raise InvalidKeyError()
hashed_token = hash_token(token)
key_data = db.find_key(hashed_token)
if not key_data or not key_data.is_active:
raise InvalidKeyError()
return key_data
领英推荐
OAuth Implementation
Our OAuth 2.0 implementation focuses on security and developer experience:
@app.route('/oauth/token', methods=['POST'])
def token():
client = authenticate_client(request)
grant_type = request.form.get('grant_type')
if grant_type == 'authorization_code':
return handle_authorization_code(request, client)
elif grant_type == 'refresh_token':
return handle_refresh_token(request, client)
raise UnsupportedGrantType()
ReBAC Authorization
We used Google's Zanzibar paper as inspiration for our ReBAC implementation:
class RelationshipService:
def check_permission(self, subject, action, object):
# Direct relationships
direct = self.get_direct_relationships(subject, object)
if self.satisfies_permission(direct, action):
return True # Inherited relationships (e.g., team membership)
inherited = self.get_inherited_relationships(subject, object)
return self.satisfies_permission(inherited, action)
Lessons Learned
Our journey in building and implementing Harbour's authentication system has taught us valuable lessons that can benefit others in similar endeavors.
Embrace Simplicity
One of our most impactful realizations was that simpler solutions often lead to better outcomes:
Design for Growth
While keeping things simple, we also learned to build with the future in mind:
Documentation Drives Adoption
Clear documentation proved to be as important as the technical implementation:
Think About Developer Experience
The success of an authentication system largely depends on how developers interact with it:
Looking Ahead
While our current authentication system serves our needs well, we're planning several enhancements to improve its capabilities:
Performance Optimizations
Audit and Compliance
Security Enhancements
Stay tuned for Part 3 of this series, where we'll explore our approach to resource design and modeling, including how we handle complex business relationships and query parameters.