Skip to content

Latest commit

 

History

History
167 lines (132 loc) · 5.49 KB

File metadata and controls

167 lines (132 loc) · 5.49 KB
title Cookie Authentication
og:title Cookie Authentication with Actix Web
description Explore how you can secure your Actix Web application by using cookies.

Description

This example shows how to use authentication within actix-web with cookies, assisted by actix-identity and actix-session. The idea is that all requests authenticate first at the login route to get a cookie, then the cookie is sent with all requests requiring authentication using the HTTP cookie header.

You can clone the example below by running the following (you'll need shuttle CLI installed):

shuttle init --from shuttle-hq/shuttle-examples --subfolder actix-web/cookie-authentication

Three Actix Web routes are registered in this file:

  • /public: a route that can be called without needing any authentication.
  • /login: a route for posting a JSON object with a username and password to get a cookie.
  • /private: a route that will display whether you're logged in or not, based on if you're logged in.

The example uses actix-identity and actix-session with a cookie store to assist with easy setup.

Code

[package]
name = "cookie-authentication"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-identity = "0.7.1"
actix-session = { version = "0.9.0", features = ["cookie-session"] }
actix-web = "4.3.1"
shuttle-actix-web = "0.57.0"
shuttle-runtime = "0.57.0"
tokio = "1.26.0"

Your main.rs should look like this:

use actix_identity::{Identity, IdentityMiddleware};
use actix_session::{config::PersistentSession, storage::CookieSessionStore, SessionMiddleware};
use actix_web::{
    cookie::{time::Duration, Key},
    error, get,
    http::StatusCode,
    middleware,
    web::{self, ServiceConfig},
    HttpMessage as _, HttpRequest, Responder,
};
use shuttle_actix_web::ShuttleActixWeb;

const FIVE_MINUTES: Duration = Duration::minutes(5);

#[get("/")]
async fn index(identity: Option<Identity>) -> actix_web::Result<impl Responder> {
    let id = match identity.map(|id| id.id()) {
        None => "anonymous".to_owned(),
        Some(Ok(id)) => id,
        Some(Err(err)) => return Err(error::ErrorInternalServerError(err)),
    };

    Ok(format!("Hello {id}"))
}

#[get("/login")]
async fn login(req: HttpRequest) -> impl Responder {
    // some kind of authentication should happen here

    // attach a verified user identity to the active session
    Identity::login(&req.extensions(), "user1".to_owned()).unwrap();

    web::Redirect::to("/").using_status_code(StatusCode::FOUND)
}

#[get("/logout")]
async fn logout(id: Identity) -> impl Responder {
    id.logout();

    web::Redirect::to("/").using_status_code(StatusCode::FOUND)
}

#[shuttle_runtime::main]
async fn main() -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
    // Generate a random secret key. Note that it is important to use a unique
    // secret key for every project. Anyone with access to the key can generate
    // authentication cookies for any user!
    //
    // When deployed the secret key should be read from deployment secrets.
    //
    // For example, a secure random key (in base64 format) can be generated with the OpenSSL CLI:
    // ```
    // openssl rand -base64 64
    // ```
    //
    // Then decoded and converted to a Key:
    // ```
    // let secret_key = Key::from(base64::decode(&private_key_base64).unwrap());
    // ```
    let secret_key = Key::generate();

    let config = move |cfg: &mut ServiceConfig| {
        cfg.service(
            web::scope("")
                .service(index)
                .service(login)
                .service(logout)
                .wrap(IdentityMiddleware::default())
                .wrap(
                    SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
                        .cookie_name("auth-example".to_owned())
                        .cookie_secure(false)
                        .session_lifecycle(PersistentSession::default().session_ttl(FIVE_MINUTES))
                        .build(),
                )
                .wrap(middleware::NormalizePath::trim())
                .wrap(middleware::Logger::default()),
        );
    };

    Ok(config.into())
}

Usage

Once you've cloned this example, launch it locally by using shuttle run. Once you've verified that it's up, you'll now be able to go to http://localhost:8000 and start trying the example out!

First, we should be able to access the public endpoint without any authentication using:

curl http://localhost:8000/public

But trying to access the private endpoint will return "Hello anonymous":

curl http://localhost:8000/private

So let's get a cookie from the login route first:

curl http://localhost:8000/login

Accessing the private endpoint with the token will now succeed:

curl --header "Authorization: Bearer <token>" http://localhost:8000/private

The token is set to expire in 5 minutes, so wait a while and try to access the private endpoint again. Once the token has expired, a user will need to get a new token from login. Since tokens usually have a longer than 5 minutes expiration time, we can create a /refresh endpoint that takes an active token and returns a new token with a refreshed expiration time.

Looking to extend this example? Here's a couple of ideas to get you started:

  • Create a frontend to host the login
  • Add a route for registering
  • Use a database to check login credentials