mirror of
https://github.com/actix/actix-extras.git
synced 2025-07-01 04:05:09 +02:00
Rebuild actix-identity
on top of actix-session
(#246)
Co-authored-by: Rob Ede <robjtede@icloud.com>
This commit is contained in:
@ -1,89 +1,238 @@
|
||||
use actix_session::Session;
|
||||
use actix_utils::future::{ready, Ready};
|
||||
use actix_web::{
|
||||
cookie::time::OffsetDateTime,
|
||||
dev::{Extensions, Payload},
|
||||
Error, FromRequest, HttpMessage as _, HttpRequest,
|
||||
http::StatusCode,
|
||||
Error, FromRequest, HttpMessage, HttpRequest, HttpResponse,
|
||||
};
|
||||
use anyhow::{anyhow, Context};
|
||||
|
||||
pub(crate) struct IdentityItem {
|
||||
pub(crate) id: Option<String>,
|
||||
pub(crate) changed: bool,
|
||||
}
|
||||
use crate::config::LogoutBehaviour;
|
||||
|
||||
/// The extractor type to obtain your identity from a request.
|
||||
/// A verified user identity. It can be used as a request extractor.
|
||||
///
|
||||
/// The lifecycle of a user identity is tied to the lifecycle of the underlying session. If the
|
||||
/// session is destroyed (e.g. the session expired), the user identity will be forgotten, de-facto
|
||||
/// forcing a user log out.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::*;
|
||||
/// use actix_web::{
|
||||
/// get, post, Responder, HttpRequest, HttpMessage, HttpResponse
|
||||
/// };
|
||||
/// use actix_identity::Identity;
|
||||
///
|
||||
/// #[get("/")]
|
||||
/// async fn index(id: Identity) -> impl Responder {
|
||||
/// // access request identity
|
||||
/// if let Some(id) = id.identity() {
|
||||
/// format!("Welcome! {}", id)
|
||||
/// async fn index(user: Option<Identity>) -> impl Responder {
|
||||
/// if let Some(user) = user {
|
||||
/// format!("Welcome! {}", user.id().unwrap())
|
||||
/// } else {
|
||||
/// "Welcome Anonymous!".to_owned()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[post("/login")]
|
||||
/// async fn login(id: Identity) -> impl Responder {
|
||||
/// // remember identity
|
||||
/// id.remember("User1".to_owned());
|
||||
///
|
||||
/// async fn login(request: HttpRequest) -> impl Responder {
|
||||
/// Identity::login(&request.extensions(), "User1".into());
|
||||
/// HttpResponse::Ok()
|
||||
/// }
|
||||
///
|
||||
/// #[post("/logout")]
|
||||
/// async fn logout(id: Identity) -> impl Responder {
|
||||
/// // remove identity
|
||||
/// id.forget();
|
||||
///
|
||||
/// async fn logout(user: Identity) -> impl Responder {
|
||||
/// user.logout();
|
||||
/// HttpResponse::Ok()
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct Identity(HttpRequest);
|
||||
|
||||
impl Identity {
|
||||
/// Return the claimed identity of the user associated request or `None` if no identity can be
|
||||
/// found associated with the request.
|
||||
pub fn identity(&self) -> Option<String> {
|
||||
Identity::get_identity(&self.0.extensions())
|
||||
}
|
||||
|
||||
/// Remember identity.
|
||||
pub fn remember(&self, identity: String) {
|
||||
if let Some(id) = self.0.extensions_mut().get_mut::<IdentityItem>() {
|
||||
id.id = Some(identity);
|
||||
id.changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// This method is used to 'forget' the current identity on subsequent requests.
|
||||
pub fn forget(&self) {
|
||||
if let Some(id) = self.0.extensions_mut().get_mut::<IdentityItem>() {
|
||||
id.id = None;
|
||||
id.changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_identity(extensions: &Extensions) -> Option<String> {
|
||||
let id = extensions.get::<IdentityItem>()?;
|
||||
id.id.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Extractor implementation for Identity type.
|
||||
///
|
||||
/// # Extractor Behaviour
|
||||
/// What happens if you try to extract an `Identity` out of a request that does not have a valid
|
||||
/// identity attached? The API will return a `401 UNAUTHORIZED` to the caller.
|
||||
///
|
||||
/// If you want to customise this behaviour, consider extracting `Option<Identity>` or
|
||||
/// `Result<Identity, actix_web::Error>` instead of a bare `Identity`: you will then be fully in
|
||||
/// control of the error path.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use actix_web::*;
|
||||
/// use actix_web::{http::header::LOCATION, get, HttpResponse, Responder};
|
||||
/// use actix_identity::Identity;
|
||||
///
|
||||
/// #[get("/")]
|
||||
/// async fn index(id: Identity) -> impl Responder {
|
||||
/// // access request identity
|
||||
/// if let Some(id) = id.identity() {
|
||||
/// format!("Welcome! {}", id)
|
||||
/// async fn index(user: Option<Identity>) -> impl Responder {
|
||||
/// if let Some(user) = user {
|
||||
/// HttpResponse::Ok().finish()
|
||||
/// } else {
|
||||
/// // Redirect to login page if unauthenticated
|
||||
/// HttpResponse::TemporaryRedirect()
|
||||
/// .insert_header((LOCATION, "/login"))
|
||||
/// .finish()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Identity(IdentityInner);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct IdentityInner {
|
||||
pub(crate) session: Session,
|
||||
pub(crate) logout_behaviour: LogoutBehaviour,
|
||||
pub(crate) is_login_deadline_enabled: bool,
|
||||
pub(crate) is_visit_deadline_enabled: bool,
|
||||
}
|
||||
|
||||
impl IdentityInner {
|
||||
fn extract(ext: &Extensions) -> Self {
|
||||
ext.get::<Self>()
|
||||
.expect(
|
||||
"No `IdentityInner` instance was found in the extensions attached to the \
|
||||
incoming request. This usually means that `IdentityMiddleware` has not been \
|
||||
registered as an application middleware via `App::wrap`. `Identity` cannot be used \
|
||||
unless the identity machine is properly mounted: register `IdentityMiddleware` as \
|
||||
a middleware for your application to fix this panic. If the problem persists, \
|
||||
please file an issue on GitHub.",
|
||||
)
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
/// Retrieve the user id attached to the current session.
|
||||
fn get_identity(&self) -> Result<String, anyhow::Error> {
|
||||
self.session
|
||||
.get::<String>(ID_KEY)
|
||||
.context("Failed to deserialize the user identifier attached to the current session")?
|
||||
.ok_or_else(|| {
|
||||
anyhow!("There is no identity information attached to the current session")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const ID_KEY: &str = "actix_identity.user_id";
|
||||
pub(crate) const LAST_VISIT_UNIX_TIMESTAMP_KEY: &str = "actix_identity.last_visited_at";
|
||||
pub(crate) const LOGIN_UNIX_TIMESTAMP_KEY: &str = "actix_identity.logged_in_at";
|
||||
|
||||
impl Identity {
|
||||
/// Return the user id associated to the current session.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{get, Responder};
|
||||
/// use actix_identity::Identity;
|
||||
///
|
||||
/// #[get("/")]
|
||||
/// async fn index(user: Option<Identity>) -> impl Responder {
|
||||
/// if let Some(user) = user {
|
||||
/// format!("Welcome! {}", user.id().unwrap())
|
||||
/// } else {
|
||||
/// "Welcome Anonymous!".to_owned()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn id(&self) -> Result<String, anyhow::Error> {
|
||||
self.0.session.get(ID_KEY)?.ok_or_else(|| {
|
||||
anyhow!("Bug: the identity information attached to the current session has disappeared")
|
||||
})
|
||||
}
|
||||
|
||||
/// Attach a valid user identity to the current session.
|
||||
///
|
||||
/// This method should be called after you have successfully authenticated the user. After
|
||||
/// `login` has been called, the user will be able to access all routes that require a valid
|
||||
/// [`Identity`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{post, Responder, HttpRequest, HttpMessage, HttpResponse};
|
||||
/// use actix_identity::Identity;
|
||||
///
|
||||
/// #[post("/login")]
|
||||
/// async fn login(request: HttpRequest) -> impl Responder {
|
||||
/// Identity::login(&request.extensions(), "User1".into());
|
||||
/// HttpResponse::Ok()
|
||||
/// }
|
||||
/// ```
|
||||
pub fn login(ext: &Extensions, id: String) -> Result<Self, anyhow::Error> {
|
||||
let inner = IdentityInner::extract(ext);
|
||||
inner.session.insert(ID_KEY, id)?;
|
||||
inner.session.insert(
|
||||
LOGIN_UNIX_TIMESTAMP_KEY,
|
||||
OffsetDateTime::now_utc().unix_timestamp(),
|
||||
)?;
|
||||
inner.session.renew();
|
||||
Ok(Self(inner))
|
||||
}
|
||||
|
||||
/// Remove the user identity from the current session.
|
||||
///
|
||||
/// After `logout` has been called, the user will no longer be able to access routes that
|
||||
/// require a valid [`Identity`].
|
||||
///
|
||||
/// The behaviour on logout is determined by [`IdentityMiddlewareBuilder::logout_behaviour`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{post, Responder, HttpResponse};
|
||||
/// use actix_identity::Identity;
|
||||
///
|
||||
/// #[post("/logout")]
|
||||
/// async fn logout(user: Identity) -> impl Responder {
|
||||
/// user.logout();
|
||||
/// HttpResponse::Ok()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [`IdentityMiddlewareBuilder::logout_behaviour`]: crate::config::IdentityMiddlewareBuilder::logout_behaviour
|
||||
pub fn logout(self) {
|
||||
match self.0.logout_behaviour {
|
||||
LogoutBehaviour::PurgeSession => {
|
||||
self.0.session.purge();
|
||||
}
|
||||
LogoutBehaviour::DeleteIdentityKeys => {
|
||||
self.0.session.remove(ID_KEY);
|
||||
if self.0.is_login_deadline_enabled {
|
||||
self.0.session.remove(LOGIN_UNIX_TIMESTAMP_KEY);
|
||||
}
|
||||
if self.0.is_visit_deadline_enabled {
|
||||
self.0.session.remove(LAST_VISIT_UNIX_TIMESTAMP_KEY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn extract(ext: &Extensions) -> Result<Self, anyhow::Error> {
|
||||
let inner = IdentityInner::extract(ext);
|
||||
inner.get_identity()?;
|
||||
Ok(Self(inner))
|
||||
}
|
||||
|
||||
pub(crate) fn logged_at(&self) -> Result<Option<OffsetDateTime>, anyhow::Error> {
|
||||
self.0
|
||||
.session
|
||||
.get(LOGIN_UNIX_TIMESTAMP_KEY)?
|
||||
.map(OffsetDateTime::from_unix_timestamp)
|
||||
.transpose()
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
pub(crate) fn last_visited_at(&self) -> Result<Option<OffsetDateTime>, anyhow::Error> {
|
||||
self.0
|
||||
.session
|
||||
.get(LAST_VISIT_UNIX_TIMESTAMP_KEY)?
|
||||
.map(OffsetDateTime::from_unix_timestamp)
|
||||
.transpose()
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extractor implementation for [`Identity`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{get, Responder};
|
||||
/// use actix_identity::Identity;
|
||||
///
|
||||
/// #[get("/")]
|
||||
/// async fn index(user: Option<Identity>) -> impl Responder {
|
||||
/// if let Some(user) = user {
|
||||
/// format!("Welcome! {}", user.id().unwrap())
|
||||
/// } else {
|
||||
/// "Welcome Anonymous!".to_owned()
|
||||
/// }
|
||||
@ -91,10 +240,17 @@ impl Identity {
|
||||
/// ```
|
||||
impl FromRequest for Identity {
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<Identity, Error>>;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
|
||||
#[inline]
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
ready(Ok(Identity(req.clone())))
|
||||
ready(Identity::extract(&req.extensions()).map_err(|err| {
|
||||
let res = actix_web::error::InternalError::from_response(
|
||||
err,
|
||||
HttpResponse::new(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
|
||||
actix_web::Error::from(res)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user