blob: 51dc341f1ae06f4b0655d22195945cb11436044e [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Canonical request credentials for MCP bearer authentication."""
from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import Any
EMPTY_CREDENTIAL = ""
@dataclass(frozen=True, slots=True)
class BearerCredentials:
"""Normalized credentials passed to every bearer authentication provider."""
scheme: str = ""
token: str = field(default="", repr=False)
client_ip: str = "unknown"
session_id: str = ""
@property
def is_bearer(self) -> bool:
return self.scheme == "bearer" and bool(self.token)
@property
def is_static_token(self) -> bool:
return self.scheme in {"bearer", "token"} and bool(self.token)
@classmethod
def from_authorization(
cls,
authorization: str | None,
*,
client_ip: str = "unknown",
session_id: str = "",
) -> "BearerCredentials":
"""Normalize an Authorization header without retaining the raw header."""
raw_header = str(authorization or "").strip()
scheme, separator, token = raw_header.partition(" ")
normalized_scheme = scheme.lower()
if (
not separator
or normalized_scheme not in {"bearer", "token"}
or not token.strip()
):
token = EMPTY_CREDENTIAL
return cls(
scheme=normalized_scheme,
token=token.strip(),
client_ip=str(client_ip or "unknown"),
session_id=str(session_id or ""),
)
def normalize_bearer_credentials(
auth_input: BearerCredentials | Mapping[str, Any],
) -> BearerCredentials:
"""Convert a legacy auth mapping once at the authentication boundary."""
if isinstance(auth_input, BearerCredentials):
return auth_input
client_ip = str(auth_input.get("client_ip") or "unknown")
session_id = str(auth_input.get("session_id") or "")
credentials = BearerCredentials.from_authorization(
auth_input.get("authorization"),
client_ip=client_ip,
session_id=session_id,
)
explicit_token = str(
auth_input.get("token") or auth_input.get("access_token") or ""
).strip()
if not explicit_token:
return credentials
scheme = credentials.scheme
if scheme not in {"bearer", "token"}:
scheme = "bearer"
return BearerCredentials(
scheme=scheme,
token=explicit_token,
client_ip=client_ip,
session_id=session_id,
)