use http::{HeaderMap, HeaderName, header::ToStrError};
use snafu::prelude::*;
use strum::EnumMessage as _;
use crate::{
core::secrets::SecretString,
error::{ToRfc6750Error, TokenErrorCode, TokenValidationError},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenType {
Bearer,
DPoP,
}
pub fn extract_token(
headers: &HeaderMap,
token_header: &HeaderName,
) -> Result<Option<(TokenType, SecretString)>, TokenExtractError> {
let Some(token_header) = headers
.get(token_header)
.map(|hv| hv.to_str().context(TokenNotStringSnafu))
.transpose()?
else {
return Ok(None);
};
let token_header_fields = token_header.split_whitespace().take(3).collect::<Vec<_>>();
if token_header_fields.len() != 2 {
InvalidTokenHeaderFormatSnafu.fail()?;
}
let (token_type, token_value) = (token_header_fields[0], token_header_fields[1]);
let token_type = if token_type.eq_ignore_ascii_case("bearer") {
TokenType::Bearer
} else if token_type.eq_ignore_ascii_case("dpop") {
TokenType::DPoP
} else {
UnsupportedTokenTypeSnafu { token_type }.fail()?
};
let access_token = SecretString::new(token_value);
Ok(Some((token_type, access_token)))
}
#[derive(Debug, Snafu, strum::EnumMessage)]
#[non_exhaustive]
pub enum TokenExtractError {
#[strum(message = "The access token header value is not a valid string")]
TokenNotString {
source: ToStrError,
},
#[strum(message = "The access token header format is invalid")]
InvalidTokenHeaderFormat,
#[strum(message = "The access token type is unsupported")]
UnsupportedTokenType {
token_type: String,
},
}
impl ToRfc6750Error for TokenExtractError {
fn attempted_scheme(&self) -> Option<TokenType> {
match self {
TokenExtractError::UnsupportedTokenType { token_type } => {
if token_type.eq_ignore_ascii_case("dpop") {
Some(TokenType::DPoP)
} else if token_type.eq_ignore_ascii_case("bearer") {
Some(TokenType::Bearer)
} else {
None
}
}
_ => None,
}
}
fn token_error(&self) -> TokenValidationError {
TokenValidationError::Client(TokenErrorCode::InvalidRequest)
}
fn error_description(&self) -> Option<String> {
self.get_message().map(str::to_string)
}
}
#[cfg(test)]
mod tests {
use http::{HeaderValue, header::AUTHORIZATION};
use rstest::rstest;
use super::*;
fn auth_headers(value: &'static str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(AUTHORIZATION, HeaderValue::from_static(value));
headers
}
fn extract(
value: &'static str,
) -> Result<Option<(TokenType, SecretString)>, TokenExtractError> {
extract_token(&auth_headers(value), &AUTHORIZATION)
}
#[rstest]
#[case::bearer("Bearer mF_9.B5f-4.1JqM", TokenType::Bearer, "mF_9.B5f-4.1JqM")]
#[case::dpop("DPoP mF_9.B5f-4.1JqM", TokenType::DPoP, "mF_9.B5f-4.1JqM")]
#[case::lower_bearer("bearer tok", TokenType::Bearer, "tok")]
#[case::upper_bearer("BEARER tok", TokenType::Bearer, "tok")]
#[case::lower_dpop("dpop tok", TokenType::DPoP, "tok")]
#[case::upper_dpop("DPOP tok", TokenType::DPoP, "tok")]
#[case::whitespace(" Bearer the-token ", TokenType::Bearer, "the-token")]
fn valid_token_is_extracted(
#[case] header: &'static str,
#[case] expected_type: TokenType,
#[case] expected_token: &str,
) {
let (token_type, token) = extract(header).unwrap().unwrap();
assert_eq!(token_type, expected_type);
assert_eq!(token.expose_secret(), expected_token);
}
#[rstest]
#[case::scheme_only("Bearer")]
#[case::empty(" ")]
#[case::three_fields("Bearer tok extra")]
fn invalid_format_is_rejected(#[case] header: &'static str) {
assert!(
matches!(
extract(header),
Err(TokenExtractError::InvalidTokenHeaderFormat)
),
"expected InvalidTokenHeaderFormat for {header:?}"
);
}
#[test]
fn absent_header_is_none() {
let headers = HeaderMap::new();
assert!(matches!(extract_token(&headers, &AUTHORIZATION), Ok(None)));
}
#[test]
fn unsupported_scheme_is_rejected() {
let err = extract("Basic dXNlcjpwYXNz").unwrap_err();
assert!(
matches!(err, TokenExtractError::UnsupportedTokenType { ref token_type } if token_type == "Basic"),
"got {err:?}"
);
}
#[test]
fn non_utf8_header_value_is_rejected() {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_bytes(b"Bearer \xff").unwrap(),
);
assert!(matches!(
extract_token(&headers, &AUTHORIZATION),
Err(TokenExtractError::TokenNotString { .. })
));
}
#[test]
fn token_is_read_from_the_configured_header() {
let header = HeaderName::from_static("x-access-token");
let mut headers = HeaderMap::new();
headers.insert(
&header,
HeaderValue::from_static("Bearer custom-header-token"),
);
let (token_type, token) = extract_token(&headers, &header).unwrap().unwrap();
assert_eq!(token_type, TokenType::Bearer);
assert_eq!(token.expose_secret(), "custom-header-token");
assert!(matches!(extract_token(&headers, &AUTHORIZATION), Ok(None)));
}
}