auth in progress

This commit is contained in:
andres
2023-06-13 14:31:05 +02:00
parent 779b235363
commit 6f2fab076d
13 changed files with 181 additions and 29 deletions

View File

@@ -10,6 +10,7 @@
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"dev": "nest start --watch",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",

View File

@@ -38,18 +38,17 @@ model User {
grades Grade[]
generalChatMessages GeneralChatMessage[]
verification Verification?
AccessToken AccessToken[]
RevokedToken RevokedToken[]
RefreshToken RefreshToken[]
@@fulltext([name, email])
}
model AccessToken {
model RevokedToken {
id String @id @default(cuid())
userId String
token String @unique
expiresAt DateTime
isRevoked Boolean @default(false)
revokedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@index([userId])
@@ -58,7 +57,7 @@ model AccessToken {
model RefreshToken {
id String @id @default(cuid())
userId String
token String @unique
token String @unique @db.VarChar(255)
expiresAt DateTime
isRevoked Boolean @default(false)
user User @relation(fields: [userId], references: [id])

View File

@@ -12,8 +12,8 @@ import * as process from 'process'
@Module({
imports: [
ConfigModule,
AuthModule,
UsersModule,
AuthModule,
PrismaModule,
MailerModule.forRoot({

View File

@@ -6,6 +6,7 @@ export class JwtPayloadExtractorGuard extends AuthGuard('payloadExtractor') {
canActivate(context: ExecutionContext) {
// Add your custom authentication logic here
// for example, call super.logIn(request) to establish a session.
console.log('JwtPayloadExtractorGuard')
return super.canActivate(context)
}

View File

@@ -2,10 +2,14 @@ import { Inject, Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { AppSettings } from '../../settings/app-settings'
import { AuthService } from '../../modules/auth/auth.service'
@Injectable()
export class JwtPayloadExtractorStrategy extends PassportStrategy(Strategy, 'payloadExtractor') {
constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) {
constructor(
@Inject(AppSettings.name) private readonly appSettings: AppSettings,
private readonly authService: AuthService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
@@ -14,6 +18,7 @@ export class JwtPayloadExtractorStrategy extends PassportStrategy(Strategy, 'pay
}
async validate(payload: any) {
console.log(payload)
const userId = payload.userId
const name = payload.name
if (payload) return { userId, name }

View File

@@ -44,7 +44,6 @@ export class AuthController {
@UseGuards(LocalAuthGuard)
@Post('sign-in')
async login(@Request() req, @Res({ passthrough: true }) res) {
console.log(req)
const userData = req.user.data
res.cookie('refreshToken', userData.refreshToken, {
httpOnly: true,
@@ -96,10 +95,12 @@ export class AuthController {
async refreshToken(@Request() req, @Response() res) {
if (!req.cookie?.refreshToken) throw new UnauthorizedException()
const userId = req.user.id
const newTokens = this.authService.createJwtTokensPair(userId, null)
const newTokens = await this.authService.createJwtTokensPair(userId)
res.cookie('refreshToken', newTokens.refreshToken, {
httpOnly: true,
secure: true,
path: '/refresh',
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
})
return { accessToken: newTokens.accessToken }
}

View File

@@ -8,5 +8,6 @@ import { LocalStrategy } from './strategies/local.strategy'
imports: [UsersModule],
controllers: [AuthController],
providers: [AuthService, LocalStrategy],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -1,32 +1,104 @@
import { Injectable } from '@nestjs/common'
import { isAfter } from 'date-fns'
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { addDays, isAfter } from 'date-fns'
import * as jwt from 'jsonwebtoken'
import * as bcrypt from 'bcrypt'
import { UsersRepository } from '../users/infrastructure/users.repository'
import * as process from 'process'
import { PrismaService } from '../../prisma.service'
@Injectable()
export class AuthService {
constructor(private usersRepository: UsersRepository) {}
constructor(private usersRepository: UsersRepository, private prisma: PrismaService) {}
createJwtTokensPair(userId: string, email: string | null) {
async revokeToken(token: string, userId: string): Promise<void> {
await this.prisma.revokedToken.create({
data: {
userId,
token,
},
})
}
async isTokenRevoked(token: string): Promise<boolean> {
const revokedToken = await this.prisma.revokedToken.findUnique({
where: { token },
})
return !!revokedToken
}
// Periodically remove old revoked tokens
async removeExpiredTokens(): Promise<void> {
const hourAgo = new Date()
hourAgo.setHours(hourAgo.getHours() - 1)
await this.prisma.revokedToken.deleteMany({
where: { revokedAt: { lt: hourAgo } },
})
}
async createJwtTokensPair(userId: string) {
const accessSecretKey = process.env.ACCESS_JWT_SECRET_KEY
const refreshSecretKey = process.env.REFRESH_JWT_SECRET_KEY
const payload: { userId: string; date: Date; email: string | null } = {
const payload: { userId: string; date: Date } = {
userId,
date: new Date(),
email,
}
const accessToken = jwt.sign(payload, accessSecretKey, { expiresIn: '1d' })
const accessToken = jwt.sign(payload, accessSecretKey, { expiresIn: '10m' })
const refreshToken = jwt.sign(payload, refreshSecretKey, {
expiresIn: '30d',
})
console.log(refreshToken.length)
// Save refresh token in the database
await this.prisma.refreshToken.create({
data: {
userId: userId,
token: refreshToken,
expiresAt: addDays(new Date(), 30),
isRevoked: false,
},
})
return {
accessToken,
refreshToken,
}
}
async checkToken(accessToken: string, refreshToken: string) {
try {
await jwt.verify(accessToken, process.env.ACCESS_JWT_SECRET_KEY)
return true
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
const dbRefreshToken = await this.prisma.refreshToken.findUnique({
where: { token: refreshToken },
})
const isTokenRevoked = await this.isTokenRevoked(accessToken)
if (isTokenRevoked) {
throw new UnauthorizedException()
}
if (dbRefreshToken && !dbRefreshToken.isRevoked && dbRefreshToken.expiresAt > new Date()) {
const newTokens = await this.createJwtTokensPair(dbRefreshToken.userId)
await this.prisma.refreshToken.update({
where: { id: dbRefreshToken.id },
data: { isRevoked: true },
})
return newTokens
}
}
throw err
}
}
async logout(accessToken: string, refreshToken: string) {
// Revoke the access token
const decoded = jwt.verify(accessToken, process.env.ACCESS_JWT_SECRET_KEY)
await this.revokeToken(accessToken, decoded.userId)
await this.prisma.refreshToken.update({
where: { token: refreshToken },
data: { isRevoked: true },
})
}
async checkCredentials(email: string, password: string) {
const user = await this.usersRepository.findUserByEmail(email)
if (!user /*|| !user.emailConfirmation.isConfirmed*/)
@@ -49,7 +121,7 @@ export class AuthService {
},
}
}
const tokensPair = this.createJwtTokensPair(user.id, user.email)
const tokensPair = await this.createJwtTokensPair(user.id)
return {
resultCode: 0,
data: tokensPair,

View File

@@ -0,0 +1,24 @@
import { Inject, Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { AppSettings } from '../../../settings/app-settings'
type JwtPayload = {
userId: string
username: string
}
@Injectable()
export class AccessTokenStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY,
})
}
async validate(payload: JwtPayload) {
return { userId: payload.userId }
}
}

View File

@@ -1,19 +1,42 @@
import { Inject, Injectable } from '@nestjs/common'
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { AppSettings } from '../../../settings/app-settings'
import { Request } from 'express'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: true,
ignoreExpiration: false,
secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY,
})
}
async validate(payload: any) {
return { userId: payload.userId }
async validate(request: Request, payload: any) {
const accessToken = request.headers.authorization?.split(' ')[1]
const refreshToken = request.cookies.refreshToken // Extract refresh token from cookies
// If there's no refresh token, simply validate the user based on payload
if (!refreshToken) {
return { userId: payload.userId }
}
try {
const newAccessToken = await this.authService.checkToken(accessToken, refreshToken)
// If new access token were issued, attach it to the response headers
if (newAccessToken) {
request.res.setHeader('Authorization', `Bearer ${newAccessToken.accessToken}`)
}
request.res.cookie('refreshToken', newAccessToken.refreshToken, {
httpOnly: true,
path: '/auth/refresh-token',
})
return { userId: payload.userId }
} catch (error) {
throw new UnauthorizedException('Invalid tokens')
}
}
}

View File

@@ -12,10 +12,10 @@ export class LocalStrategy extends PassportStrategy(Strategy) {
}
async validate(email: string, password: string): Promise<any> {
const user = await this.authService.checkCredentials(email, password)
if (user.resultCode === 1) {
const credentials = await this.authService.checkCredentials(email, password)
if (credentials.resultCode === 1) {
throw new UnauthorizedException()
}
return user
return credentials
}
}

View File

@@ -0,0 +1,27 @@
import { Inject, Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { AppSettings } from '../../../settings/app-settings'
import { Request } from 'express'
type JwtPayload = {
userId: string
username: string
}
@Injectable()
export class RefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY,
passReqToCallback: true,
})
}
validate(req: Request, payload: any) {
const refreshToken = req.get('Authorization').replace('Bearer', '').trim()
return { ...payload, refreshToken }
}
}

View File

@@ -152,12 +152,10 @@ export class UsersRepository implements IUsersRepository {
}
async revokeToken(id: string, token: string): Promise<User | null> {
const revokedToken = await this.prisma.accessToken.update({
where: {
token: token,
},
const revokedToken = await this.prisma.revokedToken.create({
data: {
isRevoked: true,
token: token,
userId: id,
},
include: {
user: true,