mirror of
https://github.com/docmost/docmost.git
synced 2025-11-20 02:21:09 +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:
@ -12,6 +12,7 @@ import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
import { findHighestUserSpaceRole } from '@docmost/db/repos/space/utils';
|
||||
import { SpaceRole } from '../../common/helpers/types/permission';
|
||||
import { getPageId } from '../collaboration.util';
|
||||
import { JwtCollabPayload, JwtType } from '../../core/auth/dto/jwt-payload';
|
||||
|
||||
@Injectable()
|
||||
export class AuthenticationExtension implements Extension {
|
||||
@ -28,12 +29,15 @@ export class AuthenticationExtension implements Extension {
|
||||
const { documentName, token } = data;
|
||||
const pageId = getPageId(documentName);
|
||||
|
||||
let jwtPayload = null;
|
||||
let jwtPayload: JwtCollabPayload;
|
||||
|
||||
try {
|
||||
jwtPayload = await this.tokenService.verifyJwt(token);
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Could not verify jwt token');
|
||||
throw new UnauthorizedException('Invalid collab token');
|
||||
}
|
||||
if (jwtPayload.type !== JwtType.COLLAB) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const userId = jwtPayload.sub;
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -16,6 +16,16 @@ export class EnvironmentService {
|
||||
);
|
||||
}
|
||||
|
||||
isHttps(): boolean {
|
||||
const appUrl = this.configService.get<string>('APP_URL');
|
||||
try {
|
||||
const url = new URL(appUrl);
|
||||
return url.protocol === 'https:';
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getPort(): number {
|
||||
return parseInt(this.configService.get<string>('PORT', '3000'));
|
||||
}
|
||||
@ -44,7 +54,6 @@ export class EnvironmentService {
|
||||
}
|
||||
|
||||
getFileUploadSizeLimit(): string {
|
||||
|
||||
return this.configService.get<string>('FILE_UPLOAD_SIZE_LIMIT', '50mb');
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import { TokenService } from '../core/auth/services/token.service';
|
||||
import { JwtType } from '../core/auth/dto/jwt-payload';
|
||||
import { OnModuleDestroy } from '@nestjs/common';
|
||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
import * as cookie from 'cookie';
|
||||
|
||||
@WebSocketGateway({
|
||||
cors: { origin: '*' },
|
||||
@ -25,10 +26,11 @@ export class WsGateway implements OnGatewayConnection, OnModuleDestroy {
|
||||
|
||||
async handleConnection(client: Socket, ...args: any[]): Promise<void> {
|
||||
try {
|
||||
const token = await this.tokenService.verifyJwt(
|
||||
client.handshake.auth?.token,
|
||||
);
|
||||
const cookies = cookie.parse(client.handshake.headers.cookie);
|
||||
const token = await this.tokenService.verifyJwt(cookies['authToken']);
|
||||
|
||||
if (token.type !== JwtType.ACCESS) {
|
||||
client.emit('Unauthorized');
|
||||
client.disconnect();
|
||||
}
|
||||
|
||||
@ -42,6 +44,7 @@ export class WsGateway implements OnGatewayConnection, OnModuleDestroy {
|
||||
|
||||
client.join([workspaceRoom, ...spaceRooms]);
|
||||
} catch (err) {
|
||||
client.emit('Unauthorized');
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user