mirror of
https://github.com/actix/actix-extras.git
synced 2024-11-27 17:22:57 +01:00
Working version of the BasicAuth
This commit is contained in:
parent
082e8371c1
commit
f64c1e8879
@ -18,3 +18,5 @@ travis-ci = { repository = "svartalf/actix-web-httpauth", branch = "master" }
|
||||
[dependencies]
|
||||
actix-web = "0.6"
|
||||
base64 = "0.9"
|
||||
percent-encoding = "1.0.1"
|
||||
bytes = "0.4.7"
|
@ -1,25 +1,24 @@
|
||||
extern crate actix_web;
|
||||
extern crate actix_web_httpauth;
|
||||
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{server, App, HttpRequest, FromRequest, Result};
|
||||
use actix_web::middleware::{Middleware, Started};
|
||||
use actix_web_httpauth::BasicAuth;
|
||||
use actix_web_httpauth::basic::{BasicAuth, Config};
|
||||
|
||||
struct AuthMiddleware;
|
||||
|
||||
impl<S> Middleware<S> for AuthMiddleware {
|
||||
fn start(&self, req: &mut HttpRequest<S>) -> Result<Started> {
|
||||
let auth = BasicAuth::extract(&req)?;
|
||||
let mut config = Config::default();
|
||||
config.realm("Restricted area".to_string());
|
||||
let auth = BasicAuth::from_request(&req, &config)?;
|
||||
|
||||
// Please note that this is only an example,
|
||||
// do not ever hardcode your credentials!
|
||||
if auth.username == "root" && auth.password == "pass" {
|
||||
Ok(Started::Done)
|
||||
} else {
|
||||
let response = req.build_response(StatusCode::UNAUTHORIZED)
|
||||
.header("WWW-Authenticate", "Basic")
|
||||
.finish();
|
||||
let response = BasicAuth::error_response(&config);
|
||||
Ok(Started::Response(response))
|
||||
}
|
||||
}
|
||||
@ -32,8 +31,8 @@ fn index(auth: BasicAuth) -> String {
|
||||
|
||||
fn main() {
|
||||
server::new(|| App::new()
|
||||
// Comment line below to pass authentication handling
|
||||
// directly to `index` handler.
|
||||
// Comment the `.middleware()` line and let `BasicAuth` extractor
|
||||
// in the `index` handler do the authentication routine
|
||||
.middleware(AuthMiddleware)
|
||||
.resource("/", |r| r.with(index)))
|
||||
.bind("127.0.0.1:8088").unwrap()
|
||||
|
61
src/basic/config.rs
Normal file
61
src/basic/config.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use std::default::Default;
|
||||
|
||||
use bytes::Bytes;
|
||||
use percent_encoding;
|
||||
use actix_web::http::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValue};
|
||||
|
||||
use challenge::Challenge;
|
||||
|
||||
/// Challenge configuration for [BasicAuth](./struct.BasicAuth.html) extractor.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
// "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A
|
||||
realm: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn realm(&mut self, value: String) -> &mut Self {
|
||||
self.realm = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
fn as_bytes(&self) -> Bytes {
|
||||
let mut bytes = Bytes::from_static(b"Basic");
|
||||
if let Some(ref realm) = self.realm {
|
||||
bytes.extend_from_slice(b" realm=\"");
|
||||
let realm = percent_encoding::utf8_percent_encode(realm, percent_encoding::SIMPLE_ENCODE_SET);
|
||||
for part in realm {
|
||||
bytes.extend_from_slice(part.as_bytes());
|
||||
}
|
||||
bytes.extend_from_slice(b"\"");
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for Config {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
|
||||
HeaderValue::from_bytes(&self.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoHeaderValue for &'a Config {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
|
||||
HeaderValue::from_bytes(&self.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
realm: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Challenge for Config {}
|
101
src/basic/mod.rs
Normal file
101
src/basic/mod.rs
Normal file
@ -0,0 +1,101 @@
|
||||
use std::string;
|
||||
use std::convert::From;
|
||||
|
||||
use base64;
|
||||
use actix_web::{HttpRequest, HttpMessage, HttpResponse, FromRequest, ResponseError};
|
||||
use actix_web::http::header;
|
||||
|
||||
mod config;
|
||||
|
||||
use errors::Error;
|
||||
pub use self::config::Config;
|
||||
|
||||
/// Extractor for `Authorization: Basic {payload}` HTTP request header.
|
||||
///
|
||||
/// If header is not present or malformed, `HTTP 401` response will be returned.
|
||||
/// See [Config](./struct.Config.html) struct also.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// As a handler-level extractor:
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web_httpauth::basic::BasicAuth;
|
||||
///
|
||||
/// pub fn handler(auth: BasicAuth) -> String {
|
||||
/// format!("Hello, {}", auth.username)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See `examples/basic.rs` file in sources
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct BasicAuth {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl BasicAuth {
|
||||
pub fn error_response(cfg: &Config) -> HttpResponse {
|
||||
Error::new(cfg.clone()).error_response()
|
||||
}
|
||||
|
||||
fn parse<S>(req: &HttpRequest<S>) -> Result<Self, ParseError> {
|
||||
let header = req.headers().get(header::AUTHORIZATION)
|
||||
.ok_or(ParseError)?
|
||||
.to_str()?;
|
||||
let mut parts = header.splitn(2, ' ');
|
||||
|
||||
// Authorization mechanism
|
||||
match parts.next() {
|
||||
Some(mechanism) if mechanism == "Basic" => (),
|
||||
_ => return Err(ParseError),
|
||||
}
|
||||
|
||||
// Authorization payload
|
||||
let payload = parts.next().ok_or(ParseError)?;
|
||||
let payload = base64::decode(payload)?;
|
||||
let payload = String::from_utf8(payload)?;
|
||||
let mut parts = payload.splitn(2, ':');
|
||||
let user = parts.next().ok_or(ParseError)?;
|
||||
let password = parts.next().ok_or(ParseError)?;
|
||||
|
||||
Ok(BasicAuth{
|
||||
username: user.to_string(),
|
||||
password: password.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<S> FromRequest<S> for BasicAuth {
|
||||
type Config = Config;
|
||||
type Result = Result<Self, Error>;
|
||||
|
||||
fn from_request(req: &HttpRequest<S>, cfg: &<Self as FromRequest<S>>::Config) -> <Self as FromRequest<S>>::Result {
|
||||
BasicAuth::parse(req).map_err(|_| Error::new(cfg.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ParseError;
|
||||
|
||||
impl From<base64::DecodeError> for ParseError {
|
||||
fn from(_: base64::DecodeError) -> Self {
|
||||
Self{}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<header::ToStrError> for ParseError {
|
||||
fn from(_: header::ToStrError) -> Self {
|
||||
Self{}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<string::FromUtf8Error> for ParseError {
|
||||
fn from(_: string::FromUtf8Error) -> Self {
|
||||
Self{}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
56
src/basic/tests.rs
Normal file
56
src/basic/tests.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use base64;
|
||||
use actix_web::FromRequest;
|
||||
use actix_web::test::TestRequest;
|
||||
|
||||
use super::BasicAuth;
|
||||
|
||||
#[test]
|
||||
fn test_valid_auth() {
|
||||
let value = format!("Basic {}", base64::encode("user:pass"));
|
||||
let req = TestRequest::with_header("Authorization", value).finish();
|
||||
let auth = BasicAuth::extract(&req);
|
||||
|
||||
assert!(auth.is_ok());
|
||||
let auth = auth.unwrap();
|
||||
assert_eq!(auth.username, "user".to_string());
|
||||
assert_eq!(auth.password, "pass".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_header() {
|
||||
let req = TestRequest::default().finish();
|
||||
let auth = BasicAuth::extract(&req);
|
||||
|
||||
assert!(auth.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_mechanism() {
|
||||
let value = format!("Digest {}", base64::encode("user:pass"));
|
||||
let req = TestRequest::with_header("Authorization", value).finish();
|
||||
let auth = BasicAuth::extract(&req);
|
||||
|
||||
assert!(auth.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_format() {
|
||||
let value = format!("Basic {}", base64::encode("user"));
|
||||
let req = TestRequest::with_header("Authorization", value).finish();
|
||||
let auth = BasicAuth::extract(&req);
|
||||
|
||||
assert!(auth.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_without_password() {
|
||||
let value = format!("Basic {}", base64::encode("user:"));
|
||||
let req = TestRequest::with_header("Authorization", value).finish();
|
||||
let auth = BasicAuth::extract(&req);
|
||||
|
||||
assert!(auth.is_ok());
|
||||
assert_eq!(auth.unwrap(), BasicAuth {
|
||||
username: "user".to_string(),
|
||||
password: "".to_string(),
|
||||
})
|
||||
}
|
6
src/challenge.rs
Normal file
6
src/challenge.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use std::fmt::Debug;
|
||||
use std::default::Default;
|
||||
|
||||
use actix_web::http::header::IntoHeaderValue;
|
||||
|
||||
pub trait Challenge: 'static + Debug + Clone + Send + Sync + IntoHeaderValue + Default {}
|
@ -1,65 +1,41 @@
|
||||
use std::fmt;
|
||||
use std::error::Error;
|
||||
use std::string;
|
||||
use std::error::Error as StdError;
|
||||
|
||||
use base64;
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::{StatusCode, header};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AuthError {
|
||||
HeaderMissing, // HTTP 401
|
||||
// TODO: Ensure that 401 should be returned if not a `Basic` mechanism is received
|
||||
InvalidMechanism, // HTTP 401 ?
|
||||
HeaderMalformed, // HTTP 400
|
||||
use basic::Config;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
challenge: Config,
|
||||
}
|
||||
|
||||
impl fmt::Display for AuthError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for AuthError {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
AuthError::HeaderMissing => "HTTP 'Authorization' header is missing",
|
||||
AuthError::InvalidMechanism => "Wrong mechanism for a HTTP 'Authorization' header, expected 'Basic'",
|
||||
AuthError::HeaderMalformed => "Malformed HTTP 'Authorization' header",
|
||||
impl Error {
|
||||
pub fn new(config: Config) -> Error {
|
||||
Error {
|
||||
challenge: config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<header::ToStrError> for AuthError {
|
||||
fn from(_: header::ToStrError) -> Self {
|
||||
AuthError::HeaderMalformed
|
||||
}
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for AuthError {
|
||||
fn from(_: base64::DecodeError) -> Self {
|
||||
AuthError::HeaderMalformed
|
||||
}
|
||||
}
|
||||
|
||||
impl From<string::FromUtf8Error> for AuthError {
|
||||
fn from(_: string::FromUtf8Error) -> Self {
|
||||
AuthError::HeaderMalformed
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for AuthError {
|
||||
impl ResponseError for Error {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let status = match *self {
|
||||
AuthError::HeaderMissing => StatusCode::UNAUTHORIZED,
|
||||
AuthError::InvalidMechanism => StatusCode::UNAUTHORIZED,
|
||||
AuthError::HeaderMalformed => StatusCode::BAD_REQUEST,
|
||||
};
|
||||
|
||||
HttpResponse::build(status)
|
||||
.header("WWW-Authenticate", "Basic")
|
||||
HttpResponse::build(StatusCode::UNAUTHORIZED)
|
||||
.header(header::WWW_AUTHENTICATE, &self.challenge)
|
||||
.finish()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
"Unauthorized request"
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.description())
|
||||
}
|
||||
}
|
||||
|
13
src/lib.rs
13
src/lib.rs
@ -1,8 +1,13 @@
|
||||
//! HTTP authorization routines for [actix-web](https://github.com/actix/actix-web) framework.
|
||||
//!
|
||||
//! Currently supported schemas:
|
||||
//! * Basic ([RFC-7617](https://tools.ietf.org/html/rfc7617))
|
||||
|
||||
extern crate bytes;
|
||||
extern crate percent_encoding;
|
||||
extern crate actix_web;
|
||||
extern crate base64;
|
||||
|
||||
mod schemes;
|
||||
mod errors;
|
||||
|
||||
pub use schemes::*;
|
||||
pub use errors::AuthError;
|
||||
mod challenge;
|
||||
pub mod basic;
|
||||
|
Loading…
Reference in New Issue
Block a user