I’m experimenting with authentication in Opium and wonder: is it possible to protect only some routes with authentication? My experiments so far suggest that all requests pass trough all middleware layers, including the one demanding authentication, before the request is handled. An Opium app is composed like the code below and I initially had hoped that the location of the authentication layer in the stack would permit to handle some routes before and others after the authentication layer but this does not seem to be the case. Authentication is not special – you could also consider logging and might want to do logging only for some routes.
Interesting. So auth is the middleware, correct? Can it be used for scopes (path components) like /api/...? So every route under /api/... would get run through auth.
No, it is not a real middleware, it’s just a wrapper which return either a redirection to the login page or process the request itself. The full implementation is here:
let auth
: (Opium.Request.t -> Opium.Response.t Lwt.t) -> Opium.Request.t -> Opium.Response.t Lwt.t
= fun f req ->
match Request.cookie "session" req with
| None -> login () (* No session value provided, redirect to the login page *)
| Some k ->
try
let id = Int64.of_string k in
let t =
(* Here is the middleware *)
Db_model.query req @@
Db_model.Session.get_user id in
match%lwt t with
| None -> login ()
| Some user ->
let () = Logs.info (fun m -> m "Got the user %s" user.login) in
f req
with Failure _ -> login ()
The code is not published as it is just a toy for me and not a real project (but I could on demand)
A rewrite of Opium’s router is in progress, and as part of the plan for the new implementation, we would like to provide an API to compose routers (and more generally rock applications), which will address the “scoping” use case.
In the mean time, here’s a function I use in some of my projects:
let scope ?(route = "") ?(middlewares = []) router routes =
ListLabels.fold_left
routes
~init:router
~f:(fun router (meth, subroute, action) ->
let filters =
ListLabels.map ~f:(fun m -> m.Rock.Middleware.filter) middlewares
in
let service = Rock.Filter.apply_all filters action in
Router.add
router
~action:service
~meth
~route:(Route.of_string (route ^ subroute)))
Which can be used to scope routes with a list of middlewares and a prefix. For instance:
There hasn’t been a concensus on the best API for this, but hopefully we’ll come up with an acceptable solution soon and this will be integrated in Opium directly.
Cool. In ReWeb the equivalent (just swapping ‘filter’ with ‘middleware’) would be:
let reg_login_server = function
| `GET, ["register"] -> User.Registration.new_
| `POST, ["register"] -> User.Registration.create
| `GET, ["login"] -> User.Session.new_
| `POST, ["login"] -> User.Session.create
| _ -> not_found
let auth_server = function
| `DELETE, ["logout"] -> User.Session.delete
| `GET, ["settings"] -> User.Settings.edit
| _ -> not_found
let users_server = function
| meth, ["register" | "login"] as path ->
User.Filter.redirect_if_authenticated @@ reg_login_server @@ (meth, path)
| meth, _user :: ["logout" | "settings"] as path ->
User.Filter.require_authenticated @@ auth_server @@ (meth, path)
| _ ->
not_found
let server = function
| meth, "users" :: path ->
User.Filter.fetch_current_user @@ users_server @@ (meth, path)
| _ ->
not_found
This would handle the routes:
GET /users/register
POST /users/register
GET /users/login
POST /users/login
GET /users/yawar/settings (username can be any identifier of course)
DELETE /users/yawar/logout
We can nest scopes at an arbitrary level, as you can imagine. The downside is you give up modelling routes at the value level and they become patterns (which has its upsides too of course).