feat: jwt auth. #7

Merged
Ivan merged 6 commits from feat-jwt-auth into master 2021-07-20 20:45:18 +08:00
12 changed files with 54 additions and 133 deletions
Showing only changes of commit 0a03bcd36e - Show all commits

23
package-lock.json generated
View File

@ -1,14 +1,15 @@
{ {
"name": "fennec-be", "name": "fennec-be",
"version": "0.1.0", "version": "0.1.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"version": "0.1.0", "version": "0.1.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@golevelup/nestjs-rabbitmq": "^1.16.1", "@golevelup/nestjs-rabbitmq": "^1.16.1",
"@nestjs-lib/auth": "^0.1.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",
@ -2892,6 +2893,18 @@
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==" "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
}, },
"node_modules/@nestjs-lib/auth": {
"version": "0.1.1",
"resolved": "https://npm.ivanli.cc/@nestjs-lib%2fauth/-/auth-0.1.1.tgz",
"integrity": "sha512-JXKvDsJudBlEXBiGyoODFpbbJabcoSaUqJY0bQHX0imidmhovx3VuGZwudALrlw1BT2NOJEO7ElFoITCfTDfGw==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^7.0.0",
"@nestjs/graphql": "^7.10.3",
"jose": "^3.14.0",
"nestjs-etcd": "^0.2.0"
}
},
"node_modules/@nestjs/cli": { "node_modules/@nestjs/cli": {
"version": "7.6.0", "version": "7.6.0",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-7.6.0.tgz", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-7.6.0.tgz",
@ -18822,6 +18835,12 @@
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==" "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
}, },
"@nestjs-lib/auth": {
"version": "0.1.1",
"resolved": "https://npm.ivanli.cc/@nestjs-lib%2fauth/-/auth-0.1.1.tgz",
"integrity": "sha512-JXKvDsJudBlEXBiGyoODFpbbJabcoSaUqJY0bQHX0imidmhovx3VuGZwudALrlw1BT2NOJEO7ElFoITCfTDfGw==",
"requires": {}
},
"@nestjs/cli": { "@nestjs/cli": {
"version": "7.6.0", "version": "7.6.0",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-7.6.0.tgz", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-7.6.0.tgz",

View File

@ -1,8 +1,8 @@
{ {
"name": "fennec-be", "name": "fennec-be",
"version": "0.1.0", "version": "0.1.1",
"description": "", "description": "a ci/cd project.",
"author": "", "author": "Ivan Li\b<ivanli2048@gmail.com>",
"private": true, "private": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"scripts": { "scripts": {
@ -22,6 +22,7 @@
}, },
"dependencies": { "dependencies": {
"@golevelup/nestjs-rabbitmq": "^1.16.1", "@golevelup/nestjs-rabbitmq": "^1.16.1",
"@nestjs-lib/auth": "^0.1.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",

View File

@ -19,7 +19,7 @@ import { ParseBodyMiddleware } from './commons/middleware/parse-body.middleware'
import { LoggerModule } from 'nestjs-pino'; import { LoggerModule } from 'nestjs-pino';
import { EtcdModule } from 'nestjs-etcd'; import { EtcdModule } from 'nestjs-etcd';
import pinoPretty from 'pino-pretty'; import pinoPretty from 'pino-pretty';
import { AccountMiddleware } from './commons/middleware/account.middleware'; import { fromPairs, map, pipe, toPairs } from 'ramda';
@Module({ @Module({
imports: [ imports: [
@ -64,6 +64,22 @@ import { AccountMiddleware } from './commons/middleware/account.middleware';
playground: true, playground: true,
autoSchemaFile: true, autoSchemaFile: true,
installSubscriptionHandlers: true, installSubscriptionHandlers: true,
context: ({ req, connection, ...args }) => {
return connection ? { req: connection.context } : { req };
},
subscriptions: {
onConnect: (connectionParams: Record<string, string>) => {
const connectionParamsWithLowerKeys = pipe(
toPairs,
map(([key, value]) => [key.toLowerCase(), value]),
fromPairs,
)(connectionParams);
return {
headers: connectionParamsWithLowerKeys,
};
},
},
}), }),
inject: [ConfigService], inject: [ConfigService],
}), }),
@ -100,8 +116,6 @@ export class AppModule implements NestModule {
.apply(RawBodyMiddleware) .apply(RawBodyMiddleware)
.forRoutes(GiteaWebhooksController) .forRoutes(GiteaWebhooksController)
.apply(ParseBodyMiddleware) .apply(ParseBodyMiddleware)
.forRoutes('*')
.apply(AccountMiddleware)
.forRoutes('*'); .forRoutes('*');
} }
} }

View File

@ -1,11 +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'; import { AuthModule } from '@nestjs-lib/auth';
@Module({ @Module({
providers: [PasswordConverter, JwtService], imports: [RedisMutexModule, AuthModule],
exports: [PasswordConverter, RedisMutexModule, JwtService], providers: [PasswordConverter],
imports: [RedisMutexModule], exports: [PasswordConverter, RedisMutexModule],
}) })
export class CommonsModule {} export class CommonsModule {}

View File

@ -1,8 +0,0 @@
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

@ -1,31 +0,0 @@
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

@ -1,59 +0,0 @@
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

@ -1,23 +0,0 @@
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,7 +7,9 @@ import { plainToClass } from 'class-transformer';
import { PipelineTaskLogger } from './pipeline-task.logger'; import { PipelineTaskLogger } from './pipeline-task.logger';
import { observableToAsyncIterable } from '@graphql-tools/utils'; import { observableToAsyncIterable } from '@graphql-tools/utils';
import { PipelineTaskEvent } from './models/pipeline-task-event'; import { PipelineTaskEvent } from './models/pipeline-task-event';
import { Roles, AccountRole } from '@nestjs-lib/auth';
@Roles(AccountRole.admin, AccountRole.super)
@Resolver() @Resolver()
export class PipelineTasksResolver { export class PipelineTasksResolver {
constructor( constructor(

View File

@ -1,3 +1,4 @@
import { Roles, AccountRole } from '@nestjs-lib/auth';
import { Query } from '@nestjs/graphql'; import { Query } from '@nestjs/graphql';
import { import {
Args, Args,
@ -10,6 +11,7 @@ import { PipelineTasksService } from '../pipeline-tasks/pipeline-tasks.service';
import { Commit, LogFields } from '../repos/dtos/log-list.model'; import { Commit, LogFields } from '../repos/dtos/log-list.model';
import { PipelinesService } from './pipelines.service'; import { PipelinesService } from './pipelines.service';
@Roles(AccountRole.admin, AccountRole.super)
@Resolver(() => Commit) @Resolver(() => Commit)
export class CommitLogsResolver { export class CommitLogsResolver {
constructor( constructor(

View File

@ -4,7 +4,9 @@ import { UpdatePipelineInput } from './dtos/update-pipeline.input';
import { Pipeline } from './pipeline.entity'; import { Pipeline } from './pipeline.entity';
import { PipelinesService } from './pipelines.service'; import { PipelinesService } from './pipelines.service';
import { ListPipelineArgs } from './dtos/list-pipelines.args'; import { ListPipelineArgs } from './dtos/list-pipelines.args';
import { Roles, AccountRole } from '@nestjs-lib/auth';
@Roles(AccountRole.admin, AccountRole.super)
@Resolver() @Resolver()
export class PipelinesResolver { export class PipelinesResolver {
constructor(private readonly service: PipelinesService) {} constructor(private readonly service: PipelinesService) {}

View File

@ -1,9 +1,11 @@
import { AccountRole, Roles } from '@nestjs-lib/auth';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateProjectInput } from './dtos/create-project.input'; import { CreateProjectInput } from './dtos/create-project.input';
import { UpdateProjectInput } from './dtos/update-project.input'; import { UpdateProjectInput } from './dtos/update-project.input';
import { Project } from './project.entity'; import { Project } from './project.entity';
import { ProjectsService } from './projects.service'; import { ProjectsService } from './projects.service';
@Roles(AccountRole.admin, AccountRole.super)
@Resolver(() => Project) @Resolver(() => Project)
export class ProjectsResolver { export class ProjectsResolver {
constructor(private readonly service: ProjectsService) {} constructor(private readonly service: ProjectsService) {}