Skip to content

Commit

Permalink
Add actix_web tutorial (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
myOmikron authored Jan 11, 2023
1 parent 04c3bf3 commit f99ed1f
Show file tree
Hide file tree
Showing 10 changed files with 627 additions and 0 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

* Eric Mark Martin (ericmarkmartin)
* Ben Wishovich (benwis)
* Niklas Pfister (myOmikron)
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ members = [
# Tutorial / Example sites.
"tutorial/server/tide",
"tutorial/server/axum",
"tutorial/server/actix_web",
"tutorial/wasm",
# Fido MDS tools
"fido-mds",
Expand Down
35 changes: 35 additions & 0 deletions tutorial/server/actix_web/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "actix_web"
version = "0.1.0"
edition = "2021"
authors = ["Niklas Pfister <git@omikron.dev>"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
# Webframework
actix-web = { version = "~4.2" }
# Session framework for actix-web
actix-session = { version = "~0.7", features = ["cookie-session"] }
# Async trait, anyhow, chrono, once_cell and rand are required for the implementation of a
# server-side memory-backed session store.
# Normally, you want to use a database / redis backend as session store, but for the simplicity of this
# tutorial, we implement our own.
async-trait = { version = "~0.1" }
anyhow = { version = "~1.0" }
chrono = { version = "~0.4" }
once_cell = { version = "~1.17" }
rand = { version = "~0.8" }

# Serve static file. Used to serve wasm
actix-files = { version = "~0.6" }

# Async runtime
tokio = { version = "~1.24", features = ["macros", "rt-multi-thread"] }

# Logging framework + facade
env_logger = { version = "~0.10" }
log = { version = "~0.4" }

# Webauthn framework
webauthn-rs = { path = "../../../webauthn-rs", features = ["danger-allow-state-serialisation"] }
261 changes: 261 additions & 0 deletions tutorial/server/actix_web/src/handler/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
use actix_session::Session;
use actix_web::web::{Data, Json, Path};
use actix_web::HttpResponse;
use log::{debug, info};
use tokio::sync::Mutex;

use crate::handler::{Error, WebResult};
use crate::startup::UserData;

/*
* Webauthn RS auth handlers.
* These files use webauthn to process the data received from each route, and are closely tied to actix_web
*/

// 1. Import the prelude - this contains everything needed for the server to function.
use webauthn_rs::prelude::*;

// 2. The first step a client (user) will carry out is requesting a credential to be
// registered. We need to provide a challenge for this. The work flow will be:
//
// ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
// │ Authenticator │ │ Browser │ │ Site │
// └───────────────┘ └───────────────┘ └───────────────┘
// │ │ │
// │ │ 1. Start Reg │
// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│
// │ │ │
// │ │ 2. Challenge │
// │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
// │ │ │
// │ 3. Select Token │ │
// ─ ─ ─│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
// 4. Verify │ │ │ │
// │ 4. Yield PubKey │ │
// └ ─ ─▶│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │
// │ │ │
// │ │ 5. Send Reg Opts │
// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│─ ─ ─
// │ │ │ │ 5. Verify
// │ │ │ PubKey
// │ │ │◀─ ─ ┘
// │ │ │─ ─ ─
// │ │ │ │ 6. Persist
// │ │ │ Credential
// │ │ │◀─ ─ ┘
// │ │ │
// │ │ │
//
// In this step, we are responding to the start reg(istration) request, and providing
// the challenge to the browser.
pub(crate) async fn start_register(
username: Path<String>,
session: Session,
webauthn_users: Data<Mutex<UserData>>,
webauthn: Data<Webauthn>,
) -> WebResult<Json<CreationChallengeResponse>> {
info!("Start register");

// We get the username from the URL, but you could get this via form submission or
// some other process. In some parts of Webauthn, you could also use this as a "display name"
// instead of a username. Generally you should consider that the user *can* and *will* change
// their username at any time.

// Since a user's username could change at anytime, we need to bind to a unique id.
// We use uuid's for this purpose, and you should generate these randomly. If the
// username does exist and is found, we can match back to our unique id. This is
// important in authentication, where presented credentials may *only* provide
// the unique id, and not the username!

let user_unique_id = {
let users_guard = webauthn_users.lock().await;
users_guard
.name_to_id
.get(username.as_str())
.copied()
.unwrap_or_else(Uuid::new_v4)
};

// Remove any previous registrations that may have occurred from the session.
session.remove("reg_state");

// If the user has any other credentials, we exclude these here so they can't be duplicate registered.
// It also hints to the browser that only new credentials should be "blinked" for interaction.
let exclude_credentials = {
let users_guard = webauthn_users.lock().await;
users_guard
.keys
.get(&user_unique_id)
.map(|keys| keys.iter().map(|sk| sk.cred_id().clone()).collect())
};

let (ccr, reg_state) = webauthn
.start_passkey_registration(user_unique_id, &username, &username, exclude_credentials)
.map_err(|e| {
debug!("challenge_register -> {:?}", e);
Error::Unknown(e)
})?;

// Note that due to the session store in use being a server side memory store, this is
// safe to store the reg_state into the session since it is not client controlled and
// not open to replay attacks. If this was a cookie store, this would be UNSAFE.
session
.insert("reg_state", (username.as_str(), user_unique_id, reg_state))
.expect("Failed to insert");

info!("Registration Successful!");
Ok(Json(ccr))
}

// 3. The browser has completed it's steps and the user has created a public key
// on their device. Now we have the registration options sent to us, and we need
// to verify these and persist them.

pub(crate) async fn finish_register(
req: Json<RegisterPublicKeyCredential>,
session: Session,
webauthn_users: Data<Mutex<UserData>>,
webauthn: Data<Webauthn>,
) -> WebResult<HttpResponse> {
let (username, user_unique_id, reg_state) = session.get("reg_state")?.unwrap();

session.remove("reg_state");

let sk = webauthn
.finish_passkey_registration(&req, &reg_state)
.map_err(|e| {
debug!("challenge_register -> {:?}", e);
Error::BadRequest(e)
})?;

let mut users_guard = webauthn_users.lock().await;

//TODO: This is where we would store the credential in a db, or persist them in some other way.

users_guard
.keys
.entry(user_unique_id)
.and_modify(|keys| keys.push(sk.clone()))
.or_insert_with(|| vec![sk.clone()]);

users_guard.name_to_id.insert(username, user_unique_id);

Ok(HttpResponse::Ok().finish())
}

// 4. Now that our public key has been registered, we can authenticate a user and verify
// that they are the holder of that security token. The work flow is similar to registration.
//
// ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
// │ Authenticator │ │ Browser │ │ Site │
// └───────────────┘ └───────────────┘ └───────────────┘
// │ │ │
// │ │ 1. Start Auth │
// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│
// │ │ │
// │ │ 2. Challenge │
// │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
// │ │ │
// │ 3. Select Token │ │
// ─ ─ ─│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
// 4. Verify │ │ │ │
// │ 4. Yield Sig │ │
// └ ─ ─▶│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │
// │ │ 5. Send Auth │
// │ │ Opts │
// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│─ ─ ─
// │ │ │ │ 5. Verify
// │ │ │ Sig
// │ │ │◀─ ─ ┘
// │ │ │
// │ │ │
//
// The user indicates the wish to start authentication and we need to provide a challenge.

pub(crate) async fn start_authentication(
username: Path<String>,
session: Session,
webauthn_users: Data<Mutex<UserData>>,
webauthn: Data<Webauthn>,
) -> WebResult<Json<RequestChallengeResponse>> {
info!("Start Authentication");
// We get the username from the URL, but you could get this via form submission or
// some other process.

// Remove any previous authentication that may have occurred from the session.
session.remove("auth_state");

// Get the set of keys that the user possesses
let users_guard = webauthn_users.lock().await;

// Look up their unique id from the username
let user_unique_id = users_guard
.name_to_id
.get(username.as_str())
.copied()
.ok_or(Error::UserNotFound)?;

let allow_credentials = users_guard
.keys
.get(&user_unique_id)
.ok_or(Error::UserHasNoCredentials)?;

let (rcr, auth_state) = webauthn
.start_passkey_authentication(allow_credentials)
.map_err(|e| {
debug!("challenge_authenticate -> {:?}", e);
Error::Unknown(e)
})?;

// Drop the mutex to allow the mut borrows below to proceed
drop(users_guard);

// Note that due to the session store in use being a server side memory store, this is
// safe to store the auth_state into the session since it is not client controlled and
// not open to replay attacks. If this was a cookie store, this would be UNSAFE.
session.insert("auth_state", (user_unique_id, auth_state))?;

Ok(Json(rcr))
}

// 5. The browser and user have completed their part of the processing. Only in the
// case that the webauthn authenticate call returns Ok, is authentication considered
// a success. If the browser does not complete this call, or *any* error occurs,
// this is an authentication failure.

pub(crate) async fn finish_authentication(
auth: Json<PublicKeyCredential>,
session: Session,
webauthn_users: Data<Mutex<UserData>>,
webauthn: Data<Webauthn>,
) -> WebResult<HttpResponse> {
let (user_unique_id, auth_state) = session.get("auth_state")?.ok_or(Error::CorruptSession)?;

session.remove("auth_state");

let auth_result = webauthn
.finish_passkey_authentication(&auth, &auth_state)
.map_err(|e| {
debug!("challenge_register -> {:?}", e);
Error::BadRequest(e)
})?;

let mut users_guard = webauthn_users.lock().await;

// Update the credential counter, if possible.
users_guard
.keys
.get_mut(&user_unique_id)
.map(|keys| {
keys.iter_mut().for_each(|sk| {
// This will update the credential if it's the matching
// one. Otherwise it's ignored. That is why it is safe to
// iterate this over the full list.
sk.update_credential(&auth_result);
})
})
.ok_or(Error::UserHasNoCredentials)?;

info!("Authentication Successful!");
Ok(HttpResponse::Ok().finish())
}
27 changes: 27 additions & 0 deletions tutorial/server/actix_web/src/handler/index.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use actix_web::HttpResponse;

pub(crate) async fn index() -> HttpResponse {
HttpResponse::Ok().body(
r#"
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebAuthn-rs Tutorial</title>
<script type="module">
import init, { run_app } from './pkg/wasm.js';
async function main() {
await init('./pkg/wasm_bg.wasm');
run_app();
}
main()
</script>
</head>
<body>
<p>Welcome to the WebAuthn Server!</p>
</body>
</html>"#,
)
}
59 changes: 59 additions & 0 deletions tutorial/server/actix_web/src/handler/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use actix_session::{SessionGetError, SessionInsertError};
use std::fmt::{Display, Formatter};

use actix_web::http::StatusCode;
use webauthn_rs::prelude::WebauthnError;

pub(crate) mod auth;
pub(crate) mod index;
pub(crate) mod serve_wasm;

/**
Type alias for Errors that implement [actix_web::ResponseError] through [Error]
*/
type WebResult<T> = Result<T, Error>;

/**
Unified errors for simpler Responses
*/
#[derive(Debug)]
pub(crate) enum Error {
Unknown(WebauthnError),
SessionGet(SessionGetError),
SessionInsert(SessionInsertError),
CorruptSession,
BadRequest(WebauthnError),
UserNotFound,
UserHasNoCredentials,
}

impl From<SessionGetError> for Error {
fn from(value: SessionGetError) -> Self {
Self::SessionGet(value)
}
}

impl From<SessionInsertError> for Error {
fn from(value: SessionInsertError) -> Self {
Self::SessionInsert(value)
}
}

impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Error::Unknown(_) => write!(f, "Unknown webauthn error"),
Error::SessionGet(_) | Error::SessionInsert(_) => write!(f, "Corrupt session"),
Error::BadRequest(_) => write!(f, "Bad request"),
Error::UserNotFound => write!(f, "User not found"),
Error::UserHasNoCredentials => write!(f, "User has no credentials"),
Error::CorruptSession => write!(f, "Corrupt session"),
}
}
}

impl actix_web::ResponseError for Error {
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
}
Loading

0 comments on commit f99ed1f

Please sign in to comment.