Skip to content

Conversation

@miguelbranco80
Copy link
Contributor

Description

WIP

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • 🔧 Refactoring (no functional changes, no api changes)
  • ⚡ Performance improvement
  • 🧪 Test improvement
  • 🔒 Security fix

Testing

  • Tests pass locally with uv run pytest
  • Linting passes with uv run ruff check .
  • Code formatting passes with uv run black --check .
  • Type checking passes with uv run mypy .
  • Added tests for new functionality (if applicable)
  • Updated documentation (if applicable)

Security Considerations

  • This change does not introduce security vulnerabilities
  • Sensitive data handling reviewed (if applicable)
  • Policy enforcement implications considered (if applicable)

Breaking Changes

If this is a breaking change, describe what users need to do to migrate:

Additional Notes

Any additional context or screenshots.

…ger, and ProviderAdapters

Implement phases 1-3 of auth redesign plan, introducing a cleaner SDK-first architecture:

AuthService (Phase 1):
- Single entry point for all authentication flows
- Factory methods from_config() and from_provider_config()
- Unified route registration and middleware building
- FastMCPAuthProvider adapter for FastMCP integration

Session & Token Infrastructure (Phase 2):
- SessionManager for session lifecycle, OAuth state, and auth codes
- TokenStore protocol with SqliteTokenStore implementation
- Fernet encryption for provider tokens at rest
- One-way hashing for MXCP tokens

Provider Adapters (Phase 3):
- ProviderAdapter protocol replacing ExternalOAuthHandler
- Thin adapters for Google, Keycloak, GitHub, Atlassian, Salesforce
- Adapters handle only IdP communication; state managed by SessionManager
- Single unified callback route in AuthService

Cleanup:
- Removed legacy GeneralOAuthAuthorizationServer
- Removed legacy providers/ handlers
- Removed unused storage operations (auth codes, clients, tokens)
- Simplified TokenStore to session operations only

BREAKING: Internal auth APIs changed; external behavior preserved.
Copy link
Contributor

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Copy link
Contributor

@bgaidioz bgaidioz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments. I believe bugs to fix.

self._auth_codes.pop(authorization_code.code, None)

# Create a session
session = await self.session_manager.create_session(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is creating a new session instead of looking up the existing one. SessionManager seems to have an implementation to recover a session from the code (consume_auth_code which sits on top of an internal mapping called _auth_codes). Instead FastMCPAuthProvider maintains its own list of auth codes in _auth_codes.

session = await self.session_manager.create_session(
client_id=client.client_id,
scopes=authorization_code.scopes,
expires_in=3600, # 1 hour default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the expiry of the session and all its details shouldn't be changing, it was created during the initial callback. All we can do now is return a token which exposes the session's details (e.g. report an expiry based on now - expiry_at of the matching session).

# In-memory storage for auth codes and clients
# (auth codes are short-lived, clients are configured)
self._auth_codes: dict[str, MXCPAuthCode] = {}
self._clients: dict[str, MXCPClient] = {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be protected by a lock?

session: The session to delete.
"""
# Remove from memory cache
self._sessions.pop(session.mxcp_token_hash, None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't that be locked?

]

for token_hash in expired_hashes:
session = self._sessions.pop(token_hash, None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicating the individual deletion code. I imagine we could factorize the bulk deletion and use it from the "delete_session" and "delete_expired_sessions".

return None

# Update last accessed
session.touch()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be persisted.

"""SQLite-based token storage with encryption support.

Provider tokens are encrypted using Fernet symmetric encryption.
MXCP tokens are stored as SHA-256 hashes for secure lookup.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MXCP tokens aren't persisted?


return Session(
session_id=row["session_id"],
mxcp_token="", # Token is not stored, only hash
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that won't work if we never return the MXCP token itself. Especially in sessions.py, there's that kind of code:

session = await self.token_store.load_session_by_token_hash(token_hash)
...
self._token_to_hash[session.mxcp_token] = token_hash

We use mxcp_token as returned by the storage.

callback_url = str(params.redirect_uri) if params.redirect_uri else ""
oauth_state = self.session_manager.create_oauth_state(
client_id=client.client_id,
redirect_uri=callback_url,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redirect_url and callback_url are set to the same URL. That seems to be the client's URL (the URL to which we'll send the MXCP token?). callback_url should probably be our callback (which providers will call to pass the OAuth code/state).

def _sync_initialize(self) -> None:
"""Synchronous database initialization."""
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With max_workers=1, the lock is redundant. Only one task runs at a time in the executor, so the lock doesn’t add safety. If setting max_workers>1 but keeping a single shared connection plus the lock, everything is still serialized by that lock, so we gain no parallelism. check_same_thread=False is only needed if the connection might be used from multiple threads.

So I'd say we could just drop the lock and leave SQLite checking the thread is the same?

"""Load a session by MXCP token hash."""
...

async def load_session_by_id(self, session_id: str) -> Session | None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think load_session_by_id isn't needed. And we miss load_session_by_code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants