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:
"use strict";
const { db } = require("@arangodb");
const users = module.context.collectionName("users");
if (!db._collection(users)) {
db._createDocumentCollection(users);
}
const sessions = module.context.collectionName("sessions");
if (!db._collection(sessions)) {
db._createDocumentCollection(sessions);
}
module.context.collection("users").ensureIndex({
type: "hash",
unique: true,
fields: ["username"]
});
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:
// in util/sessions.js
"use strict";
const sessionsMiddleware = require("@arangodb/foxx/sessions");
const sessions = sessionsMiddleware({
storage: module.context.collection("sessions"),
transport: "cookie"
});
module.exports = sessions;
// in your main file
// ...
const sessions = require("./util/sessions");
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:
"use strict";
const createAuth = require("@arangodb/foxx/auth");
const auth = createAuth();
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:
// ...
const auth = require("./util/auth");
const users = module.context.collection("users");
if (!users.firstExample({ username: "admin" })) {
users.save({
username: "admin",
password: auth.create("hunter2")
});
}
We can now put the two together to create a login route:
// ...
const auth = require("./util/auth");
const users = module.context.collection("users");
const joi = require("joi");
const createRouter = require("@arangodb/foxx/router");
const router = createRouter();
router
.post("/login", function(req, res) {
const user = users.firstExample({
username: req.body.username
});
const valid = auth.verify(
// Pretend to validate even if no user was found
user ? user.password : {},
req.body.password
);
if (!valid) res.throw("unauthorized");
// Log the user in using the key
// because usernames might change
req.session.uid = user._key;
req.sessionStorage.save(req.session);
res.send({ username: user.username });
})
.body(
joi
.object({
username: joi.string().required(),
password: joi.string().required()
})
.required()
);
To provide information about the authenticated user we can look up the session user:
router.get("/me", function(req, res) {
try {
const user = users.document(req.session.uid);
res.send({ username: user.username });
} catch (e) {
res.throw("not found");
}
});
To log a user out we can remove the user from the session:
router.post("/logout", function(req, res) {
if (req.session.uid) {
req.session.uid = null;
req.sessionStorage.save(req.session);
}
res.status("no content");
});
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:
"use strict";
const sessions = require("./util/sessions");
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:
router.get("/me", function(req, res) {
if (req.arangoUser) {
res.json({ username: req.arangoUser });
} else {
res.throw("not found");
}
});
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:
"use strict";
const sessions = module.context.collection("sessions");
// This is the secret string used to sign cookies
// you probably don't want to hardcode this.
const secret = "some secret string";
module.context.use((req, res, next) => {
// First read the session cookie if present
let sid = req.cookie("sid", { secret });
if (sid) {
try {
// Try to find a matching session
req.session = sessions.document(sid);
} catch (e) {
// No session found, cookie is invalid
sid = null;
// Clear the cookie so it will be discarded
res.cookie("sid", "", { ttl: -1, secret });
}
}
try {
// Continue handling the request
next();
} finally {
// Do this even if the request threw
if (req.session) {
if (sid) {
// Sync the session's changes to the db
sessions.update(sid, req.session);
} else {
// Create a new session with a new key
sid = sessions.save(req.session)._key;
}
// Set or update the session cookie
res.cookie("sid", sid, { ttl: 24 * 60 * 60, secret });
} else if (sid) {
// The request handler explicitly cleared
// the session, so we need to delete it
sessions.remove(sid);
// And clear the cookie too
res.cookie("sid", "", { ttl: -1, secret });
}
}
});