Compare commits
10 Commits
e52bf62066
...
ead3eb030c
| Author | SHA1 | Date | |
|---|---|---|---|
| ead3eb030c | |||
|
|
fc08583c54 | ||
|
|
368688b559 | ||
|
|
eda8c86177 | ||
|
|
650857e627 | ||
|
|
1f17bf0032 | ||
|
|
1ba1cd94fd | ||
|
|
9e37ad9cb9 | ||
|
|
bad962ffd7 | ||
|
|
6e23f89430 |
@@ -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": {},
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
####################################################################################################
|
||||
|
||||
2
.github/workflows/pr.yml
vendored
2
.github/workflows/pr.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [ 18, 20 ]
|
||||
node: [ 22 ]
|
||||
|
||||
name: Node ${{ matrix.node }} PR
|
||||
steps:
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,7 +5,7 @@ dist
|
||||
|
||||
# directus files
|
||||
*.db
|
||||
extensions/
|
||||
#extensions/*/**
|
||||
uploads/
|
||||
.env
|
||||
|
||||
|
||||
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
|
||||
|
||||
## 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
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
|
||||
323
package.json
323
package.json
@@ -1,155 +1,172 @@
|
||||
{
|
||||
"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.2.4",
|
||||
"@directus/extensions-sdk": "^11.0.1",
|
||||
"@directus/tsconfig": "^1.0.1",
|
||||
"@directus/types": "^11.0.7",
|
||||
"@directus/utils": "^11.0.6",
|
||||
"@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.12",
|
||||
"@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.6",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "^20.11.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@vitest/coverage-istanbul": "^0.34.6",
|
||||
"axios": "^1.6.7",
|
||||
"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.1.1",
|
||||
"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.2",
|
||||
"vitest": "^0.34.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@keyv/redis": "^2.8.4",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"keyv": "^4.5.4",
|
||||
"openid-client": "^5.6.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite": "^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": ">=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"
|
||||
}
|
||||
}
|
||||
}
|
||||
12586
pnpm-lock.yaml
generated
12586
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -12,28 +12,30 @@ const InvalidJWKKeys = createError('INVALID_JWKS_ISSUER_ERROR', 'No signing keys
|
||||
|
||||
|
||||
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;
|
||||
|
||||
initial_role?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function getAuthProviders(): Promise<AuthProvider[]> {
|
||||
console.log("calling auth providers")
|
||||
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'))
|
||||
@@ -52,6 +54,8 @@ export async function getAuthProviders(): Promise<AuthProvider[]> {
|
||||
client_id: env[`AUTH_${provider.toUpperCase()}_CLIENT_ID`],
|
||||
client_secret: env[`AUTH_${provider.toUpperCase()}_CLIENT_SECRET`],
|
||||
use_database: env[`AUTH_${provider.toUpperCase()}_JWT_USEDB`],
|
||||
|
||||
initial_role: env[`AUTH_${provider.toUpperCase()}_INITIAL_ROLE`]
|
||||
}));
|
||||
|
||||
|
||||
@@ -78,6 +82,7 @@ export async function getAuthProviders(): Promise<AuthProvider[]> {
|
||||
}
|
||||
|
||||
Promise.all(promises).then((values) => {
|
||||
console.log("resolved auth providers", values)
|
||||
resolve(values);
|
||||
}).catch((error) => {
|
||||
reject(error);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Accountability } 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 type { Accountability } 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";
|
||||
|
||||
const authProviders = await getAuthProviders();
|
||||
|
||||
@@ -14,111 +14,157 @@ 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
|
||||
) => {
|
||||
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, any>) => {
|
||||
return database("directus_users").insert(user).returning("*");
|
||||
};
|
||||
|
||||
// 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
|
||||
): Promise<Accountability> {
|
||||
if (accountability == null) {
|
||||
accountability = {
|
||||
user: null,
|
||||
role: null,
|
||||
admin: false,
|
||||
app: false,
|
||||
};
|
||||
}
|
||||
console.log(
|
||||
"getAccountabilityForToken called with token",
|
||||
token,
|
||||
"and iss",
|
||||
iss,
|
||||
"and accountability",
|
||||
accountability
|
||||
);
|
||||
if (accountability == null) {
|
||||
accountability = {
|
||||
user: null,
|
||||
role: null,
|
||||
admin: false,
|
||||
app: false,
|
||||
};
|
||||
}
|
||||
|
||||
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 = authProviders.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;
|
||||
console.debug("User found in database:", user);
|
||||
|
||||
if (CacheEnabled() && result.sub) {
|
||||
CacheSet(result.sub, accountability);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
const role = provider.initial_role
|
||||
user = await insertUser(database, {
|
||||
id: uuid.v4(),
|
||||
role: role,
|
||||
provider: provider.name,
|
||||
external_identifier: result.sub,
|
||||
});
|
||||
console.debug("Inserted new user:", user);
|
||||
}
|
||||
|
||||
return accountability;
|
||||
}
|
||||
if (user) {
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
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(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;
|
||||
if (CacheEnabled() && result.sub) {
|
||||
CacheSet(result.sub, accountability);
|
||||
}
|
||||
|
||||
}
|
||||
console.log(
|
||||
"Returning accountability from database:",
|
||||
accountability
|
||||
);
|
||||
|
||||
return accountability;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting user from database:", error);
|
||||
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;
|
||||
} catch (error) {
|
||||
return accountability;
|
||||
}
|
||||
|
||||
return accountability;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export default defineHook<HookConfig>(({ filter }) => {
|
||||
// get all configuration
|
||||
|
||||
filter('authenticate', (defaultAccountability: Accountability, event, context: EventContext) => {
|
||||
console.log("authenticate hook called");
|
||||
const req = <Request>event['req'];
|
||||
if(!req.token) return defaultAccountability;
|
||||
|
||||
@@ -20,6 +21,7 @@ export default defineHook<HookConfig>(({ filter }) => {
|
||||
|
||||
|
||||
const decodedToken = jwt.decode(req.token);
|
||||
console.log("decoded token", decodedToken);
|
||||
|
||||
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
|
||||
@@ -30,6 +32,7 @@ export default defineHook<HookConfig>(({ filter }) => {
|
||||
return getAccountabilityForToken(req.token, decodedToken?.iss, context.accountability, context.database)
|
||||
});
|
||||
|
||||
|
||||
/*filter('auth.jwt', (status, user, provider) => {
|
||||
|
||||
})*/
|
||||
|
||||
Reference in New Issue
Block a user