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; } // Finally, 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 }; 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; }