Feat: API Playground (#10376)
/claim #10283 --------- Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -16,6 +16,7 @@ describe('JwtAuthStrategy', () => {
|
||||
let userRepository: any;
|
||||
let dataSourceService: any;
|
||||
let typeORMService: any;
|
||||
|
||||
const jwt = {
|
||||
sub: 'sub-default',
|
||||
jti: 'jti-default',
|
||||
@ -33,6 +34,10 @@ describe('JwtAuthStrategy', () => {
|
||||
findOne: jest.fn(async () => new UserWorkspace()),
|
||||
};
|
||||
|
||||
const jwtWrapperService: any = {
|
||||
extractJwtFromRequest: jest.fn(() => () => 'token'),
|
||||
};
|
||||
|
||||
// first we test the API_KEY case
|
||||
it('should throw AuthException if type is API_KEY and workspace is not found', async () => {
|
||||
const payload = {
|
||||
@ -46,7 +51,7 @@ describe('JwtAuthStrategy', () => {
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
{} as any,
|
||||
{} as any,
|
||||
jwtWrapperService,
|
||||
typeORMService,
|
||||
dataSourceService,
|
||||
workspaceRepository,
|
||||
@ -82,7 +87,7 @@ describe('JwtAuthStrategy', () => {
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
{} as any,
|
||||
{} as any,
|
||||
jwtWrapperService,
|
||||
typeORMService,
|
||||
dataSourceService,
|
||||
workspaceRepository,
|
||||
@ -120,7 +125,7 @@ describe('JwtAuthStrategy', () => {
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
{} as any,
|
||||
{} as any,
|
||||
jwtWrapperService,
|
||||
typeORMService,
|
||||
dataSourceService,
|
||||
workspaceRepository,
|
||||
@ -152,7 +157,7 @@ describe('JwtAuthStrategy', () => {
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
{} as any,
|
||||
{} as any,
|
||||
jwtWrapperService,
|
||||
typeORMService,
|
||||
dataSourceService,
|
||||
workspaceRepository,
|
||||
@ -190,7 +195,7 @@ describe('JwtAuthStrategy', () => {
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
{} as any,
|
||||
{} as any,
|
||||
jwtWrapperService,
|
||||
typeORMService,
|
||||
dataSourceService,
|
||||
workspaceRepository,
|
||||
@ -231,7 +236,7 @@ describe('JwtAuthStrategy', () => {
|
||||
|
||||
strategy = new JwtAuthStrategy(
|
||||
{} as any,
|
||||
{} as any,
|
||||
jwtWrapperService,
|
||||
typeORMService,
|
||||
dataSourceService,
|
||||
workspaceRepository,
|
||||
|
||||
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { Strategy } from 'passport-jwt';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
@ -36,25 +36,28 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
@InjectRepository(UserWorkspace, 'core')
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKeyProvider: async (request, rawJwtToken, done) => {
|
||||
try {
|
||||
const decodedToken = this.jwtWrapperService.decode(
|
||||
rawJwtToken,
|
||||
) as JwtPayload;
|
||||
const workspaceId = decodedToken.workspaceId;
|
||||
const secret = this.jwtWrapperService.generateAppSecret(
|
||||
'ACCESS',
|
||||
workspaceId,
|
||||
);
|
||||
const jwtFromRequestFunction = jwtWrapperService.extractJwtFromRequest();
|
||||
const secretOrKeyProviderFunction = async (request, rawJwtToken, done) => {
|
||||
try {
|
||||
const decodedToken = jwtWrapperService.decode(
|
||||
rawJwtToken,
|
||||
) as JwtPayload;
|
||||
const workspaceId = decodedToken.workspaceId;
|
||||
const secret = jwtWrapperService.generateAppSecret(
|
||||
'ACCESS',
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
done(null, secret);
|
||||
} catch (error) {
|
||||
done(error, null);
|
||||
}
|
||||
},
|
||||
done(null, secret);
|
||||
} catch (error) {
|
||||
done(error, null);
|
||||
}
|
||||
};
|
||||
|
||||
super({
|
||||
jwtFromRequest: jwtFromRequestFunction,
|
||||
ignoreExpiration: false,
|
||||
secretOrKeyProvider: secretOrKeyProviderFunction,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -39,6 +39,7 @@ describe('AccessTokenService', () => {
|
||||
verifyWorkspaceToken: jest.fn(),
|
||||
decode: jest.fn(),
|
||||
generateAppSecret: jest.fn(),
|
||||
extractJwtFromRequest: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -179,6 +180,9 @@ describe('AccessTokenService', () => {
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(jwtWrapperService, 'extractJwtFromRequest')
|
||||
.mockReturnValue(() => mockToken);
|
||||
jest
|
||||
.spyOn(jwtWrapperService, 'verifyWorkspaceToken')
|
||||
.mockResolvedValue(undefined);
|
||||
@ -207,6 +211,10 @@ describe('AccessTokenService', () => {
|
||||
headers: {},
|
||||
} as Request;
|
||||
|
||||
jest
|
||||
.spyOn(jwtWrapperService, 'extractJwtFromRequest')
|
||||
.mockReturnValue(() => null);
|
||||
|
||||
await expect(service.validateTokenByRequest(mockRequest)).rejects.toThrow(
|
||||
AuthException,
|
||||
);
|
||||
|
||||
@ -4,7 +4,6 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { addMilliseconds } from 'date-fns';
|
||||
import { Request } from 'express';
|
||||
import ms from 'ms';
|
||||
import { ExtractJwt } from 'passport-jwt';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@ -125,7 +124,7 @@ export class AccessTokenService {
|
||||
}
|
||||
|
||||
async validateTokenByRequest(request: Request): Promise<AuthContext> {
|
||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
||||
const token = this.jwtWrapperService.extractJwtFromRequest()(request);
|
||||
|
||||
if (!token) {
|
||||
throw new AuthException(
|
||||
|
||||
@ -3,7 +3,9 @@ import { JwtService, JwtSignOptions, JwtVerifyOptions } from '@nestjs/jwt';
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
import { Request as ExpressRequest } from 'express';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { ExtractJwt, JwtFromRequestFunction } from 'passport-jwt';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import {
|
||||
@ -122,4 +124,20 @@ export class JwtWrapperService {
|
||||
|
||||
return accessTokenSecret;
|
||||
}
|
||||
|
||||
extractJwtFromRequest(): JwtFromRequestFunction {
|
||||
return (request: ExpressRequest) => {
|
||||
// First try to extract token from Authorization header
|
||||
const tokenFromHeader = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
||||
|
||||
if (tokenFromHeader) {
|
||||
return tokenFromHeader;
|
||||
}
|
||||
|
||||
// If not found in header, try to extract from URL query parameter
|
||||
// This is for edge cases where we don't control the origin request
|
||||
// (e.g. the REST API playground)
|
||||
return ExtractJwt.fromUrlQueryParameter('token')(request);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user