Update token verification and fix typo (#2889)

* Update token verification and fix typo

* Fix typo
This commit is contained in:
martmull 2023-12-08 17:42:08 +01:00 committed by GitHub
parent a48c9293f6
commit 9b7d7b29ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 64 additions and 82 deletions

View File

@ -3,16 +3,17 @@ import { GraphQLModule } from '@nestjs/graphql';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { APP_FILTER, ContextIdFactory, ModuleRef } from '@nestjs/core'; import { APP_FILTER, ContextIdFactory, ModuleRef } from '@nestjs/core';
import { GraphQLError, GraphQLSchema } from 'graphql';
import { YogaDriver, YogaDriverConfig } from '@graphql-yoga/nestjs'; import { YogaDriver, YogaDriverConfig } from '@graphql-yoga/nestjs';
import GraphQLJSON from 'graphql-type-json'; import GraphQLJSON from 'graphql-type-json';
import { GraphQLError, GraphQLSchema } from 'graphql'; import { TokenExpiredError, JsonWebTokenError } from 'jsonwebtoken';
import { ExtractJwt } from 'passport-jwt';
import { TokenExpiredError, JsonWebTokenError, verify } from 'jsonwebtoken';
import { WorkspaceFactory } from 'src/workspace/workspace.factory'; import { WorkspaceFactory } from 'src/workspace/workspace.factory';
import { TypeOrmExceptionFilter } from 'src/filters/typeorm-exception.filter'; import { TypeOrmExceptionFilter } from 'src/filters/typeorm-exception.filter';
import { HttpExceptionFilter } from 'src/filters/http-exception.filter'; import { HttpExceptionFilter } from 'src/filters/http-exception.filter';
import { GlobalExceptionFilter } from 'src/filters/global-exception.filter'; import { GlobalExceptionFilter } from 'src/filters/global-exception.filter';
import { TokenService } from 'src/core/auth/services/token.service';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { AppService } from './app.service'; import { AppService } from './app.service';
@ -20,11 +21,6 @@ import { CoreModule } from './core/core.module';
import { IntegrationsModule } from './integrations/integrations.module'; import { IntegrationsModule } from './integrations/integrations.module';
import { HealthModule } from './health/health.module'; import { HealthModule } from './health/health.module';
import { WorkspaceModule } from './workspace/workspace.module'; import { WorkspaceModule } from './workspace/workspace.module';
import { EnvironmentService } from './integrations/environment/environment.service';
import {
JwtAuthStrategy,
JwtPayload,
} from './core/auth/strategies/jwt.auth.strategy';
@Module({ @Module({
imports: [ imports: [
@ -38,38 +34,19 @@ import {
include: [CoreModule], include: [CoreModule],
conditionalSchema: async (request) => { conditionalSchema: async (request) => {
try { try {
// Get the JwtAuthStrategy from the AppModule // Get TokenService from AppModule
const jwtStrategy = AppModule.moduleRef.get(JwtAuthStrategy, { const tokenService = AppModule.moduleRef.get(TokenService, {
strict: false, strict: false,
}); });
// Get the EnvironmentService from the AppModule let workspace: Workspace;
const environmentService = AppModule.moduleRef.get(
EnvironmentService,
{
strict: false,
},
);
// Extract JWT from the request try {
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request.req); workspace = await tokenService.validateToken(request.req);
} catch (err) {
// If there is no token return an empty schema
if (!token) {
return new GraphQLSchema({}); return new GraphQLSchema({});
} }
// Verify and decode JWT
const decoded = verify(
token,
environmentService.getAccessTokenSecret(),
);
// Validate JWT
const { workspace } = await jwtStrategy.validate(
decoded as JwtPayload,
);
const contextId = ContextIdFactory.create(); const contextId = ContextIdFactory.create();
AppModule.moduleRef.registerRequestByContextId(request, contextId); AppModule.moduleRef.registerRequestByContextId(request, contextId);

View File

@ -1,8 +1,4 @@
import { import { BadRequestException, Injectable } from '@nestjs/common';
BadRequestException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express'; import { Request } from 'express';
@ -44,18 +40,10 @@ export class ApiRestQueryBuilderFactory {
objectMetadataItems: ObjectMetadataEntity[]; objectMetadataItems: ObjectMetadataEntity[];
objectMetadataItem: ObjectMetadataEntity; objectMetadataItem: ObjectMetadataEntity;
}> { }> {
let workspaceId; const workspace = await this.tokenService.validateToken(request);
try {
workspaceId = await this.tokenService.verifyApiKeyToken(request);
} catch (err) {
throw new UnauthorizedException(
`Invalid API key. Double check your API key or generate a new one here ${this.environmentService.getFrontBaseUrl()}/settings/developers/api-keys`,
);
}
const objectMetadataItems = const objectMetadataItems =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId); await this.objectMetadataService.findManyWithinWorkspace(workspace.id);
if (!objectMetadataItems.length) { if (!objectMetadataItems.length) {
throw new BadRequestException( throw new BadRequestException(

View File

@ -6,13 +6,13 @@ import { mapFieldMetadataToGraphqlQuery } from 'src/core/api-rest/api-rest-query
@Injectable() @Injectable()
export class CreateQueryFactory { export class CreateQueryFactory {
create(objectMetadata, depth?: number): string { create(objectMetadata, depth?: number): string {
const objectNameSingular = capitalize(
objectMetadata.objectMetadataItem.nameSingular,
);
return ` return `
mutation Create${capitalize( mutation Create${objectNameSingular}($data: ${objectNameSingular}CreateInput!) {
objectMetadata.objectMetadataItem.nameSingular, create${objectNameSingular}(data: $data) {
)}($data: CompanyCreateInput!) {
create${capitalize(
objectMetadata.objectMetadataItem.nameSingular,
)}(data: $data) {
id id
${objectMetadata.objectMetadataItem.fields ${objectMetadata.objectMetadataItem.fields
.map((field) => .map((field) =>

View File

@ -5,9 +5,11 @@ import { capitalize } from 'src/utils/capitalize';
@Injectable() @Injectable()
export class DeleteQueryFactory { export class DeleteQueryFactory {
create(objectMetadataItem): string { create(objectMetadataItem): string {
const objectNameSingular = capitalize(objectMetadataItem.nameSingular);
return ` return `
mutation Delete${capitalize(objectMetadataItem.nameSingular)}($id: ID!) { mutation Delete${objectNameSingular}($id: ID!) {
delete${capitalize(objectMetadataItem.nameSingular)}(id: $id) { delete${objectNameSingular}(id: $id) {
id id
} }
} }

View File

@ -6,18 +6,19 @@ import { mapFieldMetadataToGraphqlQuery } from 'src/core/api-rest/api-rest-query
@Injectable() @Injectable()
export class FindManyQueryFactory { export class FindManyQueryFactory {
create(objectMetadata, depth?: number): string { create(objectMetadata, depth?: number): string {
const objectNameSingular = capitalize(
objectMetadata.objectMetadataItem.nameSingular,
);
const objectNamePlural = objectMetadata.objectMetadataItem.namePlural;
return ` return `
query FindMany${capitalize(objectMetadata.objectMetadataItem.namePlural)}( query FindMany${capitalize(objectNamePlural)}(
$filter: ${capitalize( $filter: ${objectNameSingular}FilterInput,
objectMetadata.objectMetadataItem.nameSingular, $orderBy: ${objectNameSingular}OrderByInput,
)}FilterInput,
$orderBy: ${capitalize(
objectMetadata.objectMetadataItem.nameSingular,
)}OrderByInput,
$lastCursor: String, $lastCursor: String,
$limit: Float = 60 $limit: Float = 60
) { ) {
${objectMetadata.objectMetadataItem.namePlural}( ${objectNamePlural}(
filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor
) { ) {
edges { edges {

View File

@ -6,15 +6,13 @@ import { mapFieldMetadataToGraphqlQuery } from 'src/core/api-rest/api-rest-query
@Injectable() @Injectable()
export class FindOneQueryFactory { export class FindOneQueryFactory {
create(objectMetadata, depth?: number): string { create(objectMetadata, depth?: number): string {
const objectNameSingular = objectMetadata.objectMetadataItem.nameSingular;
return ` return `
query FindOne${capitalize( query FindOne${capitalize(objectNameSingular)}(
objectMetadata.objectMetadataItem.nameSingular, $filter: ${capitalize(objectNameSingular)}FilterInput!,
)}(
$filter: ${capitalize(
objectMetadata.objectMetadataItem.nameSingular,
)}FilterInput!,
) { ) {
${objectMetadata.objectMetadataItem.nameSingular}(filter: $filter) { ${objectNameSingular}(filter: $filter) {
id id
${objectMetadata.objectMetadataItem.fields ${objectMetadata.objectMetadataItem.fields
.map((field) => .map((field) =>

View File

@ -6,13 +6,13 @@ import { mapFieldMetadataToGraphqlQuery } from 'src/core/api-rest/api-rest-query
@Injectable() @Injectable()
export class UpdateQueryFactory { export class UpdateQueryFactory {
create(objectMetadata, depth?: number): string { create(objectMetadata, depth?: number): string {
const objectNameSingular = objectMetadata.objectMetadataItem.nameSingular;
return ` return `
mutation Update${capitalize( mutation Update${capitalize(
objectMetadata.objectMetadataItem.nameSingular, objectNameSingular,
)}($id: ID!, $data: CompanyUpdateInput!) { )}($id: ID!, $data: ${capitalize(objectNameSingular)}UpdateInput!) {
update${capitalize( update${capitalize(objectNameSingular)}(id: $id, data: $data) {
objectMetadata.objectMetadataItem.nameSingular,
)}(id: $id, data: $data) {
id id
${objectMetadata.objectMetadataItem.fields ${objectMetadata.objectMetadataItem.fields
.map((field) => .map((field) =>

View File

@ -5,6 +5,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity'; import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity';
import { User } from 'src/core/user/user.entity'; import { User } from 'src/core/user/user.entity';
import { JwtAuthStrategy } from 'src/core/auth/strategies/jwt.auth.strategy';
import { TokenService } from './token.service'; import { TokenService } from './token.service';
@ -19,6 +20,10 @@ describe('TokenService', () => {
provide: JwtService, provide: JwtService,
useValue: {}, useValue: {},
}, },
{
provide: JwtAuthStrategy,
useValue: {},
},
{ {
provide: EnvironmentService, provide: EnvironmentService,
useValue: {}, useValue: {},

View File

@ -11,22 +11,27 @@ import { InjectRepository } from '@nestjs/typeorm';
import { addMilliseconds } from 'date-fns'; import { addMilliseconds } from 'date-fns';
import ms from 'ms'; import ms from 'ms';
import { TokenExpiredError } from 'jsonwebtoken'; import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { Request } from 'express'; import { Request } from 'express';
import { ExtractJwt } from 'passport-jwt'; import { ExtractJwt } from 'passport-jwt';
import { JwtPayload } from 'src/core/auth/strategies/jwt.auth.strategy'; import {
JwtAuthStrategy,
JwtPayload,
} from 'src/core/auth/strategies/jwt.auth.strategy';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
import { ApiKeyToken, AuthToken } from 'src/core/auth/dto/token.entity'; import { ApiKeyToken, AuthToken } from 'src/core/auth/dto/token.entity';
import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { User } from 'src/core/user/user.entity'; import { User } from 'src/core/user/user.entity';
import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity'; import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
@Injectable() @Injectable()
export class TokenService { export class TokenService {
constructor( constructor(
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly jwtStrategy: JwtAuthStrategy,
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
@InjectRepository(User, 'core') @InjectRepository(User, 'core')
private readonly userRepository: Repository<User>, private readonly userRepository: Repository<User>,
@ -167,18 +172,22 @@ export class TokenService {
return { token }; return { token };
} }
async verifyApiKeyToken(request: Request) { async validateToken(request: Request): Promise<Workspace> {
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request); const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
if (!token) { if (!token) {
throw new UnauthorizedException('missing authentication token'); throw new UnauthorizedException('missing authentication token');
} }
const payload = await this.verifyJwt( const decoded = await this.verifyJwt(
token, token,
this.environmentService.getAccessTokenSecret(), this.environmentService.getAccessTokenSecret(),
); );
return payload.workspaceId; const { workspace } = await this.jwtStrategy.validate(
decoded as JwtPayload,
);
return workspace;
} }
async verifyLoginToken(loginToken: string): Promise<string> { async verifyLoginToken(loginToken: string): Promise<string> {
@ -290,6 +299,8 @@ export class TokenService {
} catch (error) { } catch (error) {
if (error instanceof TokenExpiredError) { if (error instanceof TokenExpiredError) {
throw new UnauthorizedException('Token has expired.'); throw new UnauthorizedException('Token has expired.');
} else if (error instanceof JsonWebTokenError) {
throw new UnauthorizedException('Token invalid.');
} else { } else {
throw new UnprocessableEntityException(); throw new UnprocessableEntityException();
} }

View File

@ -60,7 +60,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
); );
assert( assert(
apiKey.length === 1 && !apiKey[0].revokedAt, apiKey.length === 1 && !apiKey?.[0].revokedAt,
'This API Key is revoked', 'This API Key is revoked',
ForbiddenException, ForbiddenException,
); );