Skip to content

Question: How to authorise a client with Bearer header with SSE? #431

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

Open
Gelembjuk opened this issue Apr 4, 2025 · 7 comments
Open

Question: How to authorise a client with Bearer header with SSE? #431

Gelembjuk opened this issue Apr 4, 2025 · 7 comments

Comments

@Gelembjuk
Copy link

I am building the MCP server application to connect some services to LLM .
One of things i want to implement is authorisation of a user with the token.

I see it must be possible somehow. Because MCP inspector has the Authentification and Bearer Token field

Most of tutorials about MCP are related to STDIO kind of a server run. My will be SSE.

There is my code:

from mcp.server.fastmcp import FastMCP
from fastapi import FastAPI, Request, Depends, HTTPException

app = FastAPI()
mcp = FastMCP("SMB Share Server")

@mcp.tool()
def create_folder(parent_path: str, name: str) -> str:
    """Create new subfolder in the specified path"""
    return f"Folder {name} created in {parent_path}"

app.mount("/", mcp.sse_app())

How can i read Authorization header in case if it is sent by the client?

I tried to use approaches of FastAPI - setting dependency, adding request:Request to arguments but this doesn't work.

Is there a way?

@scott-clare1
Copy link

I was also hoping to do something like this too, it seems like this is possible in the typescript SDK currently. It looks like there's a couple of open PR's for adding OAuth to this project but I've not seen anything on bearer tokens.

@scott-clare1
Copy link

Have you tried mounting the server directly to a Starlette app rather than FastAPI? As per the docs: https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#mounting-to-an-existing-asgi-server. You'd then have to write some auth middleware? https://www.starlette.io/authentication/

@lucassampsouza
Copy link

I have the same question.
I created an SSE server and put basic authentication middleware and that is OK, but I can't retrieve authentication headers on tool run

To work around this, I created a class to save lastUserSession and I got that on tool run, but I know it's not the best solution.
But solved for testing purposes at the moment

But I'm waiting for someone with a better idea
Here is my sample code

from starlette.applications import Starlette
from starlette.routing import Mount
from mcp.server.fastmcp import FastMCP, Context
from mcp.server import Server
from urllib.parse import urlparse, parse_qs

from contextlib import asynccontextmanager
from collections.abc import AsyncIterator

from starlette.applications import Starlette
from starlette.authentication import (
    AuthCredentials, AuthenticationBackend, AuthenticationError, SimpleUser
)
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
import base64
import binascii

class LastUserSession:
    def __init__(self):
        self._current_session = None
        self._current_username = None
    
    def set_session(self, session_id, username):
        """Store the username of the last request"""
        self._current_session = session_id
        self._current_username = username
        #print(f"Latest session updated: {session_id} for user {username}")
    
    def get_username(self, session_id=None):
        """Get the username from the last request"""
        # session_id parameter is kept for compatibility but ignored
        return self._current_username
    
    def get_current_session(self):
        """Get the session_id from the last request"""
        return self._current_session

# Instância da classe para armazenar apenas a última sessão
user_sessions = LastUserSession()

class BasicAuthBackend(AuthenticationBackend):
    async def authenticate(self, conn):
        
        # Extrair o session_id da URL
        parsed_url = urlparse(str(conn.url))
        query_params = parse_qs(parsed_url.query)
        session_id = query_params.get('session_id', [None])[0]
        
        if "Authorization" not in conn.headers:
            return

        auth = conn.headers["Authorization"]
        try:
            scheme, credentials = auth.split()
            if scheme.lower() != 'basic':
                return
            decoded = base64.b64decode(credentials).decode("ascii")
        except (ValueError, UnicodeDecodeError, binascii.Error) as exc:
            raise AuthenticationError('Invalid basic auth credentials')

        username, _, password = decoded.partition(":")
        
        # Armazena a relação entre session_id e username usando a classe
        user_sessions.set_session(session_id, username)
        
        return AuthCredentials(["authenticated"]), SimpleUser(username)

@asynccontextmanager
async def server_lifespan(server: Server) -> AsyncIterator[dict]:
    """Manage server startup and shutdown lifecycle."""
    # Initialize resources on startup
    yield {"user": user_sessions.get_username()}

# mcp = FastMCP("Example APP", lifespan=server_lifespan, log_level='DEBUG')
mcp = FastMCP("Example APP", lifespan=server_lifespan)

# Add an addition tool
@mcp.tool()
async def who_kill_odete_roitman(ctx: Context) -> str:
    """Answer the question Who Kill Odete Roitman"""
    
    return f"{ctx.request_context.lifespan_context['user']} killed Odete Roitman"

routes=[
        Mount('/', app=mcp.sse_app()),
    ]

middleware = [
    Middleware(AuthenticationMiddleware, backend=BasicAuthBackend()),
]

# Mount the SSE server to the existing ASGI server
app = Starlette(routes=routes, middleware=middleware)

@samirbajaj
Copy link

I have the same question -- it looks like this PR will fix it?

@josx
Copy link

josx commented Apr 10, 2025

mcp.sse_app() is one Route /sse and a Mount app on /messages/.

There is a way to use starlette permission with decoration and override handle_sse func.
Somethin like:

for route in app.routes:
  if route.path == "/sse":
    original_endpoint = route.endpoint
    route.endpoint = requires("authenticated")(original_endpoint)

But I dont know how to add this decorator to the Mounted app.

@josx
Copy link

josx commented Apr 10, 2025

Finally i make it work, here anexample.

import jwt
import datetime
from starlette.applications import Starlette
from starlette.routing import Mount, Route
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.responses import JSONResponse, PlainTextResponse
from starlette.authentication import (
    AuthCredentials, AuthenticationBackend, AuthenticationError, SimpleUser
)
from starlette.authentication import requires
from starlette.middleware import Middleware
from server import mcp
from pprint import pprint
from mcp.server.sse import SseServerTransport

JWT_SECRET = "your-secret-key"

async def welcome_message(request):
    return PlainTextResponse("Demo Mcp with jwt auth")


class JWTAuthentication(AuthenticationBackend):
    async def authenticate(self, request):
        auth_header = request.headers.get("Authorization")
        if not auth_header:
            return None
        token = auth_header.split(" ")[1]
        try:
            payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
            return AuthCredentials(["authenticated"]), SimpleUser(payload)
        except jwt.ExpiredSignatureError:
            raise AuthenticationError("Expired signature")
        except jwt.InvalidTokenError:
            raise AuthenticationError("Invalid token")


async def get_token(request):
    username = request.query_params.get("username", "demo_user")
    scope = request.query_params.get("scope", "mcp:access")
    token = jwt.encode({
        "username": username,
        "scope": scope,
        "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
    }, JWT_SECRET, algorithm="HS256")
    return JSONResponse({"token": token})


# Create SSE transport
sse = SseServerTransport("/messages/")


# MCP SSE handler function
async def handle_sse(request):
    async with sse.connect_sse(request.scope, request.receive, request._send) as (
        read_stream,
        write_stream,
    ):
        await mcp._mcp_server.run(
            read_stream, write_stream, mcp._mcp_server.create_initialization_options()
        )

async def handle_messages(request):
    await sse.handle_post_message(request.scope, request.receive, request._send)


routes = [
    Route("/", endpoint=welcome_message),
    Route("/get_token", get_token),
    # MCP related routes
    Route("/sse", endpoint=requires("authenticated")(handle_sse)),
    Route("/messages/", endpoint=requires("authenticated")(handle_messages), methods=["POST"]),
]

app = Starlette(
            debug=True,
            routes=routes
        )
app.add_middleware(AuthenticationMiddleware, backend=JWTAuthentication())


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0")

it is working but in every call i got a exception:

ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File ".venv/lib/python3.11/site-packages/uvicorn/protocols/http/h11_impl.py", line 403, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".venv/lib/python3.11/site-packages/starlette/applications.py", line 112, in __call__
    await self.middleware_stack(scope, receive, send)
  File ".venv/lib/python3.11/site-packages/starlette/middleware/errors.py", line 187, in __call__
    raise exc
  File ".venv/lib/python3.11/site-packages/starlette/middleware/errors.py", line 165, in __call__
    await self.app(scope, receive, _send)
  File ".venv/lib/python3.11/site-packages/starlette/middleware/authentication.py", line 48, in __call__
    await self.app(scope, receive, send)
  File ".venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File ".venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    raise exc
  File ".venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
    await app(scope, receive, sender)
  File ".venv/lib/python3.11/site-packages/starlette/routing.py", line 714, in __call__
    await self.middleware_stack(scope, receive, send)
  File ".venv/lib/python3.11/site-packages/starlette/routing.py", line 734, in app
    await route.handle(scope, receive, send)
  File ".venv/lib/python3.11/site-packages/starlette/routing.py", line 288, in handle
    await self.app(scope, receive, send)
  File ".venv/lib/python3.11/site-packages/starlette/routing.py", line 76, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File ".venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    raise exc
  File ".venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
    await app(scope, receive, sender)
  File ".venv/lib/python3.11/site-packages/starlette/routing.py", line 74, in app
    await response(scope, receive, send)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: 'NoneType' object is not callable

@Gelembjuk
Copy link
Author

This is how i did this for now.

from mcp.server.fastmcp import FastMCP
from fastapi import FastAPI, Request

# global variable
auth_token = ""

app = FastAPI()
mcp = FastMCP("Server to manage a Linux instance")

@app.middleware("http")
async def auth_middleware(request: Request, call_next):
    auth_header = request.headers.get("Authorization")
    if auth_header:
        # extract token from the header and keep it in the global variable
        global auth_token
        auth_token = auth_header.split(" ")[1]
    
    response = await call_next(request)
    return response

@mcp.tool()
def cli_command(command: str, work_dir: str = "") -> str:
    """
    ....
    """
    # We require each request to have the auth token, raises exception if wrong
    auth_manager.verify_token(auth_token)
    .... do the job

app.mount("/", mcp.sse_app())

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

No branches or pull requests

5 participants