UNPKG

@katalysttech/auth

Version:

A flexible authentication module for NestJS applications with JWT and refresh token support

264 lines 11.7 kB
"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