Files
dss/tools/auth/atlassian_auth.py
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
2025-12-09 18:45:48 -03:00

247 lines
7.5 KiB
Python

"""
Atlassian-based Authentication
Validates users by verifying their Atlassian (Jira/Confluence) credentials.
On successful login, creates a JWT token for subsequent requests.
"""
import os
import jwt
import hashlib
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from atlassian import Jira, Confluence
from storage.database import get_connection
class AtlassianAuth:
"""
Authentication using Atlassian API credentials.
Users provide:
- Atlassian URL (Jira or Confluence)
- Email
- API Token
On successful validation, we:
1. Verify credentials against Atlassian API
2. Store user in database
3. Generate JWT token
"""
def __init__(self):
self.jwt_secret = os.getenv("JWT_SECRET", "change-me-in-production")
self.jwt_algorithm = "HS256"
self.jwt_expiry_hours = int(os.getenv("JWT_EXPIRY_HOURS", "24"))
async def verify_atlassian_credentials(
self,
url: str,
email: str,
api_token: str,
service: str = "jira"
) -> Dict[str, Any]:
"""
Verify Atlassian credentials by making a test API call.
Args:
url: Atlassian URL (e.g., https://yourcompany.atlassian.net)
email: User email
api_token: Atlassian API token (use "1234" for mock mode)
service: "jira" or "confluence"
Returns:
User info dict if valid, raises exception if invalid
"""
# Mock mode for development/testing
if api_token == "1234":
return {
"email": email,
"display_name": email.split("@")[0].title().replace(".", " ") + " (Mock)",
"account_id": "mock_" + hashlib.md5(email.encode()).hexdigest()[:8],
"atlassian_url": url or "https://mock.atlassian.net",
"service": service,
"verified": True,
"mock_mode": True
}
try:
if service == "jira":
client = Jira(url=url, username=email, password=api_token)
# Test API call - get current user
user_info = client.myself()
else: # confluence
client = Confluence(url=url, username=email, password=api_token)
# Test API call - get current user
user_info = client.get_current_user()
return {
"email": email,
"display_name": user_info.get("displayName", email),
"account_id": user_info.get("accountId"),
"atlassian_url": url,
"service": service,
"verified": True,
"mock_mode": False
}
except Exception as e:
raise ValueError(f"Invalid Atlassian credentials: {str(e)}")
def hash_api_token(self, api_token: str) -> str:
"""Hash API token for storage (we don't store plain tokens)"""
return hashlib.sha256(api_token.encode()).hexdigest()
async def login(
self,
url: str,
email: str,
api_token: str,
service: str = "jira"
) -> Dict[str, Any]:
"""
Authenticate user with Atlassian credentials.
Returns:
{
"token": "jwt_token",
"user": {...},
"expires_at": "iso_timestamp"
}
"""
# Verify credentials against Atlassian
user_info = await self.verify_atlassian_credentials(
url, email, api_token, service
)
# Hash the API token
token_hash = self.hash_api_token(api_token)
# Store or update user in database
with get_connection() as conn:
# Check if user exists
existing = conn.execute(
"SELECT id, email FROM users WHERE email = ?",
(email,)
).fetchone()
if existing:
# Update existing user
user_id = existing["id"]
conn.execute(
"""
UPDATE users
SET display_name = ?,
atlassian_url = ?,
atlassian_service = ?,
api_token_hash = ?,
last_login = ?
WHERE id = ?
""",
(
user_info["display_name"],
url,
service,
token_hash,
datetime.utcnow().isoformat(),
user_id
)
)
else:
# Create new user
cursor = conn.execute(
"""
INSERT INTO users (
email, display_name, atlassian_url, atlassian_service,
api_token_hash, created_at, last_login
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
email,
user_info["display_name"],
url,
service,
token_hash,
datetime.utcnow().isoformat(),
datetime.utcnow().isoformat()
)
)
user_id = cursor.lastrowid
# Generate JWT token
expires_at = datetime.utcnow() + timedelta(hours=self.jwt_expiry_hours)
token_payload = {
"user_id": user_id,
"email": email,
"display_name": user_info["display_name"],
"exp": expires_at,
"iat": datetime.utcnow()
}
jwt_token = jwt.encode(
token_payload,
self.jwt_secret,
algorithm=self.jwt_algorithm
)
return {
"token": jwt_token,
"user": {
"id": user_id,
"email": email,
"display_name": user_info["display_name"],
"atlassian_url": url,
"service": service
},
"expires_at": expires_at.isoformat()
}
def verify_token(self, token: str) -> Optional[Dict[str, Any]]:
"""
Verify JWT token and return user info.
Returns:
User dict if valid, None if invalid/expired
"""
try:
payload = jwt.decode(
token,
self.jwt_secret,
algorithms=[self.jwt_algorithm]
)
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
async def get_user_by_id(self, user_id: int) -> Optional[Dict[str, Any]]:
"""Get user information by ID"""
with get_connection() as conn:
user = conn.execute(
"""
SELECT id, email, display_name, atlassian_url, atlassian_service,
created_at, last_login
FROM users
WHERE id = ?
""",
(user_id,)
).fetchone()
if user:
return dict(user)
return None
# Singleton instance
_auth_instance: Optional[AtlassianAuth] = None
def get_auth() -> AtlassianAuth:
"""Get singleton auth instance"""
global _auth_instance
if _auth_instance is None:
_auth_instance = AtlassianAuth()
return _auth_instance