mirror of
https://github.com/docmost/docmost.git
synced 2025-11-19 13:41:12 +10:00
refactor: switch to HttpOnly cookie (#660)
* Switch to httpOnly cookie * create endpoint to retrieve temporary collaboration token * cleanups
This commit is contained in:
@ -6,6 +6,7 @@ import {
|
||||
NotFoundException,
|
||||
Post,
|
||||
Req,
|
||||
Res,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
@ -21,6 +22,8 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { ForgotPasswordDto } from './dto/forgot-password.dto';
|
||||
import { PasswordResetDto } from './dto/password-reset.dto';
|
||||
import { VerifyUserTokenDto } from './dto/verify-user-token.dto';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { addDays } from 'date-fns';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
@ -31,26 +34,29 @@ export class AuthController {
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('login')
|
||||
async login(@Req() req, @Body() loginInput: LoginDto) {
|
||||
return this.authService.login(loginInput, req.raw.workspaceId);
|
||||
async login(
|
||||
@Req() req,
|
||||
@Res({ passthrough: true }) res: FastifyReply,
|
||||
@Body() loginInput: LoginDto,
|
||||
) {
|
||||
const authToken = await this.authService.login(
|
||||
loginInput,
|
||||
req.raw.workspaceId,
|
||||
);
|
||||
this.setAuthCookie(res, authToken);
|
||||
}
|
||||
|
||||
/* @HttpCode(HttpStatus.OK)
|
||||
@Post('register')
|
||||
async register(@Req() req, @Body() createUserDto: CreateUserDto) {
|
||||
return this.authService.register(createUserDto, req.raw.workspaceId);
|
||||
}
|
||||
*/
|
||||
|
||||
@UseGuards(SetupGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('setup')
|
||||
async setupWorkspace(
|
||||
@Req() req,
|
||||
@Res({ passthrough: true }) res: FastifyReply,
|
||||
@Body() createAdminUserDto: CreateAdminUserDto,
|
||||
) {
|
||||
if (this.environmentService.isCloud()) throw new NotFoundException();
|
||||
return this.authService.setup(createAdminUserDto);
|
||||
|
||||
const authToken = await this.authService.setup(createAdminUserDto);
|
||||
this.setAuthCookie(res, authToken);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ -76,10 +82,15 @@ export class AuthController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('password-reset')
|
||||
async passwordReset(
|
||||
@Res({ passthrough: true }) res: FastifyReply,
|
||||
@Body() passwordResetDto: PasswordResetDto,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
return this.authService.passwordReset(passwordResetDto, workspace.id);
|
||||
const authToken = await this.authService.passwordReset(
|
||||
passwordResetDto,
|
||||
workspace.id,
|
||||
);
|
||||
this.setAuthCookie(res, authToken);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ -90,4 +101,30 @@ export class AuthController {
|
||||
) {
|
||||
return this.authService.verifyUserToken(verifyUserTokenDto, workspace.id);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('collab-token')
|
||||
async collabToken(
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
return this.authService.getCollabToken(user.id, workspace.id);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('logout')
|
||||
async logout(@Res({ passthrough: true }) res: FastifyReply) {
|
||||
res.clearCookie('authToken');
|
||||
}
|
||||
|
||||
setAuthCookie(res: FastifyReply, token: string) {
|
||||
res.setCookie('authToken', token, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
expires: addDays(new Date(), 30),
|
||||
secure: this.environmentService.isHttps(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export enum JwtType {
|
||||
ACCESS = 'access',
|
||||
REFRESH = 'refresh',
|
||||
COLLAB = 'collab',
|
||||
}
|
||||
export type JwtPayload = {
|
||||
sub: string;
|
||||
@ -9,8 +9,8 @@ export type JwtPayload = {
|
||||
type: 'access';
|
||||
};
|
||||
|
||||
export type JwtRefreshPayload = {
|
||||
export type JwtCollabPayload = {
|
||||
sub: string;
|
||||
workspaceId: string;
|
||||
type: 'refresh';
|
||||
type: 'collab';
|
||||
};
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
export interface TokensDto {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
@ -7,7 +7,6 @@ import {
|
||||
import { LoginDto } from '../dto/login.dto';
|
||||
import { CreateUserDto } from '../dto/create-user.dto';
|
||||
import { TokenService } from './token.service';
|
||||
import { TokensDto } from '../dto/tokens.dto';
|
||||
import { SignupService } from './signup.service';
|
||||
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
@ -60,24 +59,17 @@ export class AuthService {
|
||||
user.lastLoginAt = new Date();
|
||||
await this.userRepo.updateLastLogin(user.id, workspaceId);
|
||||
|
||||
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||
return { tokens };
|
||||
return this.tokenService.generateAccessToken(user);
|
||||
}
|
||||
|
||||
async register(createUserDto: CreateUserDto, workspaceId: string) {
|
||||
const user = await this.signupService.signup(createUserDto, workspaceId);
|
||||
|
||||
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||
|
||||
return { tokens };
|
||||
return this.tokenService.generateAccessToken(user);
|
||||
}
|
||||
|
||||
async setup(createAdminUserDto: CreateAdminUserDto) {
|
||||
const user = await this.signupService.initialSetup(createAdminUserDto);
|
||||
|
||||
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||
|
||||
return { tokens };
|
||||
return this.tokenService.generateAccessToken(user);
|
||||
}
|
||||
|
||||
async changePassword(
|
||||
@ -186,7 +178,7 @@ export class AuthService {
|
||||
trx,
|
||||
);
|
||||
|
||||
trx
|
||||
await trx
|
||||
.deleteFrom('userTokens')
|
||||
.where('userId', '=', user.id)
|
||||
.where('type', '=', UserTokenType.FORGOT_PASSWORD)
|
||||
@ -200,9 +192,7 @@ export class AuthService {
|
||||
template: emailTemplate,
|
||||
});
|
||||
|
||||
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||
|
||||
return { tokens };
|
||||
return this.tokenService.generateAccessToken(user);
|
||||
}
|
||||
|
||||
async verifyUserToken(
|
||||
@ -222,4 +212,12 @@ export class AuthService {
|
||||
throw new BadRequestException('Invalid or expired token');
|
||||
}
|
||||
}
|
||||
|
||||
async getCollabToken(userId: string, workspaceId: string) {
|
||||
const token = await this.tokenService.generateCollabToken(
|
||||
userId,
|
||||
workspaceId,
|
||||
);
|
||||
return { token };
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||
import { TokensDto } from '../dto/tokens.dto';
|
||||
import { JwtPayload, JwtRefreshPayload, JwtType } from '../dto/jwt-payload';
|
||||
import { JwtCollabPayload, JwtPayload, JwtType } from '../dto/jwt-payload';
|
||||
import { User } from '@docmost/db/types/entity.types';
|
||||
|
||||
@Injectable()
|
||||
@ -22,26 +21,19 @@ export class TokenService {
|
||||
return this.jwtService.sign(payload);
|
||||
}
|
||||
|
||||
async generateRefreshToken(
|
||||
async generateCollabToken(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
const payload: JwtRefreshPayload = {
|
||||
const payload: JwtCollabPayload = {
|
||||
sub: userId,
|
||||
workspaceId,
|
||||
type: JwtType.REFRESH,
|
||||
type: JwtType.COLLAB,
|
||||
};
|
||||
const expiresIn = this.environmentService.getJwtTokenExpiresIn();
|
||||
const expiresIn = '24h';
|
||||
return this.jwtService.sign(payload, { expiresIn });
|
||||
}
|
||||
|
||||
async generateTokens(user: User): Promise<TokensDto> {
|
||||
return {
|
||||
accessToken: await this.generateAccessToken(user),
|
||||
refreshToken: await this.generateRefreshToken(user.id, user.workspaceId),
|
||||
};
|
||||
}
|
||||
|
||||
async verifyJwt(token: string) {
|
||||
return this.jwtService.verifyAsync(token, {
|
||||
secret: this.environmentService.getAppSecret(),
|
||||
|
||||
@ -23,15 +23,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: (req: FastifyRequest) => {
|
||||
let accessToken = null;
|
||||
|
||||
try {
|
||||
accessToken = JSON.parse(req.cookies?.authTokens)?.accessToken;
|
||||
} catch {
|
||||
this.logger.debug('Failed to parse access token');
|
||||
}
|
||||
|
||||
return accessToken || this.extractTokenFromHeader(req);
|
||||
return req.cookies?.authToken || this.extractTokenFromHeader(req);
|
||||
},
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: environmentService.getAppSecret(),
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
HttpStatus,
|
||||
Post,
|
||||
Req,
|
||||
Res,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { WorkspaceService } from '../services/workspace.service';
|
||||
@ -29,6 +30,9 @@ import {
|
||||
WorkspaceCaslAction,
|
||||
WorkspaceCaslSubject,
|
||||
} from '../../casl/interfaces/workspace-ability.type';
|
||||
import { addDays } from 'date-fns';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('workspace')
|
||||
@ -37,6 +41,7 @@ export class WorkspaceController {
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||
private readonly workspaceAbility: WorkspaceAbilityFactory,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@ -218,10 +223,18 @@ export class WorkspaceController {
|
||||
async acceptInvite(
|
||||
@Body() acceptInviteDto: AcceptInviteDto,
|
||||
@Req() req: any,
|
||||
@Res({ passthrough: true }) res: FastifyReply,
|
||||
) {
|
||||
return this.workspaceInvitationService.acceptInvitation(
|
||||
const authToken = await this.workspaceInvitationService.acceptInvitation(
|
||||
acceptInviteDto,
|
||||
req.raw.workspaceId,
|
||||
);
|
||||
|
||||
res.setCookie('authToken', authToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
expires: addDays(new Date(), 30),
|
||||
secure: this.environmentService.isHttps(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,6 @@ import { TokenService } from '../../auth/services/token.service';
|
||||
import { nanoIdGen } from '../../../common/helpers';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||
import { TokensDto } from '../../auth/dto/tokens.dto';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceInvitationService {
|
||||
@ -254,8 +253,7 @@ export class WorkspaceInvitationService {
|
||||
});
|
||||
}
|
||||
|
||||
const tokens: TokensDto = await this.tokenService.generateTokens(newUser);
|
||||
return { tokens };
|
||||
return this.tokenService.generateAccessToken(newUser);
|
||||
}
|
||||
|
||||
async resendInvitation(
|
||||
|
||||
Reference in New Issue
Block a user