Skip to main content

arc_auth_session/
lib.rs

1//! Cookie-session authentication protocol for Arc browser applications.
2//! Browser pages are intentionally provided by `arc-auth-admin`.
3
4use actix_session::{Session, SessionExt};
5use actix_web::{
6    body::EitherBody,
7    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
8    web, Error, HttpResponse,
9};
10use arc_auth_core::{AuthError, Identity, IdentityStore};
11use arc_web::{ArcAppBuilder, ArcPlugin};
12use futures_util::future::LocalBoxFuture;
13use std::{
14    future::{ready, Ready},
15    sync::Arc,
16};
17
18pub const IDENTITY_SESSION_KEY: &str = "arc_auth_identity";
19
20pub fn identity(session: &Session) -> Option<Identity> {
21    session.get(IDENTITY_SESSION_KEY).ok().flatten()
22}
23
24pub fn cache_identity(session: &Session, identity: &Identity) {
25    let _ = session.insert(IDENTITY_SESSION_KEY, identity);
26    arc_web::helpers::session::set_session_user(
27        session,
28        &arc_web::helpers::session::SessionUser {
29            id: identity.id.clone(),
30            name: identity.name.clone(),
31            email: identity.email.clone(),
32        },
33    );
34}
35
36pub async fn authenticate(
37    session: &Session,
38    store: &dyn IdentityStore,
39    email: &str,
40    password: &str,
41) -> Result<Identity, AuthError> {
42    let identity = store.authenticate(email, password).await?;
43    cache_identity(session, &identity);
44    Ok(identity)
45}
46
47pub fn sign_out(session: &Session) {
48    session.remove(IDENTITY_SESSION_KEY);
49    arc_web::helpers::session::clear_session_user(session);
50}
51
52/// Session plugin retained as the stable protocol/middleware registration seam.
53pub struct SessionAuthPlugin;
54#[async_trait::async_trait]
55impl ArcPlugin for SessionAuthPlugin {
56    fn name(&self) -> &'static str {
57        "auth-session"
58    }
59    fn register(&self, builder: ArcAppBuilder) -> ArcAppBuilder {
60        builder
61    }
62}
63
64/// Redirect unauthenticated browser requests to the sign-in page.
65pub struct RequireSession;
66impl<S, B> Transform<S, ServiceRequest> for RequireSession
67where
68    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
69    S::Future: 'static,
70    B: 'static,
71{
72    type Response = ServiceResponse<EitherBody<B>>;
73    type Error = Error;
74    type InitError = ();
75    type Transform = SessionCheck<S>;
76    type Future = Ready<Result<Self::Transform, ()>>;
77    fn new_transform(&self, service: S) -> Self::Future {
78        ready(Ok(SessionCheck {
79            service: Arc::new(service),
80        }))
81    }
82}
83pub struct SessionCheck<S> {
84    service: Arc<S>,
85}
86impl<S, B> Service<ServiceRequest> for SessionCheck<S>
87where
88    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
89    S::Future: 'static,
90    B: 'static,
91{
92    type Response = ServiceResponse<EitherBody<B>>;
93    type Error = Error;
94    type Future = LocalBoxFuture<'static, Result<Self::Response, Error>>;
95    forward_ready!(service);
96    fn call(&self, req: ServiceRequest) -> Self::Future {
97        if req
98            .get_session()
99            .get::<Identity>(IDENTITY_SESSION_KEY)
100            .ok()
101            .flatten()
102            .is_none()
103        {
104            return Box::pin(async move {
105                Ok(req.into_response(
106                    HttpResponse::Found()
107                        .insert_header(("Location", "/signin"))
108                        .finish()
109                        .map_into_right_body(),
110                ))
111            });
112        }
113        let future = self.service.call(req);
114        Box::pin(async move { future.await.map(ServiceResponse::map_into_left_body) })
115    }
116}
117
118/// Convenience extractor for handlers that need the configured identity store.
119pub type IdentityStoreData = web::Data<dyn IdentityStore>;