forked from OpenNeo/impress-2020
Right, I had that idea while writing the comment, then forgot to actually do it lmao This is important for session expiration: we don't want you to be able to hold onto an old cookie for an account that you should be locked out of. Updating the `createdAt` value requires a new signature, so the client can't forge when this token was created, so we can be confident in our ability to expire them.
91 lines
3.6 KiB
JavaScript
91 lines
3.6 KiB
JavaScript
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;
|
|
}
|