-
Notifications
You must be signed in to change notification settings - Fork 8.2k
feat: encrypt oauth auth settings at rest #9490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
5a0b9bb
encrypt oauth auth settings at rest
jordanrfrazier 8e89ef4
[autofix.ci] apply automated fixes
autofix-ci[bot] f66f222
Fix rebase changes and add env to env server config
jordanrfrazier c385207
Correctly unmask secretstr before encryption
jordanrfrazier 9da2f0f
update mcp-composer args
jordanrfrazier ca1426d
Merge branch 'main' into mcp-composer-oauth
jordanrfrazier 75af111
[autofix.ci] apply automated fixes
autofix-ci[bot] 2cb3637
ruff
jordanrfrazier cb6d331
ruff
jordanrfrazier 175278f
ruff
jordanrfrazier 17adeba
[autofix.ci] apply automated fixes
autofix-ci[bot] 2eed48e
ruff
jordanrfrazier 074ef60
Merge branch 'main' into mcp-composer-oauth
jordanrfrazier 7cbed82
catch invalidtoken error
jordanrfrazier 54b15ba
ruff
jordanrfrazier 942dfb6
[autofix.ci] apply automated fixes
autofix-ci[bot] c5d71d9
ruff
jordanrfrazier ecee186
[autofix.ci] apply automated fixes
autofix-ci[bot] 16eec45
ruff
jordanrfrazier 4d0b3a3
Merge branch 'main' into mcp-composer-oauth
jordanrfrazier 05f7e6e
ruff
jordanrfrazier bc6b9b7
[autofix.ci] apply automated fixes
autofix-ci[bot] 73c9646
ruff
jordanrfrazier 6b22074
[autofix.ci] apply automated fixes
autofix-ci[bot] 449d2e9
fix test
jordanrfrazier 767fda6
[autofix.ci] apply automated fixes
autofix-ci[bot] a7445b7
Merge branch 'main' into mcp-composer-oauth
jordanrfrazier 58e2af7
Merge branch 'main' into mcp-composer-oauth
jordanrfrazier 9c64d76
remove oauth mention in server config
jordanrfrazier 27f3ca2
Merge branch 'main' into mcp-composer-oauth
jordanrfrazier f6c8243
[autofix.ci] apply automated fixes
autofix-ci[bot] 8950670
ruff
jordanrfrazier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
122 changes: 122 additions & 0 deletions
122
...ackend/base/langflow/alembic/versions/0882f9657f22_encrypt_existing_mcp_auth_settings_.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| """Encrypt existing MCP auth_settings credentials | ||
|
|
||
| Revision ID: 0882f9657f22 | ||
| Revises: 1cb603706752 | ||
| Create Date: 2025-08-21 20:11:26.504681 | ||
|
|
||
| """ | ||
| import json | ||
| from typing import Sequence, Union | ||
|
|
||
| from alembic import op | ||
| import sqlalchemy as sa | ||
| import sqlmodel | ||
| from sqlalchemy.engine.reflection import Inspector | ||
| from langflow.utils import migration | ||
|
|
||
|
|
||
| # revision identifiers, used by Alembic. | ||
| revision: str = '0882f9657f22' | ||
| down_revision: Union[str, None] = '1cb603706752' | ||
| branch_labels: Union[str, Sequence[str], None] = None | ||
| depends_on: Union[str, Sequence[str], None] = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| """Encrypt sensitive fields in existing auth_settings data.""" | ||
| conn = op.get_bind() | ||
|
|
||
| # Import encryption utilities | ||
| try: | ||
| from langflow.services.auth.mcp_encryption import encrypt_auth_settings | ||
| from langflow.services.deps import get_settings_service | ||
|
|
||
| # Check if the folder table exists | ||
| inspector = sa.inspect(conn) | ||
| if 'folder' not in inspector.get_table_names(): | ||
| return | ||
|
|
||
| # Query all folders with auth_settings | ||
| result = conn.execute( | ||
| sa.text("SELECT id, auth_settings FROM folder WHERE auth_settings IS NOT NULL") | ||
| ) | ||
|
|
||
| # Encrypt auth_settings for each folder | ||
| for row in result: | ||
| folder_id = row.id | ||
| auth_settings = row.auth_settings | ||
|
|
||
| if auth_settings: | ||
| try: | ||
| # Parse JSON if it's a string | ||
| if isinstance(auth_settings, str): | ||
| auth_settings_dict = json.loads(auth_settings) | ||
| else: | ||
| auth_settings_dict = auth_settings | ||
|
|
||
| # Encrypt sensitive fields | ||
| encrypted_settings = encrypt_auth_settings(auth_settings_dict) | ||
|
|
||
| # Update the record with encrypted data | ||
| if encrypted_settings: | ||
| conn.execute( | ||
| sa.text("UPDATE folder SET auth_settings = :auth_settings WHERE id = :id"), | ||
| {"auth_settings": json.dumps(encrypted_settings), "id": folder_id} | ||
| ) | ||
| except Exception as e: | ||
| # Log the error but continue with other records | ||
| print(f"Warning: Failed to encrypt auth_settings for folder {folder_id}: {e}") | ||
|
|
||
| except ImportError as e: | ||
| # If encryption utilities are not available, skip the migration | ||
| print(f"Warning: Encryption utilities not available, skipping encryption migration: {e}") | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| """Decrypt sensitive fields in auth_settings data (for rollback).""" | ||
| conn = op.get_bind() | ||
|
|
||
| # Import decryption utilities | ||
| try: | ||
| from langflow.services.auth.mcp_encryption import decrypt_auth_settings | ||
| from langflow.services.deps import get_settings_service | ||
|
|
||
| # Check if the folder table exists | ||
| inspector = sa.inspect(conn) | ||
| if 'folder' not in inspector.get_table_names(): | ||
| return | ||
|
|
||
| # Query all folders with auth_settings | ||
| result = conn.execute( | ||
| sa.text("SELECT id, auth_settings FROM folder WHERE auth_settings IS NOT NULL") | ||
| ) | ||
|
|
||
| # Decrypt auth_settings for each folder | ||
| for row in result: | ||
| folder_id = row.id | ||
| auth_settings = row.auth_settings | ||
|
|
||
| if auth_settings: | ||
| try: | ||
| # Parse JSON if it's a string | ||
| if isinstance(auth_settings, str): | ||
| auth_settings_dict = json.loads(auth_settings) | ||
| else: | ||
| auth_settings_dict = auth_settings | ||
|
|
||
| # Decrypt sensitive fields | ||
| decrypted_settings = decrypt_auth_settings(auth_settings_dict) | ||
|
|
||
| # Update the record with decrypted data | ||
| if decrypted_settings: | ||
| conn.execute( | ||
| sa.text("UPDATE folder SET auth_settings = :auth_settings WHERE id = :id"), | ||
| {"auth_settings": json.dumps(decrypted_settings), "id": folder_id} | ||
| ) | ||
| except Exception as e: | ||
| # Log the error but continue with other records | ||
| print(f"Warning: Failed to decrypt auth_settings for folder {folder_id}: {e}") | ||
|
|
||
| except ImportError as e: | ||
| # If decryption utilities are not available, skip the migration | ||
| print(f"Warning: Decryption utilities not available, skipping decryption migration: {e}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
107 changes: 107 additions & 0 deletions
107
src/backend/base/langflow/services/auth/mcp_encryption.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| """MCP Authentication encryption utilities for secure credential storage.""" | ||
|
|
||
| from typing import Any | ||
|
|
||
| from loguru import logger | ||
|
|
||
| from langflow.services.auth import utils as auth_utils | ||
| from langflow.services.deps import get_settings_service | ||
|
|
||
| # Fields that should be encrypted when stored | ||
| SENSITIVE_FIELDS = [ | ||
| "oauth_client_secret", | ||
| "oauth_token", | ||
| "bearer_token", | ||
| "password", | ||
jordanrfrazier marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "api_key", | ||
| "access_token", | ||
| "refresh_token", | ||
| ] | ||
|
|
||
|
|
||
| def encrypt_auth_settings(auth_settings: dict[str, Any] | None) -> dict[str, Any] | None: | ||
| """Encrypt sensitive fields in auth_settings dictionary. | ||
|
|
||
| Args: | ||
| auth_settings: Dictionary containing authentication settings | ||
|
|
||
| Returns: | ||
| Dictionary with sensitive fields encrypted, or None if input is None | ||
| """ | ||
| if auth_settings is None: | ||
| return None | ||
|
|
||
| settings_service = get_settings_service() | ||
| encrypted_settings = auth_settings.copy() | ||
|
|
||
| for field in SENSITIVE_FIELDS: | ||
| if encrypted_settings.get(field): | ||
| try: | ||
| # Only encrypt if the value is not already encrypted | ||
| # Try to decrypt first - if it fails, it's not encrypted | ||
| try: | ||
| auth_utils.decrypt_api_key(encrypted_settings[field], settings_service) | ||
| # If decrypt succeeds, it's already encrypted | ||
| logger.debug(f"Field {field} is already encrypted") | ||
| except Exception: | ||
| # If decrypt fails, the value is plaintext and needs encryption | ||
| encrypted_value = auth_utils.encrypt_api_key(encrypted_settings[field], settings_service) | ||
| encrypted_settings[field] = encrypted_value | ||
| logger.debug(f"Encrypted field {field}") | ||
| except Exception as e: | ||
| logger.error(f"Failed to encrypt field {field}: {e}") | ||
| raise | ||
|
|
||
| return encrypted_settings | ||
|
|
||
|
|
||
| def decrypt_auth_settings(auth_settings: dict[str, Any] | None) -> dict[str, Any] | None: | ||
| """Decrypt sensitive fields in auth_settings dictionary. | ||
|
|
||
| Args: | ||
| auth_settings: Dictionary containing encrypted authentication settings | ||
|
|
||
| Returns: | ||
| Dictionary with sensitive fields decrypted, or None if input is None | ||
| """ | ||
| if auth_settings is None: | ||
| return None | ||
|
|
||
| settings_service = get_settings_service() | ||
| decrypted_settings = auth_settings.copy() | ||
|
|
||
| for field in SENSITIVE_FIELDS: | ||
| if decrypted_settings.get(field): | ||
| try: | ||
| decrypted_value = auth_utils.decrypt_api_key(decrypted_settings[field], settings_service) | ||
| decrypted_settings[field] = decrypted_value | ||
| logger.debug(f"Decrypted field {field}") | ||
| except Exception as e: | ||
| # If decryption fails, assume the value is already plaintext | ||
| # This handles backward compatibility with existing unencrypted data | ||
| logger.debug(f"Field {field} appears to be plaintext or decryption failed: {e}") | ||
| # Keep the original value | ||
|
|
||
| return decrypted_settings | ||
|
|
||
|
|
||
| def is_encrypted(value: str) -> bool: | ||
| """Check if a value appears to be encrypted. | ||
|
|
||
| Args: | ||
| value: String value to check | ||
|
|
||
| Returns: | ||
| True if the value appears to be encrypted (base64 Fernet token) | ||
| """ | ||
| if not value: | ||
| return False | ||
|
|
||
| try: | ||
| settings_service = get_settings_service() | ||
| # Try to decrypt - if it succeeds, it's encrypted | ||
| auth_utils.decrypt_api_key(value, settings_service) | ||
| return True | ||
|
Check failure on line 104 in src/backend/base/langflow/services/auth/mcp_encryption.py
|
||
| except Exception: | ||
| # If decryption fails, it's not encrypted | ||
| return False | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.