We've been through a lot of actix-web
and actix-extras
so far. Hopefully you've learned enough
tricks to pull out some cool actix maneuvers. I would say that the biggest thing lacking is route
protection, and that's what we'll be tackling here.
The actix-web-httpauth crate
provides us with the
HttpAuthentication
middleware. We'll be using its
HttpAuthentication::bearer
version.
The Bearer authorization uses an access token to
grant access. This token is part of our request header, so requests to services that use the
HttpAuthentication
middleware must have Authorization: Bearer [token]
in their header.
So far we've been wrapping the whole App
with every middleware, and we could do the same for
HttpAuthentication
, but I want some routes to be unprotected (users/login
, users/register
,
and GET
services in general), so we'll be wrapping individual services in the middleware instead.
There are a few ways of using wrap
:
App::wrap
as we've been doing, it registers the middleware for the wholeApp
;Resource::wrap
this wraps a specificResource
only (we'll be using this version to protect some services, but in its macro form);Scope::wrap
which protects byScope
(a way of grouping services under a singular scope);
The App::wrap
version we've been using would require us to send requests with an authorization
header for every service, as HttpAuthentication
will forbid access without it, and there is no way
to open exceptions. Every request without this header would return a forbidden response, and we have
no control over it.
The other 2 are more malleable ways of setting middleware, as they don't wrap the whole application.
We're not using Scope
in any of these projects, for no particular reason, other than we have few
routes, so going the individual service routes ends up being more explicit and not cumbersome enough
to justify using those.
Service macros (get
, put
, post
, ...) have a wrap
attribute that does the wrapping for a
Resource
:
// This form could've been used to individually set up any middleware for specific services,
// it's not just an authentication thing.
#[post("/tasks", wrap = "HttpAuthentication::bearer(validator)")]
async fn insert(...) -> Result<impl Responder, AppError>
We set up HttpAuthentication
with the bearer
function, and pass into it a function that does
the validation (keep in mind that this function won't even be called if there is no authorization
header). This is the function that will handle our authorization process.
async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result<ServiceRequest, Error>
HttpAuthentication
middleware provides 2 forms of authentication, BasicAuth
and BearerAuth
, so
this function signature must match the kind you want.
The BearerAuth
extractor gives us with a way to check the
token
provided by the request header, without us having to manually dig into the req
parameter.
This middleware function will be called pre-services, so it must return a
ServiceRequest
that will be passed onwards. Note that this is not a HttpRequest
, but a ServiceRequest
, the
difference being that ServiceRequest
gives mutable access to the request contents, so we may
alter the request before it's passed down to our services.
Our validator
function uses the IdentityService
cookie embedded in the req
uest
to check if our user is currently logged in by matching the credentials.token()
with the
logged_user.token
. If they match we just pass the req
uest forward, otherwise we return some
error.
We'll be protecting services that change the database (POST
, PUT
, DELETE
), and allowing access
to any GET
request without login. There are only 2 POST
services allowed, /users/register
and
/users/login
, as we don't start the database with any user pre-registered, and logging-in has to
be accessible.
Protected services will require requests to contain a Authorization: Bearer token
, and for the
user to be previously logged in, which generates a LoggedUser
with a token, stored in the
auth-cookie
.
Trying to reach any other service without these conditions will give you a nice
ErrorUnauthorized(..)
of some sort. So you cannot insert, or delete a Task
without being logged
in first.
Up until now we were just using Identity::remember
to save the user in the auth-cookie
, but now
login
must have a way of generating an authorization token.
Our token creation is just a Hash
of the
user, not a secure way of handling things, but it's simple and good enough for this particular
project.
Things stayed relatively the same in this one, HttpAuthentication
came in as an add-on, with the
biggest changes being token generation in login
, and HttpAuthentication::bearer
being sprinkled
in services that we want to protect.
The next project, integration, contains another "project" re-structuring to allow
tests and services to live in different files. We'll be separating it into a lib
project and a
bin
that uses it.