move app to gatehouse-app
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
"""OIDC Audit Service for comprehensive OIDC event logging."""
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from flask import g
|
||||
|
||||
from gatehouse_app.models import OIDCAuditLog, OIDCClient, User
|
||||
from gatehouse_app.exceptions.validation_exceptions import NotFoundError
|
||||
|
||||
|
||||
class OIDCAuditService:
|
||||
"""Service for OIDC-specific audit logging.
|
||||
|
||||
This service provides methods to log all OIDC-related events including:
|
||||
- Authorization requests and responses
|
||||
- Token issuance and refresh
|
||||
- Token revocation
|
||||
- UserInfo endpoint access
|
||||
- Authentication failures
|
||||
"""
|
||||
|
||||
# Event type constants
|
||||
EVENT_AUTHORIZATION_REQUEST = "authorization_request"
|
||||
EVENT_AUTHORIZATION_RESPONSE = "authorization_response"
|
||||
EVENT_TOKEN_ISSUE = "token_issue"
|
||||
EVENT_TOKEN_REFRESH = "token_refresh"
|
||||
EVENT_TOKEN_REVOCATION = "token_revocation"
|
||||
EVENT_TOKEN_INTROSPECTION = "token_introspection"
|
||||
EVENT_USERINFO_ACCESS = "userinfo_access"
|
||||
EVENT_AUTHENTICATION_FAILURE = "authentication_failure"
|
||||
EVENT_AUTHORIZATION_FAILURE = "authorization_failure"
|
||||
EVENT_JWKS_ACCESS = "jwks_access"
|
||||
EVENT_REGISTRATION = "client_registration"
|
||||
|
||||
@classmethod
|
||||
def _get_request_context(cls) -> Dict:
|
||||
"""Extract request context for logging.
|
||||
|
||||
Returns:
|
||||
Dictionary with IP, user_agent, and request_id
|
||||
"""
|
||||
from flask import request
|
||||
|
||||
return {
|
||||
"ip_address": request.remote_addr if request else None,
|
||||
"user_agent": request.headers.get("User-Agent") if request else None,
|
||||
"request_id": g.get("request_id"),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def log_event(
|
||||
cls,
|
||||
event_type: str,
|
||||
client_id: str = None,
|
||||
user_id: str = None,
|
||||
success: bool = True,
|
||||
error_code: str = None,
|
||||
error_description: str = None,
|
||||
metadata: Dict = None
|
||||
) -> OIDCAuditLog:
|
||||
"""Log a generic OIDC event.
|
||||
|
||||
Args:
|
||||
event_type: Type of event
|
||||
client_id: OIDC client ID
|
||||
user_id: User ID
|
||||
success: Whether the event was successful
|
||||
error_code: Error code if failed
|
||||
error_description: Error description if failed
|
||||
metadata: Additional event metadata
|
||||
|
||||
Returns:
|
||||
OIDCAuditLog instance
|
||||
"""
|
||||
context = cls._get_request_context()
|
||||
|
||||
log = OIDCAuditLog.log_event(
|
||||
event_type=event_type,
|
||||
client_id=client_id,
|
||||
user_id=user_id,
|
||||
success=success,
|
||||
error_code=error_code,
|
||||
error_description=error_description,
|
||||
ip_address=context["ip_address"],
|
||||
user_agent=context["user_agent"],
|
||||
request_id=context["request_id"],
|
||||
event_metadata=metadata,
|
||||
)
|
||||
|
||||
return log
|
||||
|
||||
@classmethod
|
||||
def log_authorization_event(
|
||||
cls,
|
||||
client_id: str,
|
||||
user_id: str = None,
|
||||
success: bool = True,
|
||||
error_code: str = None,
|
||||
error_description: str = None,
|
||||
redirect_uri: str = None,
|
||||
scope: list = None,
|
||||
response_type: str = None
|
||||
) -> OIDCAuditLog:
|
||||
"""Log an authorization event.
|
||||
|
||||
Args:
|
||||
client_id: OIDC client ID
|
||||
user_id: User ID (if authenticated)
|
||||
success: Whether authorization was successful
|
||||
error_code: Error code if failed
|
||||
error_description: Error description if failed
|
||||
redirect_uri: Redirect URI from request
|
||||
scope: Requested scopes
|
||||
response_type: Response type (e.g., "code")
|
||||
|
||||
Returns:
|
||||
OIDCAuditLog instance
|
||||
"""
|
||||
metadata = {
|
||||
"redirect_uri": redirect_uri,
|
||||
"scope": scope,
|
||||
"response_type": response_type,
|
||||
}
|
||||
metadata = {k: v for k, v in metadata.items() if v is not None}
|
||||
|
||||
return cls.log_event(
|
||||
event_type=cls.EVENT_AUTHORIZATION_REQUEST,
|
||||
client_id=client_id,
|
||||
user_id=user_id,
|
||||
success=success,
|
||||
error_code=error_code,
|
||||
error_description=error_description,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def log_token_event(
|
||||
cls,
|
||||
client_id: str,
|
||||
user_id: str = None,
|
||||
token_type: str = "access_token",
|
||||
success: bool = True,
|
||||
error_code: str = None,
|
||||
error_description: str = None,
|
||||
grant_type: str = None,
|
||||
scopes: list = None
|
||||
) -> OIDCAuditLog:
|
||||
"""Log a token issuance or refresh event.
|
||||
|
||||
Args:
|
||||
client_id: OIDC client ID
|
||||
user_id: User ID
|
||||
token_type: Type of token issued
|
||||
success: Whether token issuance was successful
|
||||
error_code: Error code if failed
|
||||
error_description: Error description if failed
|
||||
grant_type: Grant type used (e.g., "authorization_code", "refresh_token")
|
||||
scopes: Scopes included in the token
|
||||
|
||||
Returns:
|
||||
OIDCAuditLog instance
|
||||
"""
|
||||
metadata = {
|
||||
"token_type": token_type,
|
||||
"grant_type": grant_type,
|
||||
"scopes": scopes,
|
||||
}
|
||||
metadata = {k: v for k, v in metadata.items() if v is not None}
|
||||
|
||||
return cls.log_event(
|
||||
event_type=cls.EVENT_TOKEN_ISSUE if token_type else cls.EVENT_TOKEN_REFRESH,
|
||||
client_id=client_id,
|
||||
user_id=user_id,
|
||||
success=success,
|
||||
error_code=error_code,
|
||||
error_description=error_description,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def log_userinfo_event(
|
||||
cls,
|
||||
access_token: str = None,
|
||||
user_id: str = None,
|
||||
client_id: str = None,
|
||||
success: bool = True,
|
||||
error_code: str = None,
|
||||
error_description: str = None,
|
||||
scopes_claimed: list = None
|
||||
) -> OIDCAuditLog:
|
||||
"""Log a UserInfo endpoint access event.
|
||||
|
||||
Args:
|
||||
access_token: Access token used (masked)
|
||||
user_id: User ID returned
|
||||
client_id: Client ID making the request
|
||||
success: Whether access was successful
|
||||
error_code: Error code if failed
|
||||
error_description: Error description if failed
|
||||
scopes_claimed: Scopes claimed in the request
|
||||
|
||||
Returns:
|
||||
OIDCAuditLog instance
|
||||
"""
|
||||
# Mask the access token for security
|
||||
masked_token = None
|
||||
if access_token:
|
||||
masked_token = access_token[:8] + "..." + access_token[-4:] if len(access_token) > 12 else "***"
|
||||
|
||||
metadata = {
|
||||
"token_prefix": masked_token,
|
||||
"scopes_claimed": scopes_claimed,
|
||||
}
|
||||
metadata = {k: v for k, v in metadata.items() if v is not None}
|
||||
|
||||
return cls.log_event(
|
||||
event_type=cls.EVENT_USERINFO_ACCESS,
|
||||
client_id=client_id,
|
||||
user_id=user_id,
|
||||
success=success,
|
||||
error_code=error_code,
|
||||
error_description=error_description,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def log_token_revocation_event(
|
||||
cls,
|
||||
client_id: str,
|
||||
user_id: str = None,
|
||||
token_type: str = "access_token",
|
||||
reason: str = None,
|
||||
success: bool = True,
|
||||
error_code: str = None,
|
||||
error_description: str = None
|
||||
) -> OIDCAuditLog:
|
||||
"""Log a token revocation event.
|
||||
|
||||
Args:
|
||||
client_id: OIDC client ID
|
||||
user_id: User ID
|
||||
token_type: Type of token being revoked
|
||||
reason: Revocation reason
|
||||
success: Whether revocation was successful
|
||||
error_code: Error code if failed
|
||||
error_description: Error description if failed
|
||||
|
||||
Returns:
|
||||
OIDCAuditLog instance
|
||||
"""
|
||||
metadata = {
|
||||
"token_type": token_type,
|
||||
"reason": reason,
|
||||
}
|
||||
metadata = {k: v for k, v in metadata.items() if v is not None}
|
||||
|
||||
return cls.log_event(
|
||||
event_type=cls.EVENT_TOKEN_REVOCATION,
|
||||
client_id=client_id,
|
||||
user_id=user_id,
|
||||
success=success,
|
||||
error_code=error_code,
|
||||
error_description=error_description,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def log_authentication_failure(
|
||||
cls,
|
||||
client_id: str = None,
|
||||
error_code: str = "authentication_failed",
|
||||
error_description: str = "Authentication failed",
|
||||
user_id: str = None
|
||||
) -> OIDCAuditLog:
|
||||
"""Log an authentication failure event.
|
||||
|
||||
Args:
|
||||
client_id: OIDC client ID
|
||||
error_code: Error code
|
||||
error_description: Error description
|
||||
user_id: User ID if known
|
||||
|
||||
Returns:
|
||||
OIDCAuditLog instance
|
||||
"""
|
||||
return cls.log_event(
|
||||
event_type=cls.EVENT_AUTHENTICATION_FAILURE,
|
||||
client_id=client_id,
|
||||
user_id=user_id,
|
||||
success=False,
|
||||
error_code=error_code,
|
||||
error_description=error_description,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_events_for_user(
|
||||
cls,
|
||||
user_id: str,
|
||||
limit: int = 100,
|
||||
include_deleted: bool = False
|
||||
) -> List[OIDCAuditLog]:
|
||||
"""Get audit events for a specific user.
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
limit: Maximum number of events to return
|
||||
include_deleted: Include soft-deleted events
|
||||
|
||||
Returns:
|
||||
List of OIDCAuditLog instances
|
||||
"""
|
||||
return OIDCAuditLog.get_events_for_user(user_id, limit)
|
||||
|
||||
@classmethod
|
||||
def get_events_for_client(
|
||||
cls,
|
||||
client_id: str,
|
||||
limit: int = 100
|
||||
) -> List[OIDCAuditLog]:
|
||||
"""Get audit events for a specific client.
|
||||
|
||||
Args:
|
||||
client_id: Client ID
|
||||
limit: Maximum number of events to return
|
||||
|
||||
Returns:
|
||||
List of OIDCAuditLog instances
|
||||
"""
|
||||
return OIDCAuditLog.get_events_for_client(client_id, limit)
|
||||
|
||||
@classmethod
|
||||
def get_failed_events(
|
||||
cls,
|
||||
client_id: str = None,
|
||||
user_id: str = None,
|
||||
start_date: datetime = None,
|
||||
end_date: datetime = None,
|
||||
limit: int = 100
|
||||
) -> List[OIDCAuditLog]:
|
||||
"""Get failed audit events for analysis.
|
||||
|
||||
Args:
|
||||
client_id: Optional client ID filter
|
||||
user_id: Optional user ID filter
|
||||
start_date: Optional start date filter
|
||||
end_date: Optional end date filter
|
||||
limit: Maximum number of events to return
|
||||
|
||||
Returns:
|
||||
List of failed OIDCAuditLog instances
|
||||
"""
|
||||
return OIDCAuditLog.get_failed_events(
|
||||
client_id=client_id,
|
||||
user_id=user_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_event_summary(
|
||||
cls,
|
||||
client_id: str = None,
|
||||
days: int = 30
|
||||
) -> Dict:
|
||||
"""Get a summary of audit events.
|
||||
|
||||
Args:
|
||||
client_id: Optional client ID filter
|
||||
days: Number of days to look back
|
||||
|
||||
Returns:
|
||||
Summary dictionary with event counts
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
start_date = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
|
||||
query = OIDCAuditLog.query.filter(
|
||||
OIDCAuditLog.created_at >= start_date
|
||||
)
|
||||
|
||||
if client_id:
|
||||
query = query.filter_by(client_id=client_id)
|
||||
|
||||
events = query.all()
|
||||
|
||||
# Count by event type
|
||||
event_counts = {}
|
||||
success_count = 0
|
||||
failure_count = 0
|
||||
|
||||
for event in events:
|
||||
event_type = event.event_type
|
||||
event_counts[event_type] = event_counts.get(event_type, 0) + 1
|
||||
|
||||
if event.success:
|
||||
success_count += 1
|
||||
else:
|
||||
failure_count += 1
|
||||
|
||||
return {
|
||||
"total_events": len(events),
|
||||
"successful_events": success_count,
|
||||
"failed_events": failure_count,
|
||||
"by_event_type": event_counts,
|
||||
"period_days": days,
|
||||
}
|
||||
Reference in New Issue
Block a user