Track sign-ins & IP addresses
Oh right, these are some logging-ish things that Classic DTI would perform! It's easy enough for us to keep the fields up-to-date too, so let's do it!
This commit is contained in:
parent
4c9dbf91fb
commit
c7ba61a0f1
3 changed files with 62 additions and 24 deletions
|
@ -1,7 +1,8 @@
|
|||
import { createHmac } from "crypto";
|
||||
import { normalizeRow } from "./util";
|
||||
|
||||
export async function getAuthToken({ username, password }, db) {
|
||||
|
||||
export async function getAuthToken({ username, password, ipAddress }, 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.
|
||||
|
@ -19,18 +20,12 @@ export async function getAuthToken({ username, password }, db) {
|
|||
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.)
|
||||
// Then, use the password encrpytion function to validate the password the
|
||||
// user is trying to log in with.
|
||||
const { id, encryptedPassword, passwordSalt } = normalizeRow(
|
||||
rowsFromOpenneoId[0]
|
||||
);
|
||||
const passwordHmac = createHmac("sha256", passwordSalt);
|
||||
passwordHmac.update(password);
|
||||
const encryptedProvidedPassword = passwordHmac.digest("hex");
|
||||
const encryptedProvidedPassword = encryptPassword(password, passwordSalt);
|
||||
|
||||
if (encryptedProvidedPassword !== encryptedPassword) {
|
||||
console.debug(
|
||||
|
@ -58,24 +53,53 @@ export async function getAuthToken({ username, password }, db) {
|
|||
}
|
||||
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.
|
||||
// One more thing: Update the user record to keep track of this login.
|
||||
await db.query(
|
||||
`
|
||||
UPDATE openneo_id.users
|
||||
SET last_sign_in_at = current_sign_in_at,
|
||||
current_sign_in_at = CURRENT_TIMESTAMP(),
|
||||
last_sign_in_ip = current_sign_in_ip,
|
||||
current_sign_in_ip = ?,
|
||||
sign_in_count = sign_in_count + 1,
|
||||
updated_at = CURRENT_TIMESTAMP()
|
||||
WHERE id = ? LIMIT 1;
|
||||
`,
|
||||
[ipAddress, id]
|
||||
);
|
||||
|
||||
// Finally, create and return the auth token itself. The caller will handle
|
||||
// setting it to a cookie etc.
|
||||
const authToken = createAuthToken(impressId);
|
||||
console.debug(`[getAuthToken] Succeeded: ${JSON.stringify(authToken)}`);
|
||||
return authToken;
|
||||
}
|
||||
|
||||
function createAuthToken(impressId) {
|
||||
// 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.
|
||||
const unsignedAuthToken = {
|
||||
userId: impressId,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const signature = computeSignatureForAuthToken(unsignedAuthToken);
|
||||
const authToken = { ...unsignedAuthToken, signature };
|
||||
return { ...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;
|
||||
function encryptPassword(password, passwordSalt) {
|
||||
// Use HMAC-SHA256 to encrypt the password. 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 passwordHmac = createHmac("sha256", passwordSalt);
|
||||
passwordHmac.update(password);
|
||||
return passwordHmac.digest("hex");
|
||||
}
|
||||
|
||||
export async function getUserIdFromToken(authToken) {
|
||||
|
|
|
@ -80,6 +80,12 @@ const config = {
|
|||
currentUserId = null;
|
||||
}
|
||||
|
||||
// In production, the server is behind a few proxy layers (both nginx and
|
||||
// the CDN), so this IP header will be managed by them and can be trusted.
|
||||
// (Can be null if something is set up a bit different, e.g. in local
|
||||
// development.)
|
||||
const ipAddress = req?.headers?.["x-forwarded-for"] || null;
|
||||
|
||||
return {
|
||||
db,
|
||||
currentUserId,
|
||||
|
@ -100,6 +106,7 @@ const config = {
|
|||
res.setHeader("Set-Cookie", `DTIAuthToken=; Max-Age=-1`);
|
||||
}
|
||||
},
|
||||
ipAddress,
|
||||
...buildLoaders(db),
|
||||
};
|
||||
},
|
||||
|
|
|
@ -366,8 +366,15 @@ const resolvers = {
|
|||
},
|
||||
|
||||
Mutation: {
|
||||
login: async (_, { username, password }, { setAuthToken, db }) => {
|
||||
const authToken = await getAuthToken({ username, password }, db);
|
||||
login: async (
|
||||
_,
|
||||
{ username, password },
|
||||
{ setAuthToken, db, ipAddress }
|
||||
) => {
|
||||
const authToken = await getAuthToken(
|
||||
{ username, password, ipAddress },
|
||||
db
|
||||
);
|
||||
if (authToken == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue