Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions objectstore-server/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ A request flows through several layers before reaching the storage service:
2. **Extractors**: path parameters are parsed into an
[`ObjectId`](objectstore_service::id::ObjectId) or
[`ObjectContext`](objectstore_service::id::ObjectContext). The auth token is
read from the `X-Os-Auth` header (preferred) or the standard
`Authorization` header (fallback), then validated and decoded into an
read from the `X-Os-Auth` header (preferred), the `X-Os-Auth` query
parameter, or the standard `Authorization` header (fallback), then
validated and decoded into an
[`AuthContext`](auth::AuthContext).
The optional `x-downstream-service` header is extracted for killswitch
matching.
Expand Down
5 changes: 3 additions & 2 deletions objectstore-server/src/auth/key_directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ impl TryFrom<&AuthZVerificationKey> for PublicKeyConfig {

/// Directory of keys that may be used to verify a request's auth token.
///
/// The auth token is read from the `X-Os-Auth` header (preferred) or the
/// standard `Authorization` header (fallback). This directory contains a map keyed
/// The auth token is read from the `X-Os-Auth` header (preferred), the
/// `X-Os-Auth` query parameter, or the standard `Authorization` header
/// (fallback). This directory contains a map keyed
/// on a key's ID. When verifying a JWT, the `kid` field should be read from the
/// JWT header and used to index into this directory to select the appropriate key.
#[derive(Debug)]
Expand Down
8 changes: 5 additions & 3 deletions objectstore-server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,9 @@ pub struct AuthZ {

/// Keys that may be used to verify a request's auth token.
///
/// The auth token is read from the `X-Os-Auth` header (preferred)
/// or the standard `Authorization` header (fallback). This field is a
/// The auth token is read from the `X-Os-Auth` header (preferred),
/// the `X-Os-Auth` query parameter, or the standard `Authorization`
/// header (fallback). This field is a
/// container keyed on a key's ID. When verifying a JWT, the `kid` field
/// should be read from the JWT header and used to index into this map to
/// select the appropriate key.
Expand Down Expand Up @@ -445,7 +446,8 @@ pub struct Config {
/// Content-based authorization configuration.
///
/// Controls the verification and enforcement of content-based access control based on the
/// JWT in a request's `X-Os-Auth` or `Authorization` header.
/// JWT in a request's `X-Os-Auth` header, `X-Os-Auth` query parameter, or `Authorization`
/// header.
pub auth: AuthZ,

/// A list of matchers for requests to discard without processing.
Expand Down
54 changes: 52 additions & 2 deletions objectstore-server/src/extractors/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,32 @@ const BEARER_PREFIX: &str = "Bearer ";
/// this header.
const OBJECTSTORE_AUTH_HEADER: &str = "x-os-auth";

/// Query parameter name for Objectstore authentication. Used as a fallback when
/// headers cannot be set (e.g. browser-initiated requests). The value should be
/// the raw JWT without a `Bearer` prefix.
const OBJECTSTORE_AUTH_QUERY_PARAM: &str = "X-Os-Auth";

impl FromRequestParts<ServiceState> for AuthAwareService {
type Rejection = ApiError;

async fn from_request_parts(
parts: &mut Parts,
state: &ServiceState,
) -> Result<Self, Self::Rejection> {
// Precedence: X-Os-Auth header > X-Os-Auth query param > Authorization header.
let encoded_token = parts
.headers
.get(OBJECTSTORE_AUTH_HEADER)
.or_else(|| parts.headers.get(header::AUTHORIZATION))
.and_then(|v| v.to_str().ok())
.and_then(strip_bearer);
.and_then(strip_bearer)
.or_else(|| extract_query_param(parts.uri.query(), OBJECTSTORE_AUTH_QUERY_PARAM))
.or_else(|| {
parts
.headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(strip_bearer)
});

let enforce = state.config.auth.enforce;
// Attempt to decode / verify the JWT, logging failure
Expand Down Expand Up @@ -57,6 +70,20 @@ fn strip_bearer(header_value: &str) -> Option<&str> {
}
}

/// Extracts the value of a query parameter by name from a raw query string.
///
/// Returns `None` if the query string is absent, the parameter is not present,
/// or the parameter has no value.
fn extract_query_param<'a>(query: Option<&'a str>, param: &str) -> Option<&'a str> {
query?
.split('&')
.find_map(|pair| {
let (key, value) = pair.split_once('=')?;
(key == param).then_some(value)
})
.filter(|v| !v.is_empty())
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -75,4 +102,27 @@ mod tests {
// No character boundary at end of expected prefix
assert_eq!(strip_bearer("Bearer⚠️tokenvalue"), None);
}

#[test]
fn test_extract_query_param() {
assert_eq!(extract_query_param(None, "X-Os-Auth"), None);
assert_eq!(extract_query_param(Some(""), "X-Os-Auth"), None);
assert_eq!(extract_query_param(Some("other=val"), "X-Os-Auth"), None);
assert_eq!(
extract_query_param(Some("X-Os-Auth=mytoken"), "X-Os-Auth"),
Some("mytoken")
);
assert_eq!(
extract_query_param(Some("foo=bar&X-Os-Auth=mytoken"), "X-Os-Auth"),
Some("mytoken")
);
assert_eq!(
extract_query_param(Some("X-Os-Auth=mytoken&foo=bar"), "X-Os-Auth"),
Some("mytoken")
);
// Empty value
assert_eq!(extract_query_param(Some("X-Os-Auth="), "X-Os-Auth"), None);
// No `=` sign
assert_eq!(extract_query_param(Some("X-Os-Auth"), "X-Os-Auth"), None);
}
}
Loading