Rust with Actix
This quickstart integrates a Rust application with FusionAuth. This app includes pages for both authenticated users and unauthenticated members of the public.
While this article uses Actix, the Rust OAuth2 library can also be used in your preferred framework, such as Rocket or Axum.
Prerequisites#
For this quickstart, you will need:
- Docker version 20 or later, which is the quickest way to start FusionAuth. (There are other ways.)
- Rust and Cargo version 1.7 or later.
pkg-configandlibssl-dev, if they are not already installed on your system.- On macOS and Windows, one of the following container management tools:
- Docker desktop
- OrbStack (to use Orbstack for
docker composecommands after install, rundocker context use orbstack) - Podman (in the commands below, replace
dockerwithpodman)
Clone The Code#
To begin, clone the GitHub repository for this Quickstart to your local machine:
$ git clone https://github.com/FusionAuth/fusionauth-quickstart-rust-actix-web.git
$ cd fusionauth-quickstart-rust-actix-web
$ mkdir your-application
All shell commands in this guide can be entered in a terminal in this folder. On Windows, you need to replace forward slashes with backslashes in paths.
All the files you'll create in this guide already exist in the complete-application subfolder, if you prefer to copy them to your-application.
Run FusionAuth Via Docker#
The repository includes a docker-compose.yml that starts FusionAuth along with its PostgreSQL and OpenSearch dependencies. Run the following from the repository root:
$ docker compose up -d
On first startup, FusionAuth uses the included kickstart/kickstart.json to configure itself automatically via the Kickstart feature with these settings:
- Client Id:
E9FDB985-9173-4E01-9D73-AC2D60D1DC8E - Client secret:
super-secret-secret-that-should-be-regenerated-for-production - Example user:
richard@example.com/password - Admin user:
admin@example.com/password - FusionAuth base URL:
http://localhost:9011
The Basic Actix Application#
While this guide builds a new Actix project, you can use the same method to integrate your existing project with FusionAuth.
To skip ahead and run the application instead of writing it yourself:
- Navigate into the
complete-applicationdirectory. - Follow the instructions at to Run the Application.
From here on, you'll work in the your-application directory. Install the dependencies for the web server with the code below.
$ cd your-application && cargo init && cargo add actix-web@4 actix-files@0.6.2 dotenv@0.15.0 oauth2@4.4.2
$ cargo add actix-session@0.8.0 -F cookie-session && cargo add handlebars@4.5 -F dir_source && cargo add reqwest@0.11 -F json && cargo add serde@1.0 -F derive
Add the following code to your src/main.rs file.
use actix_web::{get, post, web, App, HttpResponse, HttpServer}; // web server
use actix_files as fs; // static image files
use actix_session::{Session, SessionMiddleware, storage::CookieSessionStore, config::CookieContentSecurity}; // store auth info in browser cookies
use actix_web::cookie::{Key, SameSite};
use handlebars::Handlebars; // html templates
use std::collections::HashMap; // pass data to templates
use dotenv::dotenv; // load .env file
mod auth;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv().ok();
let handlebars_ref = setup_handlebars().await;
let key = Key::generate();
HttpServer::new(move || {
App::new()
.wrap(SessionMiddleware::builder(CookieSessionStore::default(), key.clone())
.cookie_content_security(CookieContentSecurity::Private)
.cookie_same_site(SameSite::Lax)
.build())
.service(account)
.service(change_get)
.service(change_post)
.service(index)
.service(auth::login)
.service(auth::logout)
.service(auth::callback)
.service(fs::Files::new("/static", "static").show_files_listing())
.app_data(handlebars_ref.clone())
})
.bind(("127.0.0.1", 9012))?
.run()
.await
}
async fn setup_handlebars() -> web::Data<Handlebars<'static>> {
let mut handlebars = Handlebars::new();
handlebars
.register_templates_directory(".html", "./templates")
.unwrap();
web::Data::new(handlebars)
}
#[get("/")]
async fn index(hb: web::Data<Handlebars<'_>>, session: Session) -> HttpResponse {
if let Ok(Some(_)) = session.get::<String>("email") {
return HttpResponse::Found().append_header(("Location", "/account")).finish();
}
let body = hb.render("index", &{}).unwrap();
HttpResponse::Ok().body(body)
}
#[get("/account")]
async fn account(hb: web::Data<Handlebars<'_>>, session: Session) -> HttpResponse {
if let Ok(None) | Err(_) = session.get::<String>("email") {
return HttpResponse::Found().append_header(("Location", "/")).finish();
}
let mut data = HashMap::new();
data.insert("email", session.get::<String>("email").unwrap());
let body = hb.render("account", &data).unwrap();
HttpResponse::Ok().body(body)
}
#[get("/change")]
async fn change_get(hb: web::Data<Handlebars<'_>>, session: Session) -> HttpResponse {
if let Ok(None) | Err(_) = session.get::<String>("email") {
return HttpResponse::Found().append_header(("Location", "/")).finish();
}
let mut data = HashMap::<&str, String>::new();
data.insert("email", session.get::<String>("email").unwrap().unwrap());
data.insert("isGetRequest", "true".to_string());
let body = hb.render("change", &data).unwrap();
HttpResponse::Ok().body(body)
}
#[post("/change")]
async fn change_post(hb: web::Data<Handlebars<'_>>, session: Session, form: web::Form<HashMap<String, String>>) -> HttpResponse {
if let Ok(None) | Err(_) = session.get::<String>("email") {
return HttpResponse::Found().append_header(("Location", "/")).finish();
}
let mut data = HashMap::<&str, String>::new();
data.insert("email", session.get::<String>("email").unwrap().unwrap());
data.insert("isGetRequest", "false".to_string());
if let Some(amount) = form.get("amount") {
calculate_change(amount, &mut data);
}
else {
data.insert("isError", "true".to_string());
}
let body = hb.render("change", &data).unwrap();
HttpResponse::Ok().body(body)
}
fn calculate_change(amount: &str, state: &mut HashMap::<&str, String>) -> () {
let total = match amount.parse::<f64>() {
Ok(t) => t,
Err(_) => {
state.insert("isError", "true".to_string());
return;
}
};
let rounded_total = (total * 100.0).floor() / 100.0;
state.insert("isError", (!amount.chars().all(|c| c.is_digit(10) || c == '.')).to_string());
state.insert("total", format!("{:.2}", rounded_total));
let nickels = (rounded_total / 0.05).floor().abs();
state.insert("nickels", format!("{}", nickels));
let pennies = ((rounded_total - (0.05 * nickels)) / 0.01).round().abs();
state.insert("pennies", format!("{}", pennies));
}
The main function configures the web server, adds routes to it, and starts it. The function also:
- Uses
dotenvto allow you to call any values from the.envfile later in the application. - Uses private same-site cookies to store the user's email when logged in. Actix does not provide an anti-CSRF token as they believe same-site cookies render it unnecessary.
- Adds routes from the main file to the server, and some authentication routes, which you'll add in the next section.
- Enables the Handlebars library to provide HTML templates.
The remainder of this main file is three routes: index, account, and change. They each check if the user's email is in the HTTP request cookie (meaning the user is logged in), and display an HTML template. The account and change routes send the user's email to the template.
The change route is more complicated. It has two versions: one for GET and one for POST. The POST version checks the request's form to extract a dollar amount, then calls calculate_change to convert the dollars to nickels and pennies, and returns them in the state to the Handlebars template.
Authentication#
Authentication in Rust is managed by OAuth2.
Create the file .env in the your-application directory and insert the following lines.
FUSIONAUTH_CLIENT_ID="E9FDB985-9173-4E01-9D73-AC2D60D1DC8E"
FUSIONAUTH_CLIENT_SECRET="super-secret-secret-that-should-be-regenerated-for-production"
FUSIONAUTH_URL="http://localhost:9011"
FUSIONAUTH_REDIRECT_URL="http://localhost:9012/callback"
This tells Actix where to find and connect to FusionAuth.
Authentication is handled by src/auth.rs. Create that file now and insert the following code.
use actix_web::{get, web, Error, HttpResponse, Responder};
use actix_session::{Session};
use std::env;
use oauth2::{
AuthorizationCode,
AuthUrl,
ClientId,
ClientSecret,
CsrfToken,
PkceCodeChallenge,
PkceCodeVerifier,
RedirectUrl,
Scope,
TokenResponse,
TokenUrl
};
use oauth2::basic::BasicClient;
use oauth2::reqwest::async_http_client;
use reqwest;
use serde::Deserialize;
#[get("/logout")]
async fn logout(session: Session) -> impl Responder {
let _ = session.remove("email");
HttpResponse::Found().append_header(("Location", "/")).finish()
}
#[get("/login")]
async fn login(session: Session) -> impl Responder {
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let (auth_url, csrf_token) = get_oauth_client()
.authorize_url(CsrfToken::new_random)
.add_scope(Scope::new("openid".to_string()))
.add_scope(Scope::new("email".to_string()))
.add_scope(Scope::new("profile".to_string()))
.set_pkce_challenge(pkce_challenge)
.url();
let _ = session.insert("csrf_token", csrf_token);
let _ = session.insert("pkce_verifier", pkce_verifier);
HttpResponse::Found().append_header(("Location", auth_url.to_string())).finish()
}
#[get("/callback")]
async fn callback(params: web::Query<AuthCallbackParams>, session: Session) -> Result<HttpResponse, Error> {
// confirm pkce match
let received_state = ¶ms.state;
if let Ok(saved_state) = session.get::<String>("csrf_token") {
if saved_state != Some(received_state.clone()) {
return Ok(HttpResponse::BadRequest().body("PKCE state mismatch"));
}
}
else {
return Ok(HttpResponse::InternalServerError().body("Session error"));
}
// get access token
let pkce_verifier = session.get::<String>("pkce_verifier").unwrap().unwrap();
let token_result = match get_oauth_client()
.exchange_code(AuthorizationCode::new(params.code.clone()))
.set_pkce_verifier(PkceCodeVerifier::new(pkce_verifier))
.request_async(async_http_client)
.await {
Ok(result) => result,
Err(e) => {
return Ok(HttpResponse::InternalServerError().body("Error during token exchange"));
}
};
// get email
let client = reqwest::Client::new();
let user_info_response = match client
.get(format!("{}/oauth2/userinfo", env::var("FUSIONAUTH_URL").expect("Missing FUSIONAUTH_URL")))
.bearer_auth(token_result.access_token().secret())
.send()
.await {
Ok(result) => result,
Err(e) => {
return Ok(HttpResponse::InternalServerError().body("Error during get email"));
}
};
if user_info_response.status().is_success() {
let user_info = match user_info_response.json::<UserInfo>().await {
Ok(result) => result,
Err(e) => {
return Ok(HttpResponse::InternalServerError().body("Error during get email2"));
}
};
let _ = session.insert("email", user_info.email.clone());
}
else {
return Ok(HttpResponse::InternalServerError().body("Error during get email3"));
}
Ok(HttpResponse::Found().append_header(("Location", "/account")).finish())
}
fn get_oauth_client() -> BasicClient {
BasicClient::new(
ClientId::new(env::var("FUSIONAUTH_CLIENT_ID").expect("Missing FUSIONAUTH_CLIENT_ID")),
Some(ClientSecret::new(env::var("FUSIONAUTH_CLIENT_SECRET").expect("Missing FUSIONAUTH_CLIENT_SECRET"))),
AuthUrl::new(env::var("FUSIONAUTH_URL").expect("Missing FUSIONAUTH_URL") + "/oauth2/authorize").expect("Invalid AuthUrl"),
Some(TokenUrl::new(env::var("FUSIONAUTH_URL").expect("Missing FUSIONAUTH_URL") + "/oauth2/token").expect("Invalid TokenUrl"))
)
.set_redirect_uri(RedirectUrl::new(env::var("FUSIONAUTH_REDIRECT_URL").expect("Missing FUSIONAUTH_REDIRECT_URL")).expect("Invalid RedirectUrl"))
}
#[derive(Deserialize)]
struct AuthCallbackParams {
state: String,
code: String,
}
#[derive(Deserialize)]
struct UserInfo {
email: String,
}
This code has three routes: login, logout, and callback.
logoutclears the user's session and returns them to the home page.loginusesget_oauth_clientto read your variables from the.envfile and get a new client that constructs a URL to call FusionAuth, then redirects the user to that URL.callbackdoes most of the work. The user is returned tocallbackafter logging in with FusionAuth. The function checks that the PKCE challenge is correct, retrieves the access token, and makes a final call to FusionAuth to get the user's email, which it stores in the session. Now the application considers the user logged in.
Actix automatically links the user's session to their browser by returning a cookie for the site, which is then included in every subsequent request.
Now that callback has set a cookie, you can see how authentication on the other pages is tested. The application:
- Sends the user to the account page if they already have a login cookie on the home page.
- Sends the user to the home page if they are not logged in on the account page.
Customization#
With authentication done, the last task is to create example pages that a user can browse.
CSS And HTML#
Create a static directory within your-application directory.
$ mkdir static
Copy images from the example app.
$ cp ../complete-application/static/money.jpg ../complete-application/static/changebank.svg static/
Create a stylesheet file static/changebank.css and add the following code to it.
h1 {
color: #096324;
}
h3 {
color: #096324;
margin-top: 20px;
margin-bottom: 40px;
}
a {
color: #096324;
}
p {
font-size: 18px;
}
.header-email {
color: #096324;
margin-right: 20px;
}
.fine-print {
font-size: 16px;
}
body {
font-family: sans-serif;
padding: 0px;
margin: 0px;
}
.h-row {
display: flex;
align-items: center;
}
#page-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
#page-header {
flex: 0;
display: flex;
flex-direction: column;
}
#logo-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
}
.menu-bar {
display: flex;
flex-direction: row-reverse;
align-items: center;
height: 35px;
padding: 15px 50px 15px 30px;
background-color: #096324;
font-size: 20px;
}
.menu-link {
font-weight: 600;
color: #FFFFFF;
margin-left: 40px;
}
.menu-link {
font-weight: 600;
color: #FFFFFF;
margin-left: 40px;
}
.inactive {
text-decoration-line: none;
}
.button-lg {
width: 150px;
height: 30px;
background-color: #096324;
color: #FFFFFF;
font-size: 16px;
font-weight: 700;
border-radius: 10px;
text-align: center;
padding-top: 10px;
text-decoration-line: none;
}
.column-container {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.content-container {
flex: 1;
display: flex;
flex-direction: column;
padding: 60px 20px 20px 40px;
}
.balance {
font-size: 50px;
font-weight: 800;
}
.change-label {
font-size: 20px;
margin-right: 5px;
}
.change-input {
font-size: 20px;
height: 40px;
text-align: end;
padding-right: 10px;
}
.change-submit {
font-size: 15px;
height: 40px;
margin-left: 15px;
border-radius: 5px;
}
.change-message {
font-size: 20px;
margin-bottom: 15px;
}
.error-message {
font-size: 20px;
color: #FF0000;
margin-bottom: 15px;
}
.app-container {
flex: 0;
min-width: 440px;
display: flex;
flex-direction: column;
margin-top: 40px;
margin-left: 80px;
}
.change-container {
flex: 1;
}
Next, create a templates directory within your-application. Create three pages inside the templates directory. First create the home page, index.html, and paste the following code into it.
<html lang="en">
<head>
<meta charset="utf-8" />
<title>FusionAuth OpenID and PKCE example</title>
<link rel="stylesheet" href="static/changebank.css">
</head>
<body>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<a href="/">
<img src="static/changebank.svg" alt="logo"/>
</a>
<a class="button-lg" href="login">Login</a>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link">About</a>
<a class="menu-link">Services</a>
<a class="menu-link">Products</a>
<a class="menu-link" style="text-decoration-line: underline;">Home</a>
</div>
</div>
<div style="flex: 1;">
<div class="column-container">
<div class="content-container">
<div style="margin-bottom: 100px;">
<h1>Welcome to Changebank</h1>
<p>To get started, <a href="login">log in or create a new account</a>.</p>
</div>
</div>
<div style="flex: 0;">
<img src="static/money.jpg" style="max-width: 800px;" alt="coins"/>
</div>
</div>
</div>
</div>
</body>
</html>
The index page contains nothing to note except a link to the login page <a href="/login">.
Next, create an account.html page and paste the following code into it.
<html lang="en">
<head>
<meta charset="utf-8" />
<title>FusionAuth OpenID and PKCE example</title>
<link rel="stylesheet" href="static/changebank.css">
</head>
<body>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<a href="/">
<img src="static/changebank.svg" alt="logo"/>
</a>
<div class="h-row">
<p class="header-email">{{email}}</p>
<a class="button-lg" href="/logout" onclick="">Logout</a>
</div>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link inactive" href="/change">Make Change</a>
<a class="menu-link" href="/account">Account</a>
</div>
</div>
<div style="flex: 1;">
<div class="column-container">
<div class="app-container">
<h3>Your balance</h3>
<div class="balance">$0.00</div>
</div>
</div>
</div>
</body>
</html>
The account page displays the user's email from FusionAuth with <p class="header-email">{{email}}</p>.
The account page is only visible to logged in users. If a session email is not found, the user is redirected to login.
Finally, create a change.html page and paste the following code into it.
<html lang="en">
<head>
<meta charset="utf-8" />
<title>FusionAuth OpenID and PKCE example</title>
<link rel="stylesheet" href="static/changebank.css">
</head>
<body>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<a href="/">
<img src="static/changebank.svg" alt="logo"/>
</a>
<div class="h-row">
<p class="header-email">{{email}}</p>
<a class="button-lg" href="/logout">Logout</a>
</div>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link" href="/change">Make Change</a>
<a class="menu-link inactive" href="/account">Account</a>
</div>
</div>
<div style="flex: 1;">
<div class="column-container">
<div class="app-container change-container">
<h3>We Make Change</h3>
<!-- GET REQUEST ------------------------------------------------>
{{#if (eq isGetRequest "true")}}
<div class="change-message">Please enter a dollar amount:</div>
<form method="post" action="change">
<div class="h-row">
<div class="change-label">Amount in USD: $</div>
<input class="change-input" name="amount" value="" />
<input class="change-submit" type="submit" value="Make Change" />
</div>
</form>
<!-- POST REQUEST ----------------------------------------------->
{{else}}
{{#if (eq isError "true")}}
<div class="error-message">Please enter a dollar amount:</div>
{{else}}
<div class="change-message">
We can make change for {{total}} with {{nickels}} nickels and {{pennies}} pennies!
</div>
{{/if}}
<form method="post" action="change">
<div class="h-row">
<div class="change-label">Amount in USD: $</div>
<input class="change-input" name="amount" value="{{amount}}" />
<input class="change-submit" type="submit" value="Make Change" />
</div>
</form>
{{/if}}
</div>
</div>
</div>
</div>
</body>
</html>
The HTML at the bottom of the file displays a blank form when the page first loads (GET) or the result of the calculation when returning (POST).
Run The Application#
Run your application.
$ cargo run
Browse to the app at http://localhost:9012. Log in using richard@example.com and password. The change page allows you to enter a number. If you don't log in, you won't be able to access the change or account pages.
Next Steps#
This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you're going to want to do.
FusionAuth Customization
FusionAuth gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:
- Hosted pages such as login, registration, email verification, and many more.
- Email templates.
- User data and custom claims in access token JWTs.
Security
- You may want to customize the token expiration times and policies in FusionAuth.
- Choose password rules and a hashing algorithm that meet your security needs.
Tenant and Application Management
- Model your application topology using Applications, Roles, Groups, Entities, and more.
- Set up MFA, Social login, or SAML integrations.
- Integrate with external systems using Webhooks, SCIM, and Lambdas.