2022-08-17 00:58:52 -07:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-17 01:00:54 -07:00
|
|
|
// Then, look up this user's ID in the main Dress to Impress database.
|
2022-08-17 00:58:52 -07:00
|
|
|
// (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.`
|
|
|
|
|
);
|
|
|
|
|
}
|
2022-08-17 01:07:47 -07:00
|
|
|
const unsignedAuthToken = {
|
|
|
|
|
userId: impressId,
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
};
|
2022-08-17 00:58:52 -07:00
|
|
|
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;
|
|
|
|
|
}
|