fix: 后端 JWT 验证改为 HS256 方式提升认证可靠性
This commit is contained in:
@@ -9,56 +9,53 @@ class TokenValidationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TokenVerifierUnavailableError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class JwtVerifier:
|
||||
_expected_audience = "authenticated"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
jwks_url: str,
|
||||
issuer: str,
|
||||
audience: str,
|
||||
apikey: str,
|
||||
jwt_secret: str,
|
||||
jwt_algorithm: str,
|
||||
) -> None:
|
||||
if jwt_algorithm != "HS256":
|
||||
raise TokenValidationError("Unsupported JWT algorithm")
|
||||
|
||||
self._issuer: str = issuer
|
||||
self._audience: str = audience
|
||||
self._jwks_client: jwt.PyJWKClient = jwt.PyJWKClient(
|
||||
jwks_url,
|
||||
headers={
|
||||
"apikey": apikey,
|
||||
"Authorization": f"Bearer {apikey}",
|
||||
},
|
||||
)
|
||||
self._jwt_secret: str = jwt_secret
|
||||
self._jwt_algorithm: str = jwt_algorithm
|
||||
|
||||
def verify(self, token: str) -> dict[str, Any]:
|
||||
try:
|
||||
key = self._jwks_client.get_signing_key_from_jwt(token)
|
||||
except jwt.PyJWKClientConnectionError as exc:
|
||||
raise TokenVerifierUnavailableError("Unable to fetch JWKS") from exc
|
||||
except jwt.PyJWKClientError as exc:
|
||||
raise TokenValidationError("Unable to resolve signing key") from exc
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
key.key,
|
||||
algorithms=["RS256"],
|
||||
audience=self._audience,
|
||||
issuer=self._issuer,
|
||||
options={"require": ["sub", "aud", "iss", "exp"]},
|
||||
self._jwt_secret,
|
||||
algorithms=[self._jwt_algorithm],
|
||||
options={"require": ["sub", "exp", "aud"], "verify_aud": False},
|
||||
)
|
||||
except (
|
||||
jwt.ExpiredSignatureError,
|
||||
jwt.InvalidAudienceError,
|
||||
jwt.InvalidIssuerError,
|
||||
jwt.InvalidSignatureError,
|
||||
jwt.InvalidAlgorithmError,
|
||||
jwt.DecodeError,
|
||||
jwt.PyJWTError,
|
||||
) as exc:
|
||||
raise TokenValidationError("Token validation failed") from exc
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
raise TokenValidationError("Token payload must be a JSON object")
|
||||
token_audience = payload.get("aud")
|
||||
if isinstance(token_audience, str):
|
||||
audience_match = token_audience == self._expected_audience
|
||||
elif isinstance(token_audience, list):
|
||||
audience_match = self._expected_audience in token_audience
|
||||
else:
|
||||
audience_match = False
|
||||
|
||||
if not audience_match:
|
||||
raise TokenValidationError("Token audience mismatch")
|
||||
|
||||
token_issuer = payload.get("iss")
|
||||
if token_issuer is not None and token_issuer != self._issuer:
|
||||
raise TokenValidationError("Token issuer mismatch")
|
||||
|
||||
return cast(dict[str, Any], payload)
|
||||
|
||||
@@ -8,6 +8,7 @@ from pydantic import (
|
||||
AnyHttpUrl,
|
||||
BaseModel,
|
||||
Field,
|
||||
SecretStr,
|
||||
computed_field,
|
||||
field_validator,
|
||||
model_validator,
|
||||
@@ -126,9 +127,9 @@ class SupabaseSettings(BaseModel):
|
||||
public_url: AnyHttpUrl
|
||||
anon_key: str = "CHANGE_ME"
|
||||
service_role_key: str = "CHANGE_ME"
|
||||
jwt_audience: str = "authenticated"
|
||||
jwt_secret: SecretStr | None = Field(default=None, exclude=True)
|
||||
jwt_algorithm: Literal["HS256"] = "HS256"
|
||||
jwt_issuer: str | None = None
|
||||
jwks_url: str | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def compute_defaults(self) -> "SupabaseSettings":
|
||||
@@ -136,9 +137,6 @@ class SupabaseSettings(BaseModel):
|
||||
if self.jwt_issuer is None:
|
||||
self.jwt_issuer = f"{base}/auth/v1"
|
||||
|
||||
if self.jwks_url is None:
|
||||
self.jwks_url = f"{self.jwt_issuer}/.well-known/jwks.json"
|
||||
|
||||
return self
|
||||
|
||||
@computed_field
|
||||
|
||||
Reference in New Issue
Block a user