Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63a0d5948a | |||
| bcf9c2b191 | |||
| 7d5abe7ac1 | |||
| 0f12c3b140 | |||
| f2002c2873 | |||
| 023b00141d | |||
| b573ba8022 | |||
| ead3eb030c | |||
|
|
fc08583c54 | ||
|
|
368688b559 | ||
|
|
eda8c86177 | ||
|
|
650857e627 | ||
|
|
1f17bf0032 | ||
|
|
1ba1cd94fd | ||
|
|
9e37ad9cb9 | ||
|
|
bad962ffd7 | ||
|
|
6e23f89430 | ||
|
|
e52bf62066 | ||
|
|
678f79274d | ||
|
|
2e976da1d9 | ||
|
|
0af299c8ee | ||
|
|
a49ad5f681 | ||
|
|
f219856698 | ||
|
|
d5c5c18996 | ||
|
|
733f1343a6 | ||
|
|
23fd094c5f | ||
|
|
f327e73d5f | ||
|
|
5ba0791478 | ||
|
|
a6d390bc49 | ||
|
|
832b381732 | ||
|
|
23c8602a9f | ||
|
|
075f93f550 | ||
|
|
a44fb04abe | ||
|
|
96a8406630 | ||
|
|
d39e228a38 | ||
|
|
c642b31aef | ||
|
|
c23d668e18 | ||
|
|
9c26e2d666 | ||
|
|
3584a57554 | ||
|
|
045e6f2c5f |
@@ -3,11 +3,12 @@
|
|||||||
{
|
{
|
||||||
"name": "Node.js & TypeScript",
|
"name": "Node.js & TypeScript",
|
||||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
// 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 to add to the dev container. More info: https://containers.dev/features.
|
||||||
"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": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
// "forwardPorts": [],
|
// "forwardPorts": [],
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
// "postCreateCommand": "yarn install",
|
//"postCreateCommand": "yarn install",
|
||||||
|
|
||||||
// Configure tool-specific properties.
|
// Configure tool-specific properties.
|
||||||
// "customizations": {},
|
// "customizations": {},
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ PORT=8055
|
|||||||
|
|
||||||
PUBLIC_URL="http://localhost:8055"
|
PUBLIC_URL="http://localhost:8055"
|
||||||
|
|
||||||
|
AUTH_PROVIDERS="provider"
|
||||||
|
|
||||||
AUTH_PROVIDER_CLIENT_ID=YOUR_CLIENT_ID
|
AUTH_PROVIDER_CLIENT_ID=YOUR_CLIENT_ID
|
||||||
AUTH_PROVIDER_CLIENT_SECRET=YOUR_CLIENT_SECRET
|
AUTH_PROVIDER_CLIENT_SECRET=YOUR_CLIENT_SECRET
|
||||||
AUTH_PROVIDER_ISSUER_URL=YOUR_ISSUER_URL
|
AUTH_PROVIDER_ISSUER_URL=YOUR_ISSUER_URL
|
||||||
@@ -18,8 +20,8 @@ AUTH_PROVIDER_JWT_USEDB=true
|
|||||||
|
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
### Redis
|
### Redis
|
||||||
REDIS_JWT_DB="directus-jwt"
|
REDIS_JWT_DB="2"
|
||||||
REDIS_DB="directus"
|
REDIS_DB="1"
|
||||||
REDIS_HOST="localhost"
|
REDIS_HOST="localhost"
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ REDIS_PORT=6379
|
|||||||
# you need to pass to the database instance.
|
# you need to pass to the database instance.
|
||||||
|
|
||||||
DB_CLIENT="sqlite3"
|
DB_CLIENT="sqlite3"
|
||||||
DB_FILENAME="/workspaces/directus-extension-external-jwt/data.db"
|
DB_FILENAME="./data.db"
|
||||||
|
|
||||||
|
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
|
|||||||
2
.github/workflows/pr.yml
vendored
2
.github/workflows/pr.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: [ 18, 20 ]
|
node: [ 22 ]
|
||||||
|
|
||||||
name: Node ${{ matrix.node }} PR
|
name: Node ${{ matrix.node }} PR
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 22
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
@@ -97,4 +97,4 @@ jobs:
|
|||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
SRCIMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.tags[0] }}
|
SRCIMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.tags[0] }}
|
||||||
DSTIMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
DSTIMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
run: npx semantic-release
|
run: npx semantic-release@latest
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -5,7 +5,7 @@ dist
|
|||||||
|
|
||||||
# directus files
|
# directus files
|
||||||
*.db
|
*.db
|
||||||
extensions/
|
extensions/*/**
|
||||||
uploads/
|
uploads/
|
||||||
.env
|
.env
|
||||||
|
|
||||||
@@ -16,4 +16,7 @@ uploads/
|
|||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
# Coverage
|
# Coverage
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
# test secrets
|
||||||
|
redispass
|
||||||
|
|||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
4
.idea/vcs.xml
generated
Normal 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
3
.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
16
.releaserc.yaml
Normal file
16
.releaserc.yaml
Normal 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"
|
||||||
36
README.md
36
README.md
@@ -1,26 +1,30 @@
|
|||||||
# External JWT Plugin for Directus
|
# 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.
|
## 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
|
## Configuration
|
||||||
all configuration options listed here are an extension to directus default config.
|
|
||||||
|
|
||||||
| ENV Variable | Supported values | Description |
|
All configuration options listed here are an extension to Directus' default config.
|
||||||
|------------------------------|-------------------|--------------|
|
|
||||||
| 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.
|
| ENV Variable | Supported values | Description |
|
||||||
| 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_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_APP_KEY | String | What key in the JWT payload contains a bool to allow app access
|
| AUTH_PROVIDER_JWT_ROLE_KEY | String | The key in the JWT payload that contains the role information. |
|
||||||
| 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.
|
| AUTH_PROVIDER_JWT_ADMIN_KEY | String | The key in the JWT payload that indicates if admin rights should be granted. |
|
||||||
| CACHE_JWT_NAMESPACE | String | What namespace to use in cache store.
|
| AUTH_PROVIDER_JWT_APP_KEY | String | The key in the JWT payload that allows app access if set to true. |
|
||||||
| CACHE_JWT_TTL | Number | Time to live for the cached user entry, default 5000 (5 seconds)
|
| 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. |
|
||||||
| REDIS_JWT_DB | Number | What database to use in Redis cache, default 2
|
| 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
11
compose.yml
Normal 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
|
||||||
173
extensions/directus-extension-external-jwt/package.json
Normal file
173
extensions/directus-extension-external-jwt/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
326
package.json
326
package.json
@@ -1,155 +1,173 @@
|
|||||||
{
|
{
|
||||||
"name": "@zerosubnet/directus-extension-external-jwt",
|
"name": "directus-extension-external-jwt",
|
||||||
"description": "External JWT Directus Extension allow directus to trust tokens issued by an oauth2 or OIDC provider",
|
"description": "External JWT Directus Extension allow directus to trust tokens issued by an oauth2 or OIDC provider",
|
||||||
"icon": "extension",
|
"icon": "extension",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"directus",
|
"directus",
|
||||||
"directus-extension",
|
"directus-extension",
|
||||||
"directus-custom-hook",
|
"directus-custom-hook",
|
||||||
"directus-external-jwt"
|
"directus-external-jwt"
|
||||||
],
|
],
|
||||||
"homepage": "https://github.com/Zerosubnet/directus-extension-external-jwt",
|
"homepage": "https://github.com/Zerosubnet/directus-extension-external-jwt",
|
||||||
"license": "LGPL-3.0-only",
|
"license": "LGPL-3.0-only",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "zerosubnet"
|
"name": "zerosubnet"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Zerosubnet/directus-extension-external-jwt.git"
|
"url": "https://github.com/Zerosubnet/directus-extension-external-jwt.git"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"release": {
|
"release": {
|
||||||
"branches": [
|
"branches": [
|
||||||
"main",
|
"main",
|
||||||
"next",
|
"next",
|
||||||
{
|
{
|
||||||
"name": "beta",
|
"name": "beta",
|
||||||
"prerelease": true
|
"prerelease": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@semantic-release/commit-analyzer",
|
"@semantic-release/commit-analyzer",
|
||||||
"@semantic-release/release-notes-generator",
|
"@semantic-release/release-notes-generator",
|
||||||
"@semantic-release/changelog",
|
"@semantic-release/changelog",
|
||||||
"@semantic-release/npm",
|
"@semantic-release/npm",
|
||||||
[
|
[
|
||||||
"@semantic-release/github",
|
"@semantic-release/github",
|
||||||
{
|
{
|
||||||
"assets": [
|
"assets": [
|
||||||
"dist/**"
|
"dist/**"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"@semantic-release/exec",
|
"@semantic-release/exec",
|
||||||
{
|
{
|
||||||
"tagImage": "docker tag ${SRCIMAGE} ${DSTIMAGE}:${nextRelease.version}",
|
"tagImage": "docker tag ${SRCIMAGE} ${DSTIMAGE}:${nextRelease.version}",
|
||||||
"publishImage": "docker push ${DSTIMAGE}:${nextRelease.version}"
|
"publishImage": "docker push ${DSTIMAGE}:${nextRelease.version}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"preset": "angular"
|
"preset": "angular"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"directus:extension": {
|
"directus:extension": {
|
||||||
"type": "hook",
|
"type": "hook",
|
||||||
"path": "dist/index.js",
|
"path": "dist/index.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"host": "^10.1.7"
|
"host": "^10.1.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=22.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "directus-extension build",
|
"build": "directus-extension build && npm run sync",
|
||||||
"dev": "directus-extension build -w --no-minify",
|
"dev": "directus-extension build -w --no-minify",
|
||||||
"link": "directus-extension link",
|
"link": "directus-extension link",
|
||||||
"directus": "npx directus start",
|
"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",
|
||||||
"lint": "eslint . --ext .ts",
|
"directus": "pnpm dlx directus start",
|
||||||
"test": "vitest",
|
"lint": "eslint . --ext .ts",
|
||||||
"test:coverage": "vitest run --coverage"
|
"test": "vitest",
|
||||||
},
|
"test:coverage": "vitest run --coverage",
|
||||||
"nyc": {
|
"ncu": "npx npm-check-updates --target minor --upgrade --packageFile package.json "
|
||||||
"extension": [
|
},
|
||||||
".ts",
|
"nyc": {
|
||||||
".tsx"
|
"extension": [
|
||||||
],
|
".ts",
|
||||||
"reporter": [
|
".tsx"
|
||||||
"text",
|
],
|
||||||
"lcov"
|
"reporter": [
|
||||||
],
|
"text",
|
||||||
"report-dir": "coverage",
|
"lcov"
|
||||||
"all": true,
|
],
|
||||||
"extends": "@istanbuljs/nyc-config-typescript",
|
"report-dir": "coverage",
|
||||||
"check-coverage": true,
|
"all": true,
|
||||||
"include": [
|
"extends": "@istanbuljs/nyc-config-typescript",
|
||||||
"src/**/*.[tj]s?(x)"
|
"check-coverage": true,
|
||||||
],
|
"include": [
|
||||||
"exclude": [
|
"src/**/*.[tj]s?(x)"
|
||||||
"src/_tests_/**/*.*",
|
],
|
||||||
"src/**/*.test.[tj]s?(x)"
|
"exclude": [
|
||||||
]
|
"src/_tests_/**/*.*",
|
||||||
},
|
"src/**/*.test.[tj]s?(x)"
|
||||||
"devDependencies": {
|
]
|
||||||
"@directus/errors": "^0.0.2",
|
},
|
||||||
"@directus/extensions-sdk": "^10.2.0",
|
"devDependencies": {
|
||||||
"@directus/tsconfig": "^1.0.1",
|
"@directus/errors": "^2.0.1",
|
||||||
"@directus/types": "^10.1.6",
|
"@directus/extensions-sdk": "^13.1.1",
|
||||||
"@directus/utils": "^10.0.11",
|
"@directus/tsconfig": "^3.0.0",
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
"@directus/types": "^13.1.2",
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
"@directus/utils": "^13.0.6",
|
||||||
"@semantic-release/commit-analyzer": "^10.0.4",
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
"@semantic-release/exec": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/github": "^9.2.3",
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||||
"@semantic-release/npm": "^10.0.6",
|
"@semantic-release/exec": "^7.1.0",
|
||||||
"@types/chai": "^4.3.10",
|
"@semantic-release/github": "^11.0.3",
|
||||||
"@types/chai-as-promised": "^7.1.8",
|
"@semantic-release/npm": "^12.0.1",
|
||||||
"@types/config": "^3.3.3",
|
"@types/chai": "^5.2.2",
|
||||||
"@types/express": "^4.17.21",
|
"@types/chai-as-promised": "^8.0.2",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/config": "^3.3.5",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/express": "^5.0.2",
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/lodash-es": "^4.17.11",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/mocha": "^10.0.4",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"@types/node": "^20.9.1",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@types/mocha": "^10.0.10",
|
||||||
"@vitest/coverage-istanbul": "^0.34.6",
|
"@types/node": "^22.15.29",
|
||||||
"axios": "^1.6.2",
|
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
||||||
"config": "^3.3.9",
|
"@vitest/coverage-istanbul": "^3.1.4",
|
||||||
"dotenv": "^16.3.1",
|
"axios": "^1.9.0",
|
||||||
"eslint": "^8.53.0",
|
"config": "^4.0.0",
|
||||||
"eslint-config-standard-with-typescript": "^37.0.0",
|
"dotenv": "^16.5.0",
|
||||||
"eslint-plugin-import": "^2.29.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint-plugin-n": "^15.7.0",
|
"eslint-config-standard-with-typescript": "^43.0.1",
|
||||||
"eslint-plugin-no-loops": "^0.3.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-promise": "^6.1.1",
|
"eslint-plugin-n": "^17.18.0",
|
||||||
"fs-extra": "^11.1.1",
|
"eslint-plugin-no-loops": "^0.4.0",
|
||||||
"js-yaml": "^4.1.0",
|
"eslint-plugin-promise": "^7.2.1",
|
||||||
"knex": "^2.5.1",
|
"fs-extra": "^11.3.0",
|
||||||
"lodash-es": "^4.17.21",
|
"js-yaml": "^4.1.0",
|
||||||
"nyc": "^15.1.0",
|
"lodash-es": "^4.17.21",
|
||||||
"semantic-release": "^21.1.2",
|
"nyc": "^17.1.0",
|
||||||
"sqlite3": "^5.1.6",
|
"semantic-release": "^24.2.5",
|
||||||
"ts-mocha": "^10.0.0",
|
"sqlite3": "^5.1.7",
|
||||||
"ts-node": "^10.9.1",
|
"ts-mocha": "^11.1.0",
|
||||||
"typescript": "^5.2.2",
|
"ts-node": "^10.9.2",
|
||||||
"vitest": "^0.34.6"
|
"typescript": "^5.8.3",
|
||||||
},
|
"vitest": "^3.1.4"
|
||||||
"dependencies": {
|
},
|
||||||
"@keyv/redis": "^2.8.0",
|
"dependencies": {
|
||||||
"jsonwebtoken": "^9.0.2",
|
"@directus/extensions": "^3.0.6",
|
||||||
"jwks-rsa": "^3.1.0",
|
"@keyv/redis": "^4.4.0",
|
||||||
"keyv": "^4.5.4",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"openid-client": "^5.6.1"
|
"jwks-rsa": "^3.2.0",
|
||||||
},
|
"keyv": "^5.3.3",
|
||||||
"pnpm": {
|
"openid-client": "^6.5.0",
|
||||||
"overrides": {
|
"uuid": "^11.1.0"
|
||||||
"vite@<4.3.9": "^4.3.9",
|
},
|
||||||
"zod@<=3.22.2": ">=3.22.3",
|
"pnpm": {
|
||||||
"axios@<=1.4.0": ">=1.4.1"
|
"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
8909
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
redis.conf
Normal file
6
redis.conf
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
bind 127.0.0.1 -::1
|
||||||
|
protected-mode yes
|
||||||
|
port 6379
|
||||||
|
tcp-keepalive 300
|
||||||
|
|
||||||
|
requirepass asd@123
|
||||||
@@ -1,141 +1,149 @@
|
|||||||
import { toArray } from '@directus/utils';
|
import { toArray } from "@directus/utils";
|
||||||
import {JwksClient} from 'jwks-rsa';
|
import { JwksClient } from "jwks-rsa";
|
||||||
|
|
||||||
import { Issuer } from 'openid-client';
|
import { discovery } from "openid-client";
|
||||||
|
|
||||||
import env from '../config/config';
|
import env from "../config/config";
|
||||||
import { createError } from '@directus/errors';
|
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 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 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 InvalidJWKKeys = createError("INVALID_JWKS_ISSUER_ERROR", "No signing keys in response from provider");
|
||||||
|
|
||||||
|
|
||||||
export interface AuthProvider {
|
export interface AuthProvider {
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
driver: string;
|
driver: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
client_id: string;
|
client_id: string;
|
||||||
client_secret?: string;
|
client_secret?: string;
|
||||||
trusted: boolean;
|
trusted: boolean;
|
||||||
jwks_url?: string;
|
jwks_url?: string;
|
||||||
jwks_keys?: string;
|
jwks_keys?: string;
|
||||||
issuer_url?: string;
|
issuer_url?: string;
|
||||||
|
|
||||||
admin_key?: string;
|
|
||||||
app_key?: string;
|
|
||||||
role_key?: string;
|
|
||||||
JWKSClient?: JwksClient;
|
|
||||||
use_database?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
admin_key?: string;
|
||||||
|
app_key?: string;
|
||||||
|
role_key?: string;
|
||||||
|
JWKSClient?: JwksClient;
|
||||||
|
use_database?: boolean;
|
||||||
|
|
||||||
|
default_role_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getAuthProviders(): Promise<AuthProvider[]> {
|
export async function getAuthProviders(): Promise<AuthProvider[]> {
|
||||||
console.log("calling auth providers")
|
console.log("calling auth providers _");
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const authProviders: AuthProvider[] = toArray(env['AUTH_PROVIDERS'])
|
const authProviders: AuthProvider[] = toArray(env["AUTH_PROVIDERS"])
|
||||||
.filter((provider) => provider && env[`AUTH_${provider.toUpperCase()}_DRIVER`] === ('openid' || 'oauth2'))
|
.filter(
|
||||||
.map((provider) => ({
|
(provider) =>
|
||||||
name: provider,
|
provider &&
|
||||||
label: env[`AUTH_${provider.toUpperCase()}_LABEL`],
|
["openid", "oauth2"].includes(env[`AUTH_${provider.toUpperCase()}_DRIVER`])
|
||||||
driver: env[`AUTH_${provider.toUpperCase()}_DRIVER`],
|
)
|
||||||
icon: env[`AUTH_${provider.toUpperCase()}_ICON`],
|
.map((provider) => ({
|
||||||
trusted: env[`AUTH_${provider.toUpperCase()}_TRUSTED`],
|
name: provider,
|
||||||
jwks_url: env[`AUTH_${provider.toUpperCase()}_JWKS_URL`],
|
label: env[`AUTH_${provider.toUpperCase()}_LABEL`],
|
||||||
jwks_keys: env[`AUTH_${provider.toUpperCase()}_JWKS_KEYS`],
|
driver: env[`AUTH_${provider.toUpperCase()}_DRIVER`],
|
||||||
issuer_url: env[`AUTH_${provider.toUpperCase()}_ISSUER_URL`],
|
icon: env[`AUTH_${provider.toUpperCase()}_ICON`],
|
||||||
admin_key: env[`AUTH_${provider.toUpperCase()}_JWT_ADMIN_KEY`],
|
trusted: env[`AUTH_${provider.toUpperCase()}_TRUSTED`],
|
||||||
app_key: env[`AUTH_${provider.toUpperCase()}_JWT_APP_KEY`],
|
jwks_url: env[`AUTH_${provider.toUpperCase()}_JWKS_URL`],
|
||||||
role_key: env[`AUTH_${provider.toUpperCase()}_JWT_ROLE_KEY`],
|
jwks_keys: env[`AUTH_${provider.toUpperCase()}_JWKS_KEYS`],
|
||||||
client_id: env[`AUTH_${provider.toUpperCase()}_CLIENT_ID`],
|
issuer_url: env[`AUTH_${provider.toUpperCase()}_ISSUER_URL`],
|
||||||
client_secret: env[`AUTH_${provider.toUpperCase()}_CLIENT_SECRET`],
|
admin_key: env[`AUTH_${provider.toUpperCase()}_JWT_ADMIN_KEY`],
|
||||||
use_database: env[`AUTH_${provider.toUpperCase()}_JWT_USEDB`],
|
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`],
|
||||||
|
|
||||||
|
default_role_id: env[`AUTH_${provider.toUpperCase()}_DEFAULT_ROLE_ID`]
|
||||||
if(authProviders.length === 0) return resolve([]);
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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) => {
|
const promises = [];
|
||||||
resolve(values);
|
|
||||||
}).catch((error) => {
|
|
||||||
reject(error);
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
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) {
|
async function getJWKS(provider: AuthProvider) {
|
||||||
if(provider.jwks_keys !== undefined && provider.issuer_url == null && provider.jwks_url == null) {
|
if (provider.jwks_keys !== undefined && provider.issuer_url == null && provider.jwks_url == null) {
|
||||||
const jwks_keys = JSON.parse(provider.jwks_keys);
|
const jwks_keys = JSON.parse(provider.jwks_keys);
|
||||||
const jwksClient = new JwksClient({
|
const jwksClient = new JwksClient({
|
||||||
getKeysInterceptor: () => {
|
getKeysInterceptor: () => {
|
||||||
return jwks_keys;
|
return jwks_keys;
|
||||||
},
|
},
|
||||||
jwksUri: ''
|
jwksUri: ""
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
provider.JWKSClient = jwksClient;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if(provider.issuer_url && !provider.jwks_url) {
|
provider.JWKSClient = jwksClient;
|
||||||
//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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
if (provider.jwks_url == null) throw new InvalidJWKIssuerMetadata();
|
||||||
|
|
||||||
return provider;
|
const jwksClient = await getJWKSClient(provider.jwks_url);
|
||||||
|
|
||||||
|
provider.JWKSClient = jwksClient;
|
||||||
|
|
||||||
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getJWKSClient(url: string) {
|
async function getJWKSClient(url: string) {
|
||||||
const jwksClient = new JwksClient({
|
const jwksClient = new JwksClient({
|
||||||
jwksUri: url,
|
jwksUri: url,
|
||||||
cache: true,
|
cache: true,
|
||||||
cacheMaxAge: 36000000, // 10 hours
|
cacheMaxAge: 36000000, // 10 hours
|
||||||
cacheMaxEntries: 10,
|
cacheMaxEntries: 10,
|
||||||
timeout: 30000, // 30 seconds
|
timeout: 30000 // 30 seconds
|
||||||
});
|
});
|
||||||
|
|
||||||
// try to get the keys
|
// try to get the keys
|
||||||
try {
|
try {
|
||||||
const keys = await jwksClient.getSigningKeys()
|
const keys = await jwksClient.getSigningKeys();
|
||||||
if (keys.length == 0) {
|
if (keys.length == 0) {
|
||||||
throw new InvalidJWKKeys();
|
throw new InvalidJWKKeys();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
throw new InvalidJWKSUrl();
|
} catch (error) {
|
||||||
}
|
throw new InvalidJWKSUrl();
|
||||||
|
}
|
||||||
|
|
||||||
return jwksClient;
|
return jwksClient;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,64 @@
|
|||||||
import {default as Keyv, Store} from 'keyv';
|
import { default as Keyv } from "keyv";
|
||||||
import env from './config/config';
|
import env from "./config/config";
|
||||||
import {default as KeyvRedis} from '@keyv/redis';
|
import { default as KeyvRedis } from "@keyv/redis";
|
||||||
|
|
||||||
// check if redis is defined
|
|
||||||
|
|
||||||
const cache: Keyv | null = getCache();
|
const cache: Keyv | null = getCache();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function getCache(): Keyv | null {
|
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"];
|
||||||
let namespace = env['CACHE_JWT_NAMESPACE'];
|
if (namespace == null || namespace === "") {
|
||||||
if(namespace == null || namespace === '') {
|
namespace = "exjwt";
|
||||||
namespace = 'exjwt';
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let ttl = env['CACHE_JWT_TTL'];
|
let ttl = env["CACHE_JWT_TTL"];
|
||||||
if (ttl == null || ttl === '') {
|
if (ttl == null || ttl === "") {
|
||||||
ttl = 5000
|
ttl = 5000;
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = '';
|
let uri = "";
|
||||||
let store: Store<string | undefined> | undefined = undefined;
|
let store: KeyvRedis<string | undefined> | undefined = undefined;
|
||||||
if(env['CACHE_STORE'] === 'redis') {
|
if (env["CACHE_STORE"] === "redis") {
|
||||||
uri = env['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'}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
if (uri == null || uri === "") {
|
||||||
store = new KeyvRedis(uri);
|
uri = `redis://${env["REDIS_USERNAME"] || ""}:${env["REDIS_PASSWORD"] || ""}@${env["REDIS_HOST"]}:${env["REDIS_PORT"] || "6379"} /${env["REDIS_JWT_DB"] || "2"}`;
|
||||||
} catch(e) {
|
|
||||||
throw new Error("CACHE: could not connect to database: " + e)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyv = new Keyv(uri, {
|
store = new KeyvRedis(uri);
|
||||||
namespace: namespace,
|
} catch (e) {
|
||||||
ttl,
|
throw new Error("CACHE: could not connect to database: " + e);
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
export function CacheEnabled(): boolean {
|
||||||
return cache !== null;
|
return cache !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function CacheSet(key: string, value: any) {
|
export async function CacheSet(key: string, value: unknown) {
|
||||||
if(cache === null) return false;
|
if (cache === null) return false;
|
||||||
return cache.set(key, value);
|
return cache.set(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function CacheGet(key: string) {
|
export async function CacheGet(key: string) {
|
||||||
if(cache === null) return null;
|
if (cache === null) return null;
|
||||||
return cache.get(key);
|
return cache.get(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,4 +303,4 @@ function tryJSON(value: string) {
|
|||||||
} catch {
|
} catch {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
import type { Accountability } from '@directus/types';
|
import type { Accountability, Policy, User } from "@directus/types";
|
||||||
import { getAuthProviders } from './authProvider/get-auth-providers.js';
|
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 { 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);
|
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 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
|
// TODO: optimize this function, reduce the amount of loops
|
||||||
|
|
||||||
|
|
||||||
export async function getAccountabilityForToken(
|
export async function getAccountabilityForToken(
|
||||||
token: string | null,
|
token: string | null,
|
||||||
iss: string[] | string | undefined,
|
iss: string[] | string | undefined,
|
||||||
accountability: Accountability | null,
|
accountability: Accountability | null,
|
||||||
database: Knex
|
database: Knex,
|
||||||
|
options: { ip: string | null, userAgent?: string }
|
||||||
): Promise<Accountability> {
|
): Promise<Accountability> {
|
||||||
if (accountability == null) {
|
if (accountability == null) {
|
||||||
accountability = {
|
accountability = {
|
||||||
user: null,
|
user: null,
|
||||||
role: null,
|
role: null,
|
||||||
admin: false,
|
admin: false,
|
||||||
app: false,
|
app: false,
|
||||||
};
|
roles: [],
|
||||||
}
|
ip: options.ip,
|
||||||
|
userAgent: options.userAgent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (token == null || iss == null) {
|
if (token == null || iss == null) {
|
||||||
|
return accountability;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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];
|
||||||
|
|
||||||
|
try {
|
||||||
const result = await verify_token(provider, token)
|
const result = await verify_token(provider, token);
|
||||||
|
|
||||||
|
if (provider.use_database) {
|
||||||
|
// use database to get user
|
||||||
if(provider.use_database) { // use database to get user
|
// TODO: Add caching to this function
|
||||||
// TODO: Add caching to this function
|
if (CacheEnabled() && result.sub) {
|
||||||
if (CacheEnabled() && result.sub) {
|
const cachedAccountability = await CacheGet(result.sub);
|
||||||
|
if (cachedAccountability) {
|
||||||
const cachedAccountability = await CacheGet(result.sub);
|
return cachedAccountability;
|
||||||
if (cachedAccountability) {
|
}
|
||||||
return cachedAccountability;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await database
|
try {
|
||||||
.select('directus_users.id', 'directus_users.role', 'directus_roles.admin_access', 'directus_roles.app_access')
|
let user = await getUser(database, result.sub, provider.name);
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
accountability.user = user.id;
|
if (!user) {
|
||||||
accountability.role = user.role;
|
const settings = await getSettings(database);
|
||||||
accountability.admin = user.admin_access === true || user.admin_access == 1;
|
console.debug("Settings for public registration:", settings);
|
||||||
accountability.app = user.app_access === true || user.app_access == 1;
|
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) {
|
if (user) {
|
||||||
CacheSet(result.sub, accountability);
|
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
|
console.debug("Accountability set from database:", accountability);
|
||||||
if(provider.role_key != null) {
|
return accountability;
|
||||||
if(typeof result[provider.role_key] === 'string') {
|
}
|
||||||
accountability.role = result[provider.role_key];
|
} catch (error) {
|
||||||
}
|
console.error("Error getting user from database:", error);
|
||||||
if(typeof result[provider.role_key] === 'object') {
|
return accountability;
|
||||||
accountability.role = ''
|
}
|
||||||
}
|
}
|
||||||
if(result[provider.role_key].instanceOf(Array)) {
|
|
||||||
accountability.role = result[provider.role_key][0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(provider.admin_key != null) {
|
// check if role key is set else try role key
|
||||||
accountability.admin = result[provider.admin_key];
|
if (provider.role_key != null) {
|
||||||
}
|
if (typeof result[provider.role_key] === "string") {
|
||||||
if(provider.app_key != null) {
|
accountability.role = result[provider.role_key];
|
||||||
accountability.app = result[provider.app_key];
|
}
|
||||||
}
|
if (typeof result[provider.role_key] === "object") {
|
||||||
accountability.user = result.sub;
|
accountability.role = "";
|
||||||
|
}
|
||||||
} catch (error) {
|
if (result[provider.role_key].instanceOf(Array)) {
|
||||||
return accountability;
|
accountability.role = result[provider.role_key][0];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|||||||
52
src/index.ts
52
src/index.ts
@@ -1,40 +1,28 @@
|
|||||||
import { defineHook } from '@directus/extensions-sdk';
|
import { defineHook } from "@directus/extensions-sdk";
|
||||||
import { getAccountabilityForToken } from './external-jwt/get-accountability-for-token';
|
import { getAccountabilityForToken } from "./external-jwt/get-accountability-for-token";
|
||||||
import type { Request } from 'express';
|
import type { Request } from "express";
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from "jsonwebtoken";
|
||||||
import { HookConfig } from '@directus/types';
|
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 }) => {
|
const decodedToken = jwt.decode(req.token);
|
||||||
|
|
||||||
// get all configuration
|
|
||||||
|
|
||||||
filter('authenticate', (defaultAccountability, event, context) => {
|
|
||||||
const req = <Request>event['req'];
|
|
||||||
if(!req.token) return defaultAccountability;
|
|
||||||
|
|
||||||
if(!context.database) {
|
if (typeof decodedToken === "string" || decodedToken == null) return defaultAccountability; // if token is not a jwt, let directus handle it
|
||||||
return defaultAccountability
|
if (decodedToken?.iss == "directus") return defaultAccountability; // if token issued by directus, let directus handle it
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
|
|
||||||
})*/
|
|
||||||
|
|
||||||
|
return getAccountabilityForToken(req.token, decodedToken?.iss, context.accountability, context.database, {
|
||||||
|
ip: req.ip || null,
|
||||||
|
userAgent: req.headers["user-agent"]
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": ["ES2022"],
|
"lib": [
|
||||||
"module": "ES2022",
|
"ES2022"
|
||||||
"preserveConstEnums": true,
|
],
|
||||||
"moduleResolution": "node",
|
"module": "ES2022",
|
||||||
"strict": true,
|
"preserveConstEnums": true,
|
||||||
"sourceMap": true,
|
"moduleResolution": "node",
|
||||||
"declaration": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"sourceMap": true,
|
||||||
"target": "es2022",
|
"declaration": true,
|
||||||
"types": ["node"],
|
"noUnusedLocals": true,
|
||||||
"outDir": "dist",
|
"target": "es2022",
|
||||||
"typeRoots": ["node_modules/@types"],
|
"types": [
|
||||||
"allowSyntheticDefaultImports": true,
|
"node"
|
||||||
},
|
],
|
||||||
"include": ["src/**/*.ts"],
|
"outDir": "dist",
|
||||||
"exclude": []
|
"typeRoots": [
|
||||||
|
"node_modules/@types"
|
||||||
|
],
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": []
|
||||||
}
|
}
|
||||||
|
|||||||
197
yarn-error.log
Normal file
197
yarn-error.log
Normal 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
|
||||||
Reference in New Issue
Block a user