This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import { toArray } from '@directus/utils';
|
||||
import {JwksClient} from 'jwks-rsa';
|
||||
import { toArray } from "@directus/utils";
|
||||
import { JwksClient } from "jwks-rsa";
|
||||
|
||||
import { Issuer } from 'openid-client';
|
||||
import { discovery } from "openid-client";
|
||||
|
||||
import env from '../config/config';
|
||||
import { createError } from '@directus/errors';
|
||||
import env from "../config/config";
|
||||
import { createError } from "@directus/errors";
|
||||
|
||||
const InvalidJWKIssuerMetadata = createError('INVALID_JWKS_ISSUER_ERROR', 'No JWKS_URL or JWKS_KEYS and could not discover JWKS_URL from openid metadata', 500);
|
||||
const InvalidJWKSUrl = createError('INVALID_JWKS_ISSUER_ERROR', 'Could not retrieve any valid keys from JWKS_URL', 500);
|
||||
const InvalidJWKKeys = createError('INVALID_JWKS_ISSUER_ERROR', 'No signing keys in response from provider')
|
||||
const InvalidJWKIssuerMetadata = createError("INVALID_JWKS_ISSUER_ERROR", "No JWKS_URL or JWKS_KEYS and could not discover JWKS_URL from openid metadata", 500);
|
||||
const InvalidJWKSUrl = createError("INVALID_JWKS_ISSUER_ERROR", "Could not retrieve any valid keys from JWKS_URL", 500);
|
||||
const InvalidJWKKeys = createError("INVALID_JWKS_ISSUER_ERROR", "No signing keys in response from provider");
|
||||
|
||||
|
||||
export interface AuthProvider {
|
||||
@@ -33,16 +33,14 @@ export interface AuthProvider {
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function getAuthProviders(): Promise<AuthProvider[]> {
|
||||
console.log("calling auth providers _")
|
||||
return new Promise((resolve, reject) => {
|
||||
const authProviders: AuthProvider[] = toArray(env["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")
|
||||
["openid", "oauth2"].includes(env[`AUTH_${provider.toUpperCase()}_DRIVER`])
|
||||
)
|
||||
.map((provider) => ({
|
||||
name: provider,
|
||||
@@ -60,92 +58,92 @@ export async function getAuthProviders(): Promise<AuthProvider[]> {
|
||||
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`],
|
||||
default_role_id: env[`AUTH_${provider.toUpperCase()}_DEFAULT_ROLE_ID`]
|
||||
}));
|
||||
|
||||
|
||||
if(authProviders.length === 0) return resolve([]);
|
||||
|
||||
|
||||
if (authProviders.length === 0) return resolve([]);
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (const authProvider of authProviders) {
|
||||
switch (authProvider.driver) {
|
||||
case 'openid':
|
||||
|
||||
if (!authProvider.trusted || (authProvider.issuer_url == null && authProvider.jwks_url == null && authProvider.jwks_keys == null)) break;
|
||||
//promises.push(getJWKS(authProvider.issuer_url, authProvider.jwks_url, authProvider.jwks_keys));
|
||||
promises.push(getJWKS(authProvider));
|
||||
break;
|
||||
case 'oauth2':
|
||||
if (!authProvider.trusted || (authProvider.issuer_url == null && authProvider.jwks_url == null && authProvider.jwks_keys == null)) break;
|
||||
//promises.push(getJWKS(authProvider.issuer_url, authProvider.jwks_url, authProvider.jwks_keys));
|
||||
promises.push(getJWKS(authProvider));
|
||||
break;
|
||||
}
|
||||
}
|
||||
const promises = [];
|
||||
|
||||
Promise.all(promises).then((values) => {
|
||||
console.log("resolved auth providers", values)
|
||||
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) {
|
||||
if(provider.jwks_keys !== undefined && provider.issuer_url == null && provider.jwks_url == null) {
|
||||
const jwks_keys = JSON.parse(provider.jwks_keys);
|
||||
const jwksClient = new JwksClient({
|
||||
getKeysInterceptor: () => {
|
||||
return jwks_keys;
|
||||
},
|
||||
jwksUri: ''
|
||||
})
|
||||
|
||||
if (provider.jwks_keys !== undefined && provider.issuer_url == null && provider.jwks_url == null) {
|
||||
const jwks_keys = JSON.parse(provider.jwks_keys);
|
||||
const jwksClient = new JwksClient({
|
||||
getKeysInterceptor: () => {
|
||||
return jwks_keys;
|
||||
},
|
||||
jwksUri: ""
|
||||
});
|
||||
|
||||
provider.JWKSClient = jwksClient;
|
||||
|
||||
}
|
||||
|
||||
if(provider.issuer_url && !provider.jwks_url) {
|
||||
//try to discover with openid
|
||||
const issuer = await Issuer.discover(provider.issuer_url);
|
||||
if(issuer.metadata.jwks_uri != null) {
|
||||
provider.jwks_url = issuer.metadata.jwks_uri;
|
||||
}
|
||||
}
|
||||
provider.JWKSClient = jwksClient;
|
||||
|
||||
if (provider.jwks_url == null) throw new InvalidJWKIssuerMetadata();
|
||||
}
|
||||
|
||||
const jwksClient = await getJWKSClient(provider.jwks_url);
|
||||
if (provider.issuer_url && !provider.jwks_url) {
|
||||
//try to discover with openid
|
||||
const issuer = await discovery(new URL(provider.issuer_url), provider.client_id);
|
||||
if (issuer.serverMetadata().jwks_uri != null) {
|
||||
provider.jwks_url = issuer.serverMetadata().jwks_uri;
|
||||
}
|
||||
}
|
||||
|
||||
provider.JWKSClient = jwksClient;
|
||||
|
||||
return provider;
|
||||
if (provider.jwks_url == null) throw new InvalidJWKIssuerMetadata();
|
||||
|
||||
const jwksClient = await getJWKSClient(provider.jwks_url);
|
||||
|
||||
provider.JWKSClient = jwksClient;
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
async function getJWKSClient(url: string) {
|
||||
const jwksClient = new JwksClient({
|
||||
jwksUri: url,
|
||||
cache: true,
|
||||
cacheMaxAge: 36000000, // 10 hours
|
||||
cacheMaxEntries: 10,
|
||||
timeout: 30000, // 30 seconds
|
||||
});
|
||||
const jwksClient = new JwksClient({
|
||||
jwksUri: url,
|
||||
cache: true,
|
||||
cacheMaxAge: 36000000, // 10 hours
|
||||
cacheMaxEntries: 10,
|
||||
timeout: 30000 // 30 seconds
|
||||
});
|
||||
|
||||
// try to get the keys
|
||||
try {
|
||||
const keys = await jwksClient.getSigningKeys()
|
||||
if (keys.length == 0) {
|
||||
throw new InvalidJWKKeys();
|
||||
}
|
||||
} catch (error) {
|
||||
throw new InvalidJWKSUrl();
|
||||
}
|
||||
// try to get the keys
|
||||
try {
|
||||
const keys = await jwksClient.getSigningKeys();
|
||||
if (keys.length == 0) {
|
||||
throw new InvalidJWKKeys();
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
throw new InvalidJWKSUrl();
|
||||
}
|
||||
|
||||
return jwksClient;
|
||||
}
|
||||
return jwksClient;
|
||||
}
|
||||
|
||||
@@ -1,75 +1,64 @@
|
||||
import {default as Keyv, Store} from 'keyv';
|
||||
import env from './config/config';
|
||||
import {default as KeyvRedis} from '@keyv/redis';
|
||||
|
||||
// check if redis is defined
|
||||
import { default as Keyv } from "keyv";
|
||||
import env from "./config/config";
|
||||
import { default as KeyvRedis } from "@keyv/redis";
|
||||
|
||||
const cache: Keyv | null = getCache();
|
||||
|
||||
|
||||
|
||||
|
||||
function getCache(): Keyv | null {
|
||||
if(env['CACHE_ENABLED'] !== true) return null;
|
||||
if (env["CACHE_ENABLED"] !== true) return null;
|
||||
|
||||
// check namespace
|
||||
let namespace = env['CACHE_JWT_NAMESPACE'];
|
||||
if(namespace == null || namespace === '') {
|
||||
namespace = 'exjwt';
|
||||
}
|
||||
let namespace = env["CACHE_JWT_NAMESPACE"];
|
||||
if (namespace == null || namespace === "") {
|
||||
namespace = "exjwt";
|
||||
}
|
||||
|
||||
let ttl = env['CACHE_JWT_TTL'];
|
||||
if (ttl == null || ttl === '') {
|
||||
ttl = 5000
|
||||
}
|
||||
let ttl = env["CACHE_JWT_TTL"];
|
||||
if (ttl == null || ttl === "") {
|
||||
ttl = 5000;
|
||||
}
|
||||
|
||||
let uri = '';
|
||||
let store: Store<string | undefined> | undefined = undefined;
|
||||
if(env['CACHE_STORE'] === 'redis') {
|
||||
uri = env['REDIS']
|
||||
|
||||
if(uri == null || uri === '') {
|
||||
uri = `redis://${env['REDIS_USERNAME'] || '' }:${env['REDIS_PASSWORD'] || ''}@${env['REDIS_HOST']}:${env['REDIS_PORT'] || '6379'} /${env['REDIS_JWT_DB'] || '2'}`;
|
||||
}
|
||||
let uri = "";
|
||||
let store: KeyvRedis<string | undefined> | undefined = undefined;
|
||||
if (env["CACHE_STORE"] === "redis") {
|
||||
uri = env["REDIS"];
|
||||
|
||||
try {
|
||||
store = new KeyvRedis(uri);
|
||||
} catch(e) {
|
||||
throw new Error("CACHE: could not connect to database: " + e)
|
||||
}
|
||||
|
||||
if (uri == null || uri === "") {
|
||||
uri = `redis://${env["REDIS_USERNAME"] || ""}:${env["REDIS_PASSWORD"] || ""}@${env["REDIS_HOST"]}:${env["REDIS_PORT"] || "6379"} /${env["REDIS_JWT_DB"] || "2"}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const keyv = new Keyv(uri, {
|
||||
namespace: namespace,
|
||||
ttl,
|
||||
store: store
|
||||
});
|
||||
|
||||
keyv.on('error', (err) => {
|
||||
throw new Error("CACHE: could not connect: " + err)
|
||||
});
|
||||
|
||||
return keyv
|
||||
} catch(e) {
|
||||
throw new Error("CACHE: could not connect to database: " + e)
|
||||
store = new KeyvRedis(uri);
|
||||
} catch (e) {
|
||||
throw new Error("CACHE: could not connect to database: " + e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const keyv = new Keyv(store, {
|
||||
namespace: namespace,
|
||||
ttl
|
||||
});
|
||||
|
||||
keyv.on("error", (err) => {
|
||||
throw new Error("CACHE: could not connect: " + err);
|
||||
});
|
||||
|
||||
return keyv;
|
||||
} catch (e) {
|
||||
throw new Error("CACHE: could not connect to database: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
export function CacheEnabled(): boolean {
|
||||
return cache !== null;
|
||||
return cache !== null;
|
||||
}
|
||||
|
||||
export async function CacheSet(key: string, value: any) {
|
||||
if(cache === null) return false;
|
||||
return cache.set(key, value);
|
||||
export async function CacheSet(key: string, value: unknown) {
|
||||
if (cache === null) return false;
|
||||
return cache.set(key, value);
|
||||
}
|
||||
|
||||
export async function CacheGet(key: string) {
|
||||
if(cache === null) return null;
|
||||
return cache.get(key);
|
||||
}
|
||||
if (cache === null) return null;
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
@@ -303,4 +303,4 @@ function tryJSON(value: string) {
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,16 @@ import { CacheEnabled, CacheGet, CacheSet } from "./cache.js";
|
||||
import type { Knex } from "knex";
|
||||
import * as uuid from "uuid";
|
||||
|
||||
const authProviders = await getAuthProviders();
|
||||
// Instead of using top-level await, declare a variable for providers
|
||||
let authProviders: Awaited<ReturnType<typeof 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);
|
||||
@@ -35,14 +44,14 @@ const getUser = async (
|
||||
)
|
||||
.where({
|
||||
"directus_users.external_identifier": externalIdentifier,
|
||||
"directus_users.provider": provider,
|
||||
"directus_users.provider": provider
|
||||
})
|
||||
.first();
|
||||
};
|
||||
|
||||
const insertUser = async (database: Knex, user: Record<string, any>): Promise<any> => {
|
||||
const insertUser = async (database: Knex, user: Record<string, string | undefined>): Promise<unknown> => {
|
||||
await database("directus_users").insert(user);
|
||||
return getUser(database, user.external_identifier, user.provider);
|
||||
return getUser(database, user.external_identifier, user.provider!);
|
||||
};
|
||||
|
||||
// TODO: optimize this function, reduce the amount of loops
|
||||
@@ -58,6 +67,8 @@ export async function getAccountabilityForToken(
|
||||
role: null,
|
||||
admin: false,
|
||||
app: false,
|
||||
roles: [],
|
||||
ip: null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,7 +76,7 @@ export async function getAccountabilityForToken(
|
||||
return accountability;
|
||||
}
|
||||
|
||||
const providers = authProviders.filter(
|
||||
const providers = (await initAuthProviders()).filter(
|
||||
(provider) =>
|
||||
provider.issuer_url && provider.issuer_url.includes(iss.toString())
|
||||
);
|
||||
@@ -100,7 +111,7 @@ export async function getAccountabilityForToken(
|
||||
id: uuid.v4(),
|
||||
role: provider.default_role_id,
|
||||
provider: provider.name,
|
||||
external_identifier: result.sub,
|
||||
external_identifier: result.sub
|
||||
});
|
||||
console.debug("Inserted new user:", user);
|
||||
}
|
||||
@@ -115,7 +126,7 @@ export async function getAccountabilityForToken(
|
||||
accountability.app = user.app_access === true || user.app_access == 1;
|
||||
|
||||
if (CacheEnabled() && result.sub) {
|
||||
CacheSet(result.sub, accountability);
|
||||
await CacheSet(result.sub, accountability);
|
||||
}
|
||||
|
||||
console.debug("Accountability set from database:", accountability);
|
||||
@@ -150,6 +161,9 @@ export async function getAccountabilityForToken(
|
||||
// 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;
|
||||
}
|
||||
|
||||
55
src/index.ts
55
src/index.ts
@@ -1,42 +1,35 @@
|
||||
import { defineHook } from '@directus/extensions-sdk';
|
||||
import { getAccountabilityForToken } from './external-jwt/get-accountability-for-token';
|
||||
import type { Request } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import type {HookConfig} from '@directus/extensions'
|
||||
import type { Accountability, EventContext } from '@directus/types';
|
||||
import { defineHook } from "@directus/extensions-sdk";
|
||||
import { getAccountabilityForToken } from "./external-jwt/get-accountability-for-token";
|
||||
import type { Request } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import type { Accountability, EventContext } from "@directus/types";
|
||||
|
||||
export default defineHook<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;
|
||||
export default defineHook(({ filter }) => {
|
||||
|
||||
if(!context.database) {
|
||||
return defaultAccountability
|
||||
}
|
||||
// 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;
|
||||
|
||||
|
||||
if (!context.database) {
|
||||
return defaultAccountability;
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
console.log("getting accountability for token", req.token, decodedToken?.iss, context.accountability, context.database);
|
||||
|
||||
|
||||
console.log("getting accountability for token", req.token, decodedToken?.iss, context.accountability, context.database);
|
||||
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) => {
|
||||
|
||||
})*/
|
||||
})*/
|
||||
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user