I've been creating an API using Actix and Rust, and I needed a way to allow the API to be used by a React frontend. Some routes can only be accessed if the user has a token (eg viewing your user data), while others don't require a token to be accessed (for instance, the login handler routes). Here's how I created an authentication middleware in Actix which reads a token from a header in the request and throws an error if the token is absent or invalid. This middleware works on a route-by-route basis, and is compatible with Actix's CORS handler.
extern crate actix;
extern crate actix_web;
#[macro_use]
extern crate failure;
use actix_web::middleware::{Middleware, Started};
use actix_web::middleware::cors::Cors;
use actix_web::{HttpRequest, App, ResponseError, HttpResponse, http::Method, http::header, server};
// create a middleware
struct AuthMiddleware;
impl Middleware<AppState> for AuthMiddleware {
fn start(&self, req: &HttpRequest<AppState>) -> actix_web::Result<Started> {
// don't validate CORS pre-flight requests
if req.method() == "OPTIONS" {
return Ok(Started::Done);
}
// read the AUTHORIZATION header from the request
// this can be any header you send with the request
let token = req.headers()
.get("AUTHORIZATION")
.map(|value| value.to_str().ok())
.ok_or(ServiceError::Unauthorised)?;
match token {
Some(t) => {
// check that the token is valid - up to you how you do this
verify_token(&t)?; // this returns a ServiceError::Unauthorised result if the token is invalid
return Ok(Started::Done);
},
None => Err(ServiceError::Unauthorised.into())
}
}
}
pub fn create_app() -> App<AppState> {
App::with_state(AppState {})
// set up api routes
.prefix("/api")
// this is Actix's CORS builder - wraps all routes to allow API access
.configure(|app| Cors::for_app(app)
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
.allowed_headers(vec![header::ORIGIN, header::AUTHORIZATION, header::ACCEPT, header::CONTENT_TYPE])
.supports_credentials()
.max_age(3600)
// add API routes
// /login is not auth protected, since we want to be able to access it without a token
.resource("/login", |r| {
r.method(Method::POST).with(login); // pass to login handler function
})
// the /user route is protected with the auth middleware
.resource("/user", |r| {
r.middleware(AuthMiddleware);
r.method(Method::GET).with(show_user_details); // will only get here if the Authorization token is valid
r.method(Method::DELETE).with(logout);
})
// this route is also protected - each route has to have the middleware added to be protected
.resource("/user/update", |r| {
r.middleware(AuthMiddleware);
r.method(Method::POST).with(change_password);
})
// end of the CORS builder
.register())
}
// start the Actix server
fn main() {
server::new(|| create_app())
.bind("127.0.0.1:4321")
.expect("Couldn't bind to 127.0.0.1:4321")
.run();
}
// From here on is boilerplate - not really important for the auth example
// this would contain database info etc so you can read existing passwords and validate against the token
struct AppState {}
// error display struct
#[derive(Debug, Fail)]
pub enum ServiceError {
#[fail(display = "Unauthorised")]
Unauthorised
}
impl ResponseError for ServiceError {
fn error_response(&self) -> HttpResponse {
match *self {
ServiceError::Unauthorised => HttpResponse::Unauthorized().json("Unauthorised")
}
}
}
The key part is that the middleware is added to each route, inside the CORS builder. If you wanted to protect every route, or didn't need to use CORS, then you can use the same middleware on the app as part of the with_state
builder, which will mean every route will have the middleware applied:
pub fn create_app() -> App<AppState> {
App::with_state(AppState {})
.middleware(AuthMiddleware)
// all routes are now protected, so you'd need a token to get to the /login route
.resource("/login", |r| {
r.method(Method::POST).with(login);
})
.resource("/user", |r| {
r.method(Method::GET).with(show_user_details);
r.method(Method::DELETE).with(logout);
})
...