feat: jwt auth. #7
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": []
|
||||||
|
}
|
@ -16,5 +16,9 @@ db:
|
|||||||
prefix: fennec
|
prefix: fennec
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
uri: 'amqp://fennec:fennec@192.168.31.194:5672'
|
uri: 'amqp://fennec:fennec@192.168.31.194:5672'
|
||||||
|
etcd:
|
||||||
|
hosts:
|
||||||
|
- 'http://192.168.31.194:2379'
|
||||||
|
|
||||||
workspaces:
|
workspaces:
|
||||||
root: '/Users/ivanli/Projects/fennec/workspaces'
|
root: '/Users/ivanli/Projects/fennec/workspaces'
|
11359
package-lock.json
generated
11359
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@golevelup/nestjs-rabbitmq": "^1.16.1",
|
"@golevelup/nestjs-rabbitmq": "^1.16.1",
|
||||||
"@nestjs/bull": "^0.3.1",
|
|
||||||
"@nestjs/common": "^7.5.1",
|
"@nestjs/common": "^7.5.1",
|
||||||
"@nestjs/config": "^0.6.2",
|
"@nestjs/config": "^0.6.2",
|
||||||
"@nestjs/core": "^7.5.1",
|
"@nestjs/core": "^7.5.1",
|
||||||
@ -30,19 +29,19 @@
|
|||||||
"@nestjs/platform-express": "^7.5.1",
|
"@nestjs/platform-express": "^7.5.1",
|
||||||
"@nestjs/typeorm": "^7.1.5",
|
"@nestjs/typeorm": "^7.1.5",
|
||||||
"@types/amqplib": "^0.8.0",
|
"@types/amqplib": "^0.8.0",
|
||||||
"@types/bull": "^3.15.0",
|
|
||||||
"@types/ramda": "^0.27.38",
|
"@types/ramda": "^0.27.38",
|
||||||
"apollo-server-express": "^2.19.2",
|
"apollo-server-express": "^2.19.2",
|
||||||
"bcrypt": "^5.0.0",
|
"bcrypt": "^5.0.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"bull": "^3.20.1",
|
|
||||||
"class-transformer": "^0.3.2",
|
"class-transformer": "^0.3.2",
|
||||||
"class-validator": "^0.13.1",
|
"class-validator": "^0.13.1",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.5.0",
|
||||||
"graphql-tools": "^7.0.2",
|
"graphql-tools": "^7.0.2",
|
||||||
"ioredis": "^4.25.0",
|
"ioredis": "^4.25.0",
|
||||||
|
"jose": "^3.14.0",
|
||||||
"js-yaml": "^4.0.0",
|
"js-yaml": "^4.0.0",
|
||||||
|
"nestjs-etcd": "^0.2.0",
|
||||||
"nestjs-pino": "^1.4.0",
|
"nestjs-pino": "^1.4.0",
|
||||||
"nestjs-redis": "^1.2.8",
|
"nestjs-redis": "^1.2.8",
|
||||||
"observable-to-async-generator": "^1.0.1-rc",
|
"observable-to-async-generator": "^1.0.1-rc",
|
||||||
@ -96,6 +95,9 @@
|
|||||||
"collectCoverageFrom": [
|
"collectCoverageFrom": [
|
||||||
"**/*.(t|j)s"
|
"**/*.(t|j)s"
|
||||||
],
|
],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^jose/(.*)$": "<rootDir>/../node_modules/jose/dist/node/cjs/$1"
|
||||||
|
},
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { CommonsModule } from './commons/commons.module';
|
||||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { GraphQLModule } from '@nestjs/graphql';
|
import { GraphQLModule } from '@nestjs/graphql';
|
||||||
@ -12,13 +13,13 @@ import { PipelineTasksModule } from './pipeline-tasks/pipeline-tasks.module';
|
|||||||
import configuration from './commons/config/configuration';
|
import configuration from './commons/config/configuration';
|
||||||
import { RedisModule } from 'nestjs-redis';
|
import { RedisModule } from 'nestjs-redis';
|
||||||
import { WebhooksModule } from './webhooks/webhooks.module';
|
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 { GiteaWebhooksController } from './webhooks/gitea-webhooks.controller';
|
||||||
import { ParseBodyMiddleware } from './commons/middlewares/parse-body.middleware';
|
import { ParseBodyMiddleware } from './commons/middleware/parse-body.middleware';
|
||||||
import { BullModule } from '@nestjs/bull';
|
|
||||||
import { LoggerModule } from 'nestjs-pino';
|
import { LoggerModule } from 'nestjs-pino';
|
||||||
|
import { EtcdModule } from 'nestjs-etcd';
|
||||||
import pinoPretty from 'pino-pretty';
|
import pinoPretty from 'pino-pretty';
|
||||||
|
import { AccountMiddleware } from './commons/middleware/account.middleware';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -66,17 +67,6 @@ import pinoPretty from 'pino-pretty';
|
|||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
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,
|
ProjectsModule,
|
||||||
ReposModule,
|
ReposModule,
|
||||||
PipelinesModule,
|
PipelinesModule,
|
||||||
@ -91,7 +81,15 @@ import pinoPretty from 'pino-pretty';
|
|||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
|
EtcdModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
hosts: configService.get<string>('db.etcd.hosts', 'localhost:2379'),
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
WebhooksModule,
|
WebhooksModule,
|
||||||
|
CommonsModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, AppResolver],
|
providers: [AppService, AppResolver],
|
||||||
@ -102,6 +100,8 @@ export class AppModule implements NestModule {
|
|||||||
.apply(RawBodyMiddleware)
|
.apply(RawBodyMiddleware)
|
||||||
.forRoutes(GiteaWebhooksController)
|
.forRoutes(GiteaWebhooksController)
|
||||||
.apply(ParseBodyMiddleware)
|
.apply(ParseBodyMiddleware)
|
||||||
|
.forRoutes('*')
|
||||||
|
.apply(AccountMiddleware)
|
||||||
.forRoutes('*');
|
.forRoutes('*');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { PasswordConverter } from './services/password-converter';
|
import { PasswordConverter } from './services/password-converter';
|
||||||
import { RedisMutexModule } from './redis-mutex/redis-mutex.module';
|
import { RedisMutexModule } from './redis-mutex/redis-mutex.module';
|
||||||
|
import { JwtService } from './services/jwt.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [PasswordConverter],
|
providers: [PasswordConverter, JwtService],
|
||||||
exports: [PasswordConverter, RedisMutexModule],
|
exports: [PasswordConverter, RedisMutexModule, JwtService],
|
||||||
imports: [RedisMutexModule],
|
imports: [RedisMutexModule],
|
||||||
})
|
})
|
||||||
export class CommonsModule {}
|
export class CommonsModule {}
|
||||||
|
8
src/commons/middleware/account.middleware.spec.ts
Normal file
8
src/commons/middleware/account.middleware.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
31
src/commons/middleware/account.middleware.ts
Normal file
31
src/commons/middleware/account.middleware.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
59
src/commons/services/jwt.service.spec.ts
Normal file
59
src/commons/services/jwt.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
23
src/commons/services/jwt.service.ts
Normal file
23
src/commons/services/jwt.service.ts
Normal 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'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
|
"lib": ["ES2021"],
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
|
Loading…
Reference in New Issue
Block a user