@katalysttech/auth
Version:
A flexible authentication module for NestJS applications with JWT and refresh token support
264 lines • 11.7 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthService = void 0;
const common_1 = require("@nestjs/common");
const jwt_1 = require("@nestjs/jwt");
const crypto_1 = require("crypto");
const password_service_1 = require("./services/password.service");
const refresh_token_service_1 = require("./services/refresh-token.service");
let AuthService = class AuthService {
constructor(options, jwtService, passwordService, refreshTokenService) {
this.options = options;
this.jwtService = jwtService;
this.passwordService = passwordService;
this.refreshTokenService = refreshTokenService;
}
async login(username, password) {
const usernameField = this.options.userFields.usernameField || "username";
const user = await this.options.findUserByUsername(username);
if (!user) {
throw new common_1.UnauthorizedException("Invalid credentials");
}
const hashedPassword = user[this.options.userFields.passwordField];
const isPasswordValid = await this.passwordService.comparePassword(password, hashedPassword);
if (!isPasswordValid) {
throw new common_1.UnauthorizedException("Invalid credentials");
}
return this.generateTokens(user);
}
async loginWithLinkedIn(email, linkedInId) {
const user = await this.options.findUserByLinkedinIdAndEmail(email, linkedInId);
if (!user) {
throw new common_1.UnauthorizedException("Invalid LinkedIn credentials");
}
return this.generateTokens(user);
}
async loginWithGoogle(email, googleId) {
const user = await this.options.findUserByGoogleIdAndEmail(email, googleId);
if (!user) {
throw new common_1.UnauthorizedException("Invalid Google credentials");
}
return this.generateTokens(user);
}
async refreshToken(refreshToken) {
try {
const decoded = await this.jwtService.verifyAsync(refreshToken, {
secret: this.options.refreshToken.secret,
});
const { sub: userId, tokenId } = decoded;
if (!userId || !tokenId) {
throw new common_1.UnauthorizedException("Invalid refresh token format");
}
const isValid = await this.refreshTokenService.validateToken(userId, tokenId);
if (!isValid) {
throw new common_1.UnauthorizedException("Invalid refresh token");
}
await this.refreshTokenService.revokeToken(userId, tokenId);
const user = await this.options.findUserById(userId);
if (!user) {
throw new common_1.UnauthorizedException("User not found");
}
return this.generateTokens(user);
}
catch (error) {
if (error instanceof common_1.UnauthorizedException) {
throw error;
}
throw new common_1.UnauthorizedException("Invalid refresh token");
}
}
async validateToken(token) {
try {
const decoded = this.jwtService.verify(token, {
secret: this.options.jwt.secret,
});
const isTokenValid = await this.refreshTokenService.validateToken(decoded.sub, decoded.tokenId);
if (!isTokenValid) {
throw new common_1.UnauthorizedException("Token has been revoked");
}
return decoded;
}
catch (error) {
if (error instanceof common_1.UnauthorizedException) {
throw error;
}
throw new common_1.UnauthorizedException("Invalid token");
}
}
async generateTokens(user) {
const idField = this.options.userFields.idField || "id";
const usernameField = this.options.userFields.usernameField || "username";
const payload = {
sub: user[idField],
username: user[usernameField],
};
for (const field of this.options.userFields.tokenFields) {
payload[field] = user[field];
}
const accessToken = await this.jwtService.signAsync(payload, {
secret: this.options.jwt.secret,
expiresIn: this.options.jwt.expiresIn,
});
const refreshTokenId = (0, crypto_1.randomUUID)();
const refreshTokenPayload = {
...payload,
tokenId: refreshTokenId,
};
const refreshToken = await this.jwtService.signAsync(refreshTokenPayload, {
secret: this.options.refreshToken.secret,
expiresIn: this.options.refreshToken.expiresIn,
});
const expiresInSeconds = this.getExpiresInSeconds(this.options.refreshToken.expiresIn);
const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
await this.refreshTokenService.storeToken(payload.sub, refreshTokenId, expiresAt, this.options.refreshToken.options?.deviceTracking ? {} : undefined);
return {
accessToken,
refreshToken,
expiresIn: expiresInSeconds,
};
}
getExpiresInSeconds(expiresIn) {
if (typeof expiresIn === "number")
return expiresIn;
const match = expiresIn.match(/^(\d+)([smhd])$/);
if (!match)
return 3600;
const [, value, unit] = match;
const multipliers = { s: 1, m: 60, h: 3600, d: 86400 };
return parseInt(value) * multipliers[unit];
}
async logout(refreshToken) {
try {
const decoded = await this.jwtService.verifyAsync(refreshToken, {
secret: this.options.refreshToken.secret,
});
const { sub: userId, tokenId } = decoded;
if (!userId || !tokenId) {
throw new common_1.UnauthorizedException("Invalid refresh token format");
}
await this.refreshTokenService.revokeToken(userId, tokenId);
}
catch (error) {
if (error instanceof common_1.UnauthorizedException) {
throw error;
}
throw new common_1.UnauthorizedException("Invalid refresh token");
}
}
async logoutAll(userId) {
try {
await this.refreshTokenService.revokeAllUserTokens(userId);
}
catch (error) {
throw new common_1.UnauthorizedException("Failed to logout from all devices");
}
}
async getActiveSessions(userId) {
try {
if (!userId) {
throw new common_1.UnauthorizedException("User ID is required");
}
const sessions = await this.refreshTokenService.getActiveSessions(userId);
return sessions;
}
catch (error) {
if (error instanceof common_1.UnauthorizedException) {
throw error;
}
throw new common_1.UnauthorizedException("Failed to get active sessions");
}
}
async revokeSession(userId, sessionId) {
try {
await this.refreshTokenService.revokeToken(userId, sessionId);
}
catch (error) {
throw new common_1.UnauthorizedException("Failed to revoke session");
}
}
async generatePasswordResetToken(payload) {
try {
const usernameField = this.options.userFields.usernameField || "email";
if (!payload[usernameField]) {
throw new common_1.BadRequestException(`Missing required field: ${usernameField}`);
}
const user = await this.options.findUserByUsername(payload[usernameField]);
if (!user) {
throw new common_1.BadRequestException(`User not found with ${usernameField}: ${payload[usernameField]}`);
}
const uuidField = this.options.userFields.uuidField || "uuid";
const userUuid = user[uuidField];
const resetToken = (0, crypto_1.randomUUID)();
const expiresInSeconds = 3600;
const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
if (!this.options.storeResetToken) {
throw new common_1.BadRequestException("Reset token storage not configured");
}
await this.options.storeResetToken(userUuid, resetToken, expiresAt);
return {
resetToken,
expiresIn: expiresInSeconds,
};
}
catch (error) {
if (error instanceof common_1.BadRequestException) {
throw error;
}
throw new common_1.BadRequestException("Failed to generate password reset token");
}
}
async resetPassword(resetToken, newPassword) {
try {
if (!this.options.validateResetToken ||
!this.options.updateUserPassword ||
!this.options.clearResetToken ||
!this.options.findUserByResetToken) {
throw new common_1.BadRequestException("Password reset functions not configured");
}
const user = await this.options.findUserByResetToken(resetToken);
if (!user) {
throw new common_1.UnauthorizedException("Invalid reset token");
}
const idField = this.options.userFields.idField || "id";
const uuidField = this.options.userFields.uuidField || "uuid";
const userId = user[idField];
const userUuid = user[uuidField];
const isValid = await this.options.validateResetToken(userUuid, resetToken);
if (!isValid) {
throw new common_1.UnauthorizedException("Reset token has expired or is invalid");
}
const hashedPassword = await this.passwordService.hashPassword(newPassword);
await this.options.updateUserPassword(userUuid, hashedPassword);
await this.options.clearResetToken(userUuid);
await this.refreshTokenService.revokeAllUserTokens(userId);
}
catch (error) {
if (error instanceof common_1.UnauthorizedException ||
error instanceof common_1.BadRequestException) {
throw error;
}
throw new common_1.UnauthorizedException("Failed to reset password");
}
}
};
exports.AuthService = AuthService;
exports.AuthService = AuthService = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, common_1.Inject)("AUTH_OPTIONS")),
__metadata("design:paramtypes", [Object, jwt_1.JwtService,
password_service_1.PasswordService,
refresh_token_service_1.RefreshTokenService])
], AuthService);
//# sourceMappingURL=auth.service.js.map