actix_identity/
identity.rs

1use actix_session::Session;
2use actix_utils::future::{ready, Ready};
3use actix_web::{
4    cookie::time::OffsetDateTime,
5    dev::{Extensions, Payload},
6    http::StatusCode,
7    Error, FromRequest, HttpMessage, HttpRequest, HttpResponse,
8};
9
10use crate::{
11    config::LogoutBehavior,
12    error::{
13        GetIdentityError, LoginError, LostIdentityError, MissingIdentityError, SessionExpiryError,
14    },
15};
16
17/// A verified user identity. It can be used as a request extractor.
18///
19/// The lifecycle of a user identity is tied to the lifecycle of the underlying session. If the
20/// session is destroyed (e.g. the session expired), the user identity will be forgotten, de-facto
21/// forcing a user log out.
22///
23/// # Examples
24/// ```
25/// use actix_web::{
26///     get, post, Responder, HttpRequest, HttpMessage, HttpResponse
27/// };
28/// use actix_identity::Identity;
29///
30/// #[get("/")]
31/// async fn index(user: Option<Identity>) -> impl Responder {
32///     if let Some(user) = user {
33///         format!("Welcome! {}", user.id().unwrap())
34///     } else {
35///         "Welcome Anonymous!".to_owned()
36///     }
37/// }
38///
39/// #[post("/login")]
40/// async fn login(request: HttpRequest) -> impl Responder {
41///     Identity::login(&request.extensions(), "User1".into());
42///     HttpResponse::Ok()
43/// }
44///
45/// #[post("/logout")]
46/// async fn logout(user: Identity) -> impl Responder {
47///     user.logout();
48///     HttpResponse::Ok()
49/// }
50/// ```
51///
52/// # Extractor Behavior
53/// What happens if you try to extract an `Identity` out of a request that does not have a valid
54/// identity attached? The API will return a `401 UNAUTHORIZED` to the caller.
55///
56/// If you want to customize this behavior, consider extracting `Option<Identity>` or
57/// `Result<Identity, actix_web::Error>` instead of a bare `Identity`: you will then be fully in
58/// control of the error path.
59///
60/// ## Examples
61/// ```
62/// use actix_web::{http::header::LOCATION, get, HttpResponse, Responder};
63/// use actix_identity::Identity;
64///
65/// #[get("/")]
66/// async fn index(user: Option<Identity>) -> impl Responder {
67///     if let Some(user) = user {
68///         HttpResponse::Ok().finish()
69///     } else {
70///         // Redirect to login page if unauthenticated
71///         HttpResponse::TemporaryRedirect()
72///             .insert_header((LOCATION, "/login"))
73///             .finish()
74///     }
75/// }
76/// ```
77pub struct Identity(IdentityInner);
78
79#[derive(Clone)]
80pub(crate) struct IdentityInner {
81    pub(crate) session: Session,
82    pub(crate) logout_behavior: LogoutBehavior,
83    pub(crate) is_login_deadline_enabled: bool,
84    pub(crate) is_visit_deadline_enabled: bool,
85    pub(crate) id_key: &'static str,
86    pub(crate) last_visit_unix_timestamp_key: &'static str,
87    pub(crate) login_unix_timestamp_key: &'static str,
88}
89
90impl IdentityInner {
91    fn extract(ext: &Extensions) -> Self {
92        ext.get::<Self>()
93            .expect(
94                "No `IdentityInner` instance was found in the extensions attached to the \
95                incoming request. This usually means that `IdentityMiddleware` has not been \
96                registered as an application middleware via `App::wrap`. `Identity` cannot be used \
97                unless the identity machine is properly mounted: register `IdentityMiddleware` as \
98                a middleware for your application to fix this panic. If the problem persists, \
99                please file an issue on GitHub.",
100            )
101            .to_owned()
102    }
103
104    /// Retrieve the user id attached to the current session.
105    fn get_identity(&self) -> Result<String, GetIdentityError> {
106        self.session
107            .get::<String>(self.id_key)?
108            .ok_or_else(|| MissingIdentityError.into())
109    }
110}
111
112impl Identity {
113    /// Return the user id associated to the current session.
114    ///
115    /// # Examples
116    /// ```
117    /// use actix_web::{get, Responder};
118    /// use actix_identity::Identity;
119    ///
120    /// #[get("/")]
121    /// async fn index(user: Option<Identity>) -> impl Responder {
122    ///     if let Some(user) = user {
123    ///         format!("Welcome! {}", user.id().unwrap())
124    ///     } else {
125    ///         "Welcome Anonymous!".to_owned()
126    ///     }
127    /// }
128    /// ```
129    pub fn id(&self) -> Result<String, GetIdentityError> {
130        self.0
131            .session
132            .get(self.0.id_key)?
133            .ok_or_else(|| LostIdentityError.into())
134    }
135
136    /// Attach a valid user identity to the current session.
137    ///
138    /// This method should be called after you have successfully authenticated the user. After
139    /// `login` has been called, the user will be able to access all routes that require a valid
140    /// [`Identity`].
141    ///
142    /// # Examples
143    /// ```
144    /// use actix_web::{post, Responder, HttpRequest, HttpMessage, HttpResponse};
145    /// use actix_identity::Identity;
146    ///
147    /// #[post("/login")]
148    /// async fn login(request: HttpRequest) -> impl Responder {
149    ///     Identity::login(&request.extensions(), "User1".into());
150    ///     HttpResponse::Ok()
151    /// }
152    /// ```
153    pub fn login(ext: &Extensions, id: String) -> Result<Self, LoginError> {
154        let inner = IdentityInner::extract(ext);
155        inner.session.insert(inner.id_key, id)?;
156        let now = OffsetDateTime::now_utc().unix_timestamp();
157        if inner.is_login_deadline_enabled {
158            inner.session.insert(inner.login_unix_timestamp_key, now)?;
159        }
160        if inner.is_visit_deadline_enabled {
161            inner
162                .session
163                .insert(inner.last_visit_unix_timestamp_key, now)?;
164        }
165        inner.session.renew();
166        Ok(Self(inner))
167    }
168
169    /// Remove the user identity from the current session.
170    ///
171    /// After `logout` has been called, the user will no longer be able to access routes that
172    /// require a valid [`Identity`].
173    ///
174    /// The behavior on logout is determined by [`IdentityMiddlewareBuilder::logout_behavior`].
175    ///
176    /// # Examples
177    /// ```
178    /// use actix_web::{post, Responder, HttpResponse};
179    /// use actix_identity::Identity;
180    ///
181    /// #[post("/logout")]
182    /// async fn logout(user: Identity) -> impl Responder {
183    ///     user.logout();
184    ///     HttpResponse::Ok()
185    /// }
186    /// ```
187    ///
188    /// [`IdentityMiddlewareBuilder::logout_behavior`]: crate::config::IdentityMiddlewareBuilder::logout_behavior
189    pub fn logout(self) {
190        match self.0.logout_behavior {
191            LogoutBehavior::PurgeSession => {
192                self.0.session.purge();
193            }
194            LogoutBehavior::DeleteIdentityKeys => {
195                self.0.session.remove(self.0.id_key);
196                if self.0.is_login_deadline_enabled {
197                    self.0.session.remove(self.0.login_unix_timestamp_key);
198                }
199                if self.0.is_visit_deadline_enabled {
200                    self.0.session.remove(self.0.last_visit_unix_timestamp_key);
201                }
202            }
203        }
204    }
205
206    pub(crate) fn extract(ext: &Extensions) -> Result<Self, GetIdentityError> {
207        let inner = IdentityInner::extract(ext);
208        inner.get_identity()?;
209        Ok(Self(inner))
210    }
211
212    pub(crate) fn logged_at(&self) -> Result<Option<OffsetDateTime>, GetIdentityError> {
213        Ok(self
214            .0
215            .session
216            .get(self.0.login_unix_timestamp_key)?
217            .map(OffsetDateTime::from_unix_timestamp)
218            .transpose()
219            .map_err(SessionExpiryError)?)
220    }
221
222    pub(crate) fn last_visited_at(&self) -> Result<Option<OffsetDateTime>, GetIdentityError> {
223        Ok(self
224            .0
225            .session
226            .get(self.0.last_visit_unix_timestamp_key)?
227            .map(OffsetDateTime::from_unix_timestamp)
228            .transpose()
229            .map_err(SessionExpiryError)?)
230    }
231
232    pub(crate) fn set_last_visited_at(&self) -> Result<(), LoginError> {
233        let now = OffsetDateTime::now_utc().unix_timestamp();
234        self.0
235            .session
236            .insert(self.0.last_visit_unix_timestamp_key, now)?;
237        Ok(())
238    }
239}
240
241/// Extractor implementation for [`Identity`].
242///
243/// # Examples
244/// ```
245/// use actix_web::{get, Responder};
246/// use actix_identity::Identity;
247///
248/// #[get("/")]
249/// async fn index(user: Option<Identity>) -> impl Responder {
250///     if let Some(user) = user {
251///         format!("Welcome! {}", user.id().unwrap())
252///     } else {
253///         "Welcome Anonymous!".to_owned()
254///     }
255/// }
256/// ```
257impl FromRequest for Identity {
258    type Error = Error;
259    type Future = Ready<Result<Self, Self::Error>>;
260
261    #[inline]
262    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
263        ready(Identity::extract(&req.extensions()).map_err(|err| {
264            let res = actix_web::error::InternalError::from_response(
265                err,
266                HttpResponse::new(StatusCode::UNAUTHORIZED),
267            );
268
269            actix_web::Error::from(res)
270        }))
271    }
272}