Compare commits

..

1 Commits

Author SHA1 Message Date
3f5f2fd11e Refactor code structure for improved readability and maintainability
Some checks failed
Release / Release (push) Has been cancelled
2025-05-27 21:47:20 +03:00
18 changed files with 6325 additions and 67327 deletions

View File

@@ -3,12 +3,11 @@
{ {
"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:22-bookworm", "image": "mcr.microsoft.com/devcontainers/typescript-node:1-18-bullseye",
// 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": {
@@ -22,7 +21,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": {},

View File

@@ -6,8 +6,6 @@ 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
@@ -20,8 +18,8 @@ AUTH_PROVIDER_JWT_USEDB=true
#################################################################################################### ####################################################################################################
### Redis ### Redis
REDIS_JWT_DB="2" REDIS_JWT_DB="directus-jwt"
REDIS_DB="1" REDIS_DB="directus"
REDIS_HOST="localhost" REDIS_HOST="localhost"
REDIS_PORT=6379 REDIS_PORT=6379
@@ -33,7 +31,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="./data.db" DB_FILENAME="/workspaces/directus-extension-external-jwt/data.db"
#################################################################################################### ####################################################################################################

View File

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

View File

@@ -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: 22 node-version: 18
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@latest run: npx semantic-release

8
.gitignore vendored
View File

@@ -1,11 +1,10 @@
.DS_Store .DS_Store
node_modules node_modules
.pnpm-store .pnpm-store
dist
# directus files # directus files
*.db *.db
#extensions/*/** extensions/
uploads/ uploads/
.env .env
@@ -16,7 +15,4 @@ uploads/
.vscode .vscode
# Coverage # Coverage
coverage/ coverage/
# test secrets
redispass

View File

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

View File

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

View File

@@ -1,30 +1,26 @@
# 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 options. The plugin expects to resolve the following new configuration option
The provider must issue Access tokens as JWT since this is used for verification right now. (Support for general tokens may be added later.) The provider must issues Access tokens as JWT since this is used for verification right now. Might add support for general tokens later.
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. 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.
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. 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.
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.
All configuration options listed here are an extension to Directus' default config. | ENV Variable | Supported values | Description |
|------------------------------|-------------------|--------------|
| 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_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_ADMIN_KEY | String | What key in the JWT payload contains a bool to grant admin rights |
| AUTH_PROVIDER_JWT_ROLE_KEY | String | The key in the JWT payload that contains the role information. | | AUTH_PROVIDER_JWT_APP_KEY | String | What key in the JWT payload contains a bool to allow app access
| AUTH_PROVIDER_JWT_ADMIN_KEY | String | The key in the JWT payload that indicates if admin rights should be granted. | | 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_APP_KEY | String | The key in the JWT payload that allows app access if set to true. | | CACHE_JWT_NAMESPACE | String | What namespace to use in cache store.
| 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. | | CACHE_JWT_TTL | Number | Time to live for the cached user entry, default 5000 (5 seconds)
| AUTH_PROVIDER_JWKS_URL | String | The URL from which to fetch the JSON Web Key Set (JWKS) for token verification. | | REDIS_JWT_DB | Number | What database to use in Redis cache, default 2
| 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. |

View File

@@ -1,11 +0,0 @@
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

5
dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

59603
index.js

File diff suppressed because one or more lines are too long

View File

@@ -1,172 +1,155 @@
{ {
"name": "directus-extension-external-jwt", "name": "@zerosubnet/directus-extension-external-jwt",
"description": "External JWT Directus Extension allow directus to trust tokens issued by an oauth2 or OIDC provider", "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": ">=18.0.0"
}, },
"scripts": { "scripts": {
"build": "directus-extension build && npm run sync", "build": "directus-extension build",
"dev": "directus-extension build -w --no-minify", "dev": "directus-extension build -w --no-minify",
"link": "directus-extension link", "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": "npx directus start",
"directus": "pnpm dlx directus start", "lint": "eslint . --ext .ts",
"lint": "eslint . --ext .ts", "test": "vitest",
"test": "vitest", "test:coverage": "vitest run --coverage"
"test:coverage": "vitest run --coverage" },
}, "nyc": {
"nyc": { "extension": [
"extension": [ ".ts",
".ts", ".tsx"
".tsx" ],
], "reporter": [
"reporter": [ "text",
"text", "lcov"
"lcov" ],
], "report-dir": "coverage",
"report-dir": "coverage", "all": true,
"all": true, "extends": "@istanbuljs/nyc-config-typescript",
"extends": "@istanbuljs/nyc-config-typescript", "check-coverage": true,
"check-coverage": true, "include": [
"include": [ "src/**/*.[tj]s?(x)"
"src/**/*.[tj]s?(x)" ],
], "exclude": [
"exclude": [ "src/_tests_/**/*.*",
"src/_tests_/**/*.*", "src/**/*.test.[tj]s?(x)"
"src/**/*.test.[tj]s?(x)" ]
] },
}, "devDependencies": {
"devDependencies": { "@directus/errors": "^0.0.2",
"@directus/errors": "^0.3.2", "@directus/extensions-sdk": "^10.2.0",
"@directus/extensions-sdk": "^13.0.1", "@directus/tsconfig": "^1.0.1",
"@directus/tsconfig": "^1.0.1", "@directus/types": "^10.1.6",
"@directus/types": "^11.1.2", "@directus/utils": "^10.0.11",
"@directus/utils": "^11.0.9", "@istanbuljs/nyc-config-typescript": "^1.0.2",
"@istanbuljs/nyc-config-typescript": "^1.0.2", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^10.0.4",
"@semantic-release/commit-analyzer": "^10.0.4", "@semantic-release/exec": "^6.0.3",
"@semantic-release/exec": "^6.0.3", "@semantic-release/github": "^9.2.3",
"@semantic-release/github": "^9.2.6", "@semantic-release/npm": "^10.0.6",
"@semantic-release/npm": "^10.0.6", "@types/chai": "^4.3.10",
"@types/chai": "^4.3.16", "@types/chai-as-promised": "^7.1.8",
"@types/chai-as-promised": "^7.1.8", "@types/config": "^3.3.3",
"@types/config": "^3.3.4", "@types/express": "^4.17.21",
"@types/express": "^4.17.21", "@types/fs-extra": "^11.0.4",
"@types/fs-extra": "^11.0.4", "@types/js-yaml": "^4.0.9",
"@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.5",
"@types/jsonwebtoken": "^9.0.6", "@types/lodash-es": "^4.17.11",
"@types/lodash-es": "^4.17.12", "@types/mocha": "^10.0.4",
"@types/mocha": "^10.0.6", "@types/node": "^20.9.1",
"@types/node": "^20.14.2", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@vitest/coverage-istanbul": "^0.34.6",
"@vitest/coverage-istanbul": "^0.34.6", "axios": "^1.6.2",
"axios": "^1.7.2", "config": "^3.3.9",
"config": "^3.3.11", "dotenv": "^16.3.1",
"dotenv": "^16.4.5", "eslint": "^8.53.0",
"eslint": "^8.57.0", "eslint-config-standard-with-typescript": "^37.0.0",
"eslint-config-standard-with-typescript": "^37.0.0", "eslint-plugin-import": "^2.29.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-n": "^15.7.0",
"eslint-plugin-n": "^15.7.0", "eslint-plugin-no-loops": "^0.3.0",
"eslint-plugin-no-loops": "^0.3.0", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-promise": "^6.2.0", "fs-extra": "^11.1.1",
"fs-extra": "^11.2.0", "js-yaml": "^4.1.0",
"js-yaml": "^4.1.0", "knex": "^2.5.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"semantic-release": "^21.1.2", "semantic-release": "^21.1.2",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.6",
"ts-mocha": "^10.0.0", "ts-mocha": "^10.0.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.1",
"typescript": "^5.4.5", "typescript": "^5.2.2",
"vitest": "^0.34.6" "vitest": "^0.34.6"
}, },
"dependencies": { "dependencies": {
"@directus/extensions": "^3.0.5", "@keyv/redis": "^2.8.0",
"@keyv/redis": "^2.8.5", "jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0",
"jwks-rsa": "^3.1.0", "keyv": "^4.5.4",
"keyv": "^4.5.4", "openid-client": "^5.6.1"
"openid-client": "^5.6.5", },
"uuid": "^11.1.0" "pnpm": {
}, "overrides": {
"pnpm": { "vite@<4.3.9": "^4.3.9",
"overrides": { "zod@<=3.22.2": ">=3.22.3",
"vite@<4.3.9": "^4.3.9", "axios@<=1.4.0": ">=1.4.1"
"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"
}
}
} }

13328
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

1
redispass Normal file
View File

@@ -0,0 +1 @@
asd

View File

@@ -12,30 +12,28 @@ const InvalidJWKKeys = createError('INVALID_JWKS_ISSUER_ERROR', 'No signing keys
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; admin_key?: string;
app_key?: string; app_key?: string;
role_key?: string; role_key?: string;
JWKSClient?: JwksClient; JWKSClient?: JwksClient;
use_database?: boolean; use_database?: boolean;
initial_role?: 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((provider) => provider && env[`AUTH_${provider.toUpperCase()}_DRIVER`] === ('openid' || 'oauth2'))
@@ -54,8 +52,6 @@ export async function getAuthProviders(): Promise<AuthProvider[]> {
client_id: env[`AUTH_${provider.toUpperCase()}_CLIENT_ID`], client_id: env[`AUTH_${provider.toUpperCase()}_CLIENT_ID`],
client_secret: env[`AUTH_${provider.toUpperCase()}_CLIENT_SECRET`], client_secret: env[`AUTH_${provider.toUpperCase()}_CLIENT_SECRET`],
use_database: env[`AUTH_${provider.toUpperCase()}_JWT_USEDB`], use_database: env[`AUTH_${provider.toUpperCase()}_JWT_USEDB`],
initial_role: env[`AUTH_${provider.toUpperCase()}_INITIAL_ROLE`]
})); }));
@@ -82,7 +78,6 @@ export async function getAuthProviders(): Promise<AuthProvider[]> {
} }
Promise.all(promises).then((values) => { Promise.all(promises).then((values) => {
console.log("resolved auth providers", values)
resolve(values); resolve(values);
}).catch((error) => { }).catch((error) => {
reject(error); reject(error);

View File

@@ -1,10 +1,10 @@
import type { Accountability } from "@directus/types"; import type { Accountability } 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";
const authProviders = await getAuthProviders(); const authProviders = await getAuthProviders();
@@ -14,157 +14,111 @@ 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
) => {
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 // 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
): Promise<Accountability> { ): Promise<Accountability> {
console.log( if (accountability == null) {
"getAccountabilityForToken called with token", accountability = {
token, user: null,
"and iss", role: null,
iss, admin: false,
"and accountability", app: false,
accountability };
); }
if (accountability == null) {
accountability = {
user: null,
role: null,
admin: false,
app: false,
};
}
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 providers = authProviders.filter( const provider = providers[0];
(provider) =>
provider.issuer_url && provider.issuer_url.includes(iss.toString())
);
if (providers.length === 0) return accountability;
if (providers.length > 1) {
return accountability;
}
const provider = providers[0]; try {
try {
const result = await verify_token(provider, token); const result = await verify_token(provider, token)
if (provider.use_database) {
// use database to get user
// TODO: Add caching to this function if(provider.use_database) { // use database to get user
if (CacheEnabled() && result.sub) { // TODO: Add caching to this function
const cachedAccountability = await CacheGet(result.sub); if (CacheEnabled() && result.sub) {
if (cachedAccountability) {
return cachedAccountability; const cachedAccountability = await CacheGet(result.sub);
} if (cachedAccountability) {
} return cachedAccountability;
}
}
try { const user = await database
let user = await getUser(database, result.sub, provider.name); .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;
}
console.debug("User found in database:", user); accountability.user = user.id;
accountability.role = user.role;
accountability.admin = user.admin_access === true || user.admin_access == 1;
accountability.app = user.app_access === true || user.app_access == 1;
if (!user) { if (CacheEnabled() && result.sub) {
const role = provider.initial_role CacheSet(result.sub, accountability);
user = await insertUser(database, { }
id: uuid.v4(),
role: role,
provider: provider.name,
external_identifier: result.sub,
});
console.debug("Inserted new user:", user);
}
if (user) { return accountability;
// return accountability; }
accountability.user = user.id; // check if role key is set else try role key
accountability.role = user.role; if(provider.role_key != null) {
accountability.admin = if(typeof result[provider.role_key] === 'string') {
user.admin_access === true || user.admin_access == 1; accountability.role = result[provider.role_key];
accountability.app = user.app_access === true || user.app_access == 1; }
if(typeof result[provider.role_key] === 'object') {
accountability.role = ''
}
if(result[provider.role_key].instanceOf(Array)) {
accountability.role = result[provider.role_key][0];
}
}
if (CacheEnabled() && result.sub) { if(provider.admin_key != null) {
CacheSet(result.sub, accountability); 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;
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;
}

View File

@@ -2,15 +2,15 @@ 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 type {HookConfig} from '@directus/extensions' import { HookConfig } from '@directus/types';
import type { Accountability, EventContext } from '@directus/types';
export default defineHook<HookConfig>(({ filter }) => { export default defineHook<HookConfig>(({ filter }) => {
// get all configuration // get all configuration
filter('authenticate', (defaultAccountability: Accountability, event, context: EventContext) => { filter('authenticate', (defaultAccountability, event, context) => {
console.log("authenticate hook called");
const req = <Request>event['req']; const req = <Request>event['req'];
if(!req.token) return defaultAccountability; if(!req.token) return defaultAccountability;
@@ -21,7 +21,6 @@ export default defineHook<HookConfig>(({ filter }) => {
const decodedToken = jwt.decode(req.token); 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(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 if(decodedToken?.iss == 'directus') return defaultAccountability; // if token issued by directus, let directus handle it
@@ -32,7 +31,6 @@ export default defineHook<HookConfig>(({ filter }) => {
return getAccountabilityForToken(req.token, decodedToken?.iss, context.accountability, context.database) return getAccountabilityForToken(req.token, decodedToken?.iss, context.accountability, context.database)
}); });
/*filter('auth.jwt', (status, user, provider) => { /*filter('auth.jwt', (status, user, provider) => {
})*/ })*/