1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//! Opinionated request identity service for Actix Web apps.
//!
//! [`IdentityService`] middleware can be used with different policies types to store
//! identity information.
//!
//! A cookie based policy is provided. [`CookieIdentityPolicy`] uses cookies as identity storage.
//!
//! To access current request identity, use the [`Identity`] extractor.
//!
//! ```no_run
//! use actix_web::*;
//! use actix_identity::{Identity, CookieIdentityPolicy, IdentityService};
//!
//! #[get("/")]
//! async fn index(id: Identity) -> String {
//!     // access request identity
//!     if let Some(id) = id.identity() {
//!         format!("Welcome! {}", id)
//!     } else {
//!         "Welcome Anonymous!".to_owned()
//!     }
//! }
//!
//! #[post("/login")]
//! async fn login(id: Identity) -> HttpResponse {
//!     id.remember("User1".to_owned()); // <- remember identity
//!     HttpResponse::Ok().finish()
//! }
//!
//! #[post("/logout")]
//! async fn logout(id: Identity) -> HttpResponse {
//!     id.forget();                      // <- remove identity
//!     HttpResponse::Ok().finish()
//! }
//!
//! #[actix_web::main]
//! async fn main() -> std::io::Result<()> {
//!     HttpServer::new(move || {
//!         // create cookie identity backend
//!         let policy = CookieIdentityPolicy::new(&[0; 32])
//!             .name("auth-cookie")
//!             .secure(false);
//!
//!         App::new()
//!             // wrap policy into middleware identity middleware
//!             .wrap(IdentityService::new(policy))
//!             .service(services![index, login, logout])
//!     })
//!     .bind(("0.0.0.0", 8080u16))?
//!     .run()
//!     .await
//! }
//! ```

#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]

use std::future::Future;

use actix_web::{
    dev::{ServiceRequest, ServiceResponse},
    Error, HttpMessage, Result,
};

mod cookie;
mod identity;
mod middleware;

pub use self::cookie::CookieIdentityPolicy;
pub use self::identity::Identity;
pub use self::middleware::IdentityService;

/// Identity policy.
pub trait IdentityPolicy: Sized + 'static {
    /// The return type of the middleware
    type Future: Future<Output = Result<Option<String>, Error>>;

    /// The return type of the middleware
    type ResponseFuture: Future<Output = Result<(), Error>>;

    /// Parse the session from request and load data from a service identity.
    fn from_request(&self, req: &mut ServiceRequest) -> Self::Future;

    /// Write changes to response
    fn to_response<B>(
        &self,
        identity: Option<String>,
        changed: bool,
        response: &mut ServiceResponse<B>,
    ) -> Self::ResponseFuture;
}

/// Helper trait that allows to get Identity.
///
/// It could be used in middleware but identity policy must be set before any other middleware that
/// needs identity. RequestIdentity is implemented both for `ServiceRequest` and `HttpRequest`.
pub trait RequestIdentity {
    fn get_identity(&self) -> Option<String>;
}

impl<T> RequestIdentity for T
where
    T: HttpMessage,
{
    fn get_identity(&self) -> Option<String> {
        Identity::get_identity(&self.extensions())
    }
}

#[cfg(test)]
mod tests {
    use std::time::SystemTime;

    use actix_web::{
        body::{BoxBody, EitherBody},
        dev::ServiceResponse,
        test, web, App, Error,
    };

    use super::*;

    pub(crate) const COOKIE_KEY_MASTER: [u8; 32] = [0; 32];
    pub(crate) const COOKIE_NAME: &str = "actix_auth";
    pub(crate) const COOKIE_LOGIN: &str = "test";

    #[allow(clippy::enum_variant_names)]
    pub(crate) enum LoginTimestampCheck {
        NoTimestamp,
        NewTimestamp,
        OldTimestamp(SystemTime),
    }

    #[allow(clippy::enum_variant_names)]
    pub(crate) enum VisitTimeStampCheck {
        NoTimestamp,
        NewTimestamp,
    }

    pub(crate) async fn create_identity_server<
        F: Fn(CookieIdentityPolicy) -> CookieIdentityPolicy + Sync + Send + Clone + 'static,
    >(
        f: F,
    ) -> impl actix_service::Service<
        actix_http::Request,
        Response = ServiceResponse<EitherBody<BoxBody>>,
        Error = Error,
    > {
        test::init_service(
            App::new()
                .wrap(IdentityService::new(f(CookieIdentityPolicy::new(
                    &COOKIE_KEY_MASTER,
                )
                .secure(false)
                .name(COOKIE_NAME))))
                .service(web::resource("/").to(|id: Identity| async move {
                    let identity = id.identity();
                    if identity.is_none() {
                        id.remember(COOKIE_LOGIN.to_string())
                    }
                    web::Json(identity)
                })),
        )
        .await
    }
}