msft auth integriert
This commit is contained in:
parent
40f82a3848
commit
db6b5d7985
7 changed files with 397 additions and 218 deletions
|
|
@ -394,7 +394,7 @@ class AgentEmail(AgentBase):
|
||||||
logger.error("No mydom interface available")
|
logger.error("No mydom interface available")
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# Get token data from database
|
# Get token data from database using LucyDOMInterface
|
||||||
token_data = self.mydom.getMsftToken()
|
token_data = self.mydom.getMsftToken()
|
||||||
if not token_data:
|
if not token_data:
|
||||||
logger.info("No Microsoft token found for user")
|
logger.info("No Microsoft token found for user")
|
||||||
|
|
@ -409,7 +409,36 @@ class AgentEmail(AgentBase):
|
||||||
# Get updated token data after refresh
|
# Get updated token data after refresh
|
||||||
token_data = self.mydom.getMsftToken()
|
token_data = self.mydom.getMsftToken()
|
||||||
|
|
||||||
return token_data.get("user_info"), token_data.get("access_token")
|
# Get user info from token data
|
||||||
|
user_info = token_data.get("user_info")
|
||||||
|
if not user_info:
|
||||||
|
# If user_info is not in token_data, try to get it from the token
|
||||||
|
headers = {
|
||||||
|
'Authorization': f'Bearer {token_data.get("access_token", "")}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
user_data = response.json()
|
||||||
|
user_info = {
|
||||||
|
"name": user_data.get("displayName", ""),
|
||||||
|
"email": user_data.get("userPrincipalName", ""),
|
||||||
|
"id": user_data.get("id", "")
|
||||||
|
}
|
||||||
|
# Update token data with user info
|
||||||
|
token_data["user_info"] = user_info
|
||||||
|
self.mydom.saveMsftToken(token_data)
|
||||||
|
logger.info(f"Retrieved and stored user info for {user_info.get('name', 'Unknown User')}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Failed to get user info: {response.status_code} - {response.text}")
|
||||||
|
return None, None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting user info: {str(e)}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
logger.info(f"Retrieved user info for {user_info.get('name', 'Unknown User')}")
|
||||||
|
return user_info, token_data.get("access_token")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting current user token: {str(e)}")
|
logger.error(f"Error getting current user token: {str(e)}")
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ class GatewayInterface:
|
||||||
"disabled": False,
|
"disabled": False,
|
||||||
"language": "de",
|
"language": "de",
|
||||||
"privilege": "sysadmin",
|
"privilege": "sysadmin",
|
||||||
|
"authenticationAuthority": "local",
|
||||||
"hashedPassword": self._getPasswordHash("The 1st Poweron Admin") # Use a secure password in production!
|
"hashedPassword": self._getPasswordHash("The 1st Poweron Admin") # Use a secure password in production!
|
||||||
}
|
}
|
||||||
createdUser = self.db.recordCreate("users", adminUser)
|
createdUser = self.db.recordCreate("users", adminUser)
|
||||||
|
|
@ -280,16 +281,25 @@ class GatewayInterface:
|
||||||
|
|
||||||
def getUserByUsername(self, username: str) -> Optional[Dict[str, Any]]:
|
def getUserByUsername(self, username: str) -> Optional[Dict[str, Any]]:
|
||||||
"""Returns a user by username."""
|
"""Returns a user by username."""
|
||||||
# Get all users without mandate filter
|
try:
|
||||||
users = self.db.getRecordset("users")
|
# Get users table
|
||||||
for user in users:
|
users = self.db.getRecordset("users")
|
||||||
if user.get("username") == username:
|
if not users:
|
||||||
# Log the fields present in the user record
|
return None
|
||||||
logger.debug(f"Found user {username} with fields: {list(user.keys())}")
|
|
||||||
# Return a complete copy of the user record with all fields
|
# Find user by username
|
||||||
return {**user} # Use dict unpacking to ensure we get a complete copy with all fields
|
for user in users:
|
||||||
logger.debug(f"No user found with username {username}")
|
if user.get("username") == username:
|
||||||
return None
|
logger.info(f"Found user with username {username}")
|
||||||
|
logger.debug(f"User fields: {list(user.keys())}")
|
||||||
|
return user
|
||||||
|
|
||||||
|
logger.info(f"No user found with username {username}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting user by username: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
def getUser(self, _userId: str) -> Optional[Dict[str, Any]]:
|
def getUser(self, _userId: str) -> Optional[Dict[str, Any]]:
|
||||||
"""Returns a user by ID if user has access."""
|
"""Returns a user by ID if user has access."""
|
||||||
|
|
@ -311,78 +321,70 @@ class GatewayInterface:
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def createUser(self, username: str, password: str, email: str = None,
|
def createUser(self, username: str, password: str = None, email: str = None, fullName: str = None,
|
||||||
fullName: str = None, language: str = "de", _mandateId: str = None,
|
language: str = "de", _mandateId: int = None, disabled: bool = False,
|
||||||
disabled: bool = False, privilege: str = "user") -> Dict[str, Any]:
|
privilege: str = "user", authenticationAuthority: str = "local") -> Dict[str, Any]:
|
||||||
"""Creates a new user if current user has permission."""
|
"""Create a new user"""
|
||||||
# Validate username
|
try:
|
||||||
if not username or len(username) < 3:
|
# Validate username
|
||||||
raise ValueError("Benutzername muss mindestens 3 Zeichen lang sein")
|
if not username:
|
||||||
|
raise ValueError("Username is required")
|
||||||
|
|
||||||
# Validate password
|
# Check if user already exists with the same authentication authority
|
||||||
if not password:
|
existingUser = self.getUserByUsername(username)
|
||||||
raise ValueError("Passwort ist erforderlich")
|
if existingUser and existingUser.get("authenticationAuthority") == authenticationAuthority:
|
||||||
|
raise ValueError(f"Username '{username}' already exists with {authenticationAuthority} authentication")
|
||||||
|
|
||||||
# Password requirements
|
# Validate password for local authentication
|
||||||
if len(password) < 8:
|
if authenticationAuthority == "local":
|
||||||
raise ValueError("Passwort muss mindestens 8 Zeichen lang sein")
|
if not password:
|
||||||
if not any(c.isupper() for c in password):
|
raise ValueError("Password is required for local authentication")
|
||||||
raise ValueError("Passwort muss mindestens einen Grossbuchstaben enthalten")
|
if len(password) < 8:
|
||||||
if not any(c.islower() for c in password):
|
raise ValueError("Password must be at least 8 characters long")
|
||||||
raise ValueError("Passwort muss mindestens einen Kleinbuchstaben enthalten")
|
|
||||||
if not any(c.isdigit() for c in password):
|
|
||||||
raise ValueError("Passwort muss mindestens eine Zahl enthalten")
|
|
||||||
if not any(c in "!@#$%^&*(),.?\":{}|<>" for c in password):
|
|
||||||
raise ValueError("Passwort muss mindestens ein Sonderzeichen enthalten")
|
|
||||||
|
|
||||||
# Validate email if provided
|
# Create user data
|
||||||
if email:
|
userData = {
|
||||||
import re
|
"username": username,
|
||||||
email_pattern = r'^[^\s@]+@[^\s@]+\.[^\s@]+$'
|
"email": email,
|
||||||
if not re.match(email_pattern, email):
|
"fullName": fullName,
|
||||||
raise ValueError("Ungültiges E-Mail-Format")
|
"language": language,
|
||||||
|
"_mandateId": _mandateId or self._mandateId,
|
||||||
|
"disabled": disabled,
|
||||||
|
"privilege": privilege,
|
||||||
|
"authenticationAuthority": authenticationAuthority
|
||||||
|
}
|
||||||
|
|
||||||
# Check if the username already exists
|
# Add password hash for local authentication
|
||||||
existingUser = self.getUserByUsername(username)
|
if authenticationAuthority == "local":
|
||||||
if existingUser:
|
userData["hashedPassword"] = self._getPasswordHash(password)
|
||||||
raise ValueError(f"Benutzer '{username}' existiert bereits")
|
|
||||||
|
|
||||||
# Use the provided _mandateId or the current context
|
# Create user record
|
||||||
userMandateId = _mandateId if _mandateId is not None else self._mandateId
|
createdRecord = self.db.recordCreate("users", userData)
|
||||||
|
if not createdRecord or not createdRecord.get("id"):
|
||||||
|
raise ValueError("Failed to create user record")
|
||||||
|
|
||||||
# Check if user has access to the mandate
|
# Get created user using the returned ID
|
||||||
if userMandateId != self._mandateId and self.currentUser.get("privilege") != "sysadmin":
|
createdUser = self.db.getRecordset("users", recordFilter={"id": createdRecord["id"]})
|
||||||
raise PermissionError(f"Keine Berechtigung, Benutzer in Mandat {userMandateId} zu erstellen")
|
if not createdUser or len(createdUser) == 0:
|
||||||
|
# Try to get user by username as fallback
|
||||||
|
createdUser = self.db.getRecordset("users", recordFilter={"username": userData["username"]})
|
||||||
|
if not createdUser or len(createdUser) == 0:
|
||||||
|
raise ValueError("Failed to retrieve created user")
|
||||||
|
|
||||||
if not self._canModify("users"):
|
# Clear users table from cache
|
||||||
raise PermissionError("Keine Berechtigung, Benutzer zu erstellen")
|
if hasattr(self.db, '_tablesCache') and "users" in self.db._tablesCache:
|
||||||
|
del self.db._tablesCache["users"]
|
||||||
|
|
||||||
# Check privilege escalation
|
return createdUser[0]
|
||||||
if (privilege == "sysadmin" or
|
|
||||||
(privilege == "admin" and self.currentUser.get("privilege") == "user")):
|
|
||||||
raise PermissionError(f"Keine Berechtigung, Benutzer mit höherem Privileg zu erstellen: {privilege}")
|
|
||||||
|
|
||||||
userData = {
|
except ValueError as e:
|
||||||
"_mandateId": userMandateId,
|
logger.error(f"Error creating user: {str(e)}")
|
||||||
"username": username,
|
raise
|
||||||
"email": email,
|
except Exception as e:
|
||||||
"fullName": fullName,
|
logger.error(f"Unexpected error creating user: {str(e)}")
|
||||||
"disabled": disabled,
|
raise ValueError(f"Failed to create user: {str(e)}")
|
||||||
"language": language,
|
|
||||||
"privilege": privilege,
|
|
||||||
"hashedPassword": self._getPasswordHash(password)
|
|
||||||
}
|
|
||||||
|
|
||||||
createdUser = self.db.recordCreate("users", userData)
|
def authenticateUser(self, username: str, password: str = None) -> Optional[Dict[str, Any]]:
|
||||||
|
|
||||||
# Clear the users table from cache to ensure fresh data
|
|
||||||
if "users" in self.db._tablesCache:
|
|
||||||
del self.db._tablesCache["users"]
|
|
||||||
|
|
||||||
# Return the complete user record
|
|
||||||
return createdUser
|
|
||||||
|
|
||||||
def authenticateUser(self, username: str, password: str) -> Optional[Dict[str, Any]]:
|
|
||||||
"""Authenticates a user by username and password."""
|
"""Authenticates a user by username and password."""
|
||||||
# Clear the users table from cache and reload it
|
# Clear the users table from cache and reload it
|
||||||
if "users" in self.db._tablesCache:
|
if "users" in self.db._tablesCache:
|
||||||
|
|
@ -394,13 +396,25 @@ class GatewayInterface:
|
||||||
if not user:
|
if not user:
|
||||||
raise ValueError("Benutzer nicht gefunden")
|
raise ValueError("Benutzer nicht gefunden")
|
||||||
|
|
||||||
if not self._verifyPassword(password, user.get("hashedPassword", "")):
|
|
||||||
raise ValueError("Falsches Passwort")
|
|
||||||
|
|
||||||
# Check if the user is disabled
|
# Check if the user is disabled
|
||||||
if user.get("disabled", False):
|
if user.get("disabled", False):
|
||||||
raise ValueError("Benutzer ist deaktiviert")
|
raise ValueError("Benutzer ist deaktiviert")
|
||||||
|
|
||||||
|
# Handle authentication based on authority
|
||||||
|
auth_authority = user.get("authenticationAuthority", "local")
|
||||||
|
|
||||||
|
if auth_authority == "local":
|
||||||
|
if not password:
|
||||||
|
raise ValueError("Passwort ist erforderlich")
|
||||||
|
if not self._verifyPassword(password, user.get("hashedPassword", "")):
|
||||||
|
raise ValueError("Falsches Passwort")
|
||||||
|
elif auth_authority == "microsoft":
|
||||||
|
# For Microsoft users, we don't verify the password here
|
||||||
|
# The authentication is handled by the Microsoft OAuth flow
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unbekannte Authentifizierungsmethode: {auth_authority}")
|
||||||
|
|
||||||
# Create a copy without password hash
|
# Create a copy without password hash
|
||||||
authenticatedUser = {**user}
|
authenticatedUser = {**user}
|
||||||
if "hashedPassword" in authenticatedUser:
|
if "hashedPassword" in authenticatedUser:
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ class User(BaseModel):
|
||||||
language: str = Field(description="Preferred language of the user")
|
language: str = Field(description="Preferred language of the user")
|
||||||
disabled: Optional[bool] = Field(False, description="Indicates whether the user is disabled")
|
disabled: Optional[bool] = Field(False, description="Indicates whether the user is disabled")
|
||||||
privilege: str = Field(description="Permission level") #sysadmin,admin,user
|
privilege: str = Field(description="Permission level") #sysadmin,admin,user
|
||||||
|
authenticationAuthority: str = Field(default="local", description="Authentication authority (local, microsoft)")
|
||||||
|
|
||||||
label: Label = Field(
|
label: Label = Field(
|
||||||
default=Label(default="User", translations={"en": "User", "fr": "Utilisateur"}),
|
default=Label(default="User", translations={"en": "User", "fr": "Utilisateur"}),
|
||||||
|
|
@ -62,6 +63,7 @@ class User(BaseModel):
|
||||||
"language": Label(default="Language", translations={"en": "Language", "fr": "Langue"}),
|
"language": Label(default="Language", translations={"en": "Language", "fr": "Langue"}),
|
||||||
"disabled": Label(default="Disabled", translations={"en": "Disabled", "fr": "Désactivé"}),
|
"disabled": Label(default="Disabled", translations={"en": "Disabled", "fr": "Désactivé"}),
|
||||||
"privilege": Label(default="Permission level", translations={"en": "Access level", "fr": "Niveau d'accès"}),
|
"privilege": Label(default="Permission level", translations={"en": "Access level", "fr": "Niveau d'accès"}),
|
||||||
|
"authenticationAuthority": Label(default="Authentication Authority", translations={"en": "Authentication Authority", "fr": "Autorité d'authentification"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ router.mount("/static", StaticFiles(directory=str(staticFolder), html=True), nam
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@router.get("/favicon.ico")
|
@router.get("/favicon.ico", tags=["General"])
|
||||||
async def favicon():
|
async def favicon():
|
||||||
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")
|
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")
|
||||||
|
|
||||||
|
|
@ -83,12 +83,13 @@ async def loginForAccessToken(formData: OAuth2PasswordRequestForm = Depends()):
|
||||||
data={
|
data={
|
||||||
"sub": user["username"],
|
"sub": user["username"],
|
||||||
"_mandateId": str(user["_mandateId"]), # Ensure string
|
"_mandateId": str(user["_mandateId"]), # Ensure string
|
||||||
"_userId": str(user["id"]) # Ensure string
|
"_userId": str(user["id"]), # Ensure string
|
||||||
|
"authenticationAuthority": user.get("authenticationAuthority", "local") # Add auth authority
|
||||||
},
|
},
|
||||||
expiresDelta=accessTokenExpires
|
expiresDelta=accessTokenExpires
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"User {user['username']} successfully logged in with context: _mandateId={user['_mandateId']}, _userId={user['id']}")
|
logger.info(f"User {user['username']} successfully logged in with context: _mandateId={user['_mandateId']}, _userId={user['id']}, auth={user.get('authenticationAuthority', 'local')}")
|
||||||
return {"accessToken": accessToken, "tokenType": "bearer"}
|
return {"accessToken": accessToken, "tokenType": "bearer"}
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
# Handle authentication errors
|
# Handle authentication errors
|
||||||
|
|
@ -196,3 +197,49 @@ async def registerUser(userData: Dict[str, Any]):
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to register user"
|
detail="Failed to register user"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@router.get("/api/user/available", response_model=Dict[str, Any], tags=["General"])
|
||||||
|
async def checkUsernameAvailability(
|
||||||
|
username: str,
|
||||||
|
authenticationAuthority: str = "local"
|
||||||
|
):
|
||||||
|
"""Check if a username is available for registration"""
|
||||||
|
try:
|
||||||
|
# Get root mandate and admin user IDs
|
||||||
|
adminGateway = getGatewayInterface()
|
||||||
|
rootMandateId = adminGateway.getInitialId("mandates")
|
||||||
|
adminUserId = adminGateway.getInitialId("users")
|
||||||
|
|
||||||
|
if not rootMandateId or not adminUserId:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="System is not properly initialized with root mandate and admin user"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a new gateway interface instance with admin context
|
||||||
|
adminGateway = getGatewayInterface(rootMandateId, adminUserId)
|
||||||
|
|
||||||
|
# Check if user exists
|
||||||
|
existingUser = adminGateway.getUserByUsername(username)
|
||||||
|
|
||||||
|
if not existingUser:
|
||||||
|
return {"available": True}
|
||||||
|
|
||||||
|
# If user exists, check authentication authority
|
||||||
|
if existingUser.get("authenticationAuthority") == authenticationAuthority:
|
||||||
|
return {
|
||||||
|
"available": False,
|
||||||
|
"message": f"Username already exists with {authenticationAuthority} authentication"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"available": True,
|
||||||
|
"message": f"Username exists but with different authentication authority"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking username availability: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to check username availability: {str(e)}"
|
||||||
|
)
|
||||||
|
|
@ -62,6 +62,13 @@ async def save_token_to_file(token_data, currentUser: Dict[str, Any]):
|
||||||
logger.error("No LucyDOM interface available for token storage")
|
logger.error("No LucyDOM interface available for token storage")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Ensure user info is preserved
|
||||||
|
if "user_info" not in token_data:
|
||||||
|
# Try to get user info from the token
|
||||||
|
user_info = get_user_info_from_token(token_data.get("access_token", ""))
|
||||||
|
if user_info:
|
||||||
|
token_data["user_info"] = user_info
|
||||||
|
|
||||||
# Save token to database
|
# Save token to database
|
||||||
success = mydom.saveMsftToken(token_data)
|
success = mydom.saveMsftToken(token_data)
|
||||||
if success:
|
if success:
|
||||||
|
|
@ -217,18 +224,18 @@ async def login():
|
||||||
async def auth_callback(code: str, state: str, request: Request):
|
async def auth_callback(code: str, state: str, request: Request):
|
||||||
"""Handle Microsoft OAuth callback"""
|
"""Handle Microsoft OAuth callback"""
|
||||||
try:
|
try:
|
||||||
# Create MSAL app instance
|
# Create a confidential client application
|
||||||
app = msal.ConfidentialClientApplication(
|
msal_app = msal.ConfidentialClientApplication(
|
||||||
client_id=CLIENT_ID,
|
app_config["client_id"],
|
||||||
client_credential=CLIENT_SECRET,
|
authority=app_config["authority"],
|
||||||
authority=AUTHORITY
|
client_credential=app_config["client_credential"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Exchange code for token
|
# Exchange the authorization code for tokens
|
||||||
token_response = app.acquire_token_by_authorization_code(
|
token_response = msal_app.acquire_token_by_authorization_code(
|
||||||
code=code,
|
code,
|
||||||
scopes=SCOPES,
|
SCOPES,
|
||||||
redirect_uri=REDIRECT_URI
|
redirect_uri=app_config["redirect_uri"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if "error" in token_response:
|
if "error" in token_response:
|
||||||
|
|
@ -245,7 +252,7 @@ async def auth_callback(code: str, state: str, request: Request):
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1 class="error">Authentication Failed</h1>
|
<h1 class="error">Authentication Failed</h1>
|
||||||
<p>Please try again.</p>
|
<p>Could not acquire access token.</p>
|
||||||
<script>
|
<script>
|
||||||
setTimeout(() => window.close(), 3000);
|
setTimeout(() => window.close(), 3000);
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -255,8 +262,9 @@ async def auth_callback(code: str, state: str, request: Request):
|
||||||
status_code=400
|
status_code=400
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get user info from token
|
# Get user info from the token
|
||||||
user_info = get_user_info_from_token(token_response["access_token"])
|
user_info = get_user_info_from_token(token_response["access_token"])
|
||||||
|
|
||||||
if not user_info:
|
if not user_info:
|
||||||
logger.error("Failed to get user info from token")
|
logger.error("Failed to get user info from token")
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
|
|
@ -281,7 +289,72 @@ async def auth_callback(code: str, state: str, request: Request):
|
||||||
status_code=400
|
status_code=400
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add user info to token data
|
# Get gateway interface for user operations
|
||||||
|
gateway = getGatewayInterface()
|
||||||
|
|
||||||
|
# Check if user exists
|
||||||
|
user = gateway.getUserByUsername(user_info["email"])
|
||||||
|
|
||||||
|
# If user doesn't exist, create a new user in the default mandate
|
||||||
|
if not user:
|
||||||
|
try:
|
||||||
|
# Get the root mandate ID
|
||||||
|
rootMandateId = gateway.getInitialId("mandates")
|
||||||
|
if not rootMandateId:
|
||||||
|
raise ValueError("Root mandate not found")
|
||||||
|
|
||||||
|
# Create new user with Microsoft authentication
|
||||||
|
user = gateway.createUser(
|
||||||
|
username=user_info["email"],
|
||||||
|
email=user_info["email"],
|
||||||
|
fullName=user_info.get("name", user_info["email"]),
|
||||||
|
_mandateId=rootMandateId,
|
||||||
|
authenticationAuthority="microsoft"
|
||||||
|
)
|
||||||
|
logger.info(f"Created new user for Microsoft account: {user_info['email']}")
|
||||||
|
|
||||||
|
# Verify user was created by retrieving it
|
||||||
|
user = gateway.getUserByUsername(user_info["email"])
|
||||||
|
if not user:
|
||||||
|
raise ValueError("Failed to retrieve created user")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create user for Microsoft account: {str(e)}")
|
||||||
|
return HTMLResponse(
|
||||||
|
content="""
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Registration Failed</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
|
||||||
|
.error { color: red; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 class="error">Registration Failed</h1>
|
||||||
|
<p>Could not create user account.</p>
|
||||||
|
<script>
|
||||||
|
setTimeout(() => window.close(), 3000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""",
|
||||||
|
status_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create backend token
|
||||||
|
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
access_token = createAccessToken(
|
||||||
|
data={
|
||||||
|
"sub": user["username"],
|
||||||
|
"_mandateId": str(user["_mandateId"]),
|
||||||
|
"_userId": str(user["id"]),
|
||||||
|
"authenticationAuthority": "microsoft"
|
||||||
|
},
|
||||||
|
expiresDelta=access_token_expires
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add user info to token response
|
||||||
token_response["user_info"] = user_info
|
token_response["user_info"] = user_info
|
||||||
|
|
||||||
# Store tokens in session storage for the frontend to pick up
|
# Store tokens in session storage for the frontend to pick up
|
||||||
|
|
@ -308,7 +381,8 @@ async def auth_callback(code: str, state: str, request: Request):
|
||||||
window.opener.postMessage({{
|
window.opener.postMessage({{
|
||||||
type: 'msft_auth_success',
|
type: 'msft_auth_success',
|
||||||
user: {json.dumps(user_info)},
|
user: {json.dumps(user_info)},
|
||||||
token_data: {json.dumps(token_response)}
|
token_data: {json.dumps(token_response)},
|
||||||
|
access_token: "{access_token}"
|
||||||
}}, '*');
|
}}, '*');
|
||||||
}}
|
}}
|
||||||
// Close window after 3 seconds
|
// Close window after 3 seconds
|
||||||
|
|
@ -322,27 +396,10 @@ async def auth_callback(code: str, state: str, request: Request):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Authentication failed: {str(e)}")
|
logger.error(f"Error in auth callback: {str(e)}")
|
||||||
return HTMLResponse(
|
raise HTTPException(
|
||||||
content="""
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
<html>
|
detail=f"Authentication failed: {str(e)}"
|
||||||
<head>
|
|
||||||
<title>Authentication Failed</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
|
|
||||||
.error { color: red; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1 class="error">Authentication Failed</h1>
|
|
||||||
<p>An error occurred during authentication.</p>
|
|
||||||
<script>
|
|
||||||
setTimeout(() => window.close(), 3000);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
""",
|
|
||||||
status_code=500
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/status")
|
@router.get("/status")
|
||||||
|
|
@ -368,8 +425,9 @@ async def auth_status(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser
|
||||||
"message": "Not authenticated with Microsoft"
|
"message": "Not authenticated with Microsoft"
|
||||||
})
|
})
|
||||||
|
|
||||||
# Verify token is still valid
|
# Verify token is still valid and get user info
|
||||||
if not verify_token(token_data["access_token"]):
|
user_info = get_user_info_from_token(token_data["access_token"])
|
||||||
|
if not user_info:
|
||||||
logger.info("Token invalid, attempting refresh")
|
logger.info("Token invalid, attempting refresh")
|
||||||
# Try to refresh the token
|
# Try to refresh the token
|
||||||
if not await refresh_token(_userId, currentUser):
|
if not await refresh_token(_userId, currentUser):
|
||||||
|
|
@ -380,15 +438,13 @@ async def auth_status(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser
|
||||||
})
|
})
|
||||||
# Reload token data after refresh
|
# Reload token data after refresh
|
||||||
token_data = await load_token_from_file(currentUser)
|
token_data = await load_token_from_file(currentUser)
|
||||||
|
# Get user info again after refresh
|
||||||
# Get user info from token data
|
user_info = get_user_info_from_token(token_data["access_token"])
|
||||||
user_info = token_data.get("user_info")
|
if not user_info:
|
||||||
if not user_info:
|
return JSONResponse({
|
||||||
logger.info("No user info found in token data")
|
"authenticated": False,
|
||||||
return JSONResponse({
|
"message": "Could not get user info after token refresh"
|
||||||
"authenticated": False,
|
})
|
||||||
"message": "No user information available"
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(f"User {user_info.get('name')} is authenticated")
|
logger.info(f"User {user_info.get('name')} is authenticated")
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,7 @@ async def registerUser(request: Request):
|
||||||
"""Register a new user."""
|
"""Register a new user."""
|
||||||
try:
|
try:
|
||||||
# Get request data
|
# Get request data
|
||||||
data = await request.json()
|
userData = await request.json()
|
||||||
logger.info(f"Registration request data: {data}")
|
|
||||||
|
|
||||||
# Get root mandate and admin user IDs
|
# Get root mandate and admin user IDs
|
||||||
adminGateway = getGatewayInterface()
|
adminGateway = getGatewayInterface()
|
||||||
|
|
@ -86,91 +85,110 @@ async def registerUser(request: Request):
|
||||||
|
|
||||||
if not rootMandateId or not adminUserId:
|
if not rootMandateId or not adminUserId:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="System is not properly initialized with root mandate and admin user"
|
detail="System is not properly initialized with root mandate and admin user"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a new gateway interface instance with admin context
|
# Create a new gateway interface instance with admin context
|
||||||
adminGateway = getGatewayInterface(rootMandateId, adminUserId)
|
adminGateway = getGatewayInterface(rootMandateId, adminUserId)
|
||||||
|
|
||||||
# Check required fields
|
# Set default values if not provided
|
||||||
if not data.get("username") or not data.get("password"):
|
if "language" not in userData:
|
||||||
logger.error("Missing required fields in registration request")
|
userData["language"] = "en"
|
||||||
raise HTTPException(status_code=400, detail="Username and password are required")
|
if "authenticationAuthority" not in userData:
|
||||||
|
userData["authenticationAuthority"] = "local"
|
||||||
|
|
||||||
# Create user data
|
# Validate authentication authority
|
||||||
userData = {
|
if userData["authenticationAuthority"] not in ["local", "microsoft"]:
|
||||||
"username": data["username"],
|
raise HTTPException(
|
||||||
"password": data["password"],
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
"email": data.get("email"),
|
detail=f"Invalid authentication authority: {userData['authenticationAuthority']}"
|
||||||
"fullName": data.get("fullName"),
|
)
|
||||||
"language": data.get("language", "de"),
|
|
||||||
"_mandateId": rootMandateId,
|
# Validate password for local authentication
|
||||||
"disabled": False,
|
if userData["authenticationAuthority"] == "local":
|
||||||
"privilege": "user"
|
if "password" not in userData:
|
||||||
}
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Password is required for local authentication"
|
||||||
|
)
|
||||||
|
|
||||||
# Create the user
|
# Create the user
|
||||||
logger.info(f"Attempting to create user with data: {userData}")
|
|
||||||
createdUser = adminGateway.createUser(**userData)
|
|
||||||
logger.info(f"User created successfully: {createdUser}")
|
|
||||||
|
|
||||||
# Add a small delay to ensure database consistency
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Verify the user was created and password was stored
|
|
||||||
if "hashedPassword" not in createdUser:
|
|
||||||
logger.error("Password not stored in user record")
|
|
||||||
# Try to delete the user
|
|
||||||
try:
|
|
||||||
adminGateway.deleteUser(createdUser["id"])
|
|
||||||
logger.info("Successfully deleted user after password storage failure")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to delete user after password storage failure: {str(e)}")
|
|
||||||
raise HTTPException(status_code=500, detail="Password storage failed")
|
|
||||||
|
|
||||||
logger.info("User verification successful")
|
|
||||||
|
|
||||||
# Test authentication
|
|
||||||
try:
|
try:
|
||||||
authResult = adminGateway.authenticateUser(userData["username"], userData["password"])
|
createdUser = adminGateway.createUser(
|
||||||
if not authResult:
|
username=userData["username"],
|
||||||
logger.error("Authentication test failed after user creation")
|
password=userData.get("password"),
|
||||||
|
email=userData.get("email"),
|
||||||
|
fullName=userData.get("fullName"),
|
||||||
|
language=userData["language"],
|
||||||
|
_mandateId=userData.get("_mandateId", rootMandateId),
|
||||||
|
authenticationAuthority=userData["authenticationAuthority"]
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the user was created
|
||||||
|
if not createdUser:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to create user"
|
||||||
|
)
|
||||||
|
|
||||||
|
# For local authentication, verify password was stored
|
||||||
|
if userData["authenticationAuthority"] == "local":
|
||||||
|
if "hashedPassword" not in createdUser:
|
||||||
|
logger.error("Password not stored in user record")
|
||||||
|
# Try to delete the user
|
||||||
|
try:
|
||||||
|
adminGateway.deleteUser(createdUser["id"])
|
||||||
|
logger.info("Successfully deleted user after password storage failure")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to delete user after password storage failure: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail="Password storage failed")
|
||||||
|
|
||||||
|
logger.info("User verification successful")
|
||||||
|
|
||||||
|
# Test authentication
|
||||||
|
try:
|
||||||
|
authResult = adminGateway.authenticateUser(userData["username"], userData["password"])
|
||||||
|
if not authResult:
|
||||||
|
logger.error("Authentication test failed after user creation")
|
||||||
|
# Try to delete the user
|
||||||
|
try:
|
||||||
|
adminGateway.deleteUser(createdUser["id"])
|
||||||
|
logger.info("Successfully deleted user after authentication test failure")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to delete user after authentication test failure: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail="Authentication test failed")
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"Authentication test failed: {str(e)}")
|
||||||
# Try to delete the user
|
# Try to delete the user
|
||||||
try:
|
try:
|
||||||
adminGateway.deleteUser(createdUser["id"])
|
adminGateway.deleteUser(createdUser["id"])
|
||||||
logger.info("Successfully deleted user after authentication test failure")
|
logger.info("Successfully deleted user after authentication test failure")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to delete user after authentication test failure: {str(e)}")
|
logger.error(f"Failed to delete user after authentication test failure: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Authentication test failed")
|
raise HTTPException(status_code=500, detail=f"Authentication test failed: {str(e)}")
|
||||||
except ValueError as e:
|
|
||||||
logger.error(f"Authentication test failed: {str(e)}")
|
|
||||||
# Try to delete the user
|
|
||||||
try:
|
|
||||||
adminGateway.deleteUser(createdUser["id"])
|
|
||||||
logger.info("Successfully deleted user after authentication test failure")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to delete user after authentication test failure: {str(e)}")
|
|
||||||
raise HTTPException(status_code=500, detail=f"Authentication test failed: {str(e)}")
|
|
||||||
|
|
||||||
logger.info("Authentication test successful")
|
logger.info("Authentication test successful")
|
||||||
|
|
||||||
# Return success response
|
# Remove sensitive data from response
|
||||||
return {
|
if "hashedPassword" in createdUser:
|
||||||
"message": "User registered successfully",
|
del createdUser["hashedPassword"]
|
||||||
"userId": createdUser["id"]
|
|
||||||
}
|
|
||||||
|
|
||||||
except ValueError as e:
|
return createdUser
|
||||||
logger.error(f"Validation error during registration: {str(e)}")
|
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
except HTTPException:
|
||||||
except PermissionError as e:
|
raise
|
||||||
logger.error(f"Permission error during registration: {str(e)}")
|
|
||||||
raise HTTPException(status_code=403, detail=str(e))
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Unexpected error during registration: {str(e)}")
|
logger.error(f"Unexpected error during user registration: {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
raise HTTPException(
|
||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Registration failed: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
@router.post("/register-with-msal", response_model=Dict[str, Any])
|
@router.post("/register-with-msal", response_model=Dict[str, Any])
|
||||||
async def registerUserWithMsal(userData: dict = Body(...)):
|
async def registerUserWithMsal(userData: dict = Body(...)):
|
||||||
|
|
|
||||||
|
|
@ -106,26 +106,11 @@ async def getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
|
||||||
logger.error(f"User context mismatch: token(_mandateId={_mandateId}, _userId={_userId}) vs user(_mandateId={user.get('_mandateId')}, id={user.get('id')})")
|
logger.error(f"User context mismatch: token(_mandateId={_mandateId}, _userId={_userId}) vs user(_mandateId={user.get('_mandateId')}, id={user.get('id')})")
|
||||||
raise credentialsException
|
raise credentialsException
|
||||||
|
|
||||||
|
# Add authentication authority to user data
|
||||||
|
user["authenticationAuthority"] = user.get("authenticationAuthority", "local")
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
async def getCurrentActiveUser(currentUser: Dict[str, Any] = Depends(getCurrentUser)) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Ensures that the user is active.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
currentUser: Current user data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
User data
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: If the user is disabled
|
|
||||||
"""
|
|
||||||
if currentUser.get("disabled", False):
|
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is disabled")
|
|
||||||
|
|
||||||
return currentUser
|
|
||||||
|
|
||||||
async def getUserContext(currentUser: Dict[str, Any]) -> Tuple[str, str]:
|
async def getUserContext(currentUser: Dict[str, Any]) -> Tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
Extracts the mandate ID and user ID from the current user.
|
Extracts the mandate ID and user ID from the current user.
|
||||||
|
|
@ -171,3 +156,31 @@ def getInitialContext() -> tuple[str, str]:
|
||||||
mandateId = gateway.getInitialId("mandates")
|
mandateId = gateway.getInitialId("mandates")
|
||||||
userId = gateway.getInitialId("users")
|
userId = gateway.getInitialId("users")
|
||||||
return mandateId, userId
|
return mandateId, userId
|
||||||
|
|
||||||
|
async def getCurrentActiveUser(currentUser: Dict[str, Any] = Depends(getCurrentUser)) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Gets the current active user and verifies their authentication authority.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
currentUser: The current user from getCurrentUser
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The current user data
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If user is disabled or has invalid authentication authority
|
||||||
|
"""
|
||||||
|
if currentUser.get("disabled", False):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="User is disabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_authority = currentUser.get("authenticationAuthority", "local")
|
||||||
|
if auth_authority not in ["local", "microsoft"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=f"Invalid authentication authority: {auth_authority}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return currentUser
|
||||||
Loading…
Reference in a new issue