Authentication

Foxx provides the auth module to implementbasic password verification and hashing but is not very secure unless usingthe (very slow) PBKDF2 algorithm. Alternatively you can use theOAuth 1.0a orOAuth 2.0 modules to offload identitymanagement to a trusted provider (e.g. Facebook, GitHub, Google or Twitter).

The session middleware provides a mechanismfor adding session logic to your service, using e.g. a collection orJSON Web Tokens to store the sessions between requests.

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

Implementing session authentication

In this example we’ll use two collections: a users collection to store theuser objects with names and credentials, and a sessions collection to storethe session data. We’ll also make sure usernames are uniqueby 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 sessionscollection and the “cookie” transport in a separate file, and add itto 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 partsof your service so it’s best to create it in a separate module and export itso 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 userin the setup script. Note we’re hardcoding the password here but you couldmake it configurable via aservice 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.authData : {},
  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 upthe 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 toclean up expired sessions in a script which we can periodically call via anexternal 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 arangoUserattribute of the request object if thecredentials match a valid ArangoDB user for the database.

Note: Although the presence and value of this attribute can be used toimplement a low-level authentication mechanism this is only useful if yourservice is only intended to be used by developers who already have access tothe 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. });