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 cargo-shuttle installed):

cargo 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

Cargo.toml
[package]
name = "authentication"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-identity = "0.6.0"
actix-session = { version = "0.8.0", features = ["cookie-session"] }
actix-web = "4.3.1"
shuttle-actix-web = "0.27.0"
shuttle-runtime = "0.27.0"
tokio = "1.26.0"

Your main.rs should look like this:

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

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

#[get("/")]
async fn index(identity: Option<Identity>) -> actix_web::Result<impl Responder> {
	// if not logged in, return "anonymous"
	// else, return the user ID
    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 {
    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 actix_web() -> 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!
    //
    // If the secret key is read from a file or the environment, make sure it is generated securely.
    // For example, a secure random key (in base64 format) can be generated with the OpenSSL CLI:
    // ```
    // openssl rand -base64 64
    // ```
    //
    // Then decoded and used 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 cargo 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

If you want to explore other frameworks, we have more examples with popular ones like Tower and Warp. You can find them right here.

Be sure to check out the examples repo for many more examples!