feat(auth): support HTTP bearer-token authentication (#8719)

* feat(auth): support HTTP bearer-token authentication (#8718)

Adds an opt-in bearer-token (JWT / OAuth2) authentication path to the HTTP
layer, so clients can authenticate with `Authorization: Bearer <token>`
against any `/v1/` interface. Today such requests are rejected with
`UnsupportedAuthScheme("bearer")` -> 401 before any handler runs.

The token is treated as opaque by the server; validation and identity
derivation stay in the UserProvider, so JWT/JWKS/OIDC policy remains
pluggable and out of core.

Changes:

- `auth::UserProvider` gains `auth_token(token, catalog, schema) ->
  Result<UserInfoRef>` with a default that rejects
  (`Error::UnsupportedAuthMethod`), so password-only providers keep today's
  behavior. A provider that supports token auth overrides it to validate the
  token, resolve it to a user, and authorize the connection.
- `auth::Error::UnsupportedAuthMethod` for the default-reject case.
- `servers::http::authorize::inner_auth` extracts a bearer token
  (`extract_bearer_token`) and, when present, authenticates via
  `UserProvider::auth_token`; otherwise it falls through unchanged to the
  username/password path (Basic / influxdb / splunk). Basic and bearer
  coexist on the same server.

Backward compatible: the default impl preserves existing behavior, and
non-bearer requests take the exact same path as before.

Tests:
- `extract_bearer_token` recognizes `Bearer` (either header) and ignores
  Basic/Token/Splunk/empty.
- `inner_auth` dispatches a bearer token to `auth_token` and populates the
  QueryContext user on success; rejects on failure.
- A password-only provider (default `auth_token`) rejects bearer tokens.

Refs: #8718

* chore: fmt

* refactor(auth): address bearer-auth review feedback (#8719)

Address the review comments on the HTTP bearer-token authentication PR:

- Match the `Bearer` scheme case-insensitively (RFC 9110 §11.1) via
  `eq_ignore_ascii_case`. `extract_bearer_token` previously only accepted
  `Bearer`/`bearer`, so a valid `BEARER <token>` fell through to
  `UnsupportedAuthScheme` and never reached the provider. The opaque token
  itself is deliberately not lowercased.
- Return `Option<&str>` (borrowing the request headers) instead of
  `Option<String>`, avoiding an allocation per bearer request.
- Rename `UserProvider::auth_token` -> `auth_bearer_token` for clarity.
- Route bearer-auth failures on Splunk HEC requests through `splunk_hec_err`
  (FORBIDDEN, code 4) instead of the generic 401 `ErrorResponse`, so HEC
  clients retain their `{"text":"Invalid token","code":4}` endpoint contract.

Adds test coverage for case-insensitive scheme parsing (token preserved
verbatim) and a regression test for the bearer/splunk routing path.

Signed-off-by: Ning Sun <sunning@greptime.com>

---------

Signed-off-by: Ning Sun <sunning@greptime.com>
This commit is contained in:
Ning Sun
2026-08-10 08:28:11 +00:00
committed by GitHub
parent 7fa6f5f98e
commit 9fcfeff9ce
3 changed files with 259 additions and 4 deletions
+8
View File
@@ -88,6 +88,13 @@ pub enum Error {
#[snafu(implicit)]
location: Location,
},
#[snafu(display("Unsupported authentication method: {}", method))]
UnsupportedAuthMethod {
method: String,
#[snafu(implicit)]
location: Location,
},
}
impl ErrorExt for Error {
@@ -105,6 +112,7 @@ impl ErrorExt for Error {
Error::UserPasswordMismatch { .. } => StatusCode::UserPasswordMismatch,
Error::AccessDenied { .. } => StatusCode::AccessDenied,
Error::PermissionDenied { .. } => StatusCode::PermissionDenied,
Error::UnsupportedAuthMethod { .. } => StatusCode::UserPasswordMismatch,
}
}
+27 -2
View File
@@ -34,8 +34,8 @@ use crate::common::{
PgScramSha256Verifier, auth_mysql_with_hash_stage_2,
};
use crate::error::{
IllegalParamSnafu, InvalidConfigSnafu, IoSnafu, Result, UnsupportedPasswordTypeSnafu,
UserNotFoundSnafu, UserPasswordMismatchSnafu,
IllegalParamSnafu, InvalidConfigSnafu, IoSnafu, Result, UnsupportedAuthMethodSnafu,
UnsupportedPasswordTypeSnafu, UserNotFoundSnafu, UserPasswordMismatchSnafu,
};
use crate::user_info::{DefaultUserInfo, PermissionMode};
use crate::{UserInfoRef, auth_mysql};
@@ -66,6 +66,31 @@ pub trait UserProvider: Send + Sync {
Ok(user_info)
}
/// Authenticates and authorizes an opaque bearer token (e.g. a JWT or an
/// OAuth2 access token).
///
/// Unlike [auth()](Self::auth), the caller has no `Identity`/`Password` —
/// the provider validates the token and *derives* the identity from it.
/// The token is opaque to the server, so JWT/JWKS/OIDC validation policy
/// stays pluggable and out of core.
///
/// The default rejects token auth with
/// [`Error::UnsupportedAuthMethod`], so password-only providers keep
/// today's behavior. Providers that support token auth override this to
/// validate the token, resolve it to a user, and
/// [`authorize`](Self::authorize) the connection.
async fn auth_bearer_token(
&self,
_token: &str,
_catalog: &str,
_schema: &str,
) -> Result<UserInfoRef> {
UnsupportedAuthMethodSnafu {
method: "bearer token",
}
.fail()
}
async fn postgres_auth_info(&self, _id: Identity<'_>) -> Result<PgAuthInfo> {
Ok(PgAuthInfo::Cleartext)
}