Authentication

Foxx provides the auth module to implement basic password verification and hashing but is not very secure unless using the (very slow) PBKDF2 algorithm. Alternatively you can use the OAuth 1.0a or OAuth 2.0 modules to offload identity management to a trusted provider (e.g. Facebook, GitHub, Google or Twitter).

The session middleware provides a mechanism for adding session logic to your service, using e.g. a collection or JSON Web Tokens to store the sessions between requests.

With these building blocks you can implement your own session-based authentication.

Implementing session authentication

In this example we’ll use two collections: a users collection to store the user objects with names and credentials, and a sessions collection to store the session data. We’ll also make sure usernames are unique by adding a hash index:

  1. "use strict";
  2. const { db } = require("@arangodb");
  3. const users = module.context.collectionName("users");
  4. if (!db._collection(users)) {
  5. db._createDocumentCollection(users);
  6. }
  7. const sessions = module.context.collectionName("sessions");
  8. if (!db._collection(sessions)) {
  9. db._createDocumentCollection(sessions);
  10. }
  11. module.context.collection("users").ensureIndex({
  12. type: "hash",
  13. unique: true,
  14. fields: ["username"]
  15. });

Next you should create a sessions middleware that uses the sessions collection and the “cookie” transport in a separate file, and add it to the service router:

  1. // in util/sessions.js
  2. "use strict";
  3. const sessionsMiddleware = require("@arangodb/foxx/sessions");
  4. const sessions = sessionsMiddleware({
  5. storage: module.context.collection("sessions"),
  6. transport: "cookie"
  7. });
  8. module.exports = sessions;
  1. // in your main file
  2. // ...
  3. const sessions = require("./util/sessions");
  4. module.context.use(sessions);

You’ll want to be able to use the authenticator throughout multiple parts of your service so it’s best to create it in a separate module and export it so we can import it anywhere we need it:

  1. "use strict";
  2. const createAuth = require("@arangodb/foxx/auth");
  3. const auth = createAuth();
  4. module.exports = auth;

If you want, you can now use the authenticator to help create an initial user in the setup script. Note we’re hardcoding the password here but you could make it configurable via a service configuration option:

  1. // ...
  2. const auth = require("./util/auth");
  3. const users = module.context.collection("users");
  4. if (!users.firstExample({ username: "admin" })) {
  5. users.save({
  6. username: "admin",
  7. password: auth.create("hunter2")
  8. });
  9. }

We can now put the two together to create a login route:

  1. // ...
  2. const auth = require("./util/auth");
  3. const users = module.context.collection("users");
  4. const joi = require("joi");
  5. const createRouter = require("@arangodb/foxx/router");
  6. const router = createRouter();
  7. router
  8. .post("/login", function(req, res) {
  9. const user = users.firstExample({
  10. username: req.body.username
  11. });
  12. const valid = auth.verify(
  13. // Pretend to validate even if no user was found
  14. user ? user.password : {},
  15. req.body.password
  16. );
  17. if (!valid) res.throw("unauthorized");
  18. // Log the user in using the key
  19. // because usernames might change
  20. req.session.uid = user._key;
  21. req.sessionStorage.save(req.session);
  22. res.send({ username: user.username });
  23. })
  24. .body(
  25. joi
  26. .object({
  27. username: joi.string().required(),
  28. password: joi.string().required()
  29. })
  30. .required()
  31. );

To provide information about the authenticated user we can look up the session user:

  1. router.get("/me", function(req, res) {
  2. try {
  3. const user = users.document(req.session.uid);
  4. res.send({ username: user.username });
  5. } catch (e) {
  6. res.throw("not found");
  7. }
  8. });

To log a user out we can remove the user from the session:

  1. router.post("/logout", function(req, res) {
  2. if (req.session.uid) {
  3. req.session.uid = null;
  4. req.sessionStorage.save(req.session);
  5. }
  6. res.status("no content");
  7. });

Finally when using the collection-based session storage, it’s a good idea to clean up expired sessions in a script which we can periodically call via an external tool like cron or a Foxx queue:

  1. "use strict";
  2. const sessions = require("./util/sessions");
  3. module.exports = sessions.storage.prune();

Using ArangoDB authentication

When using HTTP Basic authentication, ArangoDB will set the arangoUser attribute of the request object if the credentials match a valid ArangoDB user for the database.

Note: Although the presence and value of this attribute can be used to implement a low-level authentication mechanism this is only useful if your service is only intended to be used by developers who already have access to the HTTP API or the administrative web interface.

Example:

  1. router.get("/me", function(req, res) {
  2. if (req.arangoUser) {
  3. res.json({ username: req.arangoUser });
  4. } else {
  5. res.throw("not found");
  6. }
  7. });

Alternative sessions implementation

If you need more control than the sessions middleware provides, you can also create a basic session system with a few lines of code yourself:

  1. "use strict";
  2. const sessions = module.context.collection("sessions");
  3. // This is the secret string used to sign cookies
  4. // you probably don't want to hardcode this.
  5. const secret = "some secret string";
  6. module.context.use((req, res, next) => {
  7. // First read the session cookie if present
  8. let sid = req.cookie("sid", { secret });
  9. if (sid) {
  10. try {
  11. // Try to find a matching session
  12. req.session = sessions.document(sid);
  13. } catch (e) {
  14. // No session found, cookie is invalid
  15. sid = null;
  16. // Clear the cookie so it will be discarded
  17. res.cookie("sid", "", { ttl: -1, secret });
  18. }
  19. }
  20. try {
  21. // Continue handling the request
  22. next();
  23. } finally {
  24. // Do this even if the request threw
  25. if (req.session) {
  26. if (sid) {
  27. // Sync the session's changes to the db
  28. sessions.update(sid, req.session);
  29. } else {
  30. // Create a new session with a new key
  31. sid = sessions.save(req.session)._key;
  32. }
  33. // Set or update the session cookie
  34. res.cookie("sid", sid, { ttl: 24 * 60 * 60, secret });
  35. } else if (sid) {
  36. // The request handler explicitly cleared
  37. // the session, so we need to delete it
  38. sessions.remove(sid);
  39. // And clear the cookie too
  40. res.cookie("sid", "", { ttl: -1, secret });
  41. }
  42. }
  43. });