feat(core): Update hashing strategy for JWTs (#8810)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-03-05 15:06:29 +01:00
committed by GitHub
parent e38e96bbec
commit cdec7c9334
2 changed files with 38 additions and 31 deletions

View File

@@ -1,7 +1,7 @@
import { Service } from 'typedi';
import type { NextFunction, Response } from 'express';
import { createHash } from 'crypto';
import { JsonWebTokenError, TokenExpiredError, type JwtPayload } from 'jsonwebtoken';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import config from '@/config';
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES, Time } from '@/constants';
@@ -18,16 +18,19 @@ import { UrlService } from '@/services/url.service';
interface AuthJwtPayload {
/** User Id */
id: string;
/** User's email */
email: string | null;
/** SHA-256 hash of bcrypt hash of the user's password */
password: string | null;
/** This hash is derived from email and bcrypt of password */
hash: string;
}
interface IssuedJWT extends AuthJwtPayload {
exp: number;
}
interface PasswordResetToken {
sub: string;
hash: string;
}
@Service()
export class AuthService {
constructor(
@@ -83,11 +86,9 @@ export class AuthService {
}
issueJWT(user: User) {
const { id, email, password } = user;
const payload: AuthJwtPayload = {
id,
email,
password: password ? this.createPasswordSha(user) : null,
id: user.id,
hash: this.createJWTHash(user),
};
return this.jwtService.sign(payload, {
expiresIn: this.jwtExpiration,
@@ -104,18 +105,13 @@ export class AuthService {
where: { id: jwtPayload.id },
});
// TODO: include these checks in the cache, to avoid computed this over and over again
const passwordHash = user?.password ? this.createPasswordSha(user) : null;
if (
// If not user is found
!user ||
// or, If the user has been deactivated (i.e. LDAP users)
user.disabled ||
// or, If the password has been updated
jwtPayload.password !== passwordHash ||
// or, If the email has been updated
user.email !== jwtPayload.email
// or, If the email or password has been updated
jwtPayload.hash !== this.createJWTHash(user)
) {
throw new AuthError('Unauthorized');
}
@@ -129,10 +125,8 @@ export class AuthService {
}
generatePasswordResetToken(user: User, expiresIn = '20m') {
return this.jwtService.sign(
{ sub: user.id, passwordSha: this.createPasswordSha(user) },
{ expiresIn },
);
const payload: PasswordResetToken = { sub: user.id, hash: this.createJWTHash(user) };
return this.jwtService.sign(payload, { expiresIn });
}
generatePasswordResetUrl(user: User) {
@@ -146,7 +140,7 @@ export class AuthService {
}
async resolvePasswordResetToken(token: string): Promise<User | undefined> {
let decodedToken: JwtPayload & { passwordSha: string };
let decodedToken: PasswordResetToken;
try {
decodedToken = this.jwtService.verify(token);
} catch (e) {
@@ -171,7 +165,7 @@ export class AuthService {
return;
}
if (this.createPasswordSha(user) !== decodedToken.passwordSha) {
if (decodedToken.hash !== this.createJWTHash(user)) {
this.logger.debug('Password updated since this token was generated');
return;
}
@@ -179,10 +173,11 @@ export class AuthService {
return user;
}
private createPasswordSha({ password }: User) {
return createHash('sha256')
.update(password.slice(password.length / 2))
.digest('hex');
createJWTHash({ email, password }: User) {
const hash = createHash('sha256')
.update(email + ':' + password)
.digest('base64');
return hash.substring(0, 10);
}
/** How many **milliseconds** before expiration should a JWT be renewed */