Files
docmost/apps/server/src/common/guards/jwt-auth.guard.ts
T
Philip Okugbe e56de8eb6c feat(ee): MCP OAuth (#2432)
* feat: mcp oauth

* fix: small refactor

* fix: cleanup consent
2026-08-26 12:55:30 +01:00

115 lines
3.4 KiB
TypeScript

import {
ExecutionContext,
ForbiddenException,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
import {
OAUTH_SCOPE_KEY,
OAuthRouteScope,
} from '../decorators/oauth-scope.decorator';
import { REQUIRE_SESSION_AUTH_KEY } from '../decorators/require-session-auth.decorator';
import { JwtType } from '../../core/auth/dto/jwt-payload';
import { Reflector } from '@nestjs/core';
import { EnvironmentService } from '../../integrations/environment/environment.service';
import { addDays } from 'date-fns';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
private logger = new Logger('JwtAuthGuard');
constructor(
private reflector: Reflector,
private environmentService: EnvironmentService,
) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
handleRequest(err: any, user: any, info: any, ctx: ExecutionContext) {
if (err || !user) {
throw err || new UnauthorizedException();
}
const requiresSession = this.reflector.getAllAndOverride<boolean>(
REQUIRE_SESSION_AUTH_KEY,
[ctx.getHandler(), ctx.getClass()],
);
if (requiresSession && user.authType !== JwtType.ACCESS) {
this.logger.debug(
`session-only endpoint ${ctx.getClass()?.name}.${ctx.getHandler()?.name} refused authType ${user.authType}`,
);
throw new ForbiddenException(
'This action requires an interactive user session',
);
}
if (user.oauth) {
const required = this.reflector.getAllAndOverride<
OAuthRouteScope | undefined
>(OAUTH_SCOPE_KEY, [ctx.getHandler(), ctx.getClass()]);
if (!required) {
this.logger.warn(
`oauth scope check: no @OAuthScope metadata on ${ctx.getClass()?.name}.${ctx.getHandler()?.name}`,
);
throw new ForbiddenException('OAuth tokens cannot access this endpoint');
}
const scopes: string[] = user.oauth.scopes ?? [];
const satisfied =
required === 'read'
? scopes.includes('read') || scopes.includes('write')
: scopes.includes('write');
if (!satisfied) {
throw new ForbiddenException('insufficient_scope');
}
}
this.setJoinedWorkspacesCookie(user, ctx);
return user;
}
setJoinedWorkspacesCookie(user: any, ctx: ExecutionContext) {
if (this.environmentService.isCloud()) {
const req = ctx.switchToHttp().getRequest();
const res = ctx.switchToHttp().getResponse();
const workspaceId = user?.workspace?.id;
let workspaceIds = [];
try {
workspaceIds = req.cookies.joinedWorkspaces
? JSON.parse(req.cookies.joinedWorkspaces)
: [];
} catch (err) {
/* empty */
}
if (!workspaceIds.includes(workspaceId)) {
workspaceIds.push(workspaceId);
}
res.setCookie('joinedWorkspaces', JSON.stringify(workspaceIds), {
httpOnly: false,
domain: '.' + this.environmentService.getSubdomainHost(),
path: '/',
expires: addDays(new Date(), 365),
secure: this.environmentService.isHttps(),
});
}
}
}