feat: jwt auth. #7

Merged
Ivan merged 6 commits from feat-jwt-auth into master 2021-07-20 20:45:18 +08:00
15 changed files with 6721 additions and 4810 deletions
Showing only changes of commit 02059ee54f - Show all commits

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": []
}

View File

@ -16,5 +16,9 @@ db:
prefix: fennec
rabbitmq:
uri: 'amqp://fennec:fennec@192.168.31.194:5672'
etcd:
hosts:
- 'http://192.168.31.194:2379'
workspaces:
root: '/Users/ivanli/Projects/fennec/workspaces'

11359
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,6 @@
},
"dependencies": {
"@golevelup/nestjs-rabbitmq": "^1.16.1",
"@nestjs/bull": "^0.3.1",
"@nestjs/common": "^7.5.1",
"@nestjs/config": "^0.6.2",
"@nestjs/core": "^7.5.1",
@ -30,19 +29,19 @@
"@nestjs/platform-express": "^7.5.1",
"@nestjs/typeorm": "^7.1.5",
"@types/amqplib": "^0.8.0",
"@types/bull": "^3.15.0",
"@types/ramda": "^0.27.38",
"apollo-server-express": "^2.19.2",
"bcrypt": "^5.0.0",
"body-parser": "^1.19.0",
"bull": "^3.20.1",
"class-transformer": "^0.3.2",
"class-validator": "^0.13.1",
"debug": "^4.3.1",
"graphql": "^15.5.0",
"graphql-tools": "^7.0.2",
"ioredis": "^4.25.0",
"jose": "^3.14.0",
"js-yaml": "^4.0.0",
"nestjs-etcd": "^0.2.0",
"nestjs-pino": "^1.4.0",
"nestjs-redis": "^1.2.8",
"observable-to-async-generator": "^1.0.1-rc",
@ -96,6 +95,9 @@
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"moduleNameMapper": {
"^jose/(.*)$": "<rootDir>/../node_modules/jose/dist/node/cjs/$1"
},
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}

View File

@ -1,3 +1,4 @@
import { CommonsModule } from './commons/commons.module';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
@ -12,13 +13,13 @@ import { PipelineTasksModule } from './pipeline-tasks/pipeline-tasks.module';
import configuration from './commons/config/configuration';
import { RedisModule } from 'nestjs-redis';
import { WebhooksModule } from './webhooks/webhooks.module';
import { RawBodyMiddleware } from './commons/middlewares/raw-body.middleware';
import { RawBodyMiddleware } from './commons/middleware/raw-body.middleware';
import { GiteaWebhooksController } from './webhooks/gitea-webhooks.controller';
import { ParseBodyMiddleware } from './commons/middlewares/parse-body.middleware';
import { BullModule } from '@nestjs/bull';
import { ParseBodyMiddleware } from './commons/middleware/parse-body.middleware';
import { LoggerModule } from 'nestjs-pino';
import { EtcdModule } from 'nestjs-etcd';
import pinoPretty from 'pino-pretty';
import { AccountMiddleware } from './commons/middleware/account.middleware';
@Module({
imports: [
@ -66,17 +67,6 @@ import pinoPretty from 'pino-pretty';
}),
inject: [ConfigService],
}),
BullModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
redis: {
host: configService.get<string>('db.redis.host', 'localhost'),
port: configService.get<number>('db.redis.port', undefined),
password: configService.get<string>('db.redis.password', undefined),
},
}),
inject: [ConfigService],
}),
ProjectsModule,
ReposModule,
PipelinesModule,
@ -91,7 +81,15 @@ import pinoPretty from 'pino-pretty';
}),
inject: [ConfigService],
}),
EtcdModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
hosts: configService.get<string>('db.etcd.hosts', 'localhost:2379'),
}),
inject: [ConfigService],
}),
WebhooksModule,
CommonsModule,
],
controllers: [AppController],
providers: [AppService, AppResolver],
@ -102,6 +100,8 @@ export class AppModule implements NestModule {
.apply(RawBodyMiddleware)
.forRoutes(GiteaWebhooksController)
.apply(ParseBodyMiddleware)
.forRoutes('*')
.apply(AccountMiddleware)
.forRoutes('*');
}
}

View File

@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { PasswordConverter } from './services/password-converter';
import { RedisMutexModule } from './redis-mutex/redis-mutex.module';
import { JwtService } from './services/jwt.service';
@Module({
providers: [PasswordConverter],
exports: [PasswordConverter, RedisMutexModule],
providers: [PasswordConverter, JwtService],
exports: [PasswordConverter, RedisMutexModule, JwtService],
imports: [RedisMutexModule],
})
export class CommonsModule {}

View File

@ -0,0 +1,8 @@
import { JwtService } from '../services/jwt.service';
import { AccountMiddleware } from './account.middleware';
describe('AccountMiddleware', () => {
it('should be defined', () => {
expect(new AccountMiddleware({} as JwtService)).toBeDefined();
});
});

View File

@ -0,0 +1,31 @@
import {
Injectable,
NestMiddleware,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '../services/jwt.service';
@Injectable()
export class AccountMiddleware implements NestMiddleware {
constructor(private readonly jwtService: JwtService) {}
async use(req: any, res: any, next: () => void) {
const authPayload = req.header('authorization') ?? '';
if (!authPayload) {
req.user = req.session.user;
next();
return;
}
const token = authPayload.replace('Bearer ', '');
if (!token) {
throw new UnauthorizedException('授权凭据不合法!');
}
try {
const { payload } = await this.jwtService.verify(token);
req.user = payload;
next();
} catch (err) {
throw new UnauthorizedException('登录凭据失效或不合法!');
}
next();
}
}

View File

@ -0,0 +1,59 @@
import { Test, TestingModule } from '@nestjs/testing';
import { generateKeyPair, KeyObject } from 'crypto';
import { getClientToken } from 'nestjs-etcd';
import { promisify } from 'util';
import { JwtService } from './jwt.service';
import { SignJWT } from 'jose/jwt/sign';
describe('JwtService', () => {
let service: JwtService;
let privateKey: KeyObject;
let publicKey: KeyObject;
beforeAll(async () => {
const pair = await promisify(generateKeyPair)('ec', {
namedCurve: 'prime256v1',
});
privateKey = pair.privateKey;
publicKey = pair.publicKey;
});
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
JwtService,
{
provide: getClientToken(),
useValue: {
get: () => ({
buffer: () =>
Promise.resolve(
publicKey.export({ format: 'pem', type: 'spki' }),
),
}),
},
},
],
}).compile();
service = module.get<JwtService>(JwtService);
await service.onModuleInit();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('verify', () => {
it('normal', async () => {
const token = await new SignJWT({ userId: 'test' })
.setProtectedHeader({ alg: 'ES256' })
.setIssuedAt()
.setIssuer('urn:example:issuer')
.setAudience('urn:example:audience')
.setExpirationTime('1h')
.sign(privateKey);
await expect(service.verify(token)).resolves.toBeTruthy();
});
});
});

View File

@ -0,0 +1,23 @@
import { OnModuleInit } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { KeyObject, createPublicKey } from 'crypto';
import { jwtVerify } from 'jose/jwt/verify';
import { Etcd3, InjectClient } from 'nestjs-etcd';
@Injectable()
export class JwtService implements OnModuleInit {
publicKey: KeyObject;
constructor(@InjectClient() private readonly etcd: Etcd3) {}
async onModuleInit() {
const buff = await this.etcd
.get('commons/auth-jwt-public-key/index')
.buffer();
this.publicKey = createPublicKey(buff);
}
async verify(token: string) {
return await jwtVerify(token, this.publicKey, {
algorithms: ['PS256', 'ES256'],
});
}
}

View File

@ -7,6 +7,7 @@
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"lib": ["ES2021"],
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",