diff --git a/Cargo.lock b/Cargo.lock
index 3ba626f247e6d231df838d46c1a70c5d182c431e..6b0eac14803b6a6e18a700b923f21b7cbd099eb5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -997,6 +997,7 @@ dependencies = [
  "env_logger",
  "fs_extra",
  "futures",
+ "rand",
  "regex",
  "rustls",
  "rustls-pemfile",
diff --git a/Cargo.toml b/Cargo.toml
index 1d59dbcb2a344c6c74fb5af601a84a469a16bcbc..c06876316e54b3546d8c24ae41cd477f179d5b63 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,3 +22,4 @@ actix-web-lab = "0.17.0"
 actix-files = "0.6.2"
 actix-multipart = "0.4"
 futures = "0.3.24"
+rand = "0.8"
diff --git a/default_static/script.js b/default_static/script.js
index 2aff4460919b0fa7ed8f898d7f32ab00a2f4df9d..065f3cd0d05b7661f0b25a4b30f2910918436262 100644
--- a/default_static/script.js
+++ b/default_static/script.js
@@ -1 +1,10 @@
-console.log("Hello from default js")
\ No newline at end of file
+document.getElementById('admin-login-form').onsubmit = function (e) {
+    e.preventDefault();
+    fetch('/admin/login', { method: 'POST', body: new URLSearchParams(new FormData(e.target)) }).then(res => {
+        if (res.status >= 200 && res.status < 400) {
+            console.log(res)
+            window.location = '/admin/auth/workspace';
+        }
+
+    }).catch(err => console.log(err))
+}
\ No newline at end of file
diff --git a/src/app/args.rs b/src/app/args.rs
index 9f8a0d7742d3f8fb94bd23e6eba663c6229af737..8fa78ef3728913f442ba67d5a66db071e558a570 100644
--- a/src/app/args.rs
+++ b/src/app/args.rs
@@ -25,8 +25,11 @@ pub struct AppArgs {
     pub ssl_certs_dir: PathBuf,
 
     #[structopt(long = "adm", default_value = "admin")]
-    pub admin_id: String,
+    pub admin_username: String,
 
     #[structopt(long = "pwd", default_value = "password")]
     pub admin_pwd: String,
+
+    #[structopt(long = "admin_cookie_name", default_value = "krustacea_admin_auth")]
+    pub admin_cookie_name: String,
 }
diff --git a/src/app/config.rs b/src/app/config.rs
index f4c026d066beaa3a65cf32c14651ff7b6364141d..1a031d4684555d7a4a024982518aed90c4c64b1f 100644
--- a/src/app/config.rs
+++ b/src/app/config.rs
@@ -18,8 +18,9 @@ pub struct AppConfig {
     pub port_tls: u16,
     pub load: Option<PathBuf>,
     pub ssl_certs_dir: PathBuf,
-    pub admin_id: String,
+    pub admin_username: String,
     pub admin_pwd: String,
+    pub admin_cookie_name: String,
 }
 
 impl AppConfig {
@@ -59,8 +60,9 @@ impl AppConfig {
             port_tls: app_args.port_tls,
             load: app_args.load,
             ssl_certs_dir,
-            admin_id: app_args.admin_id,
+            admin_username: app_args.admin_username,
             admin_pwd: app_args.admin_pwd,
+            admin_cookie_name: app_args.admin_cookie_name,
         }
     }
     pub fn get_log_level(&self) -> String {
diff --git a/src/app/state.rs b/src/app/state.rs
index eb498c1c1e2c549dc7374756b14067e0e8f71af1..8b9fc0f660bf92c49a516e199f308bfa3f35c304 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -1,19 +1,56 @@
 use crate::app::{AppArgs, AppConfig, AppPreferences};
+use rand::{distributions::Alphanumeric, Rng};
 use structopt::StructOpt;
 
 #[derive(Clone)]
 pub struct AppState {
     pub config: AppConfig,
     pub preferences: AppPreferences,
-    // authentication
-    // ...
+    pub admin_auth_token: AdminAuthToken,
 }
 
 impl AppState {
     pub fn new() -> Self {
+        let config = AppConfig::new(AppArgs::from_args());
+        let admin_cookie_name = config.admin_cookie_name.to_owned();
         AppState {
-            config: AppConfig::new(AppArgs::from_args()),
+            config,
             preferences: AppPreferences {},
+            admin_auth_token: AdminAuthToken::new(admin_cookie_name),
         }
     }
 }
+
+#[derive(Clone)]
+pub struct AdminAuthToken {
+    pub value: Option<String>,
+    pub cookie_name: String,
+}
+
+impl AdminAuthToken {
+    pub fn new(cookie_name: String) -> Self {
+        AdminAuthToken {
+            value: None,
+            cookie_name,
+        }
+    }
+
+    pub fn match_value(&self, value: String) -> bool {
+        match &self.value {
+            Some(token) => token.eq(&value),
+            None => false,
+        }
+    }
+
+    pub fn generate(&mut self) {
+        // Thanks to https://stackoverflow.com/questions/54275459/how-do-i-create-a-random-string-by-sampling-from-alphanumeric-characters#54277357
+        self.value = Some(
+            rand::thread_rng()
+                .sample_iter(&Alphanumeric)
+                .take(36)
+                .map(char::from)
+                .collect::<String>()
+                .to_ascii_lowercase(),
+        )
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 3bf320a3a4ba4a48437166bae37cfb7efe126fbb..c302b94a9fb24167c65ea90649c5908626f86b7a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -14,11 +14,6 @@ use static_files::StaticFilesManager;
 use tls_config::tls_config;
 use website::WebSiteBuilder;
 
-#[actix_web::get("/admin")]
-async fn test_unauthorized() -> impl actix_web::Responder {
-    actix_web::HttpResponse::Ok().finish()
-}
-
 #[actix_web::main]
 async fn main() -> std::io::Result<()> {
     let app_state = AppState::new();
@@ -38,9 +33,8 @@ async fn main() -> std::io::Result<()> {
 
     let srv_conf = tls_config(&app_state.config);
     let static_dir = website.static_files_manager.dir.clone();
-
-    let app_state = web::Data::new(std::sync::Mutex::new(app_state));
-
+    let mut_app_state = std::sync::Mutex::new(app_state);
+    let app_state = web::Data::new(mut_app_state);
     let mut_website = web::Data::new(std::sync::Mutex::new(website));
 
     HttpServer::new(move || {
@@ -53,10 +47,13 @@ async fn main() -> std::io::Result<()> {
             .service(Files::new("/static/", &static_dir))
             .service(
                 web::scope("/admin")
-                    .wrap(AuthService {
-                        token: String::from("abc"),
-                    })
-                    .service(test_unauthorized),
+                    .service(service::admin_login)
+                    .service(service::admin_authenticate)
+                    .service(
+                        web::scope("/auth")
+                            .wrap(AuthService {})
+                            .service(service::admin_workspace),
+                    ),
             )
             .service(service::files::favicon)
             .service(service::page)
diff --git a/src/middleware/authentication.rs b/src/middleware/authentication.rs
index 99e6fd1e3af4153d80bc331dad4880eccf92636e..8b4985491aeeac25c0ce9d74ea8a0bffae9bc490 100644
--- a/src/middleware/authentication.rs
+++ b/src/middleware/authentication.rs
@@ -1,3 +1,4 @@
+use crate::{app::AdminAuthToken, AppState};
 use actix_web::{
     body::{EitherBody, MessageBody},
     dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
@@ -7,9 +8,7 @@ use futures::prelude::future::LocalBoxFuture;
 use std::future::{ready, Ready};
 
 #[derive(Clone)]
-pub struct AuthService {
-    pub token: String,
-}
+pub struct AuthService;
 
 impl<S, B> Transform<S, ServiceRequest> for AuthService
 where
@@ -25,20 +24,17 @@ where
     fn new_transform(&self, service: S) -> Self::Future {
         ready(Ok(AuthenticatedMiddleware {
             service: std::rc::Rc::new(service),
-            auth: self.clone(),
         }))
     }
 }
 
 pub struct AuthenticatedMiddleware<S> {
     service: std::rc::Rc<S>,
-    auth: AuthService,
 }
 
-async fn authenticate(req: &mut ServiceRequest, token: String) -> bool {
-    let cookie = req.cookie("auth");
-    match cookie {
-        Some(cookie) => return cookie.value().to_string().eq(&token),
+async fn authenticate(req: &mut ServiceRequest, token: &AdminAuthToken) -> bool {
+    match req.cookie(&token.cookie_name) {
+        Some(cookie) => token.match_value(cookie.value().to_string()),
         None => false,
     }
 }
@@ -55,15 +51,23 @@ where
     forward_ready!(service);
 
     fn call(&self, req: ServiceRequest) -> Self::Future {
+        let token = {
+            let app_state = req
+                .app_data::<actix_web::web::Data<std::sync::Mutex<AppState>>>()
+                .expect("Failed to extract AppState from ServiceRequest")
+                .lock()
+                .expect("Failed to lock AppState Mutex");
+            app_state.admin_auth_token.clone()
+        };
+
         let service = self.service.clone();
-        let token = self.auth.token.to_owned();
 
         Box::pin(async move {
             let mut req = req;
-            if let false = authenticate(&mut req, token).await {
+            if let false = authenticate(&mut req, &token).await {
                 return Ok(req.into_response(
                     actix_web::HttpResponse::Unauthorized()
-                        .finish()
+                        .body("Error 401 - Unauthorized") // TODO a proper 401 view ?
                         .map_into_right_body(),
                 ));
             }
diff --git a/src/service/admin.rs b/src/service/admin.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7ed3bf0704d74df84c1449ef4a3a260a68430bb4
--- /dev/null
+++ b/src/service/admin.rs
@@ -0,0 +1,91 @@
+use crate::AppState;
+use actix_web::{
+    cookie::{time::Duration, Cookie, SameSite},
+    get, post,
+    web::{Data, Form},
+    HttpRequest, HttpResponse, Responder,
+};
+
+#[get("/workspace")]
+async fn admin_workspace() -> impl Responder {
+    actix_web::HttpResponse::Ok().body("Welcome Admin")
+}
+
+#[derive(serde::Deserialize)]
+struct Credentials {
+    username: String,
+    password: String,
+}
+
+#[post("/login")]
+async fn admin_authenticate(
+    credentials: Form<Credentials>,
+    app_state: Data<std::sync::Mutex<AppState>>,
+    req: HttpRequest,
+) -> impl Responder {
+    let (admin_username, admin_pwd, cookie_name) = {
+        let app_state = app_state.lock().unwrap();
+        (
+            app_state.config.admin_username.to_owned(),
+            app_state.config.admin_pwd.to_owned(),
+            app_state.config.admin_cookie_name.to_owned(),
+        )
+    };
+
+    if admin_username.eq(&credentials.username) && admin_pwd.eq(&credentials.password) {
+        let cookie_value = {
+            let mut app_state = app_state.lock().unwrap();
+            app_state.admin_auth_token.generate();
+            app_state
+                .admin_auth_token
+                .value
+                .as_ref()
+                .unwrap()
+                .to_owned()
+        };
+
+        let cookie = Cookie::build(cookie_name, cookie_value)
+            .path("/")
+            .http_only(true)
+            .max_age(Duration::days(7))
+            .same_site(SameSite::Strict)
+            .secure(true)
+            .finish();
+        return HttpResponse::Accepted().cookie(cookie).finish();
+    } else {
+        let mut res = HttpResponse::Unauthorized().finish();
+        match req.cookie(&cookie_name) {
+            Some(_) => {
+                res.del_cookie(&cookie_name);
+                return res;
+            }
+            None => return res,
+        }
+    }
+}
+
+#[get("/login")]
+pub async fn admin_login() -> impl Responder {
+    // TODO real HTML doc - create a module with admin views and a js file to load.
+    HttpResponse::Ok().body(
+        "
+<form id='admin-login-form'>
+    <input type='text' name='username'/>
+    <input type='password' name='password' /> 
+    <input type='submit' />
+</form>
+<script>
+document.getElementById('admin-login-form').onsubmit = function (e) {
+    e.preventDefault();
+    fetch('/admin/login', { method: 'POST', body: new URLSearchParams(new FormData(e.target)) }).then(res => {
+        if (res.status >= 200 && res.status < 400) {
+            console.log(res)
+            window.location = '/admin/auth/workspace';
+        }
+
+    }).catch(err => console.log(err))
+}
+</script>
+",
+    )
+}
diff --git a/src/service/mod.rs b/src/service/mod.rs
index b27ee732153f842b45a10dd99898468a3e696b8d..61f75aee4dccd0132a927a8ab004bde5b73b488d 100644
--- a/src/service/mod.rs
+++ b/src/service/mod.rs
@@ -1,3 +1,5 @@
+mod admin;
 pub mod files;
 mod page;
+pub use admin::*;
 pub use page::*;