credentials.py•3.4 kB
"""
Credentials elicitation for Apple Find My authentication.
"""
from dataclasses import dataclass
from fastmcp import Context
from secrets_manager import secrets_manager
@dataclass
class AppleCredentials:
"""Apple ID credentials."""
apple_id: str
password: str
async def elicit_credentials(ctx: Context) -> tuple[str, str]:
"""Elicit Apple ID and password from user."""
credentials = await ctx.elicit(
"Please provide your Apple ID credentials:",
response_type=AppleCredentials
)
if credentials.action == "cancel":
raise ValueError("Authentication cancelled")
if credentials.action == "decline":
raise ValueError("Apple ID credentials required")
apple_id = credentials.data.apple_id.strip()
password = credentials.data.password.strip()
if not apple_id:
raise ValueError("Apple ID cannot be empty")
if not password:
raise ValueError("Password cannot be empty")
# Store both Apple ID and password in our secrets manager
secrets_manager.store_secret("APPLE_ID", apple_id)
secrets_manager.store_secret("APPLE_PASSWORD", password)
await ctx.info("✅ Apple ID and password stored securely")
return apple_id, password
async def get_apple_id_from_secrets() -> str:
"""Get Apple ID from secrets manager."""
return secrets_manager.retrieve_secret("APPLE_ID") or ""
async def get_apple_password_from_secrets() -> str:
"""Get Apple password from secrets manager."""
return secrets_manager.retrieve_secret("APPLE_PASSWORD") or ""
async def get_2fa_code(ctx: Context) -> str:
"""Elicit 2FA verification code."""
while True:
code_result = await ctx.elicit(
"Enter 6-digit verification code from your Apple device:",
response_type=str
)
if code_result.action == "cancel":
raise ValueError("Authentication cancelled")
if code_result.action == "decline":
raise ValueError("Verification code required")
code = code_result.data.strip()
if len(code) == 6 and code.isdigit():
return code
await ctx.info("❌ Invalid code format. Enter 6 digits.")
async def select_device(ctx: Context, devices: list) -> int:
"""Elicit device selection for 2SA."""
device_list = [
f"{i}: {d.get('deviceName', f'SMS to {d.get("phoneNumber", "Unknown")}')}"
for i, d in enumerate(devices)
]
await ctx.info("Available devices:\n" + "\n".join(device_list))
device_result = await ctx.elicit(
f"Select device (0-{len(devices)-1}):",
response_type=[str(i) for i in range(len(devices))]
)
if device_result.action == "cancel":
raise ValueError("Authentication cancelled")
if device_result.action == "decline":
raise ValueError("Device selection required")
return int(device_result.data)
async def get_2sa_code(ctx: Context) -> str:
"""Elicit 2SA verification code."""
code_result = await ctx.elicit(
"Enter verification code:",
response_type=str
)
if code_result.action == "cancel":
raise ValueError("Authentication cancelled")
if code_result.action == "decline":
raise ValueError("Verification code required")
code = code_result.data.strip()
if not code:
raise ValueError("Code cannot be empty")
return code