Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip, implements simple api-key header extraction for middleware verif… #1

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
113 changes: 113 additions & 0 deletions actix-web-httpauth/src/extractors/api_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//! Extractor for the "Basic" HTTP Authentication Scheme.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Basic/API-Key/ in all the docs


use std::borrow::Cow;

use actix_utils::future::{ready, Ready};
use actix_web::{dev::Payload, http::header::Header, FromRequest, HttpRequest};

use super::{config::AuthExtractorConfig, errors::AuthenticationError};
use crate::headers::{
api_key::{APIKey, XAPIKey},
www_authenticate::basic::Basic as Challenge,
};

/// [`BasicAuth`] extractor configuration used for [`WWW-Authenticate`] header later.
///
/// [`WWW-Authenticate`]: crate::headers::www_authenticate::WwwAuthenticate
#[derive(Debug, Clone, Default)]
pub struct Config(Challenge);

impl Config {
/// Set challenge `realm` attribute.
///
/// The "realm" attribute indicates the scope of protection in the manner described in HTTP/1.1
/// [RFC 2617 §1.2](https://tools.ietf.org/html/rfc2617#section-1.2).
pub fn realm<T>(mut self, value: T) -> Config
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this is not valid for API-Key auth. However I see the Bearer extractor has realm, so maybe I am wrong.

The config that would be useful is what is the name of the header containing the api-key. i.e. it shouldnt be hard-coded to x-api-key.
If not configurable, the docs for the module should indicate the limitations compared with https://swagger.io/docs/specification/authentication/api-keys/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re Bearer realm - it is allowed in the spec https://datatracker.ietf.org/doc/html/rfc6750#section-3

wrt X-API-Key, we should see if there is anyone using realm with this header

where
T: Into<Cow<'static, str>>,
{
self.0.realm = Some(value.into());
self
}
}

impl AsRef<Challenge> for Config {
fn as_ref(&self) -> &Challenge {
&self.0
}
}

impl AuthExtractorConfig for Config {
type Inner = Challenge;

fn into_inner(self) -> Self::Inner {
self.0
}
}

/// Extractor for HTTP Basic auth.
///
/// # Examples
/// ```
/// use actix_web_httpauth::extractors::basic::BasicAuth;
///
/// async fn index(auth: BasicAuth) -> String {
/// format!("Hello, {}!", auth.user_id())
/// }
/// ```
///
/// If authentication fails, this extractor fetches the [`Config`] instance from the [app data] in
/// order to properly form the `WWW-Authenticate` response header.
///
/// # Examples
/// ```
/// use actix_web::{web, App};
/// use actix_web_httpauth::extractors::basic::{self, BasicAuth};
///
/// async fn index(auth: BasicAuth) -> String {
/// format!("Hello, {}!", auth.user_id())
/// }
///
/// App::new()
/// .app_data(basic::Config::default().realm("Restricted area"))
/// .service(web::resource("/index.html").route(web::get().to(index)));
/// ```
///
/// [app data]: https://docs.rs/actix-web/4/actix_web/struct.App.html#method.app_data
#[derive(Debug, Clone)]
pub struct APIKeyAuth(APIKey);

impl APIKeyAuth {
/// Returns client's user-ID.
pub fn api_key(&self) -> &str {
self.0.api_key()
}
}

impl From<APIKey> for APIKeyAuth {
fn from(api_key: APIKey) -> Self {
Self(api_key)
}
}

impl FromRequest for APIKeyAuth {
type Future = Ready<Result<Self, Self::Error>>;
type Error = AuthenticationError<Challenge>;

fn from_request(req: &HttpRequest, _: &mut Payload) -> <Self as FromRequest>::Future {
ready(
XAPIKey::<APIKey>::parse(req)
.map(|auth| APIKeyAuth(auth.into_scheme()))
.map_err(|err| {
log::debug!("`APIKeAuth` extract error: {}", err);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APIKeAuth->APIKeyAuth


let challenge = req
.app_data::<Config>()
.map(|config| config.0.clone())
.unwrap_or_default();

AuthenticationError::new(challenge)
}),
)
}
}
1 change: 1 addition & 0 deletions actix-web-httpauth/src/extractors/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Type-safe authentication information extractors.

pub mod api_key;
pub mod basic;
pub mod bearer;
mod config;
Expand Down
84 changes: 84 additions & 0 deletions actix-web-httpauth/src/headers/api_key/header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::fmt;

use actix_web::{
error::ParseError,
http::header::{Header, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION},
HttpMessage,
};

use crate::headers::api_key::scheme::Scheme;

/// `Authorization` header, defined in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.2)
///
/// The "Authorization" header field allows a user agent to authenticate itself with an origin
/// server—usually, but not necessarily, after receiving a 401 (Unauthorized) response. Its value
/// consists of credentials containing the authentication information of the user agent for the
/// realm of the resource being requested.
///
/// `Authorization` is generic over an [authentication scheme](Scheme).
///
/// # Examples
/// ```
/// # use actix_web::{HttpRequest, Result, http::header::Header};
/// # use actix_web_httpauth::headers::authorization::{Authorization, Basic};
/// fn handler(req: HttpRequest) -> Result<String> {
/// let auth = Authorization::<Basic>::parse(&req)?;
///
/// Ok(format!("Hello, {}!", auth.as_ref().user_id()))
/// }
/// ```
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct XAPIKey<S: Scheme>(S);

impl<S: Scheme> XAPIKey<S> {
/// Consumes `X-API-KEY` header and returns inner [`Scheme`] implementation.
pub fn into_scheme(self) -> S {
self.0
}
}

impl<S: Scheme> From<S> for XAPIKey<S> {
fn from(scheme: S) -> XAPIKey<S> {
XAPIKey(scheme)
}
}

impl<S: Scheme> AsRef<S> for XAPIKey<S> {
fn as_ref(&self) -> &S {
&self.0
}
}

impl<S: Scheme> AsMut<S> for XAPIKey<S> {
fn as_mut(&mut self) -> &mut S {
&mut self.0
}
}

impl<S: Scheme> fmt::Display for XAPIKey<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}

impl<S: Scheme> Header for XAPIKey<S> {
#[inline]
fn name() -> HeaderName {
HeaderName::from_static("x-api-key")
}

fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError> {
let header = msg.headers().get(Self::name()).ok_or(ParseError::Header)?;
let scheme = S::parse(header).map_err(|_| ParseError::Header)?;

Ok(XAPIKey(scheme))
}
}

impl<S: Scheme> TryIntoHeaderValue for XAPIKey<S> {
type Error = <S as TryIntoHeaderValue>::Error;

fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
self.0.try_into_value()
}
}
9 changes: 9 additions & 0 deletions actix-web-httpauth/src/headers/api_key/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! `Authorization` header and various auth schemes.

mod header;
mod scheme;

pub use self::{
header::XAPIKey,
scheme::{api_key::APIKey, Scheme},
};
113 changes: 113 additions & 0 deletions actix-web-httpauth/src/headers/api_key/scheme/api_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use std::{borrow::Cow, fmt, str};

Check warning on line 2 in actix-web-httpauth/src/headers/api_key/scheme/api_key.rs

View workflow job for this annotation

GitHub Actions / fmt

Diff in /home/runner/work/actix-extras/actix-extras/actix-web-httpauth/src/headers/api_key/scheme/api_key.rs
use actix_web::http::header::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue};

use crate::headers::api_key::Scheme;
use crate::headers::errors::ParseError;

/// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617)
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct APIKey {
api_key: Cow<'static, str>,
}

impl APIKey {
/// Creates `Basic` credentials with provided `user_id` and optional
/// `password`.
///
/// # Examples
/// ```
/// # use actix_web_httpauth::headers::authorization::Basic;
/// let credentials = Basic::new("Alladin", Some("open sesame"));
/// ```
pub fn new<U>(api_key: U) -> APIKey
where
U: Into<Cow<'static, str>>,
{
APIKey {
api_key: api_key.into(),
}
}

/// Returns client's user-ID.
pub fn api_key(&self) -> &str {
&self.api_key.as_ref()
}
}

impl Scheme for APIKey {
fn parse(header: &HeaderValue) -> Result<Self, ParseError> {
// "Basic *" length
if header.len() < 36 {
return Err(ParseError::Invalid);
}
let api_key = header.to_str()?.to_string();

Ok(APIKey::new(api_key))
}
}

impl fmt::Debug for APIKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!("APIKey: ******"))
}
}

impl fmt::Display for APIKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!("APIKey: ******"))
}
}

impl TryIntoHeaderValue for APIKey {
type Error = InvalidHeaderValue;

fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
let value = String::from(self.api_key);
HeaderValue::from_maybe_shared(value)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_header() {
let key = "0451f2f1-74a7-4b8c-994d-2f67675ba07c";
let value = HeaderValue::from_static(key);
let scheme = APIKey::parse(&value);

assert!(scheme.is_ok());
let scheme = scheme.unwrap();
assert_eq!(scheme.api_key, key);
}

#[test]
fn test_empty_header() {
let value = HeaderValue::from_static("");
let scheme = APIKey::parse(&value);

assert!(scheme.is_err());
}

#[test]
fn test_wrong_scheme() {
let value = HeaderValue::from_static("THOUSHALLNOTPASS please?");
let scheme = APIKey::parse(&value);

assert!(scheme.is_err());
}

#[test]
fn test_into_header_value() {
let key = "0451f2f1-74a7-4b8c-994d-2f67675ba07c";
let basic = APIKey {
api_key: key.into(),
};

let result = basic.try_into_value();
assert!(result.is_ok());
assert_eq!(result.unwrap(), HeaderValue::from_static(key));
}
}
13 changes: 13 additions & 0 deletions actix-web-httpauth/src/headers/api_key/scheme/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use std::fmt::{Debug, Display};

use actix_web::http::header::{HeaderValue, TryIntoHeaderValue};

pub mod api_key;

use crate::headers::errors::ParseError;

/// Authentication scheme for [`Authorization`](super::Authorization) header.
pub trait Scheme: TryIntoHeaderValue + Debug + Display + Clone + Send + Sync {
/// Try to parse an authentication scheme from the `Authorization` header.
fn parse(header: &HeaderValue) -> Result<Self, ParseError>;
}
2 changes: 0 additions & 2 deletions actix-web-httpauth/src/headers/authorization/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
//! `Authorization` header and various auth schemes.

mod errors;
mod header;
mod scheme;

pub use self::{
errors::ParseError,
header::Authorization,
scheme::{basic::Basic, bearer::Bearer, Scheme},
};
3 changes: 2 additions & 1 deletion actix-web-httpauth/src/headers/authorization/scheme/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
use actix_web::{
http::header::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue},
web::{BufMut, BytesMut},
};

Check warning on line 6 in actix-web-httpauth/src/headers/authorization/scheme/basic.rs

View workflow job for this annotation

GitHub Actions / fmt

Diff in /home/runner/work/actix-extras/actix-extras/actix-web-httpauth/src/headers/authorization/scheme/basic.rs
use base64::{prelude::BASE64_STANDARD, Engine};

use crate::headers::authorization::{errors::ParseError, Scheme};
use crate::headers::authorization::Scheme;
use crate::headers::errors::ParseError;

/// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617)
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

use actix_web::{
http::header::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue},
web::{BufMut, BytesMut},

Check warning on line 5 in actix-web-httpauth/src/headers/authorization/scheme/bearer.rs

View workflow job for this annotation

GitHub Actions / fmt

Diff in /home/runner/work/actix-extras/actix-extras/actix-web-httpauth/src/headers/authorization/scheme/bearer.rs
};

use crate::headers::authorization::{errors::ParseError, scheme::Scheme};
use crate::headers::authorization::scheme::Scheme;
use crate::headers::errors::ParseError;

/// Credentials for `Bearer` authentication scheme, defined in [RFC 6750].
///
Expand Down
2 changes: 1 addition & 1 deletion actix-web-httpauth/src/headers/authorization/scheme/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use actix_web::http::header::{HeaderValue, TryIntoHeaderValue};
pub mod basic;
pub mod bearer;

use crate::headers::authorization::errors::ParseError;
use crate::headers::errors::ParseError;

/// Authentication scheme for [`Authorization`](super::Authorization) header.
pub trait Scheme: TryIntoHeaderValue + Debug + Display + Clone + Send + Sync {
Expand Down
3 changes: 3 additions & 0 deletions actix-web-httpauth/src/headers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
//! Typed HTTP headers.

pub mod api_key;
pub mod authorization;
///
pub mod errors;
pub mod www_authenticate;
Loading
Loading