mirror of
https://github.com/actix/actix-extras.git
synced 2024-11-23 23:51:06 +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]
|
[dependencies]
|
||||||
actix-web = "0.6"
|
actix-web = "0.6"
|
||||||
base64 = "0.9"
|
base64 = "0.9"
|
||||||
|
percent-encoding = "1.0.1"
|
||||||
|
bytes = "0.4.7"
|
@ -1,25 +1,24 @@
|
|||||||
extern crate actix_web;
|
extern crate actix_web;
|
||||||
extern crate actix_web_httpauth;
|
extern crate actix_web_httpauth;
|
||||||
|
|
||||||
use actix_web::http::StatusCode;
|
|
||||||
use actix_web::{server, App, HttpRequest, FromRequest, Result};
|
use actix_web::{server, App, HttpRequest, FromRequest, Result};
|
||||||
use actix_web::middleware::{Middleware, Started};
|
use actix_web::middleware::{Middleware, Started};
|
||||||
use actix_web_httpauth::BasicAuth;
|
use actix_web_httpauth::basic::{BasicAuth, Config};
|
||||||
|
|
||||||
struct AuthMiddleware;
|
struct AuthMiddleware;
|
||||||
|
|
||||||
impl<S> Middleware<S> for AuthMiddleware {
|
impl<S> Middleware<S> for AuthMiddleware {
|
||||||
fn start(&self, req: &mut HttpRequest<S>) -> Result<Started> {
|
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,
|
// Please note that this is only an example,
|
||||||
// do not ever hardcode your credentials!
|
// do not ever hardcode your credentials!
|
||||||
if auth.username == "root" && auth.password == "pass" {
|
if auth.username == "root" && auth.password == "pass" {
|
||||||
Ok(Started::Done)
|
Ok(Started::Done)
|
||||||
} else {
|
} else {
|
||||||
let response = req.build_response(StatusCode::UNAUTHORIZED)
|
let response = BasicAuth::error_response(&config);
|
||||||
.header("WWW-Authenticate", "Basic")
|
|
||||||
.finish();
|
|
||||||
Ok(Started::Response(response))
|
Ok(Started::Response(response))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,8 +31,8 @@ fn index(auth: BasicAuth) -> String {
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
server::new(|| App::new()
|
server::new(|| App::new()
|
||||||
// Comment line below to pass authentication handling
|
// Comment the `.middleware()` line and let `BasicAuth` extractor
|
||||||
// directly to `index` handler.
|
// in the `index` handler do the authentication routine
|
||||||
.middleware(AuthMiddleware)
|
.middleware(AuthMiddleware)
|
||||||
.resource("/", |r| r.with(index)))
|
.resource("/", |r| r.with(index)))
|
||||||
.bind("127.0.0.1:8088").unwrap()
|
.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::fmt;
|
||||||
use std::error::Error;
|
use std::error::Error as StdError;
|
||||||
use std::string;
|
|
||||||
|
|
||||||
use base64;
|
|
||||||
use actix_web::HttpResponse;
|
use actix_web::HttpResponse;
|
||||||
use actix_web::error::ResponseError;
|
use actix_web::error::ResponseError;
|
||||||
use actix_web::http::{StatusCode, header};
|
use actix_web::http::{StatusCode, header};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
use basic::Config;
|
||||||
pub enum AuthError {
|
|
||||||
HeaderMissing, // HTTP 401
|
#[derive(Debug)]
|
||||||
// TODO: Ensure that 401 should be returned if not a `Basic` mechanism is received
|
pub struct Error {
|
||||||
InvalidMechanism, // HTTP 401 ?
|
challenge: Config,
|
||||||
HeaderMalformed, // HTTP 400
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for AuthError {
|
impl Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
pub fn new(config: Config) -> Error {
|
||||||
f.write_str(self.description())
|
Error {
|
||||||
}
|
challenge: config,
|
||||||
}
|
|
||||||
|
|
||||||
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 From<header::ToStrError> for AuthError {
|
impl ResponseError for Error {
|
||||||
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 {
|
|
||||||
fn error_response(&self) -> HttpResponse {
|
fn error_response(&self) -> HttpResponse {
|
||||||
let status = match *self {
|
HttpResponse::build(StatusCode::UNAUTHORIZED)
|
||||||
AuthError::HeaderMissing => StatusCode::UNAUTHORIZED,
|
.header(header::WWW_AUTHENTICATE, &self.challenge)
|
||||||
AuthError::InvalidMechanism => StatusCode::UNAUTHORIZED,
|
|
||||||
AuthError::HeaderMalformed => StatusCode::BAD_REQUEST,
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse::build(status)
|
|
||||||
.header("WWW-Authenticate", "Basic")
|
|
||||||
.finish()
|
.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 actix_web;
|
||||||
extern crate base64;
|
extern crate base64;
|
||||||
|
|
||||||
mod schemes;
|
|
||||||
mod errors;
|
mod errors;
|
||||||
|
mod challenge;
|
||||||
pub use schemes::*;
|
pub mod basic;
|
||||||
pub use errors::AuthError;
|
|
||||||
|
Loading…
Reference in New Issue
Block a user