impress-2020/src/server/auth-by-db.js

92 lines
3.6 KiB
JavaScript
Raw Normal View History

import { createHmac } from "crypto";
import { normalizeRow } from "./util";
export async function getAuthToken({ username, password }, db) {
// For legacy reasons (and I guess decent security reasons too!), auth info
// is stored in a users table in a separate database. First, look up the user
// in that database, and get their encrypted auth info.
const [rowsFromOpenneoId] = await db.query(
`
SELECT id, encrypted_password, password_salt FROM openneo_id.users
WHERE name = ?;
`,
[username]
);
if (rowsFromOpenneoId.length === 0) {
console.debug(
`[getAuthToken] Failed: No user named ${JSON.stringify(username)}.`
);
return null;
}
// Then, use the HMAC-SHA256 algorithm to validate the password the user is
// trying to log in with. The random salt for this user, saved in the
// database, is the HMAC "key". (That way, if our database leaks, each user's
// password would need to be cracked individually, instead of being
// susceptible to attacks where you match our database against a database of
// SHA256 hashes for common passwords.)
const { id, encryptedPassword, passwordSalt } = normalizeRow(
rowsFromOpenneoId[0]
);
const passwordHmac = createHmac("sha256", passwordSalt);
passwordHmac.update(password);
const encryptedProvidedPassword = passwordHmac.digest("hex");
if (encryptedProvidedPassword !== encryptedPassword) {
console.debug(
`[getAuthToken] Failed: Encrypted input password ` +
`${JSON.stringify(encryptedProvidedPassword)} ` +
`did not match for user ${JSON.stringify(username)}.`
);
return null;
}
// Then, look up this user's ID in the main Dress to Impress database.
// (For silly legacy reasons, it can be - and in our current database is
// always! - different than the ID in the Openneo ID database.)
const [rowsFromOpenneoImpress] = await db.query(
`
SELECT id FROM openneo_impress.users WHERE remote_id = ?;
`,
[id]
);
if (rowsFromOpenneoImpress.length === 0) {
// TODO: Auto-create the impress row in this case? will it ever happen tho?
throw new Error(
`Syncing error: user exists in openneo_id, but not openneo_impress.`
);
}
const { id: impressId } = normalizeRow(rowsFromOpenneoImpress[0]);
// Finally, create the auth token object. This contains a `userId` field, a
// `createdAt` field, and a signature of the object with every field but the
// `signature` field. The signature also uses HMAC-SHA256 (which doesn't at
// all need to be in sync with the password hashing, but it's a good
// algorithm so we chose it again), and the key this time is a secret global
// value called `DTI_AUTH_TOKEN_SECRET`. This proves that the auth token was
// generated by the app, because only the app knows the secret.
if (process.env["DTI_AUTH_TOKEN_SECRET"] == null) {
throw new Error(
`The DTI_AUTH_TOKEN_SECRET environment variable is missing. ` +
`The server admin should create a random secret, and save it in the ` +
`.env file.`
);
}
const unsignedAuthToken = {
userId: impressId,
createdAt: new Date().toISOString(),
};
const authTokenHmac = createHmac(
"sha256",
process.env["DTI_AUTH_TOKEN_SECRET"]
);
authTokenHmac.update(JSON.stringify(unsignedAuthToken));
const signature = authTokenHmac.digest("hex");
const authToken = { ...unsignedAuthToken, signature };
// Login success! Return the auth token. The caller will handle setting it to
// a cookie etc.
console.debug(`[getAuthToken] Succeeded: ${JSON.stringify(authToken)}`);
return authToken;
}