This commit is contained in:
admin 2025-08-21 21:44:44 +02:00
parent 3700deec8b
commit e7d7f47b53
11 changed files with 5003 additions and 22 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.vscode .vscode
.idea .idea
dist dist
coverage
node_modules node_modules

View File

@ -1,5 +1,6 @@
.vscode .vscode
.idea .idea
coverage
node_modules node_modules
src src
.gitignore .gitignore

56
README.md Normal file
View File

@ -0,0 +1,56 @@
# Jwt Generator
A NestJs Library to generate JWTs.
## Install
```bash
npm i --save @apihub24/jwt-generator
```
## Requirements
| Dependency | Description |
| ------------------------- | ---------------------------------------------------------- |
| @nestjs/config | use the NestJs Config Module to get some keys listed below |
| @apihub24/session_service | used to create new Sessions when a new Token was created |
| JWT_SECRET | NestJs Config Key for Secret used to Sign JWT |
| JWT_ISSUER | NestJs Config Key for the JWT Issuer |
| JWT_AUDIENCE | NestJs Config Key for the JWT Audience |
## Usage
```typescript
import {
JwtGeneratorModule,
JwtService,
APIHUB24_TOKEN_SERVICE_INJECTION_KEY,
} from "@apihub24/jwt-generator";
// we use the @apihub24/session_service implementation from the InMemorySessionsModule
import { InMemorySessionsModule } from "@apihub24/in-memory-sessions";
// use the takeExportedProviders function from NestJs Helper Module to select the exported Providers
import { takeExportedProviders } from "@apihub24/nestjs-helper";
// use JwtGeneratorModule
const inMemorySessionsModule = InMemorySessionsModule.forRoot();
const jwtGeneratorModule = JwtGeneratorModule.forRoot([
...takeExportedProviders(inMemorySessionsModule),
]);
@Module({
imports: [jwtGeneratorModule],
})
// now you can inject the service with injection Token
import * as tokenAuthentication from "@apihub24/token-authentication";
export class Test {
constructor(
@Inject(APIHUB24_TOKEN_SERVICE_INJECTION_KEY) jwtGenerator: JwtService
);
async test(session: tokenAuthentication.ISession) {
const token = await this.jwtGenerator.generate(session);
}
}
```

4662
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,29 @@
{ {
"name": "@apihub24/jwt-generator", "name": "@apihub24/jwt-generator",
"version": "1.0.0", "version": "1.0.1",
"description": "", "description": "",
"main": "dist/index.js", "main": "dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"scripts": { "scripts": {
"build": "tsc" "build": "rimraf ./dist && tsc",
"test": "jest",
"test:coverage": "jest --coverage",
"pub": "npm publish",
"rel": "npm i && npm run build && npm run test:coverage"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^11.1.6", "@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"@apihub24/token-authentication": "^1.0.0" "@apihub24/token-authentication": "^1.0.3"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.9.2" "rimraf": "^6.0.1",
"@nestjs/testing": "^11.1.6",
"typescript": "^5.9.2",
"@types/jest": "^30.0.0",
"jest": "^30.0.0",
"ts-jest": "^29.4.1"
}, },
"keywords": [], "keywords": [],
"author": { "author": {
@ -25,5 +34,26 @@
"license": "MIT", "license": "MIT",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": ".",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "./coverage",
"testEnvironment": "node",
"roots": [
"<rootDir>/src/"
],
"moduleNameMapper": {}
} }
} }

View File

@ -0,0 +1,34 @@
import { Test, TestingModule } from "@nestjs/testing";
import {
APIHUB24_TOKEN_SERVICE_INJECTION_KEY,
JwtGeneratorModule,
JwtService,
} from "./";
import { SessionServiceMock } from "../test/session.service.mock";
describe("JwtGeneratorModule Tests", () => {
let module: TestingModule | null = null;
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
JwtGeneratorModule.forRoot([
{
provide: "@apihub24/session_service",
useClass: SessionServiceMock,
},
]),
],
}).compile();
});
it("should export injection Key @apihub24/token_service", () => {
expect(APIHUB24_TOKEN_SERVICE_INJECTION_KEY).toBeDefined();
});
it("should get JwtService by injection Key", () => {
expect(module).toBeDefined();
const service = module?.get("@apihub24/token_service");
expect(service).toBeDefined();
});
});

View File

@ -1,13 +1,15 @@
import { DynamicModule, Module, Provider } from '@nestjs/common'; import { DynamicModule, Module, Provider } from "@nestjs/common";
import { JwtService } from './jwt.service'; import { JwtService } from "./jwt.service";
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from "@nestjs/config";
export const APIHUB24_TOKEN_SERVICE_INJECTION_KEY = "@apihub24/token_service";
@Module({}) @Module({})
export class JwtGeneratorModule { export class JwtGeneratorModule {
static forRoot(requiredProviders: Provider[]): DynamicModule { static forRoot(requiredProviders: Provider[]): DynamicModule {
const providers = [ const providers = [
{ {
provide: '@apihub24/token_service', provide: APIHUB24_TOKEN_SERVICE_INJECTION_KEY,
useClass: JwtService, useClass: JwtService,
}, },
...requiredProviders, ...requiredProviders,

169
src/jwt.service.spec.ts Normal file
View File

@ -0,0 +1,169 @@
import { ISession } from "@apihub24/token-authentication";
import { APIHUB24_TOKEN_SERVICE_INJECTION_KEY, JwtGeneratorModule } from "./";
import { Test, TestingModule } from "@nestjs/testing";
import { SessionServiceMock } from "../test/session.service.mock";
describe("JwtService Tests", () => {
let module: TestingModule | null = null;
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
JwtGeneratorModule.forRoot([
{
provide: "@apihub24/session_service",
useClass: SessionServiceMock,
},
]),
],
}).compile();
});
it("should generate JWT", async () => {
expect(module).toBeDefined();
const service = module?.get(APIHUB24_TOKEN_SERVICE_INJECTION_KEY);
expect(service).toBeDefined();
const session: ISession = {
id: "1",
account: {
id: "1",
accountName: "MockAccount",
email: "MockAccount@example.de",
passwordHash: "hash",
emailVerified: true,
active: true,
groups: [],
},
metaData: {},
};
const token = await service?.generate(session, session.account.accountName);
expect(token).toBeDefined();
expect(token?.length).toBeGreaterThan(3);
});
it("should verify valid JWT", async () => {
expect(module).toBeDefined();
const service = module?.get(APIHUB24_TOKEN_SERVICE_INJECTION_KEY);
expect(service).toBeDefined();
const session: ISession = {
id: "1",
account: {
id: "1",
accountName: "MockAccount",
email: "MockAccount@example.de",
passwordHash: "hash",
emailVerified: true,
active: true,
groups: [],
},
metaData: {},
};
const token = await service?.generate(session, session.account.accountName);
expect(token).toBeDefined();
expect(token?.length).toBeGreaterThan(3);
expect(await service?.validate(token ?? "")).toBeTruthy();
});
it("should not verify invalid JWT", async () => {
expect(module).toBeDefined();
const service = module?.get(APIHUB24_TOKEN_SERVICE_INJECTION_KEY);
expect(service).toBeDefined();
expect(await service?.validate("")).toBeFalsy();
});
it("should get Account from JWT", async () => {
expect(module).toBeDefined();
const service = module?.get(APIHUB24_TOKEN_SERVICE_INJECTION_KEY);
const sessionService = module?.get("@apihub24/session_service");
expect(service).toBeDefined();
const session: ISession = {
id: "1",
account: {
id: "1",
accountName: "MockAccount",
email: "MockAccount@example.de",
passwordHash: "hash",
emailVerified: true,
active: true,
groups: [],
},
metaData: {},
};
jest.spyOn(sessionService, "getById").mockReturnValue(session);
const token = await service?.generate(session, session.account.accountName);
expect(token).toBeDefined();
expect(token?.length).toBeGreaterThan(3);
const account = await service?.getAccount(token ?? "");
expect(account).toBeDefined();
expect(account?.id).toBe(session.account.id);
expect(account?.accountName).toBe(session.account.accountName);
});
it("should not get Account from invalid JWT", async () => {
expect(module).toBeDefined();
const service = module?.get(APIHUB24_TOKEN_SERVICE_INJECTION_KEY);
const sessionService = module?.get("@apihub24/session_service");
expect(service).toBeDefined();
expect(await service?.getAccount("")).toBeNull();
});
it("should not get Account if no Session available", async () => {
expect(module).toBeDefined();
const service = module?.get(APIHUB24_TOKEN_SERVICE_INJECTION_KEY);
const sessionService = module?.get("@apihub24/session_service");
expect(service).toBeDefined();
const session: ISession = {
id: "1",
account: {
id: "1",
accountName: "MockAccount",
email: "MockAccount@example.de",
passwordHash: "hash",
emailVerified: true,
active: true,
groups: [],
},
metaData: {},
};
jest.spyOn(sessionService, "getById").mockReturnValue(null);
const token = await service?.generate(session, session.account.accountName);
expect(token).toBeDefined();
expect(token?.length).toBeGreaterThan(3);
expect(await service?.getAccount(token ?? "")).toBeNull();
});
it("should not get Account if no Account in Session available", async () => {
expect(module).toBeDefined();
const service = module?.get(APIHUB24_TOKEN_SERVICE_INJECTION_KEY);
const sessionService = module?.get("@apihub24/session_service");
expect(service).toBeDefined();
const session: ISession = {
id: "1",
account: {
id: "1",
accountName: "MockAccount",
email: "MockAccount@example.de",
passwordHash: "hash",
emailVerified: true,
active: true,
groups: [],
},
metaData: {},
};
jest.spyOn(sessionService, "getById").mockReturnValue({
id: "1",
account: null,
metaData: {},
});
const token = await service?.generate(session, session.account.accountName);
expect(token).toBeDefined();
expect(token?.length).toBeGreaterThan(3);
expect(await service?.getAccount(token ?? "")).toBeNull();
});
});

View File

@ -4,15 +4,20 @@ import { ConfigService } from "@nestjs/config";
import jwt, { JwtPayload } from "jsonwebtoken"; import jwt, { JwtPayload } from "jsonwebtoken";
@Injectable() @Injectable()
export class JwtService implements tokenAuthentication.TokenService { export class JwtService implements tokenAuthentication.ITokenService {
constructor( constructor(
@Inject(ConfigService) @Inject(ConfigService)
private readonly configService: ConfigService, private readonly configService: ConfigService,
@Inject("@apihub24/session_service") @Inject("@apihub24/session_service")
private readonly sessionService: tokenAuthentication.SessionService private readonly sessionService: tokenAuthentication.ISessionService
) {} ) {}
generate(session: tokenAuthentication.Session): Promise<string> { generate(
session: tokenAuthentication.ISession,
subject: string,
expires?: string,
algorithm?: tokenAuthentication.Algorithm
): Promise<string> {
return Promise.resolve( return Promise.resolve(
jwt.sign( jwt.sign(
{ {
@ -20,7 +25,13 @@ export class JwtService implements tokenAuthentication.TokenService {
accountId: session.account.id, accountId: session.account.id,
}, },
this.getSecretKey(), this.getSecretKey(),
{ expiresIn: "1h" } {
expiresIn: expires ?? "1h",
algorithm: algorithm ?? "HS512",
subject,
issuer: this.getIssuer(),
audience: this.getAudience(),
}
) )
); );
} }
@ -35,7 +46,9 @@ export class JwtService implements tokenAuthentication.TokenService {
} }
} }
async getAccount(token: string): Promise<tokenAuthentication.Account | null> { async getAccount(
token: string
): Promise<tokenAuthentication.IAccount | null> {
try { try {
const payload = jwt.verify(token, this.getSecretKey()) as JwtPayload; const payload = jwt.verify(token, this.getSecretKey()) as JwtPayload;
if (typeof payload?.sessionId !== "string") { if (typeof payload?.sessionId !== "string") {
@ -58,4 +71,12 @@ export class JwtService implements tokenAuthentication.TokenService {
private getSecretKey(): string { private getSecretKey(): string {
return this.configService.get<string>("JWT_SECRET", "thisisnotsigrid"); return this.configService.get<string>("JWT_SECRET", "thisisnotsigrid");
} }
private getIssuer(): string {
return this.configService.get<string>("JWT_ISSUER", "unknown");
}
private getAudience(): string {
return this.configService.get<string>("JWT_AUDIENCE", "unknown");
}
} }

View File

@ -0,0 +1,20 @@
import {
IAccount,
ISession,
ISessionService,
} from "@apihub24/token-authentication";
export class SessionServiceMock implements ISessionService {
create(account: IAccount): Promise<ISession> {
throw new Error("Method not implemented.");
}
getBy(filter: (account: IAccount) => boolean): Promise<ISession[]> {
throw new Error("Method not implemented.");
}
getById(sessionId: string): Promise<ISession | null> {
throw new Error("Method not implemented.");
}
remove(sessionId: string): Promise<void> {
throw new Error("Method not implemented.");
}
}

View File

@ -22,5 +22,6 @@
"strictBindCallApply": false, "strictBindCallApply": false,
"noFallthroughCasesInSwitch": false "noFallthroughCasesInSwitch": false
}, },
"include": ["./src"] "include": ["./src"],
"exclude": ["**/*.spec.ts", "**/*.mock.ts"]
} }