feat: initial commit

This commit is contained in:
Krise
2023-08-02 11:42:48 +00:00
parent 91ad3a16b6
commit 681e14ab9f
17 changed files with 2663 additions and 54785 deletions

View File

@@ -8,6 +8,13 @@
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers-contrib/features/redis-homebrew:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"ZixuanChen.vitest-explorer"
]
}
}
// Use 'forwardPorts' to make a list of ports inside the container available locally.

4
.eslintignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
dist/
.pnpm-store/
test/

View File

@@ -1,4 +1,4 @@
module.exports = {
{
"env": {
"browser": false,
"es2021": true
@@ -7,25 +7,17 @@ module.exports = {
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"overrides": [
{
"env": {
"node": true
},
"files": [
".eslintrc.{js,cjs}"
],
"parserOptions": {
"sourceType": "script"
}
}
],
"plugins": [
"@typescript-eslint",
"no-loops"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
},
"rules": {},
"parser": "@typescript-eslint/parser",
}
"root": true
}

View File

@@ -9,15 +9,16 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [ 18 ]
node: [ 18, 20 ]
name: Node ${{ matrix.node }} sample
name: Node ${{ matrix.node }} Release
steps:
- uses: actions/checkout@v3
- name: Run linting rules and tests
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm run lint
- run: npm run test
cache: 'pnpm'
- run: pnpm install
- run: pnpm lint
- run: pnpm test

View File

@@ -5,20 +5,27 @@ on:
branches: [ main ]
jobs:
build:
check:
# The type of runner that the job will run on
runs-on: ubuntu-latest
strategy:
matrix:
node: [ 16, 18, 20 ]
node: [ 18, 20 ]
name: Node ${{ matrix.node }} sample
name: Node ${{ matrix.node }} PR
steps:
- uses: actions/checkout@v3
- name: Run linting rules and tests
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm run lint
- run: npm run test
cache: 'pnpm'
- run: pnpm install
name: install dependencies
- run: pnpm lint
name: linting
- run: pnpm test
name: testing
- name: 'Report Coverage'
if: always() # Also generate the report if tests are failing
uses: davelosert/vitest-coverage-report-action@v2

View File

@@ -26,12 +26,13 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: npm clean-install
run: pnpm install
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
run: npm audit signatures
run: pnpm audit signatures
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
#NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release

2
.gitignore vendored
View File

@@ -12,3 +12,5 @@ uploads/
# Code editor files
.vscode
# Coverage
coverage/

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
# .npmrc
engine-strict=true

8784
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"name": "directus-extension-external-jwt",
"name": "@zerosubnet/directus-extension-external-jwt",
"description": "External JWT Directus Extension allow directus to trust tokens issued by an oauth2 or OIDC provider",
"icon": "extension",
"version": "1.0.0",
@@ -10,17 +10,54 @@
"directus-external-jwt"
],
"type": "module",
"release": {
"branches": [
"main",
"next",
{
"name": "beta",
"prerelease": true
}
]
},
"directus:extension": {
"type": "hook",
"path": "dist/index.js",
"source": "src/index.ts",
"host": "^10.1.7"
},
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"build": "directus-extension build",
"dev": "directus-extension build -w --no-minify",
"link": "directus-extension link",
"directus": "npx directus start"
"directus": "directus start",
"lint": "eslint . --ext .ts",
"test": "vitest",
"test:coverage": "vitest run --coverage"
},
"nyc": {
"extension": [
".ts",
".tsx"
],
"reporter": [
"text",
"lcov"
],
"report-dir": "coverage",
"all": true,
"extends": "@istanbuljs/nyc-config-typescript",
"check-coverage": true,
"include": [
"src/**/*.[tj]s?(x)"
],
"exclude": [
"src/_tests_/**/*.*",
"src/**/*.test.[tj]s?(x)"
]
},
"devDependencies": {
"@directus/errors": "^0.0.2",
@@ -28,31 +65,43 @@
"@directus/tsconfig": "^1.0.0",
"@directus/types": "^10.1.3",
"@directus/utils": "^10.0.8",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@types/chai": "^4.3.5",
"@types/chai-as-promised": "^7.1.5",
"@types/config": "^3.3.0",
"@types/expect": "^24.3.0",
"@types/express": "^4.17.17",
"@types/fs-extra": "^11.0.1",
"@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^9.0.2",
"@types/lodash-es": "^4.17.8",
"@types/mocha": "^10.0.1",
"@types/node": "^20.4.5",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@vitest/coverage-istanbul": "^0.34.1",
"axios": "^1.4.0",
"config": "^3.3.9",
"dotenv": "^16.3.1",
"eslint": "^8.0.1",
"eslint-config-standard-with-typescript": "^37.0.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
"eslint-plugin-no-loops": "^0.3.0",
"eslint-plugin-promise": "^6.0.0",
"fs-extra": "^11.1.1",
"js-yaml": "^4.1.0",
"knex": "^2.5.1",
"lodash-es": "^4.17.21",
"typescript": "^5.1.6"
"nyc": "^15.1.0",
"sqlite3": "^5.1.6",
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.6",
"vitest": "^0.34.1"
},
"dependencies": {
"jsonwebtoken": "^9.0.1",
"jwks-rsa": "^3.0.1",
"openid-client": "^5.4.3",
"sqlite3": "^5.1.6"
"openid-client": "^5.4.3"
}
}
}

2845
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,110 @@
import {verify_token} from './verify-token';
import jwt from 'jsonwebtoken';
import {JwksClient} from 'jwks-rsa';
import { AuthProvider } from './authProvider/get-auth-providers';
import * as crypto from 'crypto';
import { describe, expect, it} from 'vitest'
const jwkJsonString1 = `{"keys":[{"p":"3O0m6YlnSufblB1oCYC8IIZzgx6ZnLhQBk1IYIuWJvP3OU5mKLtHE_l2kgMpM_Y95PUepwDeUdS4bACa156WjCXMBbnbwWRrGNt3uXZKlp0Krq8AH5GI-GYDCK6VE_JnmGimdHQRWrbuKEc6r0uaTJCAIoJOcL81GVLLKQZuvpM","kty":"RSA","q":"yhIy5xsSLda3cpG_L3X1ItYLSTz0i68KDqDSy-p9fq5GRD8-1JSzWa4nuBHDi0Xr6nv-icNFREFwEU9kAIY0DoRwytBvgxKMSt1XEptYi8_FpNqxeY-rEzLVEjGI2YHVMWSXOkFqtNCjAa0ByRzRsSEgmcZubfBiB2tC1yiK4GE","d":"OeEj21lbEYJbTiVDHqkeMaDhI-kMKUEJfT8XOJmaEL_b45Ij1l4eGvYyNiRyygq5lLM4g-tmnwVaPDCb9Nnmdwkwe9PaqQZGfq3ef-s-tlVDEZ2w4CdvmwvIyAqcYvLBF-QPmZy-RXRVYh3sfbJrjB-_RiQ2y3uWv-KQVwkqi8RAkHS-uwYeGyG4QNPU9wSMmyNPnnVFDJE-Q1xHlkIct28eiqdryOqP-NTBH_FC-IJLRk7i2cOnY5KRyR2rjpK56C4CjwhjVyHREsgFSfXfDZrnM6SPXDYerL345kbZ9qZdLUKlLHlKLe0_lC0FwC5ddexZ8nwbJxdi4AqLh1MKAQ","e":"AQAB","use":"sig","kid":"1","qi":"GtHMNkscK5xayJI4nmPH258C-JeOdFKAOQLHq1F73S9SG5XbbgJXnELym75MW0ChnnZSYkwav7FpIhTAdFYDoxLzE69xK6wJaqKgfQ1MP8GU5pNmWXlcr9taHTjp93uE-VC2xlyd5S_HCut1hK2NOhYuMTCp_S2t4WcOeP1kB3A","dp":"BqeGAobG-7ScIov4NEESaZBjLlHfop7Smj39rhrGPQogKjO4VAXAEFP1RFSgCxahqqHPeIxIJgLYQziJcXEva60_xfRhMCQMLcV-h7GOcZbtWXGf-VNy4rh_4uUPTHiCsk6EpQFR_H-CEOiEVf9a-G9pzKBMKI051jduMyAkec0","alg":"RS256","dq":"E2pmO7BtMbxUygxY-11xHVTFptbVhGpgJAGt32v7fOWP2NTe25wiE3bZWCVUzZf9T_1z-papnCJRe0hOioJalB6Dm-klHcn1cugLir0kZ-Kh0fI1ZUG5pVGYCXR6-rMv4dwRb8aDUzZMw0d1SXaca1GMiVn4mFWlhaL3vCaoGWE","n":"rmLVTsXLl0W6tbh1jFY02gu1HHWw8G6RKZ9DmQHR_LoxkIAiXx3mHu68o1ahjCnYozjt6aNKOHGoFHbqgN6FVKOJbVygxnvmEoFU_lNnHYQ0zmQE3tywqKqmIS54HAwZW6u6TMkDgm55WBI_yKAq6pJyXsIbHL6XJ79qCMYdbv9xkwtITqopY4brmStu8kj8LbNeIEwCk3073oG9fE-QF1EM37imIaLIdR06ykpGRSt0l423UUFyNJem-J2j2FHP_UdkUw-ybczVy15hUtPq0m1Vr65urRV60xW_OJ36OrbayrJdoipSFgM_P8zsLqkphm7tavx9LomX1pkbCfTVsw"}]}`
const jwkJsonString2 = `{"keys":[{"p":"yrhAJ_9znp0fTIEuDrPzoHfg64e8iDJz0nrB4c-VZdIMlWTIwHjCOeDQVCav1t2dtP5z6Dge04T0Vo5JABEY_hIy8iGGvq8J4fh20a9OKO00o9quk-BkIJPtqFiokcnQ53Eq4YYuSFhiyKPdipgyvRMmcM9NAOazSr3L-nT608s","kty":"RSA","q":"tXmhxZqKCGdRNIWK0Pyr3_yEhEMuf0jXpMByqSVhgyDLeS2g3pSfOxxTrgPPFgGiiQ1vYHUiCyY7rQMduft4cmiFnaOYeNXGHNY9A43zQjLmlh4V8scR--e_H2IX8v2AgbQ5-P-684mTLGnG2GmX1_U02WgcDnssACpGBWV0PI0","d":"c_egspmdr6FnWepug-i35qAqM4GyQQ8OTf95ZHCyvcAJXMvJud_8soKd6MwxaSLO_pnJhd8OKbgVOBQ6tBMYlygjQDlxjdLIz9r1juABE1HujcPFJMAcE9RoTtJ_kszdMFOEoPTEG2r8wdXEgV5M6I4Da85ss8lmGwDfT6EyUK9tcnmMegUbHGGLDmgQ0ZTLwICKKZCpchDqnMxf2gRZNkbFeQSDLCL0bsBlkqTsu-dNG1lVteFuRQmRvv5wrQN5QzQG0i2MhDkQCm0M8nmg-QU62RLfITXdOJp0tf0eaX91-zF3FVE3letIBETQMNyHOeUL0Z7KK4Dmxo6dbs2cyQ","e":"AQAB","use":"sig","kid":"1","qi":"G7V1Rerlh_siUxRj0I2AY4-ZD5GwLzCuNcoxKRXnmoyQyii-bBqWHxRUY65kraErj1p2ZBYvVtaEp3-MjtBzZxF28mztgt6gpkn-anX8uS6gPawO2g1TGdle9f7oHhaJBzW3kyhxXHtUKTrzMUj9Srx54Ze6r_R5qk8zNKnuopk","dp":"jdFIULMNF7Gj68mThwWtMl2rJBrZcg6ZqG3opSirw4em9fyD1OKmPgdgtv45lX-EjNJWE-bu6drhdIwl1b4gVd41dd6ufUfHCibgOOEDNO59HQQnjZw1b_UNFfCwPQ2K797juNI-Hq52rRa2Lfc7x7pV8iWUIUVDuM3-nUCpGPE","alg":"RS256","dq":"qx0EN5GvI5tfy3k72jDVU38D6L58AlLJ2rQHqYvwtTbgBOPMMvPKbG8aTBOVWTezbS043qezsPWdAVbV2b7O5Hm_u1M9enp_skMkBsz7GWlrWRMHOQMR5weug8X3tQvo9uPcYfen7OjE1_TpJLf0EBJKgdCT2-eyJnm1ynLONiU","n":"j7SWjPUHJ8yRyvdhkmDMsTiVr7IJoGgCgLe-JjiZ193DfOyxmL2p4UxU_xrhXvAecaY1i0ddIVttX0Dy6uY1SRmc9Iu-knc8B75jjwyoXnZq5_B8WHOsPVJpC4T8N1a8rdrqqRXyefxyqnw6fRAz2JLcZa3R_y_x2Sob7qJ8ZolJkHxEfJA4BGOvAYnfoBOKQhXcp_z1YbMuiP3-UB35VjW5chokHH-LIzhZj8Q5TYmK0LIEeZxWN46DnxxJrcqriDLFIf0oVn3aafyHj7OuS6NWCdpzG8sVadOPHLpakoRiM_6-kW7U3N4vaIWZNttCFTgaVR_XbqqCGaiLRsY6zw"}]}`
const jwksJson1 = JSON.parse(jwkJsonString1)
const jwksJson2 = JSON.parse(jwkJsonString2)
const key1 = crypto.createPrivateKey({format: 'jwk', key: JSON.parse(jwkJsonString1).keys[0]})
//const key2 = crypto.createPrivateKey({format: 'jwk', key: JSON.parse(jwkJsonString2).keys[0]})
const jwksClient1 = new JwksClient({
cache: false,
jwksUri: 'https://test.com',
fetcher: () => { return Promise.resolve({keys:[
{
kty: jwksJson1.keys[0].kty,
alg: jwksJson1.keys[0].alg,
kid: jwksJson1.keys[0].kid,
n: jwksJson1.keys[0].n,
e: jwksJson1.keys[0].e,
use: jwksJson1.keys[0].use,
}
]}) },
})
const jwksClient2 = new JwksClient({
cache: false,
jwksUri: 'https://test.com',
fetcher: () => { return Promise.resolve({keys:[
{
kty: jwksJson2.keys[0].kty,
alg: jwksJson2.keys[0].alg,
kid: jwksJson2.keys[0].kid,
n: jwksJson2.keys[0].n,
e: jwksJson2.keys[0].e,
use: jwksJson2.keys[0].use,
}
]}) },
})
describe('verify_token', () => {
it('should reject if JWKSClient is not initialized', () => {
return verify_token({} as AuthProvider, '').catch(err => {
expect(err).to.equal('JWKSClient not initialized');
})
})
it('should reject if token is invalid', () => {
const invalidToken = jwt.sign({foo: 'bar'}, 'invalidkeyforjws', {algorithm: 'HS256'});
const provider: AuthProvider = {
JWKSClient: jwksClient1,
label: 'test',
driver: 'openid',
trusted: true,
issuer_url: 'https://test.com',
name: 'test',
client_id: 'test',
}
return verify_token(provider, invalidToken).then((result) => {
expect(result).to.be.undefined;
}).catch(err => {
expect(err).to.be.instanceOf(jwt.JsonWebTokenError);
})
})
it('should accept if token is valid', async () => {
const validToken = jwt.sign({foo: 'bar'}, key1, {algorithm: 'RS256', keyid: "1"});
const provider: AuthProvider = {
JWKSClient: jwksClient1,
label: 'test',
driver: 'openid',
trusted: true,
issuer_url: 'https://test.com',
name: 'test',
client_id: 'test',
}
return verify_token(provider, validToken).then((result) => {
expect(result).to.be.instanceOf(Object);
})
})
it('should deny if token is signed by wrong issuer', async () => {
const validToken = jwt.sign({foo: 'bar'}, key1, {algorithm: 'RS256', keyid: "1"});
const provider: AuthProvider = {
JWKSClient: jwksClient2,
label: 'test',
driver: 'openid',
trusted: true,
issuer_url: 'https://test.com',
name: 'test',
client_id: 'test',
}
return verify_token(provider, validToken).catch((err) => {
expect(err).to.be.instanceOf(jwt.JsonWebTokenError);
})
})
})

10
src/index.test.ts Normal file
View File

@@ -0,0 +1,10 @@
import { describe, expect, it } from 'vitest'
import {default as index} from '../src/index'
describe('index', () => {
it('should export a function ', () => {
expect(index).to.be.instanceOf(Function);
})
})

View File

@@ -2,8 +2,11 @@ import { defineHook } from '@directus/extensions-sdk';
import { getAccountabilityForToken } from './external-jwt/get-accountability-for-token';
import type { Request } from 'express';
import jwt from 'jsonwebtoken';
import { HookConfig } from '@directus/types';
export default defineHook(({ filter }) => {
export default defineHook<HookConfig>(({ filter }) => {
// get all configuration

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,20 @@
{
"compilerOptions": {
"esModuleInterop": true,
"lib": ["es2020"],
"module": "es2022",
"lib": ["ES2022"],
"module": "ES2022",
"preserveConstEnums": true,
"moduleResolution": "node",
"strict": true,
"sourceMap": true,
"declaration": true,
"noUnusedLocals": true,
"target": "es2022",
"types": ["node"],
"outDir": "dist"
"outDir": "dist",
"typeRoots": ["node_modules/@types"],
"allowSyntheticDefaultImports": true,
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
"include": ["src/**/*.ts"],
"exclude": []
}

13
vitest.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'istanbul', // or 'v8'
reporter: ['text', 'json-summary', 'json'],
all: true,
include: ['src/**'],
reportsDirectory: './coverage',
},
},
})