Skip to content

Commit

Permalink
Merge pull request #296 from red-kite-solutions/api_key_authentication
Browse files Browse the repository at this point in the history
Api key authentication
  • Loading branch information
lm-sec authored Sep 3, 2024
2 parents ba7ff3c + 9c2ef4b commit 8f89d45
Show file tree
Hide file tree
Showing 69 changed files with 2,489 additions and 1,518 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ dmypy.json
# node stuff
node_modules
.yarn/install-state.gz
package-lock.json
# Ignore DevSpace cache and log folder
.devspace/
devspace.*.yaml
Expand Down Expand Up @@ -166,4 +167,4 @@ lerna-debug.log*
/.nyc_output
**/certificates.yml
*.zip
/aws
/aws
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@
"devDependencies": {
"husky": "^7.0.4",
"typescript": "^5.0.0"
},
"dependencies": {
"@nestjs/passport": "^10.0.3",
"passport": "^0.7.0",
"passport-headerapikey": "^1.2.2"
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { HttpException, HttpStatus } from '@nestjs/common';

export class HttpServerErrorException extends HttpException {
constructor() {
super('Error', HttpStatus.INTERNAL_SERVER_ERROR);
constructor(message: string = 'Error') {
super(message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

export class HttpConflictException extends HttpException {
constructor() {
super('Conflict', HttpStatus.CONFLICT);
constructor(message: string = 'Conflict') {
super(message, HttpStatus.CONFLICT);
}
}

export class HttpForbiddenException extends HttpException {
constructor(message: string | null = null) {
super(message ?? 'Invalid credentials', HttpStatus.FORBIDDEN);
constructor(message: string = 'Invalid credentials') {
super(message, HttpStatus.FORBIDDEN);
}
}

Expand All @@ -28,13 +28,13 @@ export class HttpNotFoundException extends HttpException {
}

export class HttpBadRequestException extends HttpException {
constructor(message: string | null = null) {
super(message ?? 'Error in request parameters', HttpStatus.BAD_REQUEST);
constructor(message: string = 'Error in request parameters') {
super(message, HttpStatus.BAD_REQUEST);
}
}

export class HttpNotImplementedException extends HttpException {
constructor() {
super('Feature not yet implemented', HttpStatus.NOT_IMPLEMENTED);
constructor(message: string = 'Feature not yet implemented') {
super(message, HttpStatus.NOT_IMPLEMENTED);
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AppService } from './app.service';
import { Role } from './auth/constants';
import { Roles } from './auth/decorators/roles.decorator';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
import { RolesGuard } from './auth/guards/role.guard';
import { ApiKeyStrategy } from './auth/strategies/api-key.strategy';
import { JwtStrategy } from './auth/strategies/jwt.strategy';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Roles(Role.ReadOnly)
@UseGuards(AuthGuard([JwtStrategy.name, ApiKeyStrategy.name]), RolesGuard)
@Get()
getVersion(): string {
return this.appService.getVersion();
}

@Roles(Role.ReadOnly)
@UseGuards(JwtAuthGuard, RolesGuard)
@Get('ping')
ping(): string {
return 'pong';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,14 @@ describe('AppController (e2e)', () => {
await app.close();
});

it('Should get the root of the application to check if it is alive (GET /)', async () => {
it('Should be unauthorized get the root of the application (GET /)', async () => {
const r = await request(app.getHttpServer()).get('/');
expect(r.statusCode).toBe(HttpStatus.OK);
expect(r.text.length).toBeGreaterThan(0);
});

it('Should be unauthorized to get the ping route while unauthenticated (GET /ping)', async () => {
const r = await request(app.getHttpServer()).get('/ping');
expect(r.statusCode).toBe(HttpStatus.UNAUTHORIZED);
});

afterAll(async () => {
await app.close();
it('Should the ping route while (GET /ping)', async () => {
const r = await request(app.getHttpServer()).get('/ping');
expect(r.statusCode).toBe(HttpStatus.OK);
expect(r.text.length).toBeGreaterThan(0);
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ApiKeyModule } from '../database/api-key/api-key.module';
import { UsersModule } from '../database/users/users.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { jwtConstants } from './constants';
import { ApiKeyStrategy } from './strategies/api-key.strategy';
import { JwtSocketioStrategy } from './strategies/jwt-socketio.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
Expand All @@ -19,6 +21,7 @@ import { RefreshTokenStrategy } from './strategies/refresh-token.strategy';
secret: jwtConstants.secret,
signOptions: { expiresIn: jwtConstants.expirationTime },
}),
ApiKeyModule,
],
controllers: [AuthController],
providers: [
Expand All @@ -28,6 +31,7 @@ import { RefreshTokenStrategy } from './strategies/refresh-token.strategy';
JwtStrategy,
RefreshTokenStrategy,
JwtSocketioStrategy,
ApiKeyStrategy,
],
exports: [AuthService],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ApiKeyService } from '../database/api-key/api-key.service';
import { UserDocument } from '../database/users/users.model';
import { UsersService } from '../database/users/users.service';
import { jwtConstants, rtConstants } from './constants';
Expand All @@ -10,6 +11,7 @@ export class AuthService {
constructor(
private usersService: UsersService,
public jwtService: JwtService,
private apiKeyService: ApiKeyService,
) {}

public async validateUser(email: string, pass: string): Promise<any> {
Expand Down Expand Up @@ -55,4 +57,8 @@ export class AuthService {
public async isAuthenticationSetup() {
return await this.usersService.isFirstUserCreated();
}

public async findValidApiKey(key: string) {
return await this.apiKeyService.findValidApiKey(key);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Request } from 'express';
import { Role } from './constants';

export interface UserAuthContext {
id: string;
role: Role;
email?: string;
apiKeyId?: string;
}

export type AuthenticatedRequest = Request & { user: UserAuthContext };
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class ApiKeyGuard extends AuthGuard('ApiKeyStrategy') {
constructor() {
super();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
export class JwtAuthGuard extends AuthGuard('JwtStrategy') {
constructor() {
super();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserAuthContext } from '../auth.types';
import { Role, roleIsAuthorized } from '../constants';
import { ROLES_KEY } from '../decorators/roles.decorator';

Expand All @@ -20,7 +21,7 @@ export class RolesGuard implements CanActivate {
}

const request = context.switchToHttp().getRequest();
const user = request.user;
const user: UserAuthContext = request.user;

return roleIsAuthorized(user.role, requiredRole);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
import { ApiKeyDocument } from '../../database/api-key/api-key.model';
import { AuthService } from '../auth.service';
import { UserAuthContext } from '../auth.types';

@Injectable()
export class ApiKeyStrategy extends PassportStrategy(
HeaderAPIKeyStrategy,
'ApiKeyStrategy',
) {
constructor(private authService: AuthService) {
super(
{ header: 'x-api-key', prefix: '' },
true,
async (apikey, done, req) => {
const apiKeyDocument: ApiKeyDocument =
await authService.findValidApiKey(apikey);

if (!apiKeyDocument) return done(null, false);

const user: UserAuthContext = {
id: apiKeyDocument.userId.toString(),
apiKeyId: apiKeyDocument._id.toString(),
role: apiKeyDocument.role,
};

return done(null, user);
},
);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserAuthContext } from '../auth.types';
import { jwtConstants } from '../constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
export class JwtStrategy extends PassportStrategy(Strategy, 'JwtStrategy') {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
Expand All @@ -13,7 +14,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
});
}

public async validate(payload: any) {
public validate(payload: any): UserAuthContext {
return { id: payload.id, email: payload.email, role: payload.role };
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Controller, Get, Logger, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Role } from '../../../auth/constants';
import { Roles } from '../../../auth/decorators/roles.decorator';
import { JwtAuthGuard } from '../../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../../auth/guards/role.guard';
import { ApiKeyStrategy } from '../../../auth/strategies/api-key.strategy';
import { JwtStrategy } from '../../../auth/strategies/jwt.strategy';
import { ConfigService } from './config.service';
import { JobPodConfiguration } from './job-pod-config/job-pod-config.model';

@UseGuards(JwtAuthGuard, RolesGuard)
@UseGuards(AuthGuard([JwtStrategy.name, ApiKeyStrategy.name]), RolesGuard)
@Controller('admin/config')
export class ConfigController {
private logger = new Logger(ConfigController.name);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Controller, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Role } from '../../auth/constants';
import { Roles } from '../../auth/decorators/roles.decorator';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../auth/guards/role.guard';
import { ApiKeyStrategy } from '../../auth/strategies/api-key.strategy';
import { JwtStrategy } from '../../auth/strategies/jwt.strategy';
import { AlarmService } from './alarm.service';

@UseGuards(JwtAuthGuard, RolesGuard)
@UseGuards(AuthGuard([JwtStrategy.name, ApiKeyStrategy.name]), RolesGuard)
@Roles(Role.Admin)
@Controller('alarms')
export class AlarmController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,27 @@ describe('Alarm Controller (e2e)', () => {
let testData: TestingData;

beforeAll(async () => {
console.log('Alarm controller -- Before module fixture');
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
console.log('Alarm controller -- After module fixture');

app = moduleFixture.createNestApplication();

console.log('Alarm controller -- After create nest application');
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
}),
);
console.log('Alarm controller -- After use global pipes');
await app.init();
console.log('Alarm controller -- After app init');
testData = await initTesting(app);
console.log('Alarm controller -- After init testing for test data');
});

beforeEach(async () => {
await cleanup();
});

afterAll(async () => {
await cleanup();
await app.close();
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { MongooseModule } from '@nestjs/mongoose';
import { ApiKeySchema } from './api-key.model';

export const ApiKeyModelModule = MongooseModule.forFeature([
{
name: 'apikey',
schema: ApiKeySchema,
},
]);
Loading

0 comments on commit 8f89d45

Please sign in to comment.