From 2546ab8b3bb3a5bbbdbf72dc3c61cd2432a683e1 Mon Sep 17 00:00:00 2001 From: Dieter Wimberger Date: Sat, 23 Nov 2024 13:04:35 +0100 Subject: [PATCH] initial commit --- .auto-changelog | 9 ++ .editorconfig | 22 ++++ .github/workflows/build.yml | 16 +++ .github/workflows/cache.yml | 9 ++ .github/workflows/lint.yml | 30 ++++++ .github/workflows/npm.yml | 29 ++++++ .github/workflows/release.yml | 80 +++++++++++++++ .github/workflows/semantic.yml | 14 +++ .github/workflows/test.yml | 16 +++ .gitignore | 10 ++ .npmignore | 0 .prettierignore | 2 + .prettierrc | 4 + .release-it.json | 87 ++++++++++++++++ CONTRIBUTING.md | 36 +++++++ LICENSE | 38 +++++++ README.md | 64 ++++++++++++ nest-cli.json | 8 ++ package.json | 84 +++++++++++++++ src/authclient.constants.ts | 8 ++ src/authclient.module-definition.ts | 5 + src/authclient.module-options.ts | 40 ++++++++ src/authclient.module.ts | 15 +++ src/authclient.service.spec.ts | 78 ++++++++++++++ src/authclient.service.ts | 153 ++++++++++++++++++++++++++++ src/index.ts | 5 + tsconfig.build.json | 4 + tsconfig.json | 23 +++++ tsconfig.node.json | 15 +++ tslint.json | 18 ++++ 30 files changed, 922 insertions(+) create mode 100644 .auto-changelog create mode 100644 .editorconfig create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/cache.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/npm.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/semantic.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .release-it.json create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 nest-cli.json create mode 100644 package.json create mode 100644 src/authclient.constants.ts create mode 100644 src/authclient.module-definition.ts create mode 100644 src/authclient.module-options.ts create mode 100644 src/authclient.module.ts create mode 100644 src/authclient.service.spec.ts create mode 100644 src/authclient.service.ts create mode 100644 src/index.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 tslint.json diff --git a/.auto-changelog b/.auto-changelog new file mode 100644 index 0000000..6ed71cd --- /dev/null +++ b/.auto-changelog @@ -0,0 +1,9 @@ +{ + "package": true, + "output": "CHANGELOG.md", + "template": "compact", + "unreleased": false, + "commitLimit": false, + "ignoreCommitPattern": "^(?!.*\\(\\#\\d+\\)$).*", + "releaseSummary": false +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..da92bb2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.java] +ij_java_imports_layout = $*, |, javax.**, java.**, |, * + +[*.gradle] +indent_size = 4 + +[*.md] +max_line_length = off +trim_trailing_whitespace = false + +[Makefile] +indent_size = 4 \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f6a9a4f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,16 @@ +name: build +on: + push: + branches: [ 'main' ] + pull_request: + branches: [ 'main' ] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build + uses: ./.github/actions/build + - name: Run Build + run: npm nest build + \ No newline at end of file diff --git a/.github/workflows/cache.yml b/.github/workflows/cache.yml new file mode 100644 index 0000000..3d58830 --- /dev/null +++ b/.github/workflows/cache.yml @@ -0,0 +1,9 @@ +name: cache +on: + pull_request: + types: + - closed +jobs: + cache_cleanup: + name: Clean up cache + uses: evva-sfw/workflows/.github/workflows/cache.yml@main diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..2ab26b5 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,30 @@ +name: lint +on: + push: + branches: [ 'main' ] + pull_request: + branches: [ 'main' ] +permissions: + id-token: write + attestations: write +jobs: + lint_all: + runs-on: ubuntu-latest + permissions: + id-token: write + attestations: write + steps: + - uses: actions/checkout@v3 + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Cache node_modules + id: node-modules + uses: actions/cache@v4 + with: + path: | + node_modules + key: node-modules + - run: npm i + - run: npm run lint diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml new file mode 100644 index 0000000..708fe66 --- /dev/null +++ b/.github/workflows/npm.yml @@ -0,0 +1,29 @@ +name: npm +on: + push: + branches: [ 'main' ] + pull_request: + branches: [ 'main' ] +jobs: + npm_check-package-sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Cache node_modules + id: node-modules + uses: actions/cache@v4 + with: + path: | + node_modules + test-app/node_modules + key: node-modules + - name: NPM Install + run: npm i + - name: Check if git is clean + uses: CatChen/check-git-status-action@v1 + with: + fail-if-not-clean: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7996679 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,80 @@ +name: Release and publish to npm +on: + workflow_dispatch: + inputs: + input_version: + type: choice + description: What type of release? + options: + - patch + - minor + - major + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + attestations: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ssh-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} + - name: Debug inputs + run: | + echo "Type of release:${{ github.event.inputs.input_version }} from user ${GITHUB_ACTOR}" + git config --list + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Cache node_modules + id: node-modules + uses: actions/cache@v4 + with: + path: | + node_modules + test-app/node_modules + .yarn/cache + key: node-modules + - name: Import GPG key + id: import-gpg + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + git_user_signingkey: true + git_commit_gpgsign: true + + - name: GPG user IDs + run: | + echo "fingerprint: ${{ steps.import-gpg.outputs.fingerprint }}" + echo "keyid: ${{ steps.import-gpg.outputs.keyid }}" + echo "name: ${{ steps.import-gpg.outputs.name }}" + echo "email: ${{ steps.import-gpg.outputs.email }}" + + - name: git config + run: | + git config user.name "${{ steps.import-gpg.outputs.name }}" + git config user.email "${{ steps.import-gpg.outputs.email }}" + + - name: Install dependencies + run: npm i + + - name: Authenticate with registry + run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Run release-it + run: npx release-it ${{ github.event.inputs.input_version }} --ci + env: + BOT_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} + + - name: Attest + uses: actions/attest-build-provenance@v1 + with: + subject-path: '${{ github.workspace }}/*.tgz' diff --git a/.github/workflows/semantic.yml b/.github/workflows/semantic.yml new file mode 100644 index 0000000..bbda34b --- /dev/null +++ b/.github/workflows/semantic.yml @@ -0,0 +1,14 @@ +name: semantic +on: + pull_request_target: + types: + - opened + - edited + - synchronize + - reopened +permissions: + pull-requests: read +jobs: + semantic_pr-name: + name: semantic_pr-name + uses: evva-sfw/workflows/.github/workflows/semantic.yml@main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4e3f532 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,16 @@ +name: test +on: + push: + branches: [ 'main' ] + pull_request: + branches: [ 'main' ] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Capacitor Web + uses: ./.github/actions/capacitor-setup-web + - name: Run Tests + run: npm run test + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ad5580 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +.idea +.VSCode +dist +yarn.lock +*.tsbuildinfo +# Package results +*.gz +*.tgz \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..e69de29 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a7e8e21 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +build +dist \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..dcb7279 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/.release-it.json b/.release-it.json new file mode 100644 index 0000000..2c74947 --- /dev/null +++ b/.release-it.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://unpkg.com/release-it@17/schema/release-it.json", + "hooks": { + "after:bump": [ + "rm -f *.tgz", + "yarn pack --out '%s_%v.tgz'", + "npx auto-changelog -c .auto-changelog" + ] + }, + "git": { + "commitMessage": "chore: release ${version}", + "tagName": "${version}", + "commitArgs": [ + "-S" + ], + "tagArgs": [ + "-s" + ] + }, + "npm": { + "publish": true, + "publishArgs": [ + "--provenance" + ] + }, + "github": { + "release": true, + "assets": [ + "*.tgz" + ], + "tokenRef": "BOT_GITHUB_TOKEN" + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": { + "name": "conventionalcommits", + "types": [ + { + "type": "feat", + "section": "๐ŸŽ‰ Features" + }, + { + "type": "fix", + "section": "๐Ÿ› Bug Fixes" + }, + { + "type": "perf", + "section": "โšก๏ธ Performance Improvements" + }, + { + "type": "revert", + "section": "โช๏ธ Reverts" + }, + { + "type": "docs", + "section": "๐Ÿ“ Documentation" + }, + { + "type": "style", + "section": "๐ŸŽจ Styles" + }, + { + "type": "refactor", + "section": "๐Ÿ”€ Code Refactoring" + }, + { + "type": "test", + "section": "๐Ÿงช Tests" + }, + { + "type": "build", + "section": "โš™๏ธ Build System" + }, + { + "type": "ci", + "section": "๐Ÿš€ Continuous Integration" + }, + { + "type": "chore", + "section": "๐Ÿงน Chore" + } + ] + }, + "infile": "CHANGELOG.md" + } + } +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7af541d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +# Contributing + +This guide provides instructions for contributing to this Capacitor plugin. + +## Developing + +### Local Setup + +1. Fork and clone the repo. +1. Install the dependencies. + + ```shell + npm install + ``` + +### Scripts + +#### `npm run build` + +It will compile the TypeScript code from `src/` into ESM JavaScript in `dist/`. These files are used in apps with bundlers when your plugin is imported. + +#### `npm run lint` / `npm run fmt` + +Check formatting and code quality, autoformat/autofix if possible. + +This template is integrated with TSLint, Prettier. Using these tools is completely optional, but strives to have consistent code style and structure for easier cooperation. + +## Publishing + +There is a `prepack` hook in `package.json` which prepares the plugin before publishing, so all you need to do is run: + +```shell +npm publish +``` + +> **Note**: The [`files`](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#files) array in `package.json` specifies which files get published. If you rename files/directories or add files elsewhere, you may need to update it. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7b294fd --- /dev/null +++ b/LICENSE @@ -0,0 +1,38 @@ +Copyright (C) 2024 EVVA Sicherheitstechnologie GmbH All Rights Reserved. + +This EVVA software is supplied to you by EVVA Sicherheitstechnologie GmbH ("EVVA") +in consideration of your agreement to the following terms, and your use, +installation, modification or redistribution of this EVVA software constitutes +acceptance of these terms. If you do not agree with these terms, please do not +use, install, modify or redistribute this EVVA software. + +In consideration of your agreement to abide by the following terms, and +subject to these terms, EVVA grants you a personal, non-exclusive +license, under EVVA's copyrights in this original EVVA software (the +"EVVA Software"), to use, reproduce, modify and redistribute the EVVA +Software, with or without modifications, in source and/or binary forms; +provided that if you redistribute the EVVA Software in its entirety and +without modifications, you must retain this notice and the following +text and disclaimers in all such redistributions of the EVVA Software. +Neither the name, trademarks, service marks or logos of EVVA Sicherheitstechnologie GmbH may +be used to endorse or promote products derived from the EVVA Software +without specific prior written permission from EVVA. Except as +expressly stated in this notice, no other rights or licenses, express or +implied, are granted by EVVA herein, including but not limited to any +patent rights that may be infringed by your derivative works or by other +works in which the EVVA Software may be incorporated. + +The EVVA Software is provided by EVVA on an "AS IS" basis. EVVA +MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION +THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE, REGARDING THE EVVA SOFTWARE OR ITS USE AND +OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + +IN NO EVENT SHALL EVVA BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, +MODIFICATION AND/OR DISTRIBUTION OF THE EVVA SOFTWARE, HOWEVER CAUSED +AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), +STRICT LIABILITY OR OTHERWISE, EVEN IF EVVA HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..970f1a9 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +## Description + +Client implementation for the EVVA [Auth Service]. + +## Build & Package +```bash +# Nest Build +$ nest build +``` + +## Usage + +``` + import {ConfigService } from '@nestjs/config'; + import { + AuthClientModule, + AuthClientService, + AuthClientOptions, + SFW_AUTH_ENDPOINT, + SFW_AUTH_TENANT, + SFW_AUTH_CLIENT, + SFW_AUTH_SECRET, + SFW_AUTH_VALIDITY, + VAULT_JWTROLE_IDENTIFIER, + VAULT_ENDPOINT, + VAULT_CA, +} from 'nest-sfw-auth-client'; + + AuthClientModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => + ({ + sfwAuthEndpoint: configService.get(SFW_AUTH_ENDPOINT), //use optional + sfwAuthTenant: configService.get(SFW_AUTH_TENANT), + sfwAuthClientId: configService.get(SFW_AUTH_CLIENT), + sfwAuthClientSecret: configService.get(SFW_AUTH_SECRET), + sfwAuthValidity: parseInt(configService.get(SFW_AUTH_VALIDITY)), // in seconds, see spec + vaultRoleId: configService.get(AULT_JWTROLE_IDENTIFIER), + vaultEndpoint: configService.get(VAULT_ENDPOINT), + vaultCA: configService.get(VAULT_CA) + }) as AuthClientOptions, + }), +``` + +When using the ConfigService, make sure that the variables are loaded before accessing them. +This usually works as follows: +``` +export class MyModule implements OnModuleInit { + + + async onModuleInit() { + await ConfigModule.envVariablesLoaded; + } +} +``` + +## Support + +## Stay in touch + +## License + +Proprietary diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9ffd245 --- /dev/null +++ b/package.json @@ -0,0 +1,84 @@ +{ + "name": "@evva/nest-auth-client", + "version": "1.0.0", + "description": "An Auth Service Client for Nest.js", + "main": "dist/index.js", + "types": "dist/types/index.d.ts", + "files": [ + "dist", + "package.json", + "README.md", + "LICENSE" + ], + "author": "EVVA Sicherheitstechnologie GmbH", + "license": "SEE LICENSE IN ", + "repository": { + "type": "git", + "url": "git+https://github.com/evva-sfw/nest-auth-client.git" + }, + "bugs": { + "url": "https://github.com/evva-sfw/nest-auth-client/issues" + }, + "keywords": [ + "nestjs", + "auth", + "client" + ], + "scripts": { + "build": "rimraf dist && tsc -p tsconfig.build.json", + "format": "prettier --write \"src/**/*.ts\"", + "lint": "tslint -p tsconfig.json -c tslint.json", + "fmt": "npx tslint -p tsconfig.json -c tslint.json --fix && npx prettier -- --write", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json", + "prepack": "npx nest build" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0|| ^ 10.0.0", + "@nestjs/core": "^8.0.0|| ^ 10.0.0", + "luxon": "^3.5.0", + "node-vault": "evva-sfw/node-vault#feature/jwt-login" + }, + "devDependencies": { + "@nestjs/common": "^10.4.7", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.4.7", + "@nestjs/testing": "^10.4.7", + "@types/express": "^5.0.0", + "@types/jest": "29.5.14", + "@types/mqtt": "^2.5.0", + "@types/node": "^22.9.0", + "prettier": "3.3.3", + "reflect-metadata": "^0.2.2", + "rimraf": "^6.0.1", + "rxjs": "^7.8.1", + "supertest": "^7.0.0", + "ts-jest": "29.2.5", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "tsc-watch": "6.2.1", + "tsconfig-paths": "4.2.0", + "tslint": "6.1.3", + "typescript": "^5.6.3", + "@release-it/conventional-changelog": "^9.0.3", + "auto-changelog": "^2.5.0", + "release-it": "^17.10.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/src/authclient.constants.ts b/src/authclient.constants.ts new file mode 100644 index 0000000..9b20f1b --- /dev/null +++ b/src/authclient.constants.ts @@ -0,0 +1,8 @@ +export const VAULT_JWTROLE_IDENTIFIER = 'VAULT_JWTROLE_IDENTIFIER'; +export const VAULT_ENDPOINT = 'VAULT_ENDPOINT'; +export const VAULT_CA = 'VAULT_CA'; +export const SFW_AUTH_ENDPOINT = 'SFW_AUTH_ENDPOINT'; +export const SFW_AUTH_TENANT = 'SFW_AUTH_TENANT'; +export const SFW_AUTH_CLIENT = 'SFW_AUTH_CLIENT'; +export const SFW_AUTH_SECRET = 'SFW_AUTH_SECRET'; +export const SFW_AUTH_VALIDITY = 'SFW_AUTH_VALIDITY'; \ No newline at end of file diff --git a/src/authclient.module-definition.ts b/src/authclient.module-definition.ts new file mode 100644 index 0000000..0beedfc --- /dev/null +++ b/src/authclient.module-definition.ts @@ -0,0 +1,5 @@ +import { ConfigurableModuleBuilder } from '@nestjs/common'; +import { AuthClientModuleOptions } from './authclient.module-options'; + +export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = + new ConfigurableModuleBuilder().setClassMethodName('forRoot').build(); \ No newline at end of file diff --git a/src/authclient.module-options.ts b/src/authclient.module-options.ts new file mode 100644 index 0000000..f7ffe00 --- /dev/null +++ b/src/authclient.module-options.ts @@ -0,0 +1,40 @@ + +export interface AuthClientModuleOptions { + + /** + * SFW Auth service Endpoint + */ + sfwAuthEndpoint: string; + /** + * SFW Auth Service tenant + */ + sfwAuthTenant: string; + /** + * SFW Auth Service client identifier + */ + sfwAuthClientId: string; + /** + * SFW Auth Service client secret + */ + sfwAuthClientSecret: string; + /** + * SFW Auth Service requested token validity. + */ + sfwAuthValidity?: number; + + /** + * Vault Service endpoint. + */ + vaultEndpoint?: string; + + /** + * Vault Service CA. + */ + vaultCA?: string; + + /** + * Vault Service role id. + */ + vaultRoleId?: string; + +} diff --git a/src/authclient.module.ts b/src/authclient.module.ts new file mode 100644 index 0000000..de3fa4c --- /dev/null +++ b/src/authclient.module.ts @@ -0,0 +1,15 @@ +import { + Global, + Module, +} from '@nestjs/common'; +import { AuthClientService } from './authclient.service'; +import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } from './authclient.module-definition'; + +@Global() +@Module({ + providers: [AuthClientService], + exports: [AuthClientService, MODULE_OPTIONS_TOKEN], +}) +export class AuthClientModule extends ConfigurableModuleClass { + +} \ No newline at end of file diff --git a/src/authclient.service.spec.ts b/src/authclient.service.spec.ts new file mode 100644 index 0000000..fe30f15 --- /dev/null +++ b/src/authclient.service.spec.ts @@ -0,0 +1,78 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthClientService } from './authclient.service'; +import { AuthClientModule } from './authclient.module'; + + +/** + * HKDF Test vectors + */ +const HKDF_TESTVECTORS = [ + { + algorithm: 'SHA-256', + input: '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', + salt: '000102030405060708090a0b0c', + info: 'f0f1f2f3f4f5f6f7f8f9', + length: 42, + output: + '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865', + }, + { + algorithm: 'SHA-256', + input: + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f', + salt: '606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf', + info: 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', + length: 82, + output: + 'b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87', + }, + { + algorithm: 'SHA-256', + input: '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', + salt: '', + info: '', + length: 42, + output: + '8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8', + }, + { + algorithm: 'SHA-256', + input: '4a1a5bf082915001d09314a7f25d294768be73885b1f9047aaf8b00fbe7f1fef', + salt: 'bfc95280e34add1dd4a8d8114aaad91908ef5b7e90501db032c43a111ef5f948', + info: '', + length: 32, + output: '08c027a0ecc6d04bd3a861f0d5dbe1bdc6eecd541c513ffe8e1cc1c846430023', + }, +]; + + +describe('AuthClientService', () => { + let authClientService: AuthClientService; + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + imports: [AuthClientModule.forRoot({ + sfwAuthEndpoint: 'https://test.service/auth', + sfwAuthTenant: 'test', + sfwAuthClientId: 'test', + sfwAuthClientSecret: 'testsecret' + })], + providers: [AuthClientService], + }).compile(); + authClientService = app.get(AuthClientService); + + }); + + it('#_keyDerive pass tests', async () => { + for (const entry of HKDF_TESTVECTORS) { + const derivedKey = authClientService._keyDerive( + Buffer.from(entry.input, 'hex'), + Buffer.from(entry.salt, 'hex'), + Buffer.from(entry.info, 'hex'), + entry.length, + ); + const actual = derivedKey.toString('hex'); + const expected = String(entry.output); + expect(actual).toEqual(expected); + } + }); +}); diff --git a/src/authclient.service.ts b/src/authclient.service.ts new file mode 100644 index 0000000..701807f --- /dev/null +++ b/src/authclient.service.ts @@ -0,0 +1,153 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { DateTime } from 'luxon'; +import * as crypto from 'node:crypto'; + +import nodeVault from 'node-vault'; +import { MODULE_OPTIONS_TOKEN } from './authclient.module-definition'; +import { AuthClientModuleOptions } from './authclient.module-options'; + +const HKDF_HASH = 'sha256'; +const HKDF_DERIVED_KEY_BYTELENGTH = 32; +const AUTH_TOKEN_SEPARATOR = '-'; + +@Injectable() +export class AuthClientService { + private readonly logger = new Logger('AuthClientService'); + private authToken: string; + private vault: nodeVault.client; + constructor(@Inject(MODULE_OPTIONS_TOKEN) private authClientModuleOptions: AuthClientModuleOptions) { + } + + /** + * Fetch the vault token. + */ + async getVaultToken() { + this.logger.log('getVaultToken'); + if (!this.authToken) { + this.logger.error('Proceeed to SFW Authentication first.'); + } + const vaultRoleId = this.authClientModuleOptions.vaultRoleId; + const vaultEndpoint = this.authClientModuleOptions.vaultEndpoint; + const vaultCA = this.authClientModuleOptions.vaultCA; + + const options = { + apiVersion: 'v1', + endpoint: vaultEndpoint, + requestOptions: { + ca: vaultCA, + }, + }; + const vault = nodeVault(options); + try { + vault.jwtLogin({ + role: vaultRoleId, + jwt: this.authToken, + }); + } catch (e) { + this.logger.error(e); + } + this.vault = vault; + } + + /** + * Fetches the JWT token for M2M authentication. + */ + async fetchSfwAuthToken(): Promise { + const sfwAuthValidity = this.authClientModuleOptions.sfwAuthValidity; + const sfwAuthEndpoint = this.authClientModuleOptions.sfwAuthEndpoint; + const sfwAuthTenant = this.authClientModuleOptions.sfwAuthTenant; + const sfwAuthClientIdentifier = this.authClientModuleOptions.sfwAuthClientId; + const secret = this.authClientModuleOptions.sfwAuthClientSecret; + + this.logger.debug(`fetchSfwAuthToken ${sfwAuthEndpoint}, ${sfwAuthTenant}, ${sfwAuthClientIdentifier}`); + try { + const authToken = this._generateAuthToken(sfwAuthTenant, sfwAuthClientIdentifier, Buffer.from(secret, 'base64')); + const authUrl = `${sfwAuthEndpoint}/auth/2/${sfwAuthTenant}/${sfwAuthClientIdentifier}/${authToken}?validity=${sfwAuthValidity}`; + const response = await fetch(authUrl); + this.authToken = await response.text(); + } catch (e) { + this.logger.error(e); + return 'OBVIOUSLYNOTOKEN'; + } + return this.authToken; + } + + /** + * Getter for MQTT username. + * @returns the username as required for the MQTT endpoint. + */ + getSfwAuthUsername(): string { + const sfwAuthTenant = this.authClientModuleOptions.sfwAuthTenant; + const sfwAuthClientIdentifier = this.authClientModuleOptions.sfwAuthClientId; + + return `sfw-auth/clients/${sfwAuthTenant}/${sfwAuthClientIdentifier}`; + } + + /** + * The vault token must be fetched first with getToken() + * @param {String} path Vault path to secret. For example: secret/infrastructure/services/internal/bamboo/approles/sfw-approle-test + * @async + */ + async getSecret(path) : Promise{ + return new Promise(function (resolve, reject) { + this.vault.read(path) + .then((value) => { + resolve(value.data); + }) + .catch(function (err) { + this.logger.error(JSON.stringify(err)); + reject(); + }); + }.bind(this)); + } + + _keyDerive(key: Buffer, salt: Buffer, info: Buffer, length = HKDF_DERIVED_KEY_BYTELENGTH): Buffer { + // 1. Extract + const extract = crypto.createHmac(HKDF_HASH, salt).update(key).digest(); + + // 2. Expand + const infoLen = info.length; + const steps = Math.ceil(length / 32); + const t = Buffer.alloc(32 * steps + infoLen + 1); + for (let c = 1, start = 0, end = 0; c <= steps; ++c) { + // add info + info.copy(t, end); + // add counter + t[end + infoLen] = c; + + crypto + .createHmac(HKDF_HASH, extract) + // use view: T(C) = T(C-1) | info | C + .update(t.subarray(start, end + infoLen + 1)) + .digest() + // put back to the same buffer + .copy(t, end); + + start = end; // used for T(C-1) start + end += 32; // used for T(C-1) end & overall end + } + return t.subarray(0, length); + } + _generateTimestamp() { + const timestamp = DateTime.now() + .toMillis() + .toString(16) + .padStart(16, '0'); + return timestamp; + } + _generateAuthToken(tenant: string, client: string, secret: Buffer): string { + const timestamp = this._generateTimestamp(); + const salt = crypto.randomBytes(16); + const info = Buffer.from([tenant, client, timestamp].join(''), 'ascii'); + const tokenKey = this._keyDerive( + secret, + salt, + info + ) + .toString('hex') + .toLowerCase(); + + const authToken = [timestamp, salt.toString('hex'), tokenKey].join(AUTH_TOKEN_SEPARATOR); + return authToken; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..b76ad5b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,5 @@ +export * from './authclient.module-options'; +export * from './authclient.module-definition'; +export * from './authclient.constants'; +export * from './authclient.module'; +export * from './authclient.service'; diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..21b99e8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "es2017", + "sourceMap": true, + "outDir": "./dist", + "declarationDir": "./dist/types", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "allowJs": true + } +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..7d1d075 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,15 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "include": [ + ], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } + } + \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..295536a --- /dev/null +++ b/tslint.json @@ -0,0 +1,18 @@ +{ + "defaultSeverity": "error", + "extends": ["tslint:recommended"], + "jsRules": { + "no-unused-expression": true + }, + "rules": { + "quotemark": [true, "single"], + "member-access": [false], + "ordered-imports": [false], + "max-line-length": [true, 150], + "member-ordering": [false], + "interface-name": [false], + "arrow-parens": false, + "object-literal-sort-keys": false + }, + "rulesDirectory": [] + } \ No newline at end of file