Compare commits

40 Commits
main ... beta

Author SHA1 Message Date
63a0d5948a fix 🐛: type fix and role assertion
Some checks failed
Release / Release (push) Has been cancelled
2025-06-01 15:58:00 +03:00
bcf9c2b191 fix 🐛: added user roles array to acc
Some checks failed
Release / Release (push) Has been cancelled
2025-06-01 15:51:11 +03:00
7d5abe7ac1 chore 🎫: fetch default role from settings
Some checks failed
Release / Release (push) Has been cancelled
2025-06-01 15:35:06 +03:00
0f12c3b140 chore 🎫: cleanup
Some checks failed
Release / Release (push) Has been cancelled
2025-06-01 15:16:25 +03:00
f2002c2873 feat: add debug log for accountability retrieval in authenticate hook
Some checks failed
Release / Release (push) Has been cancelled
2025-05-28 06:37:50 +03:00
023b00141d fix: ensure user insertion is awaited and add debug log for accountability
Some checks failed
Release / Release (push) Has been cancelled
2025-05-28 06:29:20 +03:00
b573ba8022 feat: update AuthProvider interface and improve user role handling
Some checks failed
Release / Release (push) Has been cancelled
2025-05-28 05:55:10 +03:00
ead3eb030c initial
Some checks failed
Release / Release (push) Has been cancelled
2025-05-27 21:42:25 +03:00
zs-ko
fc08583c54 ci: update node version to 22 2025-02-20 14:11:05 +00:00
zs-ko
368688b559 fix: resolve scurity vulnerabilites 2025-02-20 14:05:38 +00:00
zs-ko
eda8c86177 update sdk 2025-02-20 13:47:33 +00:00
zs-ko
650857e627 ci: add releaserc and set node version to 20 2024-06-05 10:28:49 +00:00
zs-ko
1f17bf0032 ci: use latest semantic-release 2024-06-05 10:05:43 +00:00
zs-ko
1ba1cd94fd chore: add back dockerfile 2024-06-05 10:01:02 +00:00
zs-ko
9e37ad9cb9 fix: patch vite version to resolve CVE-2024-31207 fixes #8 2024-06-05 09:59:33 +00:00
Kristoffer
bad962ffd7 fix: upgrade directus sdk to version 11 (#20)
* fix: bump sdk version

* fix: update directus sdk version to 11.0.7

---------

Co-authored-by: zs-ko <zs-ko@users.noreply.github.com>
2024-06-05 11:52:05 +02:00
Kristoffer
6e23f89430 remove old test credential file 2024-03-10 10:57:15 +00:00
Kristoffer
e52bf62066 fix: add default redis config for testing 2024-03-10 10:56:40 +00:00
Kristoffer
678f79274d fix: upgrade packages to version 10.2.2 2024-03-10 10:56:21 +00:00
Kristoffer
2e976da1d9 Merge branch 'main' into beta 2024-03-09 18:45:32 +01:00
Kristoffer
0af299c8ee Merge branch 'main' into beta 2023-11-15 22:15:45 +01:00
Kristoffer
a49ad5f681 fix: Patch workflow and packages (#15)
* fix: patch vulnerabilities and add arm64 support (#13)

* fix: update vite version for directus/extension (#12)

Co-authored-by: Kristoffer <zs-ko@users.noreply.github.com>

* update lock

* fix: add override for vite dependency

* fix: add error messages on cache

* fix: add blank to username/password if undefined

* fix: add missing configurations to redis. add more catch statemsnts

* doc: add REDIS_JWT_DB

* add arm64

* fix: resolve vulnerabilities in get-func-name,postcss,zod

* fix: update chai

* disable default attestations

* fix: correct image build for arm

* fix: remove quemu

* fix: typo in runner name

* fix runner

* fix: add platform to build and push resolves arm64

* fix: resolve invalid lock file

---------

Co-authored-by: Kristoffer <zs-ko@users.noreply.github.com>

* fix: bump directus sdk (#14)

Co-authored-by: zs-ko <zs-ko@users.noreply.github.com>

* ci: add audit to ci

---------

Co-authored-by: Kristoffer <zs-ko@users.noreply.github.com>
2023-11-15 22:10:57 +01:00
Kristoffer
f219856698 fix: resolve invalid lock file 2023-10-13 18:27:25 +02:00
Kristoffer
d5c5c18996 Merge branch 'main' into beta 2023-10-13 18:26:08 +02:00
Kristoffer
733f1343a6 fix: add platform to build and push resolves arm64 2023-10-13 18:19:18 +02:00
Kristoffer
23fd094c5f fix runner 2023-10-13 18:14:59 +02:00
Kristoffer
f327e73d5f fix: typo in runner name 2023-10-13 18:11:18 +02:00
Kristoffer
5ba0791478 fix: remove quemu 2023-10-13 18:10:29 +02:00
Kristoffer
a6d390bc49 fix: correct image build for arm 2023-10-13 18:08:56 +02:00
Kristoffer
832b381732 disable default attestations 2023-10-13 18:02:10 +02:00
Kristoffer
23c8602a9f fix: update chai 2023-10-13 17:55:30 +02:00
Kristoffer
075f93f550 fix: resolve vulnerabilities in get-func-name,postcss,zod 2023-10-13 17:51:33 +02:00
Kristoffer
a44fb04abe add arm64 2023-10-13 17:25:33 +02:00
Kristoffer
96a8406630 doc: add REDIS_JWT_DB 2023-08-30 14:14:45 +00:00
Kristoffer
d39e228a38 fix: add missing configurations to redis. add more catch statemsnts 2023-08-30 14:12:19 +00:00
Kristoffer
c642b31aef fix: add blank to username/password if undefined 2023-08-30 13:54:13 +00:00
Kristoffer
c23d668e18 fix: add error messages on cache 2023-08-30 13:52:18 +00:00
Kristoffer
9c26e2d666 fix: add override for vite dependency 2023-08-30 13:09:53 +00:00
Kristoffer
3584a57554 update lock 2023-08-30 12:56:14 +00:00
Kristoffer
045e6f2c5f fix: update vite version for directus/extension (#12)
Co-authored-by: Kristoffer <zs-ko@users.noreply.github.com>
2023-08-30 14:54:36 +02:00
25 changed files with 70013 additions and 9406 deletions

View File

@@ -3,11 +3,12 @@
{
"name": "Node.js & TypeScript",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-18-bullseye",
"image": "mcr.microsoft.com/devcontainers/typescript-node:22-bookworm",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers-contrib/features/redis-homebrew:1": {}
"ghcr.io/devcontainers-contrib/features/redis-homebrew:1": {},
"ghcr.io/devcontainers/features/python:1": {}
},
"customizations": {
"vscode": {
@@ -21,7 +22,7 @@
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
//"postCreateCommand": "yarn install",
// Configure tool-specific properties.
// "customizations": {},

View File

@@ -6,6 +6,8 @@ PORT=8055
PUBLIC_URL="http://localhost:8055"
AUTH_PROVIDERS="provider"
AUTH_PROVIDER_CLIENT_ID=YOUR_CLIENT_ID
AUTH_PROVIDER_CLIENT_SECRET=YOUR_CLIENT_SECRET
AUTH_PROVIDER_ISSUER_URL=YOUR_ISSUER_URL
@@ -18,8 +20,8 @@ AUTH_PROVIDER_JWT_USEDB=true
####################################################################################################
### Redis
REDIS_JWT_DB="directus-jwt"
REDIS_DB="directus"
REDIS_JWT_DB="2"
REDIS_DB="1"
REDIS_HOST="localhost"
REDIS_PORT=6379
@@ -31,7 +33,7 @@ REDIS_PORT=6379
# you need to pass to the database instance.
DB_CLIENT="sqlite3"
DB_FILENAME="/workspaces/directus-extension-external-jwt/data.db"
DB_FILENAME="./data.db"
####################################################################################################

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [ 18, 20 ]
node: [ 22 ]
name: Node ${{ matrix.node }} PR
steps:

View File

@@ -39,7 +39,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 22
cache: 'pnpm'
- name: Set up QEMU
@@ -97,4 +97,4 @@ jobs:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
SRCIMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.tags[0] }}
DSTIMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
run: npx semantic-release
run: npx semantic-release@latest

7
.gitignore vendored
View File

@@ -5,7 +5,7 @@ dist
# directus files
*.db
extensions/
extensions/*/**
uploads/
.env
@@ -16,4 +16,7 @@ uploads/
.vscode
# Coverage
coverage/
coverage/
# test secrets
redispass

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

4
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings" defaultProject="true" />
</project>

3
.prettierrc Normal file
View File

@@ -0,0 +1,3 @@
{
}

16
.releaserc.yaml Normal file
View File

@@ -0,0 +1,16 @@
releases:
branches:
- name: main
- name: beta
prerelease: true
plugins:
- "@semantic-release/commit-analyzer"
- "@semantic-release/git":
assets:
- README.md
message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
- "@semantic-release/release-notes-generator":
assets:
- path: dist/*.js
label: JS distribution
- "@semantic-release/github"

View File

@@ -1,26 +1,30 @@
# External JWT Plugin for Directus
## This plugin serves as a way to make Directus trust externally signed JWT tokens from an OIDC or OAuth2 provider.
The plugin expects to resolve the following new configuration option
The plugin expects to resolve the following new configuration options.
The provider must issues Access tokens as JWT since this is used for verification right now. Might add support for general tokens later.
The provider must issue Access tokens as JWT since this is used for verification right now. (Support for general tokens may be added later.)
If USEDB are enabled the extension will try to search for the user in the database by looking at the sub in the JWT token. The user must exists and all roles for that use will be used.
If USEDB is enabled the extension will try to search for the user in the database by looking at the sub in the JWT token. The user must exist and all roles for that user will be used.
When using USEDB you should also enable the caching option to reduce the time spent against the api and reduce the number of db lookups. The cache stores the user object in the cache based on the sub in the token.
When using USEDB you should also enable the caching option to reduce the time spent against the API and reduce the number of DB lookups. The cache stores the user object based on the sub in the token.
USEDB also validates that the issuer is the same as assigned to the user.
USEDB also validates that the issuer is the same as assigned to the user.
## Configuration
all configuration options listed here are an extension to directus default config.
| ENV Variable | Supported values | Description |
|------------------------------|-------------------|--------------|
| AUTH_PROVIDER_TRUSTED | True/False | Must be true for the provider to be considered as trusted. Note, do not trust public providers as these can generate tokens that you cannot control.
| AUTH_PROVIDER_JWT_ROLE_KEY | String | What key in the JWT payload contains the role |
| AUTH_PROVIDER_JWT_ADMIN_KEY | String | What key in the JWT payload contains a bool to grant admin rights |
| AUTH_PROVIDER_JWT_APP_KEY | String | What key in the JWT payload contains a bool to allow app access
| AUTH_PROVIDER_JWT_USEDB | Bool | If enabled/true the plugin will resolve the user and roles from the directus database using the token. For OIDC the sub is used. Should not be used without a Redis Cache enabled.
| CACHE_JWT_NAMESPACE | String | What namespace to use in cache store.
| CACHE_JWT_TTL | Number | Time to live for the cached user entry, default 5000 (5 seconds)
| REDIS_JWT_DB | Number | What database to use in Redis cache, default 2
All configuration options listed here are an extension to Directus' default config.
| ENV Variable | Supported values | Description |
|------------------------------|-------------------------|--------------|
| AUTH_PROVIDER_TRUSTED | `true`/`false` | Must be true for the provider to be considered trusted. **Warning:** Do not trust public providers as they can generate tokens that you cannot control. |
| AUTH_PROVIDER_JWT_ROLE_KEY | String | The key in the JWT payload that contains the role information. |
| AUTH_PROVIDER_JWT_ADMIN_KEY | String | The key in the JWT payload that indicates if admin rights should be granted. |
| AUTH_PROVIDER_JWT_APP_KEY | String | The key in the JWT payload that allows app access if set to true. |
| AUTH_PROVIDER_JWT_USEDB | Boolean | If enabled, the plugin resolves the user and roles from the Directus database using the token (“sub” for OIDC). Should be used only with an enabled Redis Cache. |
| AUTH_PROVIDER_JWKS_URL | String | The URL from which to fetch the JSON Web Key Set (JWKS) for token verification. |
| AUTH_PROVIDER_JWKS_KEYS | JSON | Inline JSON Web Keys for token verification if not using a JWKS URL. |
| CACHE_JWT_NAMESPACE | String | The namespace used in the cache store for JWT-related entries. |
| CACHE_JWT_TTL | Number | Time to live (in milliseconds) for the cached user entry. Default is 5000 (5 seconds). |
| REDIS_JWT_DB | Number | The Redis database number to use for JWT caching. Default is 2. |

11
compose.yml Normal file
View File

@@ -0,0 +1,11 @@
services:
directus:
image: directus/directus:latest
container_name: directus
env_file: .env
ports:
- "8055:8055"
volumes:
- ./.docker/data:/data
- ./dist:/directus/extensions/directus-extension-external-jwt/dist
- ./package.json:/directus/extensions/directus-extension-external-jwt/package.json

View File

@@ -0,0 +1,173 @@
{
"name": "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",
"keywords": [
"directus",
"directus-extension",
"directus-custom-hook",
"directus-external-jwt"
],
"homepage": "https://github.com/Zerosubnet/directus-extension-external-jwt",
"license": "LGPL-3.0-only",
"author": {
"name": "zerosubnet"
},
"repository": {
"type": "git",
"url": "https://github.com/Zerosubnet/directus-extension-external-jwt.git"
},
"type": "module",
"release": {
"branches": [
"main",
"next",
{
"name": "beta",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
[
"@semantic-release/github",
{
"assets": [
"dist/**"
]
}
],
[
"@semantic-release/exec",
{
"tagImage": "docker tag ${SRCIMAGE} ${DSTIMAGE}:${nextRelease.version}",
"publishImage": "docker push ${DSTIMAGE}:${nextRelease.version}"
}
]
],
"preset": "angular"
},
"publishConfig": {
"access": "public"
},
"directus:extension": {
"type": "hook",
"path": "dist/index.js",
"source": "src/index.ts",
"host": "^10.1.7"
},
"engines": {
"node": ">=22.0.0"
},
"scripts": {
"build": "directus-extension build && npm run sync",
"dev": "directus-extension build -w --no-minify",
"link": "directus-extension link",
"sync": "rm -rf ./extensions/directus-extension-external-jwt && mkdir -p ./extensions/directus-extension-external-jwt/dist && ln ./package.json ./extensions/directus-extension-external-jwt/package.json && ln ./dist/index.js ./extensions/directus-extension-external-jwt/dist/index.js",
"directus": "pnpm dlx directus start",
"lint": "eslint . --ext .ts",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"ncu": "npx npm-check-updates --target minor --upgrade --packageFile package.json "
},
"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": "^2.0.1",
"@directus/extensions-sdk": "^13.1.1",
"@directus/tsconfig": "^3.0.0",
"@directus/types": "^13.1.2",
"@directus/utils": "^13.0.6",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/exec": "^7.1.0",
"@semantic-release/github": "^11.0.3",
"@semantic-release/npm": "^12.0.1",
"@types/chai": "^5.2.2",
"@types/chai-as-promised": "^8.0.2",
"@types/config": "^3.3.5",
"@types/express": "^5.0.2",
"@types/fs-extra": "^11.0.4",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.9",
"@types/lodash-es": "^4.17.12",
"@types/mocha": "^10.0.10",
"@types/node": "^22.15.29",
"@typescript-eslint/eslint-plugin": "^8.33.0",
"@vitest/coverage-istanbul": "^3.1.4",
"axios": "^1.9.0",
"config": "^4.0.0",
"dotenv": "^16.5.0",
"eslint": "^9.28.0",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-n": "^17.18.0",
"eslint-plugin-no-loops": "^0.4.0",
"eslint-plugin-promise": "^7.2.1",
"fs-extra": "^11.3.0",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"nyc": "^17.1.0",
"semantic-release": "^24.2.5",
"sqlite3": "^5.1.7",
"ts-mocha": "^11.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3",
"vitest": "^3.1.4"
},
"dependencies": {
"@directus/extensions": "^3.0.6",
"@keyv/redis": "^4.4.0",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.2.0",
"keyv": "^5.3.3",
"openid-client": "^6.5.0",
"uuid": "^11.1.0"
},
"pnpm": {
"overrides": {
"vite@<4.3.9": "^4.3.9",
"vite@>4.3.9": "^4.5.3",
"zod@<=3.22.2": ">=3.22.3",
"axios@<=1.4.0": ">=1.4.1",
"axios@>=1.3.2 <=1.7.3": ">=1.7.4",
"micromatch@<4.0.8": ">=4.0.8",
"vite@>=4.0.0 <4.5.4": ">=4.5.4",
"vite@>=4.0.0 <=4.5.3": ">=4.5.4",
"rollup@>=3.0.0 <3.29.5": ">=3.29.5",
"cross-spawn@>=7.0.0 <7.0.5": ">=7.0.5",
"nanoid@<3.3.8": ">=3.3.8",
"@octokit/request-error@>=1.0.0 <5.1.1": ">=5.1.1",
"@octokit/endpoint@>=9.0.5 <9.0.6": ">=9.0.6",
"@octokit/request@>=1.0.0 <9.2.1": ">=9.2.1",
"@octokit/plugin-paginate-rest@>=1.0.0 <11.4.1": ">=11.4.1",
"serialize-javascript@<6.0.2": ">=6.0.2",
"esbuild@<=0.24.2": ">=0.25.0",
"vite@<=4.5.5": ">=4.5.6"
}
}
}

59603
index.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,155 +1,173 @@
{
"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",
"keywords": [
"directus",
"directus-extension",
"directus-custom-hook",
"directus-external-jwt"
],
"homepage": "https://github.com/Zerosubnet/directus-extension-external-jwt",
"license": "LGPL-3.0-only",
"author": {
"name": "zerosubnet"
},
"repository": {
"type": "git",
"url": "https://github.com/Zerosubnet/directus-extension-external-jwt.git"
},
"type": "module",
"release": {
"branches": [
"main",
"next",
{
"name": "beta",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
[
"@semantic-release/github",
{
"assets": [
"dist/**"
]
}
],
[
"@semantic-release/exec",
{
"tagImage": "docker tag ${SRCIMAGE} ${DSTIMAGE}:${nextRelease.version}",
"publishImage": "docker push ${DSTIMAGE}:${nextRelease.version}"
}
]
],
"preset": "angular"
},
"publishConfig": {
"access": "public"
},
"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",
"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",
"@directus/extensions-sdk": "^10.2.0",
"@directus/tsconfig": "^1.0.1",
"@directus/types": "^10.1.6",
"@directus/utils": "^10.0.11",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^10.0.4",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/github": "^9.2.3",
"@semantic-release/npm": "^10.0.6",
"@types/chai": "^4.3.10",
"@types/chai-as-promised": "^7.1.8",
"@types/config": "^3.3.3",
"@types/express": "^4.17.21",
"@types/fs-extra": "^11.0.4",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.5",
"@types/lodash-es": "^4.17.11",
"@types/mocha": "^10.0.4",
"@types/node": "^20.9.1",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@vitest/coverage-istanbul": "^0.34.6",
"axios": "^1.6.2",
"config": "^3.3.9",
"dotenv": "^16.3.1",
"eslint": "^8.53.0",
"eslint-config-standard-with-typescript": "^37.0.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-no-loops": "^0.3.0",
"eslint-plugin-promise": "^6.1.1",
"fs-extra": "^11.1.1",
"js-yaml": "^4.1.0",
"knex": "^2.5.1",
"lodash-es": "^4.17.21",
"nyc": "^15.1.0",
"semantic-release": "^21.1.2",
"sqlite3": "^5.1.6",
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"vitest": "^0.34.6"
},
"dependencies": {
"@keyv/redis": "^2.8.0",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
"keyv": "^4.5.4",
"openid-client": "^5.6.1"
},
"pnpm": {
"overrides": {
"vite@<4.3.9": "^4.3.9",
"zod@<=3.22.2": ">=3.22.3",
"axios@<=1.4.0": ">=1.4.1"
}
}
}
"name": "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",
"keywords": [
"directus",
"directus-extension",
"directus-custom-hook",
"directus-external-jwt"
],
"homepage": "https://github.com/Zerosubnet/directus-extension-external-jwt",
"license": "LGPL-3.0-only",
"author": {
"name": "zerosubnet"
},
"repository": {
"type": "git",
"url": "https://github.com/Zerosubnet/directus-extension-external-jwt.git"
},
"type": "module",
"release": {
"branches": [
"main",
"next",
{
"name": "beta",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
[
"@semantic-release/github",
{
"assets": [
"dist/**"
]
}
],
[
"@semantic-release/exec",
{
"tagImage": "docker tag ${SRCIMAGE} ${DSTIMAGE}:${nextRelease.version}",
"publishImage": "docker push ${DSTIMAGE}:${nextRelease.version}"
}
]
],
"preset": "angular"
},
"publishConfig": {
"access": "public"
},
"directus:extension": {
"type": "hook",
"path": "dist/index.js",
"source": "src/index.ts",
"host": "^10.1.7"
},
"engines": {
"node": ">=22.0.0"
},
"scripts": {
"build": "directus-extension build && npm run sync",
"dev": "directus-extension build -w --no-minify",
"link": "directus-extension link",
"sync": "rm -rf ./extensions/directus-extension-external-jwt && mkdir -p ./extensions/directus-extension-external-jwt/dist && ln ./package.json ./extensions/directus-extension-external-jwt/package.json && ln ./dist/index.js ./extensions/directus-extension-external-jwt/dist/index.js",
"directus": "pnpm dlx directus start",
"lint": "eslint . --ext .ts",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"ncu": "npx npm-check-updates --target minor --upgrade --packageFile package.json "
},
"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": "^2.0.1",
"@directus/extensions-sdk": "^13.1.1",
"@directus/tsconfig": "^3.0.0",
"@directus/types": "^13.1.2",
"@directus/utils": "^13.0.6",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/exec": "^7.1.0",
"@semantic-release/github": "^11.0.3",
"@semantic-release/npm": "^12.0.1",
"@types/chai": "^5.2.2",
"@types/chai-as-promised": "^8.0.2",
"@types/config": "^3.3.5",
"@types/express": "^5.0.2",
"@types/fs-extra": "^11.0.4",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.9",
"@types/lodash-es": "^4.17.12",
"@types/mocha": "^10.0.10",
"@types/node": "^22.15.29",
"@typescript-eslint/eslint-plugin": "^8.33.0",
"@vitest/coverage-istanbul": "^3.1.4",
"axios": "^1.9.0",
"config": "^4.0.0",
"dotenv": "^16.5.0",
"eslint": "^9.28.0",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-n": "^17.18.0",
"eslint-plugin-no-loops": "^0.4.0",
"eslint-plugin-promise": "^7.2.1",
"fs-extra": "^11.3.0",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"nyc": "^17.1.0",
"semantic-release": "^24.2.5",
"sqlite3": "^5.1.7",
"ts-mocha": "^11.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3",
"vitest": "^3.1.4"
},
"dependencies": {
"@directus/extensions": "^3.0.6",
"@keyv/redis": "^4.4.0",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.2.0",
"keyv": "^5.3.3",
"openid-client": "^6.5.0",
"uuid": "^11.1.0"
},
"pnpm": {
"overrides": {
"vite@<4.3.9": "^4.3.9",
"vite@>4.3.9": "^4.5.3",
"zod@<=3.22.2": ">=3.22.3",
"axios@<=1.4.0": ">=1.4.1",
"axios@>=1.3.2 <=1.7.3": ">=1.7.4",
"micromatch@<4.0.8": ">=4.0.8",
"vite@>=4.0.0 <4.5.4": ">=4.5.4",
"vite@>=4.0.0 <=4.5.3": ">=4.5.4",
"rollup@>=3.0.0 <3.29.5": ">=3.29.5",
"cross-spawn@>=7.0.0 <7.0.5": ">=7.0.5",
"nanoid@<3.3.8": ">=3.3.8",
"@octokit/request-error@>=1.0.0 <5.1.1": ">=5.1.1",
"@octokit/endpoint@>=9.0.5 <9.0.6": ">=9.0.6",
"@octokit/request@>=1.0.0 <9.2.1": ">=9.2.1",
"@octokit/plugin-paginate-rest@>=1.0.0 <11.4.1": ">=11.4.1",
"serialize-javascript@<6.0.2": ">=6.0.2",
"esbuild@<=0.24.2": ">=0.25.0",
"vite@<=4.5.5": ">=4.5.6"
}
}
}

8909
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
redis.conf Normal file
View File

@@ -0,0 +1,6 @@
bind 127.0.0.1 -::1
protected-mode yes
port 6379
tcp-keepalive 300
requirepass asd@123

View File

@@ -1 +0,0 @@
asd

View File

@@ -1,141 +1,149 @@
import { toArray } from '@directus/utils';
import {JwksClient} from 'jwks-rsa';
import { toArray } from "@directus/utils";
import { JwksClient } from "jwks-rsa";
import { Issuer } from 'openid-client';
import { discovery } from "openid-client";
import env from '../config/config';
import { createError } from '@directus/errors';
import env from "../config/config";
import { createError } from "@directus/errors";
const InvalidJWKIssuerMetadata = createError('INVALID_JWKS_ISSUER_ERROR', 'No JWKS_URL or JWKS_KEYS and could not discover JWKS_URL from openid metadata', 500);
const InvalidJWKSUrl = createError('INVALID_JWKS_ISSUER_ERROR', 'Could not retrieve any valid keys from JWKS_URL', 500);
const InvalidJWKKeys = createError('INVALID_JWKS_ISSUER_ERROR', 'No signing keys in response from provider')
const InvalidJWKIssuerMetadata = createError("INVALID_JWKS_ISSUER_ERROR", "No JWKS_URL or JWKS_KEYS and could not discover JWKS_URL from openid metadata", 500);
const InvalidJWKSUrl = createError("INVALID_JWKS_ISSUER_ERROR", "Could not retrieve any valid keys from JWKS_URL", 500);
const InvalidJWKKeys = createError("INVALID_JWKS_ISSUER_ERROR", "No signing keys in response from provider");
export interface AuthProvider {
label: string;
name: string;
driver: string;
icon?: string;
client_id: string;
client_secret?: string;
trusted: boolean;
jwks_url?: string;
jwks_keys?: string;
issuer_url?: string;
admin_key?: string;
app_key?: string;
role_key?: string;
JWKSClient?: JwksClient;
use_database?: boolean;
}
label: string;
name: string;
driver: string;
icon?: string;
client_id: string;
client_secret?: string;
trusted: boolean;
jwks_url?: string;
jwks_keys?: string;
issuer_url?: string;
admin_key?: string;
app_key?: string;
role_key?: string;
JWKSClient?: JwksClient;
use_database?: boolean;
default_role_id?: string;
}
export async function getAuthProviders(): Promise<AuthProvider[]> {
console.log("calling auth providers")
return new Promise((resolve, reject) => {
const authProviders: AuthProvider[] = toArray(env['AUTH_PROVIDERS'])
.filter((provider) => provider && env[`AUTH_${provider.toUpperCase()}_DRIVER`] === ('openid' || 'oauth2'))
.map((provider) => ({
name: provider,
label: env[`AUTH_${provider.toUpperCase()}_LABEL`],
driver: env[`AUTH_${provider.toUpperCase()}_DRIVER`],
icon: env[`AUTH_${provider.toUpperCase()}_ICON`],
trusted: env[`AUTH_${provider.toUpperCase()}_TRUSTED`],
jwks_url: env[`AUTH_${provider.toUpperCase()}_JWKS_URL`],
jwks_keys: env[`AUTH_${provider.toUpperCase()}_JWKS_KEYS`],
issuer_url: env[`AUTH_${provider.toUpperCase()}_ISSUER_URL`],
admin_key: env[`AUTH_${provider.toUpperCase()}_JWT_ADMIN_KEY`],
app_key: env[`AUTH_${provider.toUpperCase()}_JWT_APP_KEY`],
role_key: env[`AUTH_${provider.toUpperCase()}_JWT_ROLE_KEY`],
client_id: env[`AUTH_${provider.toUpperCase()}_CLIENT_ID`],
client_secret: env[`AUTH_${provider.toUpperCase()}_CLIENT_SECRET`],
use_database: env[`AUTH_${provider.toUpperCase()}_JWT_USEDB`],
}));
console.log("calling auth providers _");
return new Promise((resolve, reject) => {
const authProviders: AuthProvider[] = toArray(env["AUTH_PROVIDERS"])
.filter(
(provider) =>
provider &&
["openid", "oauth2"].includes(env[`AUTH_${provider.toUpperCase()}_DRIVER`])
)
.map((provider) => ({
name: provider,
label: env[`AUTH_${provider.toUpperCase()}_LABEL`],
driver: env[`AUTH_${provider.toUpperCase()}_DRIVER`],
icon: env[`AUTH_${provider.toUpperCase()}_ICON`],
trusted: env[`AUTH_${provider.toUpperCase()}_TRUSTED`],
jwks_url: env[`AUTH_${provider.toUpperCase()}_JWKS_URL`],
jwks_keys: env[`AUTH_${provider.toUpperCase()}_JWKS_KEYS`],
issuer_url: env[`AUTH_${provider.toUpperCase()}_ISSUER_URL`],
admin_key: env[`AUTH_${provider.toUpperCase()}_JWT_ADMIN_KEY`],
app_key: env[`AUTH_${provider.toUpperCase()}_JWT_APP_KEY`],
role_key: env[`AUTH_${provider.toUpperCase()}_JWT_ROLE_KEY`],
client_id: env[`AUTH_${provider.toUpperCase()}_CLIENT_ID`],
client_secret: env[`AUTH_${provider.toUpperCase()}_CLIENT_SECRET`],
use_database: env[`AUTH_${provider.toUpperCase()}_JWT_USEDB`],
if(authProviders.length === 0) return resolve([]);
default_role_id: env[`AUTH_${provider.toUpperCase()}_DEFAULT_ROLE_ID`]
}));
const promises = [];
if (authProviders.length === 0) return resolve([]);
for (const authProvider of authProviders) {
switch (authProvider.driver) {
case 'openid':
if (!authProvider.trusted || (authProvider.issuer_url == null && authProvider.jwks_url == null && authProvider.jwks_keys == null)) break;
//promises.push(getJWKS(authProvider.issuer_url, authProvider.jwks_url, authProvider.jwks_keys));
promises.push(getJWKS(authProvider));
break;
case 'oauth2':
if (!authProvider.trusted || (authProvider.issuer_url == null && authProvider.jwks_url == null && authProvider.jwks_keys == null)) break;
//promises.push(getJWKS(authProvider.issuer_url, authProvider.jwks_url, authProvider.jwks_keys));
promises.push(getJWKS(authProvider));
break;
}
}
Promise.all(promises).then((values) => {
resolve(values);
}).catch((error) => {
reject(error);
})
const promises = [];
});
for (const authProvider of authProviders) {
switch (authProvider.driver) {
case "openid":
if (!authProvider.trusted || (authProvider.issuer_url == null && authProvider.jwks_url == null && authProvider.jwks_keys == null)) break;
//promises.push(getJWKS(authProvider.issuer_url, authProvider.jwks_url, authProvider.jwks_keys));
promises.push(getJWKS(authProvider));
break;
case "oauth2":
if (!authProvider.trusted || (authProvider.issuer_url == null && authProvider.jwks_url == null && authProvider.jwks_keys == null)) break;
//promises.push(getJWKS(authProvider.issuer_url, authProvider.jwks_url, authProvider.jwks_keys));
promises.push(getJWKS(authProvider));
break;
}
}
Promise.all(promises).then((values) => {
console.log("resolved auth providers", values);
resolve(values);
}).catch((error) => {
reject(error);
});
});
}
async function getJWKS(provider: AuthProvider) {
if(provider.jwks_keys !== undefined && provider.issuer_url == null && provider.jwks_url == null) {
const jwks_keys = JSON.parse(provider.jwks_keys);
const jwksClient = new JwksClient({
getKeysInterceptor: () => {
return jwks_keys;
},
jwksUri: ''
})
if (provider.jwks_keys !== undefined && provider.issuer_url == null && provider.jwks_url == null) {
const jwks_keys = JSON.parse(provider.jwks_keys);
const jwksClient = new JwksClient({
getKeysInterceptor: () => {
return jwks_keys;
},
jwksUri: ""
});
provider.JWKSClient = jwksClient;
}
if(provider.issuer_url && !provider.jwks_url) {
//try to discover with openid
const issuer = await Issuer.discover(provider.issuer_url);
if(issuer.metadata.jwks_uri != null) {
provider.jwks_url = issuer.metadata.jwks_uri;
}
}
provider.JWKSClient = jwksClient;
if (provider.jwks_url == null) throw new InvalidJWKIssuerMetadata();
}
const jwksClient = await getJWKSClient(provider.jwks_url);
if (provider.issuer_url && !provider.jwks_url) {
//try to discover with openid
const issuer = await discovery(new URL(provider.issuer_url), provider.client_id);
if (issuer.serverMetadata().jwks_uri != null) {
provider.jwks_url = issuer.serverMetadata().jwks_uri;
}
}
provider.JWKSClient = jwksClient;
return provider;
if (provider.jwks_url == null) throw new InvalidJWKIssuerMetadata();
const jwksClient = await getJWKSClient(provider.jwks_url);
provider.JWKSClient = jwksClient;
return provider;
}
async function getJWKSClient(url: string) {
const jwksClient = new JwksClient({
jwksUri: url,
cache: true,
cacheMaxAge: 36000000, // 10 hours
cacheMaxEntries: 10,
timeout: 30000, // 30 seconds
});
const jwksClient = new JwksClient({
jwksUri: url,
cache: true,
cacheMaxAge: 36000000, // 10 hours
cacheMaxEntries: 10,
timeout: 30000 // 30 seconds
});
// try to get the keys
try {
const keys = await jwksClient.getSigningKeys()
if (keys.length == 0) {
throw new InvalidJWKKeys();
}
} catch (error) {
throw new InvalidJWKSUrl();
}
// try to get the keys
try {
const keys = await jwksClient.getSigningKeys();
if (keys.length == 0) {
throw new InvalidJWKKeys();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
throw new InvalidJWKSUrl();
}
return jwksClient;
}
return jwksClient;
}

View File

@@ -1,75 +1,64 @@
import {default as Keyv, Store} from 'keyv';
import env from './config/config';
import {default as KeyvRedis} from '@keyv/redis';
// check if redis is defined
import { default as Keyv } from "keyv";
import env from "./config/config";
import { default as KeyvRedis } from "@keyv/redis";
const cache: Keyv | null = getCache();
function getCache(): Keyv | null {
if(env['CACHE_ENABLED'] !== true) return null;
if (env["CACHE_ENABLED"] !== true) return null;
// check namespace
let namespace = env['CACHE_JWT_NAMESPACE'];
if(namespace == null || namespace === '') {
namespace = 'exjwt';
}
let namespace = env["CACHE_JWT_NAMESPACE"];
if (namespace == null || namespace === "") {
namespace = "exjwt";
}
let ttl = env['CACHE_JWT_TTL'];
if (ttl == null || ttl === '') {
ttl = 5000
}
let ttl = env["CACHE_JWT_TTL"];
if (ttl == null || ttl === "") {
ttl = 5000;
}
let uri = '';
let store: Store<string | undefined> | undefined = undefined;
if(env['CACHE_STORE'] === 'redis') {
uri = env['REDIS']
if(uri == null || uri === '') {
uri = `redis://${env['REDIS_USERNAME'] || '' }:${env['REDIS_PASSWORD'] || ''}@${env['REDIS_HOST']}:${env['REDIS_PORT'] || '6379'} /${env['REDIS_JWT_DB'] || '2'}`;
}
let uri = "";
let store: KeyvRedis<string | undefined> | undefined = undefined;
if (env["CACHE_STORE"] === "redis") {
uri = env["REDIS"];
try {
store = new KeyvRedis(uri);
} catch(e) {
throw new Error("CACHE: could not connect to database: " + e)
}
if (uri == null || uri === "") {
uri = `redis://${env["REDIS_USERNAME"] || ""}:${env["REDIS_PASSWORD"] || ""}@${env["REDIS_HOST"]}:${env["REDIS_PORT"] || "6379"} /${env["REDIS_JWT_DB"] || "2"}`;
}
try {
const keyv = new Keyv(uri, {
namespace: namespace,
ttl,
store: store
});
keyv.on('error', (err) => {
throw new Error("CACHE: could not connect: " + err)
});
return keyv
} catch(e) {
throw new Error("CACHE: could not connect to database: " + e)
store = new KeyvRedis(uri);
} catch (e) {
throw new Error("CACHE: could not connect to database: " + e);
}
}
try {
const keyv = new Keyv(store, {
namespace: namespace,
ttl
});
keyv.on("error", (err) => {
throw new Error("CACHE: could not connect: " + err);
});
return keyv;
} catch (e) {
throw new Error("CACHE: could not connect to database: " + e);
}
}
export function CacheEnabled(): boolean {
return cache !== null;
return cache !== null;
}
export async function CacheSet(key: string, value: any) {
if(cache === null) return false;
return cache.set(key, value);
export async function CacheSet(key: string, value: unknown) {
if (cache === null) return false;
return cache.set(key, value);
}
export async function CacheGet(key: string) {
if(cache === null) return null;
return cache.get(key);
}
if (cache === null) return null;
return cache.get(key);
}

View File

@@ -303,4 +303,4 @@ function tryJSON(value: string) {
} catch {
return value;
}
}
}

View File

@@ -1,12 +1,21 @@
import type { Accountability } from '@directus/types';
import { getAuthProviders } from './authProvider/get-auth-providers.js';
import type { Knex } from 'knex';
import { verify_token } from './verify-token.js';
import { CacheEnabled, CacheGet, CacheSet } from './cache.js';
import type { Accountability, Policy, User } from "@directus/types";
import { getAuthProviders } from "./authProvider/get-auth-providers.js";
import { verify_token } from "./verify-token.js";
import { CacheEnabled, CacheGet, CacheSet } from "./cache.js";
import type { Knex } from "knex";
import * as uuid from "uuid";
// Instead of using top-level await, declare a variable for providers
let authProviders: Awaited<ReturnType<typeof getAuthProviders>>;
const authProviders = await getAuthProviders();
// Initialize providers function to be called at runtime
const initAuthProviders = async () => {
if (!authProviders) {
authProviders = await getAuthProviders();
}
return authProviders;
};
/*
const MissingJWTHeaderError = createError('INVALID_JWKS_ISSUER_ERROR', 'No header in JWT Token', 500);
@@ -14,111 +23,160 @@ const NoValidKeysError = createError('INVALID_JWKS_ISSUER_ERROR', 'could not ret
const NoAuthProvidersError = createError('INVALID_JWKS_ISSUER_ERROR', 'No auth providers in the list', 500);
*/
const getUser = async (
database: Knex,
externalIdentifier: string | undefined,
provider: string
): Promise<User & Policy> => {
return database
.select(
"directus_users.id",
"directus_users.role",
"directus_policies.admin_access"
)
.from("directus_users")
.leftJoin("directus_roles", "directus_users.role", "directus_roles.id")
.leftJoin("directus_access", "directus_users.role", "directus_access.role")
.leftJoin(
"directus_policies",
"directus_access.policy",
"directus_policies.id"
)
.where({
"directus_users.external_identifier": externalIdentifier,
"directus_users.provider": provider
})
.first();
};
const insertUser = async (database: Knex, user: Record<string, string | undefined>) => {
await database("directus_users").insert(user);
return getUser(database, user.external_identifier, user.provider!);
};
const getSettings = async (database: Knex) => {
return database
.select(
"directus_settings.public_registration_role"
)
.from("directus_settings")
.first();
};
// TODO: optimize this function, reduce the amount of loops
export async function getAccountabilityForToken(
token: string | null,
iss: string[] | string | undefined,
accountability: Accountability | null,
database: Knex
token: string | null,
iss: string[] | string | undefined,
accountability: Accountability | null,
database: Knex,
options: { ip: string | null, userAgent?: string }
): Promise<Accountability> {
if (accountability == null) {
accountability = {
user: null,
role: null,
admin: false,
app: false,
};
}
if (accountability == null) {
accountability = {
user: null,
role: null,
admin: false,
app: false,
roles: [],
ip: options.ip,
userAgent: options.userAgent
};
}
if (token == null || iss == null) {
return accountability
}
const providers = authProviders.filter((provider) => provider.issuer_url && iss.includes(provider.issuer_url));
if(providers.length === 0) return accountability;
if(providers.length > 1) {
return accountability;
}
if (token == null || iss == null) {
return accountability;
}
const provider = providers[0];
const providers = (await initAuthProviders()).filter(
(provider) =>
provider.issuer_url && provider.issuer_url.includes(iss.toString())
);
if (providers.length === 0) return accountability;
if (providers.length > 1) {
return accountability;
}
try {
const provider = providers[0];
const result = await verify_token(provider, token)
try {
const result = await verify_token(provider, token);
if(provider.use_database) { // use database to get user
// TODO: Add caching to this function
if (CacheEnabled() && result.sub) {
const cachedAccountability = await CacheGet(result.sub);
if (cachedAccountability) {
return cachedAccountability;
}
}
if (provider.use_database) {
// use database to get user
// TODO: Add caching to this function
if (CacheEnabled() && result.sub) {
const cachedAccountability = await CacheGet(result.sub);
if (cachedAccountability) {
return cachedAccountability;
}
}
const user = await database
.select('directus_users.id', 'directus_users.role', 'directus_roles.admin_access', 'directus_roles.app_access')
.from('directus_users')
.leftJoin('directus_roles', 'directus_users.role', 'directus_roles.id')
.where({
'directus_users.external_identifier': result.sub,
'directus_users.provider': provider.name,
})
.first();
if(!user) {
return accountability;
}
try {
let user = await getUser(database, result.sub, provider.name);
accountability.user = user.id;
accountability.role = user.role;
accountability.admin = user.admin_access === true || user.admin_access == 1;
accountability.app = user.app_access === true || user.app_access == 1;
if (!user) {
const settings = await getSettings(database);
console.debug("Settings for public registration:", settings);
user = await insertUser(database, {
id: uuid.v4(),
role: settings.public_registration_role || provider.default_role_id,
provider: provider.name,
external_identifier: result.sub
});
console.debug("Inserted new user:", user);
}
if (CacheEnabled() && result.sub) {
CacheSet(result.sub, accountability);
}
if (user) {
const userRoleId = user.role?.id || user.role as unknown as string;
accountability.user = user.id;
accountability.role = userRoleId;
accountability.roles = [userRoleId];
accountability.admin = user.admin_access;
accountability.app = user.app_access;
return accountability;
}
if (CacheEnabled() && result.sub) {
await CacheSet(result.sub, accountability);
}
// check if role key is set else try role key
if(provider.role_key != null) {
if(typeof result[provider.role_key] === 'string') {
accountability.role = result[provider.role_key];
}
if(typeof result[provider.role_key] === 'object') {
accountability.role = ''
}
if(result[provider.role_key].instanceOf(Array)) {
accountability.role = result[provider.role_key][0];
}
}
console.debug("Accountability set from database:", accountability);
return accountability;
}
} catch (error) {
console.error("Error getting user from database:", error);
return accountability;
}
}
if(provider.admin_key != null) {
accountability.admin = result[provider.admin_key];
}
if(provider.app_key != null) {
accountability.app = result[provider.app_key];
}
accountability.user = result.sub;
} catch (error) {
return accountability;
}
return accountability;
// check if role key is set else try role key
if (provider.role_key != null) {
if (typeof result[provider.role_key] === "string") {
accountability.role = result[provider.role_key];
}
if (typeof result[provider.role_key] === "object") {
accountability.role = "";
}
if (result[provider.role_key].instanceOf(Array)) {
accountability.role = result[provider.role_key][0];
}
}
}
// if (provider.admin_key != null) {
// accountability.admin = result[provider.admin_key];
// }
// if (provider.app_key != null) {
// accountability.app = result[provider.app_key];
// }
// accountability.user = result.sub;
// accountability.role = "d737d4bd-ae35-4a68-a907-e913bcdfcc53";
// accountability.admin = true;
// accountability.app = true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
return accountability;
}
return accountability;
}

View File

@@ -1,40 +1,28 @@
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';
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 type { Accountability, EventContext } from "@directus/types";
export default defineHook(({ filter }) => {
filter("authenticate", (defaultAccountability: Accountability, event, context: EventContext) => {
const req = <Request>event["req"];
if (!req.token) return defaultAccountability;
if (!context.database) {
return defaultAccountability;
}
export default defineHook<HookConfig>(({ filter }) => {
// get all configuration
filter('authenticate', (defaultAccountability, event, context) => {
const req = <Request>event['req'];
if(!req.token) return defaultAccountability;
const decodedToken = jwt.decode(req.token);
if(!context.database) {
return defaultAccountability
}
const decodedToken = jwt.decode(req.token);
if(typeof decodedToken === 'string' || decodedToken == null) return defaultAccountability; // if token is not a jwt, let directus handle it
if(decodedToken?.iss == 'directus') return defaultAccountability; // if token issued by directus, let directus handle it
return getAccountabilityForToken(req.token, decodedToken?.iss, context.accountability, context.database)
});
/*filter('auth.jwt', (status, user, provider) => {
})*/
if (typeof decodedToken === "string" || decodedToken == null) return defaultAccountability; // if token is not a jwt, let directus handle it
if (decodedToken?.iss == "directus") return defaultAccountability; // if token issued by directus, let directus handle it
return getAccountabilityForToken(req.token, decodedToken?.iss, context.accountability, context.database, {
ip: req.ip || null,
userAgent: req.headers["user-agent"]
});
});
});

View File

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

197
yarn-error.log Normal file
View File

@@ -0,0 +1,197 @@
Arguments:
/Users/ian/.nvm/versions/node/v20.18.0/bin/node /usr/local/bin/yarn
PATH:
/opt/homebrew/opt/openjdk@17/bin:/Users/ian/.nvm/versions/node/v20.18.0/bin:/Users/ian/Library/pnpm:/Users/ian/.bun/bin:/Library/Frameworks/Python.framework/Versions/3.7/bin:/Library/Frameworks/Python.framework/Versions/3.9/bin:/Library/Frameworks/Python.framework/Versions/3.12/bin:/Library/Frameworks/Python.framework/Versions/2.7/bin:/Library/Frameworks/Python.framework/Versions/3.10/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Applications/VMware Fusion.app/Contents/Public:/Users/ian/development/repos/directus-extension-external-jwt/node_modules/.bin:/Users/ian/.orbstack/bin:/platform-tools:/emulator:/platform-tools:/opt/homebrew/bin:/opt/homebrew/sbin:/Users/ian/go/bin/:/Users/ian/Library/pnpm:/Users/ian/Library/Android/sdk/emulator:/Users/ian/Library/Android/sdk/platform-tools:/Users/ian/Library/Python/3.7/bin:/Users/ian/.dotnet/tools:/Users/ian/.yarn/bin
Yarn version:
1.22.19
Node version:
20.18.0
Platform:
darwin arm64
Trace:
Error: EEXIST: file already exists, mkdir '/Users/ian/development/repos/directus-extension-external-jwt/node_modules/@directus/extensions-sdk/node_modules/@directus'
npm manifest:
{
"name": "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",
"keywords": [
"directus",
"directus-extension",
"directus-custom-hook",
"directus-external-jwt"
],
"homepage": "https://github.com/Zerosubnet/directus-extension-external-jwt",
"license": "LGPL-3.0-only",
"author": {
"name": "zerosubnet"
},
"repository": {
"type": "git",
"url": "https://github.com/Zerosubnet/directus-extension-external-jwt.git"
},
"type": "module",
"release": {
"branches": [
"main",
"next",
{
"name": "beta",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
[
"@semantic-release/github",
{
"assets": [
"dist/**"
]
}
],
[
"@semantic-release/exec",
{
"tagImage": "docker tag ${SRCIMAGE} ${DSTIMAGE}:${nextRelease.version}",
"publishImage": "docker push ${DSTIMAGE}:${nextRelease.version}"
}
]
],
"preset": "angular"
},
"publishConfig": {
"access": "public"
},
"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 && npm run sync",
"dev": "directus-extension build -w --no-minify",
"link": "directus-extension link",
"sync": "rm -rf ./extensions/directus-extension-external-jwt && mkdir -p ./extensions/directus-extension-external-jwt/dist && ln ./package.json ./extensions/directus-extension-external-jwt/package.json && ln ./dist/index.js ./extensions/directus-extension-external-jwt/dist/index.js",
"directus": "pnpm dlx 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.3.2",
"@directus/extensions-sdk": "^13.0.1",
"@directus/tsconfig": "^1.0.1",
"@directus/types": "^11.1.2",
"@directus/utils": "^11.0.9",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^10.0.4",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/github": "^9.2.6",
"@semantic-release/npm": "^10.0.6",
"@types/chai": "^4.3.16",
"@types/chai-as-promised": "^7.1.8",
"@types/config": "^3.3.4",
"@types/express": "^4.17.21",
"@types/fs-extra": "^11.0.4",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.6",
"@types/lodash-es": "^4.17.12",
"@types/mocha": "^10.0.6",
"@types/node": "^20.14.2",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@vitest/coverage-istanbul": "^0.34.6",
"axios": "^1.7.2",
"config": "^3.3.11",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-config-standard-with-typescript": "^37.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-no-loops": "^0.3.0",
"eslint-plugin-promise": "^6.2.0",
"fs-extra": "^11.2.0",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"nyc": "^15.1.0",
"semantic-release": "^21.1.2",
"sqlite3": "^5.1.7",
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"vitest": "^0.34.6"
},
"dependencies": {
"@directus/extensions": "^3.0.5",
"@keyv/redis": "^2.8.5",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
"keyv": "^4.5.4",
"openid-client": "^5.6.5",
"uuid": "^11.1.0"
},
"pnpm": {
"overrides": {
"vite@<4.3.9": "^4.3.9",
"vite@>4.3.9": "^4.5.3",
"zod@<=3.22.2": ">=3.22.3",
"axios@<=1.4.0": ">=1.4.1",
"axios@>=1.3.2 <=1.7.3": ">=1.7.4",
"micromatch@<4.0.8": ">=4.0.8",
"vite@>=4.0.0 <4.5.4": ">=4.5.4",
"vite@>=4.0.0 <=4.5.3": ">=4.5.4",
"rollup@>=3.0.0 <3.29.5": ">=3.29.5",
"cross-spawn@>=7.0.0 <7.0.5": ">=7.0.5",
"nanoid@<3.3.8": ">=3.3.8",
"@octokit/request-error@>=1.0.0 <5.1.1": ">=5.1.1",
"@octokit/endpoint@>=9.0.5 <9.0.6": ">=9.0.6",
"@octokit/request@>=1.0.0 <9.2.1": ">=9.2.1",
"@octokit/plugin-paginate-rest@>=1.0.0 <11.4.1": ">=11.4.1",
"serialize-javascript@<6.0.2": ">=6.0.2",
"esbuild@<=0.24.2": ">=0.25.0",
"vite@<=4.5.5": ">=4.5.6"
}
}
}
yarn manifest:
No manifest
Lockfile:
No lockfile

9417
yarn.lock Normal file

File diff suppressed because it is too large Load Diff