first commit
This commit is contained in:
237
app/core/security.py
Normal file
237
app/core/security.py
Normal file
@ -0,0 +1,237 @@
|
||||
"""
|
||||
Security Utilities
|
||||
|
||||
Provides password hashing, JWT token management, and other security functions
|
||||
following the Python Quick Start Guide best practices.
|
||||
"""
|
||||
|
||||
import bcrypt
|
||||
import jwt
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
|
||||
# ==========================================
|
||||
# Password Hashing (bcrypt)
|
||||
# ==========================================
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""
|
||||
Hash a password using bcrypt.
|
||||
|
||||
Args:
|
||||
password: Plain text password
|
||||
|
||||
Returns:
|
||||
Hashed password as string
|
||||
|
||||
Example:
|
||||
hashed = hash_password("mypassword123")
|
||||
"""
|
||||
password_bytes = password.encode('utf-8')
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(password_bytes, salt)
|
||||
return hashed.decode('utf-8')
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""
|
||||
Verify a password against a hash.
|
||||
|
||||
Args:
|
||||
plain_password: Plain text password to check
|
||||
hashed_password: Hashed password to compare against
|
||||
|
||||
Returns:
|
||||
True if password matches, False otherwise
|
||||
|
||||
Example:
|
||||
if verify_password("mypassword123", user.password_hash):
|
||||
# Password is correct
|
||||
"""
|
||||
password_bytes = plain_password.encode('utf-8')
|
||||
hash_bytes = hashed_password.encode('utf-8')
|
||||
return bcrypt.checkpw(password_bytes, hash_bytes)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# JWT Token Management
|
||||
# ==========================================
|
||||
|
||||
def create_jwt_token(
|
||||
payload: Dict[str, Any],
|
||||
secret_key: str,
|
||||
algorithm: str = 'HS256',
|
||||
expires_in: int = 3600
|
||||
) -> str:
|
||||
"""
|
||||
Create a JWT token with expiration.
|
||||
|
||||
Args:
|
||||
payload: Token payload data
|
||||
secret_key: Secret key for signing
|
||||
algorithm: JWT algorithm (HS256, RS256, etc.)
|
||||
expires_in: Token lifetime in seconds
|
||||
|
||||
Returns:
|
||||
Encoded JWT token string
|
||||
|
||||
Example:
|
||||
token = create_jwt_token(
|
||||
payload={'user_id': 123, 'role': 'admin'},
|
||||
secret_key=app.config['SECRET_KEY'],
|
||||
expires_in=3600
|
||||
)
|
||||
"""
|
||||
payload = payload.copy()
|
||||
expire = datetime.utcnow() + timedelta(seconds=expires_in)
|
||||
payload.update({'exp': expire, 'iat': datetime.utcnow()})
|
||||
|
||||
return jwt.encode(payload, secret_key, algorithm=algorithm)
|
||||
|
||||
|
||||
def decode_jwt_token(
|
||||
token: str,
|
||||
secret_key: str,
|
||||
algorithm: str = 'HS256'
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Decode and verify a JWT token.
|
||||
|
||||
Args:
|
||||
token: JWT token string
|
||||
secret_key: Secret key for verification
|
||||
algorithm: JWT algorithm used
|
||||
|
||||
Returns:
|
||||
Decoded payload dict, or None if invalid
|
||||
|
||||
Example:
|
||||
payload = decode_jwt_token(token, app.config['SECRET_KEY'])
|
||||
if payload:
|
||||
user_id = payload['user_id']
|
||||
"""
|
||||
try:
|
||||
payload = jwt.decode(token, secret_key, algorithms=[algorithm])
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
# Token has expired
|
||||
return None
|
||||
except jwt.InvalidTokenError:
|
||||
# Token is invalid
|
||||
return None
|
||||
|
||||
|
||||
def create_id_token(
|
||||
user_data: Dict[str, Any],
|
||||
client_id: str,
|
||||
issuer: str,
|
||||
private_key: str,
|
||||
algorithm: str = 'RS256',
|
||||
expires_in: int = 3600
|
||||
) -> str:
|
||||
"""
|
||||
Create an OIDC ID Token (JWT).
|
||||
|
||||
Args:
|
||||
user_data: User information (sub, email, name, etc.)
|
||||
client_id: OAuth client ID (aud claim)
|
||||
issuer: OIDC issuer URL (iss claim)
|
||||
private_key: Private key for RS256 signing
|
||||
algorithm: JWT algorithm (should be RS256 for OIDC)
|
||||
expires_in: Token lifetime in seconds
|
||||
|
||||
Returns:
|
||||
Encoded ID token string
|
||||
|
||||
Example:
|
||||
id_token = create_id_token(
|
||||
user_data={'sub': 'user-123', 'email': 'user@example.com'},
|
||||
client_id='my-app',
|
||||
issuer='https://auth.example.com',
|
||||
private_key=app.config['OIDC_JWT_PRIVATE_KEY']
|
||||
)
|
||||
"""
|
||||
now = datetime.utcnow()
|
||||
payload = {
|
||||
'iss': issuer,
|
||||
'sub': user_data.get('sub'),
|
||||
'aud': client_id,
|
||||
'exp': now + timedelta(seconds=expires_in),
|
||||
'iat': now,
|
||||
**user_data # Include all user claims
|
||||
}
|
||||
|
||||
return jwt.encode(payload, private_key, algorithm=algorithm)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# Token Generation
|
||||
# ==========================================
|
||||
|
||||
def generate_secure_token(length: int = 32) -> str:
|
||||
"""
|
||||
Generate a cryptographically secure random token.
|
||||
|
||||
Args:
|
||||
length: Token length in bytes (default 32)
|
||||
|
||||
Returns:
|
||||
URL-safe token string
|
||||
|
||||
Example:
|
||||
auth_code = generate_secure_token(32)
|
||||
access_token = generate_secure_token(64)
|
||||
"""
|
||||
return secrets.token_urlsafe(length)
|
||||
|
||||
|
||||
def generate_client_secret() -> str:
|
||||
"""
|
||||
Generate a secure client secret for OIDC clients.
|
||||
|
||||
Returns:
|
||||
URL-safe client secret string
|
||||
|
||||
Example:
|
||||
client_secret = generate_client_secret()
|
||||
"""
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# Password Validation
|
||||
# ==========================================
|
||||
|
||||
def validate_password_strength(password: str) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate password strength.
|
||||
|
||||
Args:
|
||||
password: Password to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
|
||||
Rules:
|
||||
- Minimum 8 characters
|
||||
- At least one digit (optional but recommended)
|
||||
|
||||
Example:
|
||||
is_valid, error = validate_password_strength("password123")
|
||||
if not is_valid:
|
||||
raise ValueError(error)
|
||||
"""
|
||||
if len(password) < 8:
|
||||
return False, "Password must be at least 8 characters long"
|
||||
|
||||
# Optional: Check for digit
|
||||
# if not any(char.isdigit() for char in password):
|
||||
# return False, "Password must contain at least one digit"
|
||||
|
||||
# Optional: Check for uppercase
|
||||
# if not any(char.isupper() for char in password):
|
||||
# return False, "Password must contain at least one uppercase letter"
|
||||
|
||||
return True, None
|
||||
Reference in New Issue
Block a user