mirror of
https://github.com/directus/directus.git
synced 2026-01-23 03:08:08 -05:00
New OpenID and OAuth2 drivers (#8660)
* Moved over oauth impl to new interface * Fixed most build issues and started addind schema to auth drivers * Finished up OAuth2 and OpenID drivers * Removed unused migration and utils * Fixed minor todos * Removed old oauth flow * Changed oauth flow to re-use refresh token * Added new oauth frontend * Added font awesome social icons * Updated authentication documentation * Update api/src/auth/drivers/oauth2.ts * Tested implementation and fixed incorrect validation * Updated docs * Improved OAuth error handling and re-enabled creating users with provider/identifier * Removed Session config from docs * Update app/src/components/v-icon/v-icon.vue * Removed oauth need to define default roleID * Added FormatTitle to SSO links * Prevent local auth without password * Store OAuth access token in session data * Update docs/guides/api-config.md * Fixed copy and removed fontawesome-vue dependency * More docs fixes * Crucialy importend type fiks * Update package-lock * Remove is-email-allowed check In favor of more advanced version based on filtering coming later * Fix JSON type casting * Delete unused util * Update type signature to include name * Add warning when code isn't found in oauth url and remove obsolete imports * Auto-continue on successful SSO login * Tweak type signature * More type casting shenanigans * Please the TS gods * Check for missing token before crashing Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
@@ -104,10 +104,8 @@
|
||||
"execa": "^5.1.1",
|
||||
"exifr": "^7.1.2",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.2",
|
||||
"flat": "^5.0.2",
|
||||
"fs-extra": "^10.0.0",
|
||||
"grant": "^5.4.14",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql-compose": "^9.0.1",
|
||||
"inquirer": "^8.1.1",
|
||||
@@ -130,6 +128,7 @@
|
||||
"nodemailer": "^6.6.1",
|
||||
"object-hash": "^2.2.0",
|
||||
"openapi3-ts": "^2.0.0",
|
||||
"openid-client": "^4.9.0",
|
||||
"ora": "^5.4.0",
|
||||
"otplib": "^12.0.1",
|
||||
"pino": "6.13.3",
|
||||
|
||||
@@ -47,8 +47,8 @@ import { track } from './utils/track';
|
||||
import { validateEnv } from './utils/validate-env';
|
||||
import { validateStorage } from './utils/validate-storage';
|
||||
import { register as registerWebhooks } from './webhooks';
|
||||
import { session } from './middleware/session';
|
||||
import { flushCaches } from './cache';
|
||||
import { registerAuthProviders } from './auth';
|
||||
import { Url } from './utils/url';
|
||||
|
||||
export default async function createApp(): Promise<express.Application> {
|
||||
@@ -74,6 +74,8 @@ export default async function createApp(): Promise<express.Application> {
|
||||
|
||||
await flushCaches();
|
||||
|
||||
await registerAuthProviders();
|
||||
|
||||
const extensionManager = getExtensionManager();
|
||||
|
||||
await extensionManager.initialize();
|
||||
@@ -148,9 +150,6 @@ export default async function createApp(): Promise<express.Application> {
|
||||
app.use(rateLimiter);
|
||||
}
|
||||
|
||||
// We only rely on cookie-sessions in the oAuth flow where it's required
|
||||
app.use(session);
|
||||
|
||||
app.use(authenticate);
|
||||
|
||||
app.use(checkIP);
|
||||
|
||||
@@ -2,10 +2,12 @@ import getDatabase from './database';
|
||||
import env from './env';
|
||||
import logger from './logger';
|
||||
import { AuthDriver } from './auth/auth';
|
||||
import { LocalAuthDriver } from './auth/drivers/';
|
||||
import { LocalAuthDriver, OAuth2AuthDriver, OpenIDAuthDriver } from './auth/drivers';
|
||||
import { DEFAULT_AUTH_PROVIDER } from './constants';
|
||||
import { InvalidConfigException } from './exceptions';
|
||||
import { AuthDriverOptions } from './types';
|
||||
import { getConfigFromEnv } from './utils/get-config-from-env';
|
||||
import { getSchema } from './utils/get-schema';
|
||||
import { toArray } from '@directus/shared/utils';
|
||||
|
||||
const providerNames = toArray(env.AUTH_PROVIDERS);
|
||||
@@ -13,11 +15,6 @@ const providerNames = toArray(env.AUTH_PROVIDERS);
|
||||
const providers: Map<string, AuthDriver> = new Map();
|
||||
|
||||
export function getAuthProvider(provider: string): AuthDriver {
|
||||
// When providers haven't been registered yet
|
||||
if (providerNames.length !== providers.size) {
|
||||
registerProviders();
|
||||
}
|
||||
|
||||
if (!providers.has(provider)) {
|
||||
throw new InvalidConfigException('Auth provider not configured', { provider });
|
||||
}
|
||||
@@ -25,16 +22,12 @@ export function getAuthProvider(provider: string): AuthDriver {
|
||||
return providers.get(provider)!;
|
||||
}
|
||||
|
||||
function getProviderInstance(driver: string, config: Record<string, any>): AuthDriver | undefined {
|
||||
switch (driver) {
|
||||
case 'local':
|
||||
return new LocalAuthDriver(getDatabase(), config);
|
||||
}
|
||||
}
|
||||
export async function registerAuthProviders(): Promise<void> {
|
||||
const options = { knex: getDatabase(), schema: await getSchema() };
|
||||
const defaultProvider = getProviderInstance('local', options)!;
|
||||
|
||||
function registerProviders() {
|
||||
// Register default provider
|
||||
providers.set(DEFAULT_AUTH_PROVIDER, getProviderInstance('local', {}) as AuthDriver);
|
||||
providers.set(DEFAULT_AUTH_PROVIDER, defaultProvider);
|
||||
|
||||
if (!env.AUTH_PROVIDERS) {
|
||||
return;
|
||||
@@ -56,7 +49,7 @@ function registerProviders() {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = getProviderInstance(driver, { provider: name, ...config });
|
||||
const provider = getProviderInstance(driver, options, { provider: name, ...config });
|
||||
|
||||
if (!provider) {
|
||||
logger.warn(`Invalid "${driver}" auth driver.`);
|
||||
@@ -66,3 +59,20 @@ function registerProviders() {
|
||||
providers.set(name, provider);
|
||||
});
|
||||
}
|
||||
|
||||
function getProviderInstance(
|
||||
driver: string,
|
||||
options: AuthDriverOptions,
|
||||
config: Record<string, any> = {}
|
||||
): AuthDriver | undefined {
|
||||
switch (driver) {
|
||||
case 'local':
|
||||
return new LocalAuthDriver(options, config);
|
||||
|
||||
case 'oauth2':
|
||||
return new OAuth2AuthDriver(options, config);
|
||||
|
||||
case 'openid':
|
||||
return new OpenIDAuthDriver(options, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
import { User, SessionData } from '../types';
|
||||
import { AuthDriverOptions, SchemaOverview, User, SessionData } from '../types';
|
||||
|
||||
export abstract class AuthDriver {
|
||||
knex: Knex;
|
||||
schema: SchemaOverview;
|
||||
|
||||
constructor(knex: Knex, _config: Record<string, any>) {
|
||||
this.knex = knex;
|
||||
constructor(options: AuthDriverOptions, _config: Record<string, any>) {
|
||||
this.knex = options.knex;
|
||||
this.schema = options.schema;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,8 +48,9 @@ export abstract class AuthDriver {
|
||||
* @param _sessionData Session data
|
||||
* @throws InvalidCredentialsException
|
||||
*/
|
||||
async refresh(_user: User, _sessionData: SessionData): Promise<void> {
|
||||
async refresh(_user: User, sessionData: SessionData): Promise<SessionData> {
|
||||
/* Optional */
|
||||
return sessionData;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from './local';
|
||||
export * from './oauth2';
|
||||
export * from './openid';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import argon2 from 'argon2';
|
||||
import { AuthDriver } from '../auth';
|
||||
import { User } from '../../types';
|
||||
import { User, SessionData } from '../../types';
|
||||
import { InvalidCredentialsException, InvalidPayloadException } from '../../exceptions';
|
||||
import { AuthenticationService } from '../../services';
|
||||
import { Router } from 'express';
|
||||
@@ -11,9 +11,6 @@ import ms from 'ms';
|
||||
import { respond } from '../../middleware/respond';
|
||||
|
||||
export class LocalAuthDriver extends AuthDriver {
|
||||
/**
|
||||
* Get user id by email
|
||||
*/
|
||||
async getUserID(payload: Record<string, any>): Promise<string> {
|
||||
if (!payload.email) {
|
||||
throw new InvalidCredentialsException();
|
||||
@@ -32,19 +29,14 @@ export class LocalAuthDriver extends AuthDriver {
|
||||
return user.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify user password
|
||||
*/
|
||||
async verify(user: User, password?: string): Promise<void> {
|
||||
if (!user.password || !(await argon2.verify(user.password, password as string))) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
}
|
||||
|
||||
async login(user: User, payload: Record<string, any>): Promise<null> {
|
||||
if (payload.password) {
|
||||
await this.verify(user, payload.password);
|
||||
}
|
||||
async login(user: User, payload: Record<string, any>): Promise<SessionData> {
|
||||
await this.verify(user, payload.password);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
291
api/src/auth/drivers/oauth2.ts
Normal file
291
api/src/auth/drivers/oauth2.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import { Router } from 'express';
|
||||
import { Issuer, Client, generators, errors } from 'openid-client';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import ms from 'ms';
|
||||
import { LocalAuthDriver } from './local';
|
||||
import { getAuthProvider } from '../../auth';
|
||||
import env from '../../env';
|
||||
import { AuthenticationService, UsersService } from '../../services';
|
||||
import { AuthDriverOptions, User, AuthData, SessionData } from '../../types';
|
||||
import { InvalidCredentialsException, ServiceUnavailableException, InvalidConfigException } from '../../exceptions';
|
||||
import { respond } from '../../middleware/respond';
|
||||
import asyncHandler from '../../utils/async-handler';
|
||||
import { Url } from '../../utils/url';
|
||||
import logger from '../../logger';
|
||||
|
||||
export class OAuth2AuthDriver extends LocalAuthDriver {
|
||||
client: Client;
|
||||
redirectUrl: string;
|
||||
usersService: UsersService;
|
||||
config: Record<string, any>;
|
||||
|
||||
constructor(options: AuthDriverOptions, config: Record<string, any>) {
|
||||
super(options, config);
|
||||
|
||||
const { authorizeUrl, accessUrl, profileUrl, clientId, clientSecret, ...additionalConfig } = config;
|
||||
|
||||
if (!authorizeUrl || !accessUrl || !profileUrl || !clientId || !clientSecret || !additionalConfig.provider) {
|
||||
throw new InvalidConfigException('Invalid provider config', { provider: additionalConfig.provider });
|
||||
}
|
||||
|
||||
const redirectUrl = new Url(env.PUBLIC_URL).addPath('auth', 'login', additionalConfig.provider, 'callback');
|
||||
|
||||
this.redirectUrl = redirectUrl.toString();
|
||||
this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
|
||||
this.config = additionalConfig;
|
||||
|
||||
const issuer = new Issuer({
|
||||
authorization_endpoint: authorizeUrl,
|
||||
token_endpoint: accessUrl,
|
||||
userinfo_endpoint: profileUrl,
|
||||
issuer: additionalConfig.provider,
|
||||
});
|
||||
|
||||
this.client = new issuer.Client({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uris: [this.redirectUrl],
|
||||
response_types: ['code'],
|
||||
});
|
||||
}
|
||||
|
||||
generateCodeVerifier(): string {
|
||||
return generators.codeVerifier();
|
||||
}
|
||||
|
||||
generateAuthUrl(codeVerifier: string): string {
|
||||
try {
|
||||
return this.client.authorizationUrl({
|
||||
scope: this.config.scope ?? 'email',
|
||||
code_challenge: generators.codeChallenge(codeVerifier),
|
||||
code_challenge_method: 'S256',
|
||||
access_type: 'offline',
|
||||
});
|
||||
} catch (e) {
|
||||
throw handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchUserId(identifier: string): Promise<string | undefined> {
|
||||
const user = await this.knex
|
||||
.select('id')
|
||||
.from('directus_users')
|
||||
.whereRaw('LOWER(??) = ?', ['email', identifier.toLowerCase()])
|
||||
.orWhereRaw('LOWER(??) = ?', ['external_identifier', identifier.toLowerCase()])
|
||||
.first();
|
||||
|
||||
return user?.id;
|
||||
}
|
||||
|
||||
async getUserID(payload: Record<string, any>): Promise<string> {
|
||||
if (!payload.code || !payload.codeVerifier) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
let tokenSet;
|
||||
let userInfo;
|
||||
|
||||
try {
|
||||
tokenSet = await this.client.grant({
|
||||
grant_type: 'authorization_code',
|
||||
code: payload.code,
|
||||
redirect_uri: this.redirectUrl,
|
||||
code_verifier: payload.codeVerifier,
|
||||
});
|
||||
userInfo = await this.client.userinfo(tokenSet);
|
||||
} catch (e) {
|
||||
throw handleError(e);
|
||||
}
|
||||
|
||||
const { emailKey, identifierKey, allowPublicRegistration } = this.config;
|
||||
|
||||
const email = userInfo[emailKey ?? 'email'] as string | undefined;
|
||||
// Fallback to email if explicit identifier not found
|
||||
const identifier = (userInfo[identifierKey] as string | undefined) ?? email;
|
||||
|
||||
if (!identifier) {
|
||||
logger.warn(`Failed to find user identifier for provider "${this.config.provider}"`);
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
const userId = await this.fetchUserId(identifier);
|
||||
|
||||
if (userId) {
|
||||
// Update user refreshToken if provided
|
||||
if (tokenSet.refresh_token) {
|
||||
await this.usersService.updateOne(userId, {
|
||||
auth_data: JSON.stringify({ refreshToken: tokenSet.refresh_token }),
|
||||
});
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
// Is public registration allowed?
|
||||
if (!allowPublicRegistration) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
// If email matches identifier, don't set "external_identifier"
|
||||
const emailIsIdentifier = email?.toLowerCase() === identifier.toLowerCase();
|
||||
|
||||
await this.usersService.createOne({
|
||||
provider: this.config.provider,
|
||||
email: email,
|
||||
external_identifier: !emailIsIdentifier ? identifier : undefined,
|
||||
role: this.config.defaultRoleId,
|
||||
auth_data: tokenSet.refresh_token && JSON.stringify({ refreshToken: tokenSet.refresh_token }),
|
||||
});
|
||||
|
||||
return (await this.fetchUserId(identifier)) as string;
|
||||
}
|
||||
|
||||
async login(user: User, sessionData: SessionData): Promise<SessionData> {
|
||||
return this.refresh(user, sessionData);
|
||||
}
|
||||
|
||||
async refresh(user: User, sessionData: SessionData): Promise<SessionData> {
|
||||
let authData = user.auth_data as AuthData;
|
||||
|
||||
if (typeof authData === 'string') {
|
||||
try {
|
||||
authData = JSON.parse(authData);
|
||||
} catch {
|
||||
logger.warn(`Session data isn't valid JSON: ${authData}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!authData?.refreshToken) {
|
||||
return sessionData;
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenSet = await this.client.refresh(authData.refreshToken);
|
||||
return { accessToken: tokenSet.access_token };
|
||||
} catch (e) {
|
||||
throw handleError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = (e: any) => {
|
||||
if (e instanceof errors.OPError) {
|
||||
if (e.error === 'invalid_grant') {
|
||||
// Invalid token
|
||||
return new InvalidCredentialsException();
|
||||
}
|
||||
// Server response error
|
||||
return new ServiceUnavailableException('Service returned unexpected response', {
|
||||
service: 'openid',
|
||||
message: e.error_description,
|
||||
});
|
||||
} else if (e instanceof errors.RPError) {
|
||||
// Internal client error
|
||||
return new InvalidCredentialsException();
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
export function createOAuth2AuthRouter(providerName: string): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
(req, res) => {
|
||||
const provider = getAuthProvider(providerName) as OAuth2AuthDriver;
|
||||
const codeVerifier = provider.generateCodeVerifier();
|
||||
const token = jwt.sign({ verifier: codeVerifier, redirect: req.query.redirect }, env.SECRET as string, {
|
||||
expiresIn: '5m',
|
||||
issuer: 'directus',
|
||||
});
|
||||
|
||||
res.cookie(`oauth2.${providerName}`, token, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
return res.redirect(provider.generateAuthUrl(codeVerifier));
|
||||
},
|
||||
respond
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/callback',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const token = req.cookies[`oauth2.${providerName}`];
|
||||
|
||||
if (!token) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
const { verifier, redirect } = jwt.verify(token, env.SECRET as string, { issuer: 'directus' }) as {
|
||||
verifier: string;
|
||||
redirect: string;
|
||||
};
|
||||
|
||||
const authenticationService = new AuthenticationService({
|
||||
accountability: {
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
role: null,
|
||||
},
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
let authResponse;
|
||||
|
||||
try {
|
||||
res.clearCookie(`oauth2.${providerName}`);
|
||||
|
||||
const { code } = req.query;
|
||||
|
||||
if (!code) {
|
||||
logger.warn(`Couldn't extract oAuth2 code from query: ${JSON.stringify(req.query)}`);
|
||||
}
|
||||
|
||||
authResponse = await authenticationService.login(providerName, {
|
||||
code: req.query.code,
|
||||
codeVerifier: verifier,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.warn(error);
|
||||
|
||||
if (redirect) {
|
||||
let reason = 'UNKNOWN_EXCEPTION';
|
||||
|
||||
if (error instanceof ServiceUnavailableException) {
|
||||
reason = 'SERVICE_UNAVAILABLE';
|
||||
} else if (error instanceof InvalidCredentialsException) {
|
||||
reason = 'INVALID_USER';
|
||||
}
|
||||
|
||||
return res.redirect(`${redirect.split('?')[0]}?reason=${reason}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken, expires } = authResponse;
|
||||
|
||||
if (redirect) {
|
||||
res.cookie(env.REFRESH_TOKEN_COOKIE_NAME, refreshToken, {
|
||||
httpOnly: true,
|
||||
domain: env.REFRESH_TOKEN_COOKIE_DOMAIN,
|
||||
maxAge: ms(env.REFRESH_TOKEN_TTL as string),
|
||||
secure: env.REFRESH_TOKEN_COOKIE_SECURE ?? false,
|
||||
sameSite: (env.REFRESH_TOKEN_COOKIE_SAME_SITE as 'lax' | 'strict' | 'none') || 'strict',
|
||||
});
|
||||
|
||||
return res.redirect(redirect);
|
||||
}
|
||||
|
||||
res.locals.payload = {
|
||||
data: { access_token: accessToken, refresh_token: refreshToken, expires },
|
||||
};
|
||||
|
||||
next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
286
api/src/auth/drivers/openid.ts
Normal file
286
api/src/auth/drivers/openid.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { Router } from 'express';
|
||||
import { Issuer, Client, generators, errors } from 'openid-client';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import ms from 'ms';
|
||||
import { LocalAuthDriver } from './local';
|
||||
import { getAuthProvider } from '../../auth';
|
||||
import env from '../../env';
|
||||
import { AuthenticationService, UsersService } from '../../services';
|
||||
import { AuthDriverOptions, User, AuthData, SessionData } from '../../types';
|
||||
import { InvalidCredentialsException, ServiceUnavailableException, InvalidConfigException } from '../../exceptions';
|
||||
import { respond } from '../../middleware/respond';
|
||||
import asyncHandler from '../../utils/async-handler';
|
||||
import { Url } from '../../utils/url';
|
||||
import logger from '../../logger';
|
||||
|
||||
export class OpenIDAuthDriver extends LocalAuthDriver {
|
||||
client: Promise<Client>;
|
||||
redirectUrl: string;
|
||||
usersService: UsersService;
|
||||
config: Record<string, any>;
|
||||
|
||||
constructor(options: AuthDriverOptions, config: Record<string, any>) {
|
||||
super(options, config);
|
||||
|
||||
const { issuerUrl, clientId, clientSecret, ...additionalConfig } = config;
|
||||
|
||||
if (!issuerUrl || !clientId || !clientSecret || !additionalConfig.provider) {
|
||||
throw new InvalidConfigException('Invalid provider config', { provider: additionalConfig.provider });
|
||||
}
|
||||
|
||||
const redirectUrl = new Url(env.PUBLIC_URL).addPath('auth', 'login', additionalConfig.provider, 'callback');
|
||||
|
||||
this.redirectUrl = redirectUrl.toString();
|
||||
this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
|
||||
this.config = additionalConfig;
|
||||
this.client = new Promise((resolve, reject) => {
|
||||
Issuer.discover(issuerUrl)
|
||||
.then((issuer) => {
|
||||
resolve(
|
||||
new issuer.Client({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uris: [this.redirectUrl],
|
||||
response_types: ['code'],
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
generateCodeVerifier(): string {
|
||||
return generators.codeVerifier();
|
||||
}
|
||||
|
||||
async generateAuthUrl(codeVerifier: string): Promise<string> {
|
||||
try {
|
||||
const client = await this.client;
|
||||
return client.authorizationUrl({
|
||||
scope: this.config.scope ?? 'openid profile email',
|
||||
code_challenge: generators.codeChallenge(codeVerifier),
|
||||
code_challenge_method: 'S256',
|
||||
access_type: 'offline',
|
||||
});
|
||||
} catch (e) {
|
||||
throw handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchUserId(identifier: string): Promise<string | undefined> {
|
||||
const user = await this.knex
|
||||
.select('id')
|
||||
.from('directus_users')
|
||||
.whereRaw('LOWER(??) = ?', ['email', identifier.toLowerCase()])
|
||||
.orWhereRaw('LOWER(??) = ?', ['external_identifier', identifier.toLowerCase()])
|
||||
.first();
|
||||
|
||||
return user?.id;
|
||||
}
|
||||
|
||||
async getUserID(payload: Record<string, any>): Promise<string> {
|
||||
if (!payload.code || !payload.codeVerifier) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
let tokenSet;
|
||||
let userInfo;
|
||||
|
||||
try {
|
||||
const client = await this.client;
|
||||
tokenSet = await client.callback(
|
||||
this.redirectUrl,
|
||||
{ code: payload.code },
|
||||
{ code_verifier: payload.codeVerifier }
|
||||
);
|
||||
userInfo = await client.userinfo(tokenSet);
|
||||
} catch (e) {
|
||||
throw handleError(e);
|
||||
}
|
||||
|
||||
const { identifierKey, allowPublicRegistration, requireVerifiedEmail } = this.config;
|
||||
|
||||
const email = userInfo.email as string;
|
||||
// Fallback to email if explicit identifier not found
|
||||
const identifier = (userInfo[identifierKey ?? 'sub'] as string | undefined) ?? email;
|
||||
|
||||
if (!identifier) {
|
||||
logger.warn(`Failed to find user identifier for provider "${this.config.provider}"`);
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
const userId = await this.fetchUserId(identifier);
|
||||
|
||||
if (userId) {
|
||||
// Update user refreshToken if provided
|
||||
if (tokenSet.refresh_token) {
|
||||
await this.usersService.updateOne(userId, {
|
||||
auth_data: JSON.stringify({ refreshToken: tokenSet.refresh_token }),
|
||||
});
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
const isEmailVerified = !requireVerifiedEmail || userInfo.email_verified;
|
||||
|
||||
// Is public registration allowed?
|
||||
if (!allowPublicRegistration || !isEmailVerified) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
// If email matches identifier, don't set "external_identifier"
|
||||
const emailIsIdentifier = email?.toLowerCase() === identifier.toLowerCase();
|
||||
|
||||
await this.usersService.createOne({
|
||||
provider: this.config.provider,
|
||||
first_name: userInfo.given_name,
|
||||
last_name: userInfo.family_name,
|
||||
email: email,
|
||||
external_identifier: !emailIsIdentifier ? identifier : undefined,
|
||||
role: this.config.defaultRoleId,
|
||||
auth_data: tokenSet.refresh_token && JSON.stringify({ refreshToken: tokenSet.refresh_token }),
|
||||
});
|
||||
|
||||
return (await this.fetchUserId(identifier)) as string;
|
||||
}
|
||||
|
||||
async login(user: User, sessionData: SessionData): Promise<SessionData> {
|
||||
return this.refresh(user, sessionData);
|
||||
}
|
||||
|
||||
async refresh(user: User, sessionData: SessionData): Promise<SessionData> {
|
||||
let authData = user.auth_data as AuthData;
|
||||
|
||||
if (typeof authData === 'string') {
|
||||
try {
|
||||
authData = JSON.parse(authData);
|
||||
} catch {
|
||||
logger.warn(`Session data isn't valid JSON: ${authData}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!authData?.refreshToken) {
|
||||
return sessionData;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await this.client;
|
||||
const tokenSet = await client.refresh(authData.refreshToken);
|
||||
return { accessToken: tokenSet.access_token };
|
||||
} catch (e) {
|
||||
throw handleError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = (e: any) => {
|
||||
if (e instanceof errors.OPError) {
|
||||
if (e.error === 'invalid_grant') {
|
||||
// Invalid token
|
||||
return new InvalidCredentialsException();
|
||||
}
|
||||
// Server response error
|
||||
return new ServiceUnavailableException('Service returned unexpected response', {
|
||||
service: 'openid',
|
||||
message: e.error_description,
|
||||
});
|
||||
} else if (e instanceof errors.RPError) {
|
||||
// Internal client error
|
||||
return new InvalidCredentialsException();
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
export function createOpenIDAuthRouter(providerName: string): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res) => {
|
||||
const provider = getAuthProvider(providerName) as OpenIDAuthDriver;
|
||||
const codeVerifier = provider.generateCodeVerifier();
|
||||
const token = jwt.sign({ verifier: codeVerifier, redirect: req.query.redirect }, env.SECRET as string, {
|
||||
expiresIn: '5m',
|
||||
issuer: 'directus',
|
||||
});
|
||||
|
||||
res.cookie(`openid.${providerName}`, token, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
return res.redirect(await provider.generateAuthUrl(codeVerifier));
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/callback',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const token = req.cookies[`openid.${providerName}`];
|
||||
const { verifier, redirect } = jwt.verify(token, env.SECRET as string, { issuer: 'directus' }) as {
|
||||
verifier: string;
|
||||
redirect: string;
|
||||
};
|
||||
|
||||
const authenticationService = new AuthenticationService({
|
||||
accountability: {
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
role: null,
|
||||
},
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
let authResponse;
|
||||
|
||||
try {
|
||||
res.clearCookie(`openid.${providerName}`);
|
||||
|
||||
authResponse = await authenticationService.login(providerName, {
|
||||
code: req.query.code,
|
||||
codeVerifier: verifier,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.warn(error);
|
||||
|
||||
if (redirect) {
|
||||
let reason = 'UNKNOWN_EXCEPTION';
|
||||
|
||||
if (error instanceof ServiceUnavailableException) {
|
||||
reason = 'SERVICE_UNAVAILABLE';
|
||||
} else if (error instanceof InvalidCredentialsException) {
|
||||
reason = 'INVALID_USER';
|
||||
}
|
||||
|
||||
return res.redirect(`${redirect.split('?')[0]}?reason=${reason}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken, expires } = authResponse;
|
||||
|
||||
if (redirect) {
|
||||
res.cookie(env.REFRESH_TOKEN_COOKIE_NAME, refreshToken, {
|
||||
httpOnly: true,
|
||||
domain: env.REFRESH_TOKEN_COOKIE_DOMAIN,
|
||||
maxAge: ms(env.REFRESH_TOKEN_TTL as string),
|
||||
secure: env.REFRESH_TOKEN_COOKIE_SECURE ?? false,
|
||||
sameSite: (env.REFRESH_TOKEN_COOKIE_SAME_SITE as 'lax' | 'strict' | 'none') || 'strict',
|
||||
});
|
||||
|
||||
return res.redirect(redirect);
|
||||
}
|
||||
|
||||
res.locals.payload = {
|
||||
data: { access_token: accessToken, refresh_token: refreshToken, expires },
|
||||
};
|
||||
|
||||
next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -41,9 +41,9 @@ REFRESH_TOKEN_COOKIE_SAME_SITE="lax"
|
||||
REFRESH_TOKEN_COOKIE_NAME="directus_refresh_token"
|
||||
|
||||
####################################################################################################
|
||||
## SSO (OAuth) Providers
|
||||
## Auth Providers
|
||||
|
||||
OAUTH_PROVIDERS=""
|
||||
AUTH_PROVIDERS=""
|
||||
|
||||
####################################################################################################
|
||||
## Extensions
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import grant from 'grant';
|
||||
import ms from 'ms';
|
||||
import emitter, { emitAsyncSafe } from '../emitter';
|
||||
import env from '../env';
|
||||
import {
|
||||
InvalidCredentialsException,
|
||||
RouteNotFoundException,
|
||||
ServiceUnavailableException,
|
||||
InvalidPayloadException,
|
||||
} from '../exceptions';
|
||||
import grantConfig from '../grant';
|
||||
import { InvalidPayloadException } from '../exceptions';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { AuthenticationService, UsersService } from '../services';
|
||||
import asyncHandler from '../utils/async-handler';
|
||||
import { getAuthProviders } from '../utils/get-auth-providers';
|
||||
import getEmailFromProfile from '../utils/get-email-from-profile';
|
||||
import { toArray } from '@directus/shared/utils';
|
||||
import logger from '../logger';
|
||||
import { createLocalAuthRouter } from '../auth/drivers';
|
||||
import { createLocalAuthRouter, createOAuth2AuthRouter, createOpenIDAuthRouter } from '../auth/drivers';
|
||||
import { DEFAULT_AUTH_PROVIDER } from '../constants';
|
||||
|
||||
const router = Router();
|
||||
@@ -30,6 +20,15 @@ for (const authProvider of authProviders) {
|
||||
switch (authProvider.driver) {
|
||||
case 'local':
|
||||
authRouter = createLocalAuthRouter(authProvider.name);
|
||||
break;
|
||||
|
||||
case 'oauth2':
|
||||
authRouter = createOAuth2AuthRouter(authProvider.name);
|
||||
break;
|
||||
|
||||
case 'openid':
|
||||
authRouter = createOpenIDAuthRouter(authProvider.name);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!authRouter) {
|
||||
@@ -189,156 +188,4 @@ router.get(
|
||||
respond
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/oauth',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const providers = toArray(env.OAUTH_PROVIDERS);
|
||||
res.locals.payload = { data: env.OAUTH_PROVIDERS ? providers : null };
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/oauth/:provider',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const config = { ...grantConfig };
|
||||
delete config.defaults;
|
||||
|
||||
const availableProviders = Object.keys(config);
|
||||
|
||||
if (availableProviders.includes(req.params.provider) === false) {
|
||||
throw new RouteNotFoundException(`/auth/oauth/${req.params.provider}`);
|
||||
}
|
||||
|
||||
if (req.query?.redirect && req.session) {
|
||||
req.session.redirect = req.query.redirect as string;
|
||||
}
|
||||
|
||||
const hookPayload = {
|
||||
provider: req.params.provider,
|
||||
redirect: req.query?.redirect,
|
||||
};
|
||||
|
||||
emitAsyncSafe(`oauth.${req.params.provider}.redirect`, {
|
||||
event: `oauth.${req.params.provider}.redirect`,
|
||||
action: 'redirect',
|
||||
schema: req.schema,
|
||||
payload: hookPayload,
|
||||
accountability: req.accountability,
|
||||
user: null,
|
||||
});
|
||||
|
||||
await emitter.emitAsync(`oauth.${req.params.provider}.redirect.before`, {
|
||||
event: `oauth.${req.params.provider}.redirect.before`,
|
||||
action: 'redirect',
|
||||
schema: req.schema,
|
||||
payload: hookPayload,
|
||||
accountability: req.accountability,
|
||||
user: null,
|
||||
});
|
||||
|
||||
next();
|
||||
})
|
||||
);
|
||||
|
||||
router.use(grant.express()(grantConfig));
|
||||
|
||||
router.get(
|
||||
'/oauth/:provider/callback',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const redirect = req.session.redirect;
|
||||
|
||||
const accountability = {
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
role: null,
|
||||
};
|
||||
|
||||
const authenticationService = new AuthenticationService({
|
||||
accountability: accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
let authResponse: { accessToken: any; refreshToken: any; expires: any; id?: any };
|
||||
|
||||
const hookPayload = req.session.grant.response;
|
||||
|
||||
await emitter.emitAsync(`oauth.${req.params.provider}.login.before`, hookPayload, {
|
||||
event: `oauth.${req.params.provider}.login.before`,
|
||||
action: 'oauth.login',
|
||||
schema: req.schema,
|
||||
payload: hookPayload,
|
||||
accountability: accountability,
|
||||
status: 'pending',
|
||||
user: null,
|
||||
});
|
||||
|
||||
const emitStatus = (status: 'fail' | 'success') => {
|
||||
emitAsyncSafe(`oauth.${req.params.provider}.login`, hookPayload, {
|
||||
event: `oauth.${req.params.provider}.login`,
|
||||
action: 'oauth.login',
|
||||
schema: req.schema,
|
||||
payload: hookPayload,
|
||||
accountability: accountability,
|
||||
status,
|
||||
user: null,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const email = getEmailFromProfile(req.params.provider, req.session.grant.response?.profile);
|
||||
|
||||
req.session?.destroy(() => {
|
||||
// Do nothing
|
||||
});
|
||||
|
||||
// Workaround to use the default local auth provider to validate
|
||||
// the email and login without a password.
|
||||
authResponse = await authenticationService.login(DEFAULT_AUTH_PROVIDER, { email });
|
||||
} catch (error: any) {
|
||||
emitStatus('fail');
|
||||
|
||||
logger.warn(error);
|
||||
|
||||
if (redirect) {
|
||||
let reason = 'UNKNOWN_EXCEPTION';
|
||||
|
||||
if (error instanceof ServiceUnavailableException) {
|
||||
reason = 'SERVICE_UNAVAILABLE';
|
||||
} else if (error instanceof InvalidCredentialsException) {
|
||||
reason = 'INVALID_USER';
|
||||
}
|
||||
|
||||
return res.redirect(`${redirect.split('?')[0]}?reason=${reason}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken, expires } = authResponse;
|
||||
|
||||
emitStatus('success');
|
||||
|
||||
if (redirect) {
|
||||
res.cookie(env.REFRESH_TOKEN_COOKIE_NAME, refreshToken, {
|
||||
httpOnly: true,
|
||||
domain: env.REFRESH_TOKEN_COOKIE_DOMAIN,
|
||||
maxAge: ms(env.REFRESH_TOKEN_TTL as string),
|
||||
secure: env.REFRESH_TOKEN_COOKIE_SECURE ?? false,
|
||||
sameSite: (env.REFRESH_TOKEN_COOKIE_SAME_SITE as 'lax' | 'strict' | 'none') || 'strict',
|
||||
});
|
||||
|
||||
return res.redirect(redirect);
|
||||
} else {
|
||||
res.locals.payload = {
|
||||
data: { access_token: accessToken, refresh_token: refreshToken, expires },
|
||||
};
|
||||
|
||||
return next();
|
||||
}
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
13
api/src/database/migrations/20211009A-add-auth-data.ts
Normal file
13
api/src/database/migrations/20211009A-add-auth-data.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_users', (table) => {
|
||||
table.json('auth_data');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_users', (table) => {
|
||||
table.dropColumn('auth_data');
|
||||
});
|
||||
}
|
||||
@@ -154,7 +154,6 @@ fields:
|
||||
- field: provider
|
||||
width: half
|
||||
interface: select-dropdown
|
||||
readonly: true
|
||||
options:
|
||||
choices:
|
||||
- text: $t:default_provider
|
||||
@@ -165,4 +164,6 @@ fields:
|
||||
options:
|
||||
iconRight: account_circle
|
||||
interface: input
|
||||
readonly: true
|
||||
|
||||
- field: auth_data
|
||||
hidden: true
|
||||
|
||||
@@ -30,8 +30,6 @@ const defaults: Record<string, any> = {
|
||||
RATE_LIMITER_DURATION: 1,
|
||||
RATE_LIMITER_STORE: 'memory',
|
||||
|
||||
SESSION_STORE: 'memory',
|
||||
|
||||
ACCESS_TOKEN_TTL: '15m',
|
||||
REFRESH_TOKEN_TTL: '7d',
|
||||
REFRESH_TOKEN_COOKIE_SECURE: false,
|
||||
@@ -58,8 +56,6 @@ const defaults: Record<string, any> = {
|
||||
|
||||
AUTH_PROVIDERS: '',
|
||||
|
||||
OAUTH_PROVIDERS: '',
|
||||
|
||||
EXTENSIONS_PATH: './extensions',
|
||||
|
||||
EMAIL_FROM: 'no-reply@directus.io',
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Grant is the oAuth library
|
||||
*/
|
||||
|
||||
import env from './env';
|
||||
import { toArray } from '@directus/shared/utils';
|
||||
import { getConfigFromEnv } from './utils/get-config-from-env';
|
||||
|
||||
const enabledProviders = toArray(env.OAUTH_PROVIDERS).map((provider) => provider.toLowerCase());
|
||||
|
||||
const config: any = {
|
||||
defaults: {
|
||||
origin: env.PUBLIC_URL,
|
||||
transport: 'session',
|
||||
prefix: '/auth/oauth',
|
||||
response: ['tokens', 'profile'],
|
||||
},
|
||||
};
|
||||
|
||||
for (const provider of enabledProviders) {
|
||||
config[provider] = getConfigFromEnv(`OAUTH_${provider.toUpperCase()}_`, undefined, 'underscore');
|
||||
}
|
||||
|
||||
export default config;
|
||||
@@ -1,28 +0,0 @@
|
||||
import expressSession, { Store } from 'express-session';
|
||||
import env from '../env';
|
||||
import { getConfigFromEnv } from '../utils/get-config-from-env';
|
||||
import getDatabase from '../database';
|
||||
let store: Store | undefined = undefined;
|
||||
|
||||
if (env.SESSION_STORE === 'redis') {
|
||||
const Redis = require('ioredis');
|
||||
const RedisStore = require('connect-redis')(expressSession);
|
||||
|
||||
const redisClient = new Redis(env.SESSION_REDIS || getConfigFromEnv('SESSION_REDIS_'));
|
||||
store = new RedisStore({ client: redisClient });
|
||||
}
|
||||
|
||||
if (env.SESSION_STORE === 'memcache') {
|
||||
const MemcachedStore = require('connect-memcached')(expressSession);
|
||||
store = new MemcachedStore(getConfigFromEnv('SESSION_MEMCACHE_'));
|
||||
}
|
||||
|
||||
if (env.SESSION_STORE === 'database') {
|
||||
const KnexSessionStore = require('connect-session-knex')(expressSession);
|
||||
store = new KnexSessionStore({
|
||||
knex: getDatabase(),
|
||||
tablename: 'oauth_sessions', // optional. Defaults to 'sessions'
|
||||
});
|
||||
}
|
||||
|
||||
export const session = expressSession({ store, secret: env.SECRET as string, saveUninitialized: false, resave: false });
|
||||
@@ -14,9 +14,10 @@ import { TFAService } from './tfa';
|
||||
import { AbstractServiceOptions, Action, SchemaOverview, Session, User, SessionData } from '../types';
|
||||
import { Accountability } from '@directus/shared/types';
|
||||
import { SettingsService } from './settings';
|
||||
import { merge, clone, cloneDeep } from 'lodash';
|
||||
import { merge, clone, cloneDeep, omit } from 'lodash';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { stall } from '../utils/stall';
|
||||
import logger from '../logger';
|
||||
|
||||
const loginAttemptsLimiter = createRateLimiter({ duration: 0 });
|
||||
|
||||
@@ -60,7 +61,8 @@ export class AuthenticationService {
|
||||
'role',
|
||||
'tfa_secret',
|
||||
'provider',
|
||||
'external_identifier'
|
||||
'external_identifier',
|
||||
'auth_data'
|
||||
)
|
||||
.from('directus_users')
|
||||
.where('id', await provider.getUserID(cloneDeep(payload)))
|
||||
@@ -191,7 +193,7 @@ export class AuthenticationService {
|
||||
expires: refreshTokenExpiration,
|
||||
ip: this.accountability?.ip,
|
||||
user_agent: this.accountability?.userAgent,
|
||||
data: sessionData,
|
||||
data: sessionData && JSON.stringify(sessionData),
|
||||
});
|
||||
|
||||
await this.knex('directus_sessions').delete().where('expires', '<', new Date());
|
||||
@@ -242,7 +244,8 @@ export class AuthenticationService {
|
||||
'u.status',
|
||||
'u.role',
|
||||
'u.provider',
|
||||
'u.external_identifier'
|
||||
'u.external_identifier',
|
||||
'u.auth_data'
|
||||
)
|
||||
.from('directus_sessions as s')
|
||||
.innerJoin('directus_users as u', 's.user', 'u.id')
|
||||
@@ -253,10 +256,20 @@ export class AuthenticationService {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
const { data: sessionData, ...user } = record;
|
||||
let { data: sessionData } = record;
|
||||
const user = omit(record, 'data');
|
||||
|
||||
if (typeof sessionData === 'string') {
|
||||
try {
|
||||
sessionData = JSON.parse(sessionData);
|
||||
} catch {
|
||||
logger.warn(`Session data isn't valid JSON: ${sessionData}`);
|
||||
}
|
||||
}
|
||||
|
||||
const provider = getAuthProvider(user.provider);
|
||||
await provider.refresh(clone(user), sessionData);
|
||||
|
||||
const newSessionData = await provider.refresh(clone(user), sessionData as SessionData);
|
||||
|
||||
const accessToken = jwt.sign({ id: user.id }, env.SECRET as string, {
|
||||
expiresIn: env.ACCESS_TOKEN_TTL,
|
||||
@@ -267,7 +280,11 @@ export class AuthenticationService {
|
||||
const refreshTokenExpiration = new Date(Date.now() + ms(env.REFRESH_TOKEN_TTL as string));
|
||||
|
||||
await this.knex('directus_sessions')
|
||||
.update({ token: newRefreshToken, expires: refreshTokenExpiration })
|
||||
.update({
|
||||
token: newRefreshToken,
|
||||
expires: refreshTokenExpiration,
|
||||
data: newSessionData && JSON.stringify(newSessionData),
|
||||
})
|
||||
.where({ token: refreshToken });
|
||||
|
||||
await this.knex('directus_users').update({ last_access: new Date() }).where({ id: user.id });
|
||||
@@ -292,6 +309,7 @@ export class AuthenticationService {
|
||||
'u.role',
|
||||
'u.provider',
|
||||
'u.external_identifier',
|
||||
'u.auth_data',
|
||||
's.data'
|
||||
)
|
||||
.from('directus_sessions as s')
|
||||
@@ -300,10 +318,19 @@ export class AuthenticationService {
|
||||
.first();
|
||||
|
||||
if (record) {
|
||||
const { data: sessionData, ...user } = record;
|
||||
let { data: sessionData } = record;
|
||||
const user = omit(record, 'data');
|
||||
|
||||
if (typeof sessionData === 'string') {
|
||||
try {
|
||||
sessionData = JSON.parse(sessionData);
|
||||
} catch {
|
||||
logger.warn(`Session data isn't valid JSON: ${sessionData}`);
|
||||
}
|
||||
}
|
||||
|
||||
const provider = getAuthProvider(user.provider);
|
||||
await provider.logout(clone(user), sessionData);
|
||||
await provider.logout(clone(user), sessionData as SessionData);
|
||||
|
||||
await this.knex.delete().from('directus_sessions').where('token', refreshToken);
|
||||
}
|
||||
@@ -320,7 +347,8 @@ export class AuthenticationService {
|
||||
'status',
|
||||
'role',
|
||||
'provider',
|
||||
'external_identifier'
|
||||
'external_identifier',
|
||||
'auth_data'
|
||||
)
|
||||
.from('directus_users')
|
||||
.where('id', userID)
|
||||
|
||||
@@ -82,21 +82,23 @@ export class UsersService extends ItemsService {
|
||||
fields: ['auth_password_policy'],
|
||||
});
|
||||
|
||||
if (policyRegExString) {
|
||||
const wrapped = policyRegExString.startsWith('/') && policyRegExString.endsWith('/');
|
||||
const regex = new RegExp(wrapped ? policyRegExString.slice(1, -1) : policyRegExString);
|
||||
if (!policyRegExString) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const password of passwords) {
|
||||
if (regex.test(password) === false) {
|
||||
throw new FailedValidationException({
|
||||
message: `Provided password doesn't match password policy`,
|
||||
path: ['password'],
|
||||
type: 'custom.pattern.base',
|
||||
context: {
|
||||
value: password,
|
||||
},
|
||||
});
|
||||
}
|
||||
const wrapped = policyRegExString.startsWith('/') && policyRegExString.endsWith('/');
|
||||
const regex = new RegExp(wrapped ? policyRegExString.slice(1, -1) : policyRegExString);
|
||||
|
||||
for (const password of passwords) {
|
||||
if (!regex.test(password)) {
|
||||
throw new FailedValidationException({
|
||||
message: `Provided password doesn't match password policy`,
|
||||
path: ['password'],
|
||||
type: 'custom.pattern.base',
|
||||
context: {
|
||||
value: password,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,16 +143,6 @@ export class UsersService extends ItemsService {
|
||||
await this.checkPasswordPolicy(passwords);
|
||||
}
|
||||
|
||||
for (const user of data) {
|
||||
if (user.provider !== undefined) {
|
||||
throw new InvalidPayloadException(`You can't set the "provider" value manually.`);
|
||||
}
|
||||
|
||||
if (user.external_identifier !== undefined) {
|
||||
throw new InvalidPayloadException(`You can't set the "external_identifier" value manually.`);
|
||||
}
|
||||
}
|
||||
|
||||
return await super.createMany(data, opts);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { Knex } from 'knex';
|
||||
import { SchemaOverview } from './schema';
|
||||
|
||||
export interface AuthDriverOptions {
|
||||
knex: Knex;
|
||||
schema: SchemaOverview;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
first_name: string | null;
|
||||
@@ -8,12 +16,15 @@ export interface User {
|
||||
role: string | null;
|
||||
provider: string;
|
||||
external_identifier: string | null;
|
||||
auth_data: string | Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export type SessionData = Record<string, any> | null;
|
||||
export type AuthData = Record<string, any> | null;
|
||||
|
||||
export interface Session {
|
||||
token: string;
|
||||
expires: Date;
|
||||
data: SessionData;
|
||||
data: string | null;
|
||||
}
|
||||
|
||||
export type SessionData = Record<string, any> | null;
|
||||
|
||||
10
api/src/types/express-session.d.ts
vendored
10
api/src/types/express-session.d.ts
vendored
@@ -1,10 +0,0 @@
|
||||
import { SessionData } from 'express-session';
|
||||
|
||||
export = SessionData;
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
redirect: string;
|
||||
grant: any;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import env from '../env';
|
||||
interface AuthProvider {
|
||||
name: string;
|
||||
driver: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export function getAuthProviders(): AuthProvider[] {
|
||||
@@ -12,5 +13,6 @@ export function getAuthProviders(): AuthProvider[] {
|
||||
.map((provider) => ({
|
||||
name: provider,
|
||||
driver: env[`AUTH_${provider.toUpperCase()}_DRIVER`],
|
||||
icon: env[`AUTH_${provider.toUpperCase()}_ICON`],
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { get } from 'lodash';
|
||||
import env from '../env';
|
||||
import { ServiceUnavailableException } from '../exceptions';
|
||||
|
||||
// The path in JSON to fetch the email address from the profile.
|
||||
// Note: a lot of services use `email` as the path. We fall back to that as default, so no need to
|
||||
// map it here
|
||||
const profileMap: Record<string, string> = {};
|
||||
|
||||
/**
|
||||
* Extract the email address from a given user profile coming from a providers API
|
||||
*
|
||||
* Falls back to OAUTH_<PROVIDER>_PROFILE_EMAIL if we don't have it preconfigured yet, and defaults
|
||||
* to `email` if nothing is set
|
||||
*
|
||||
* This is used in the SSO flow to extract the users
|
||||
*/
|
||||
export default function getEmailFromProfile(provider: string, profile: Record<string, any>): string {
|
||||
const path = profileMap[provider] || env[`OAUTH_${provider.toUpperCase()}_PROFILE_EMAIL`] || 'email';
|
||||
|
||||
const email = get(profile, path);
|
||||
|
||||
if (!email) {
|
||||
throw new ServiceUnavailableException(
|
||||
`Email address not found. Profile "${JSON.stringify(profile)}", path: "${path}"`,
|
||||
{
|
||||
service: 'oauth',
|
||||
provider,
|
||||
profile,
|
||||
path,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
@@ -54,8 +54,8 @@ async function getEnvInfo(event: string) {
|
||||
email: {
|
||||
transport: env.EMAIL_TRANSPORT,
|
||||
},
|
||||
oauth: {
|
||||
providers: env.OAUTH_PROVIDERS.split(',')
|
||||
auth: {
|
||||
providers: env.AUTH_PROVIDERS.split(',')
|
||||
.map((v: string) => v.trim())
|
||||
.filter((v: string) => v),
|
||||
},
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"@directus/extensions-sdk": "9.0.0-rc.98",
|
||||
"@directus/format-title": "9.0.0-rc.98",
|
||||
"@directus/shared": "9.0.0-rc.98",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fullcalendar/core": "5.10.0",
|
||||
"@fullcalendar/daygrid": "5.10.0",
|
||||
"@fullcalendar/interaction": "5.10.0",
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
@click="emitClick"
|
||||
>
|
||||
<component :is="customIconName" v-if="customIconName" />
|
||||
<socialIcon v-else-if="socialIconName" :name="socialIconName" />
|
||||
<i v-else :class="{ filled }">{{ name }}</i>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { defineComponent, computed, h, PropType } from 'vue';
|
||||
import { library, icon, findIconDefinition, IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import { fab } from '@fortawesome/free-brands-svg-icons';
|
||||
import useSizeClass, { sizeProps } from '@/composables/size-class';
|
||||
|
||||
import CustomIconDirectus from './custom-icons/directus.vue';
|
||||
@@ -35,6 +38,8 @@ import CustomIconFolderMove from './custom-icons/folder_move.vue';
|
||||
import CustomIconFolderLock from './custom-icons/folder_lock.vue';
|
||||
import CustomIconLogout from './custom-icons/logout.vue';
|
||||
|
||||
library.add(fab);
|
||||
|
||||
const customIcons: string[] = [
|
||||
'directus',
|
||||
'bookmark_save',
|
||||
@@ -56,6 +61,478 @@ const customIcons: string[] = [
|
||||
'logout',
|
||||
];
|
||||
|
||||
const socialIcons: string[] = [
|
||||
'500px',
|
||||
'accessible_icon',
|
||||
'accusoft',
|
||||
'acquisitions_incorporated',
|
||||
'adn',
|
||||
'adversal',
|
||||
'affiliatetheme',
|
||||
'airbnb',
|
||||
'algolia',
|
||||
'alipay',
|
||||
'amazon',
|
||||
'amazon_pay',
|
||||
'amilia',
|
||||
'android',
|
||||
'angellist',
|
||||
'angrycreative',
|
||||
'angular',
|
||||
'app_store',
|
||||
'app_store_ios',
|
||||
'apper',
|
||||
'apple',
|
||||
'apple_pay',
|
||||
'artstation',
|
||||
'asymmetrik',
|
||||
'atlassian',
|
||||
'audible',
|
||||
'autoprefixer',
|
||||
'avianex',
|
||||
'aviato',
|
||||
'aws',
|
||||
'bandcamp',
|
||||
'battle_net',
|
||||
'behance',
|
||||
'behance_square',
|
||||
'bimobject',
|
||||
'bitbucket',
|
||||
'bitcoin',
|
||||
'bity',
|
||||
'black_tie',
|
||||
'blackberry',
|
||||
'blogger',
|
||||
'blogger_b',
|
||||
'bluetooth',
|
||||
'bluetooth_b',
|
||||
'bootstrap',
|
||||
'btc',
|
||||
'buffer',
|
||||
'buromobelexperte',
|
||||
'buy_n_large',
|
||||
'buysellads',
|
||||
'canadian_maple_leaf',
|
||||
'cc_amazon_pay',
|
||||
'cc_amex',
|
||||
'cc_apple_pay',
|
||||
'cc_diners_club',
|
||||
'cc_discover',
|
||||
'cc_jcb',
|
||||
'cc_mastercard',
|
||||
'cc_paypal',
|
||||
'cc_stripe',
|
||||
'cc_visa',
|
||||
'centercode',
|
||||
'centos',
|
||||
'chrome',
|
||||
'chromecast',
|
||||
'cloudflare',
|
||||
'cloudscale',
|
||||
'cloudsmith',
|
||||
'cloudversify',
|
||||
'codepen',
|
||||
'codiepie',
|
||||
'confluence',
|
||||
'connectdevelop',
|
||||
'contao',
|
||||
'cotton_bureau',
|
||||
'cpanel',
|
||||
'creative_commons',
|
||||
'creative_commons_by',
|
||||
'creative_commons_nc',
|
||||
'creative_commons_nc_eu',
|
||||
'creative_commons_nc_jp',
|
||||
'creative_commons_nd',
|
||||
'creative_commons_pd',
|
||||
'creative_commons_pd_alt',
|
||||
'creative_commons_remix',
|
||||
'creative_commons_sa',
|
||||
'creative_commons_sampling',
|
||||
'creative_commons_sampling_plus',
|
||||
'creative_commons_share',
|
||||
'creative_commons_zero',
|
||||
'critical_role',
|
||||
'css3',
|
||||
'css3_alt',
|
||||
'cuttlefish',
|
||||
'd_and_d',
|
||||
'd_and_d_beyond',
|
||||
'dailymotion',
|
||||
'dashcube',
|
||||
'deezer',
|
||||
'delicious',
|
||||
'deploydog',
|
||||
'deskpro',
|
||||
'dev',
|
||||
'deviantart',
|
||||
'dhl',
|
||||
'diaspora',
|
||||
'digg',
|
||||
'digital_ocean',
|
||||
'discord',
|
||||
'discourse',
|
||||
'dochub',
|
||||
'docker',
|
||||
'draft2digital',
|
||||
'dribbble',
|
||||
'dribbble_square',
|
||||
'dropbox',
|
||||
'drupal',
|
||||
'dyalog',
|
||||
'earlybirds',
|
||||
'ebay',
|
||||
'edge',
|
||||
'edge_legacy',
|
||||
'elementor',
|
||||
'ello',
|
||||
'ember',
|
||||
'empire',
|
||||
'envira',
|
||||
'erlang',
|
||||
'ethereum',
|
||||
'etsy',
|
||||
'evernote',
|
||||
'expeditedssl',
|
||||
'facebook',
|
||||
'facebook_f',
|
||||
'facebook_messenger',
|
||||
'facebook_square',
|
||||
'fantasy_flight_games',
|
||||
'fedex',
|
||||
'fedora',
|
||||
'figma',
|
||||
'firefox',
|
||||
'firefox_browser',
|
||||
'first_order',
|
||||
'first_order_alt',
|
||||
'firstdraft',
|
||||
'flickr',
|
||||
'flipboard',
|
||||
'fly',
|
||||
'font_awesome',
|
||||
'font_awesome_alt',
|
||||
'font_awesome_flag',
|
||||
'fonticons',
|
||||
'fonticons_fi',
|
||||
'fort_awesome',
|
||||
'fort_awesome_alt',
|
||||
'forumbee',
|
||||
'foursquare',
|
||||
'free_code_camp',
|
||||
'freebsd',
|
||||
'fulcrum',
|
||||
'galactic_republic',
|
||||
'galactic_senate',
|
||||
'get_pocket',
|
||||
'gg',
|
||||
'gg_circle',
|
||||
'git',
|
||||
'git_alt',
|
||||
'git_square',
|
||||
'github',
|
||||
'github_alt',
|
||||
'github_square',
|
||||
'gitkraken',
|
||||
'gitlab',
|
||||
'gitter',
|
||||
'glide',
|
||||
'glide_g',
|
||||
'gofore',
|
||||
'goodreads',
|
||||
'goodreads_g',
|
||||
'google',
|
||||
'google_drive',
|
||||
'google_pay',
|
||||
'google_play',
|
||||
'google_plus',
|
||||
'google_plus_g',
|
||||
'google_plus_square',
|
||||
'google_wallet',
|
||||
'gratipay',
|
||||
'grav',
|
||||
'gripfire',
|
||||
'grunt',
|
||||
'guilded',
|
||||
'gulp',
|
||||
'hacker_news',
|
||||
'hacker_news_square',
|
||||
'hackerrank',
|
||||
'hips',
|
||||
'hire_a_helper',
|
||||
'hive',
|
||||
'hooli',
|
||||
'hornbill',
|
||||
'hotjar',
|
||||
'houzz',
|
||||
'html5',
|
||||
'hubspot',
|
||||
'ideal',
|
||||
'imdb',
|
||||
'innosoft',
|
||||
'instagram',
|
||||
'instagram_square',
|
||||
'instalod',
|
||||
'intercom',
|
||||
'internet_explorer',
|
||||
'invision',
|
||||
'ioxhost',
|
||||
'itch_io',
|
||||
'itunes',
|
||||
'itunes_note',
|
||||
'java',
|
||||
'jedi_order',
|
||||
'jenkins',
|
||||
'jira',
|
||||
'joget',
|
||||
'joomla',
|
||||
'js',
|
||||
'js_square',
|
||||
'jsfiddle',
|
||||
'kaggle',
|
||||
'keybase',
|
||||
'keycdn',
|
||||
'kickstarter',
|
||||
'kickstarter_k',
|
||||
'korvue',
|
||||
'laravel',
|
||||
'lastfm',
|
||||
'lastfm_square',
|
||||
'leanpub',
|
||||
'less',
|
||||
'line',
|
||||
'linkedin',
|
||||
'linkedin_in',
|
||||
'linode',
|
||||
'linux',
|
||||
'lyft',
|
||||
'magento',
|
||||
'mailchimp',
|
||||
'mandalorian',
|
||||
'markdown',
|
||||
'mastodon',
|
||||
'maxcdn',
|
||||
'mdb',
|
||||
'medapps',
|
||||
'medium',
|
||||
'medium_m',
|
||||
'medrt',
|
||||
'meetup',
|
||||
'megaport',
|
||||
'mendeley',
|
||||
'microblog',
|
||||
'microsoft',
|
||||
'mix',
|
||||
'mixcloud',
|
||||
'mixer',
|
||||
'mizuni',
|
||||
'modx',
|
||||
'monero',
|
||||
'napster',
|
||||
'neos',
|
||||
'nimblr',
|
||||
'node',
|
||||
'node_js',
|
||||
'npm',
|
||||
'ns8',
|
||||
'nutritionix',
|
||||
'octopus_deploy',
|
||||
'odnoklassniki',
|
||||
'odnoklassniki_square',
|
||||
'old_republic',
|
||||
'opencart',
|
||||
'openid',
|
||||
'opera',
|
||||
'optin_monster',
|
||||
'orcid',
|
||||
'osi',
|
||||
'page4',
|
||||
'pagelines',
|
||||
'palfed',
|
||||
'patreon',
|
||||
'paypal',
|
||||
'penny_arcade',
|
||||
'perbyte',
|
||||
'periscope',
|
||||
'phabricator',
|
||||
'phoenix_framework',
|
||||
'phoenix_squadron',
|
||||
'php',
|
||||
'pied_piper',
|
||||
'pied_piper_alt',
|
||||
'pied_piper_hat',
|
||||
'pied_piper_pp',
|
||||
'pied_piper_square',
|
||||
'pinterest',
|
||||
'pinterest_p',
|
||||
'pinterest_square',
|
||||
'playstation',
|
||||
'product_hunt',
|
||||
'pushed',
|
||||
'python',
|
||||
'qq',
|
||||
'quinscape',
|
||||
'quora',
|
||||
'r_project',
|
||||
'raspberry_pi',
|
||||
'ravelry',
|
||||
'react',
|
||||
'reacteurope',
|
||||
'readme',
|
||||
'rebel',
|
||||
'red_river',
|
||||
'reddit',
|
||||
'reddit_alien',
|
||||
'reddit_square',
|
||||
'redhat',
|
||||
'renren',
|
||||
'replyd',
|
||||
'researchgate',
|
||||
'resolving',
|
||||
'rev',
|
||||
'rocketchat',
|
||||
'rockrms',
|
||||
'rust',
|
||||
'safari',
|
||||
'salesforce',
|
||||
'sass',
|
||||
'schlix',
|
||||
'scribd',
|
||||
'searchengin',
|
||||
'sellcast',
|
||||
'sellsy',
|
||||
'servicestack',
|
||||
'shirtsinbulk',
|
||||
'shopify',
|
||||
'shopware',
|
||||
'simplybuilt',
|
||||
'sistrix',
|
||||
'sith',
|
||||
'sketch',
|
||||
'skyatlas',
|
||||
'skype',
|
||||
'slack',
|
||||
'slack_hash',
|
||||
'slideshare',
|
||||
'snapchat',
|
||||
'snapchat_ghost',
|
||||
'snapchat_square',
|
||||
'soundcloud',
|
||||
'sourcetree',
|
||||
'speakap',
|
||||
'speaker_deck',
|
||||
'spotify',
|
||||
'squarespace',
|
||||
'stack_exchange',
|
||||
'stack_overflow',
|
||||
'stackpath',
|
||||
'staylinked',
|
||||
'steam',
|
||||
'steam_square',
|
||||
'steam_symbol',
|
||||
'sticker_mule',
|
||||
'strava',
|
||||
'stripe',
|
||||
'stripe_s',
|
||||
'studiovinari',
|
||||
'stumbleupon',
|
||||
'stumbleupon_circle',
|
||||
'superpowers',
|
||||
'supple',
|
||||
'suse',
|
||||
'swift',
|
||||
'symfony',
|
||||
'teamspeak',
|
||||
'telegram',
|
||||
'telegram_plane',
|
||||
'tencent_weibo',
|
||||
'the_red_yeti',
|
||||
'themeco',
|
||||
'themeisle',
|
||||
'think_peaks',
|
||||
'tiktok',
|
||||
'trade_federation',
|
||||
'trello',
|
||||
'tumblr',
|
||||
'tumblr_square',
|
||||
'twitch',
|
||||
'twitter',
|
||||
'twitter_square',
|
||||
'typo3',
|
||||
'uber',
|
||||
'ubuntu',
|
||||
'uikit',
|
||||
'umbraco',
|
||||
'uncharted',
|
||||
'uniregistry',
|
||||
'unity',
|
||||
'unsplash',
|
||||
'untappd',
|
||||
'ups',
|
||||
'usb',
|
||||
'usps',
|
||||
'ussunnah',
|
||||
'vaadin',
|
||||
'viacoin',
|
||||
'viadeo',
|
||||
'viadeo_square',
|
||||
'viber',
|
||||
'vimeo',
|
||||
'vimeo_square',
|
||||
'vimeo_v',
|
||||
'vine',
|
||||
'vk',
|
||||
'vnv',
|
||||
'vuejs',
|
||||
'watchman_monitoring',
|
||||
'waze',
|
||||
'weebly',
|
||||
'weibo',
|
||||
'weixin',
|
||||
'whatsapp',
|
||||
'whatsapp_square',
|
||||
'whmcs',
|
||||
'wikipedia_w',
|
||||
'windows',
|
||||
'wix',
|
||||
'wizards_of_the_coast',
|
||||
'wodu',
|
||||
'wolf_pack_battalion',
|
||||
'wordpress',
|
||||
'wordpress_simple',
|
||||
'wpbeginner',
|
||||
'wpexplorer',
|
||||
'wpforms',
|
||||
'wpressr',
|
||||
'xbox',
|
||||
'xing',
|
||||
'xing_square',
|
||||
'y_combinator',
|
||||
'yahoo',
|
||||
'yammer',
|
||||
'yandex',
|
||||
'yandex_international',
|
||||
'yarn',
|
||||
'yelp',
|
||||
'yoast',
|
||||
'youtube',
|
||||
'youtube_square',
|
||||
'zhihu',
|
||||
];
|
||||
|
||||
const SocialIcon = defineComponent({
|
||||
props: {
|
||||
name: {
|
||||
type: String as PropType<IconName>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const socialIcon = icon(findIconDefinition({ prefix: 'fab', iconName: this.name }));
|
||||
return h({ template: socialIcon?.html[0] });
|
||||
},
|
||||
});
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
CustomIconDirectus,
|
||||
@@ -76,6 +553,7 @@ export default defineComponent({
|
||||
CustomIconFolderMove,
|
||||
CustomIconFolderLock,
|
||||
CustomIconLogout,
|
||||
SocialIcon,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
@@ -125,9 +603,15 @@ export default defineComponent({
|
||||
return null;
|
||||
});
|
||||
|
||||
const socialIconName = computed<string | null>(() => {
|
||||
if (socialIcons.includes(props.name)) return props.name.replace(/_/g, '-');
|
||||
return null;
|
||||
});
|
||||
|
||||
return {
|
||||
sizeClass,
|
||||
customIconName,
|
||||
socialIconName,
|
||||
emitClick,
|
||||
};
|
||||
|
||||
@@ -182,6 +666,11 @@ body {
|
||||
display: inline-block;
|
||||
color: inherit;
|
||||
fill: currentColor;
|
||||
|
||||
&.svg-inline--fa {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-click {
|
||||
|
||||
@@ -69,3 +69,5 @@ export const MODULE_BAR_DEFAULT = [
|
||||
locked: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const AUTH_SSO_DRIVERS = ['oauth2', 'openid'];
|
||||
|
||||
@@ -305,7 +305,13 @@ export default defineComponent({
|
||||
];
|
||||
|
||||
const fieldsFiltered = computed(() => {
|
||||
return fields.value.filter((field: Field) => fieldsDenyList.includes(field.field) === false);
|
||||
return fields.value.filter((field: Field) => {
|
||||
// These fields should only be editable when creating new users
|
||||
if (!isNew.value && ['provider', 'external_identifier'].includes(field.field)) {
|
||||
field.meta.readonly = true;
|
||||
}
|
||||
return !fieldsDenyList.includes(field.field);
|
||||
});
|
||||
});
|
||||
|
||||
const { formFields } = useFormFields(fieldsFiltered);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { defineComponent, ref, onMounted } from 'vue';
|
||||
|
||||
import api from '@/api';
|
||||
import { hydrate } from '@/hydrate';
|
||||
@@ -37,6 +37,12 @@ export default defineComponent({
|
||||
|
||||
fetchUser();
|
||||
|
||||
onMounted(() => {
|
||||
if ('continue' in router.currentRoute.value.query) {
|
||||
hydrateAndLogin();
|
||||
}
|
||||
});
|
||||
|
||||
return { t, name, lastPage, loading, hydrateAndLogin };
|
||||
|
||||
async function fetchUser() {
|
||||
|
||||
@@ -17,19 +17,20 @@
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<sso-links />
|
||||
<sso-links :providers="providers" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, ref, computed, watch } from 'vue';
|
||||
import { defineComponent, ref, computed, watch, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import ssoLinks from '../sso-links.vue';
|
||||
import { login } from '@/auth';
|
||||
import { RequestError } from '@/api';
|
||||
import api, { RequestError } from '@/api';
|
||||
import { translateAPIError } from '@/lang';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
|
||||
type Credentials = {
|
||||
email: string;
|
||||
@@ -50,8 +51,11 @@ export default defineComponent({
|
||||
const error = ref<RequestError | string | null>(null);
|
||||
const otp = ref<string | null>(null);
|
||||
const requiresTFA = ref(false);
|
||||
const providers = ref([]);
|
||||
const userStore = useUserStore();
|
||||
|
||||
onMounted(() => fetchProviders());
|
||||
|
||||
watch(email, () => {
|
||||
if (requiresTFA.value === true) requiresTFA.value = false;
|
||||
});
|
||||
@@ -68,7 +72,28 @@ export default defineComponent({
|
||||
return null;
|
||||
});
|
||||
|
||||
return { t, errorFormatted, error, email, password, onSubmit, loggingIn, translateAPIError, otp, requiresTFA };
|
||||
return {
|
||||
t,
|
||||
errorFormatted,
|
||||
error,
|
||||
email,
|
||||
password,
|
||||
onSubmit,
|
||||
loggingIn,
|
||||
translateAPIError,
|
||||
otp,
|
||||
requiresTFA,
|
||||
providers,
|
||||
};
|
||||
|
||||
async function fetchProviders() {
|
||||
try {
|
||||
const response = await api.get('/auth');
|
||||
providers.value = response.data.data;
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (email.value === null || password.value === null) return;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<template>
|
||||
<div class="sso-links">
|
||||
<template v-if="providers && providers.length > 0">
|
||||
<template v-if="ssoProviders.length > 0">
|
||||
<v-divider />
|
||||
|
||||
<a v-for="provider in providers" :key="provider.name" class="sso-link" :href="provider.link">
|
||||
{{ t('log_in_with', { provider: provider.name }) }}
|
||||
<a v-for="provider in ssoProviders" :key="provider.name" class="sso-link" :href="provider.link">
|
||||
<div class="sso-icon">
|
||||
<v-icon :name="provider.icon" />
|
||||
</div>
|
||||
<div class="sso-title">
|
||||
{{ t('log_in_with', { provider: provider.name }) }}
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
@@ -12,40 +17,40 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, ref, onMounted } from 'vue';
|
||||
import api from '@/api';
|
||||
import { defineComponent, ref, watch, toRefs, PropType } from 'vue';
|
||||
import { AuthProvider } from '@/types';
|
||||
import { AUTH_SSO_DRIVERS } from '@/constants';
|
||||
import { getRootPath } from '@/utils/get-root-path';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
import formatTitle from '@directus/format-title';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
props: {
|
||||
providers: {
|
||||
type: Array as PropType<AuthProvider[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const providers = ref([]);
|
||||
const loading = ref(false);
|
||||
const { providers } = toRefs(props);
|
||||
|
||||
onMounted(() => fetchProviders());
|
||||
const ssoProviders = ref<{ name: string; link: string; icon: string }[]>([]);
|
||||
|
||||
return { t, providers };
|
||||
watch(providers, () => {
|
||||
ssoProviders.value = providers.value
|
||||
.filter((provider: AuthProvider) => AUTH_SSO_DRIVERS.includes(provider.driver))
|
||||
.map((provider: AuthProvider) => ({
|
||||
name: formatTitle(provider.name),
|
||||
link: `${getRootPath()}auth/login/${provider.name}?redirect=${window.location.href.replace(
|
||||
location.search,
|
||||
'?continue'
|
||||
)}`,
|
||||
icon: provider.icon ?? 'account_circle',
|
||||
}));
|
||||
});
|
||||
|
||||
async function fetchProviders() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get('/auth/oauth/');
|
||||
|
||||
providers.value = response.data.data?.map((providerName: string) => {
|
||||
return {
|
||||
name: providerName,
|
||||
link: `${getRootPath()}auth/oauth/${providerName.toLowerCase()}?redirect=${window.location.href}`,
|
||||
};
|
||||
});
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
return { t, ssoProviders };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -56,19 +61,39 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.sso-link {
|
||||
display: block;
|
||||
$sso-link-border-width: 2px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: var(--input-height);
|
||||
text-align: center;
|
||||
background-color: var(--background-normal);
|
||||
border: $sso-link-border-width var(--background-normal) solid;
|
||||
border-radius: var(--border-radius);
|
||||
transition: background var(--fast) var(--transition);
|
||||
transition: border-color var(--fast) var(--transition);
|
||||
|
||||
.sso-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--input-height);
|
||||
margin: -$sso-link-border-width;
|
||||
background-color: var(--background-normal-alt);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
span {
|
||||
--v-icon-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.sso-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px 0 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-normal-alt);
|
||||
border-color: var(--background-normal-alt);
|
||||
}
|
||||
|
||||
& + & {
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './collections';
|
||||
export * from './error';
|
||||
export * from './insights';
|
||||
export * from './notifications';
|
||||
export * from './login';
|
||||
|
||||
5
app/src/types/login.ts
Normal file
5
app/src/types/login.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface AuthProvider {
|
||||
driver: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
}
|
||||
@@ -54,55 +54,60 @@ possible to cache assets for way longer than you would with the actual content.
|
||||
ASSETS_CACHE_TTL="7d"
|
||||
```
|
||||
|
||||
## oAuth (Single Sign-On (SSO) / OpenID)
|
||||
## Auth
|
||||
|
||||
Directus' oAuth integration provides a powerful alternative way to authenticate into your project. Directus will ask you
|
||||
to login on the external service, and if your user exists in Directus, you'll be logged in automatically.
|
||||
#### Multiple Providers
|
||||
|
||||
Directus relies on [`grant`](https://www.npmjs.com/package/grant) for the handling of the oAuth flow. This means that
|
||||
there's hundreds of services that are supported out of the box. For example, enabling logging in through GitHub is as
|
||||
easy as creating an [oAuth app in GitHub](https://github.com/settings/developers) and adding the following to Directus:
|
||||
You can configure multiple providers for handling authentication in Directus. This allows for different options when
|
||||
logging in. To do this, you can provide a CSV of provider names, and provide a config block for each of them:
|
||||
|
||||
```
|
||||
OAUTH_PROVIDERS="github"
|
||||
OAUTH_GITHUB_KEY="99d3...c3c4"
|
||||
OAUTH_GITHUB_SECRET="34ae...f963"
|
||||
AUTH_PROVIDERS="google,adobe"
|
||||
|
||||
AUTH_GOOGLE_DRIVER="openid"
|
||||
AUTH_GOOGLE_CLIENT_ID="<google_application_id>"
|
||||
AUTH_GOOGLE_CLIENT_SECRET= "<google_application_secret_key>"
|
||||
AUTH_GOOGLE_ISSUER_URL="https://accounts.google.com"
|
||||
AUTH_GOOGLE_ALLOW_PUBLIC_REGISTRATION="true"
|
||||
AUTH_GOOGLE_DEFAULT_ROLE_ID="<directus_role_id>"
|
||||
AUTH_GOOGLE_ICON="google"
|
||||
|
||||
AUTH_ADOBE_DRIVER="oauth2"
|
||||
AUTH_ADOBE_CLIENT_ID="<adobe_application_id>"
|
||||
AUTH_ADOBE_CLIENT_SECRET="<adobe_application_secret_key>"
|
||||
AUTH_ADOBE_AUTHORIZE_URL="https://ims-na1.adobelogin.com/ims/authorize/v2"
|
||||
AUTH_ADOBE_ACCESS_URL="https://ims-na1.adobelogin.com/ims/token/v3"
|
||||
AUTH_ADOBE_PROFILE_URL="https://ims-na1.adobelogin.com/ims/userinfo/v2"
|
||||
AUTH_ADOBE_ALLOW_PUBLIC_REGISTRATION="true"
|
||||
AUTH_ADOBE_DEFAULT_ROLE_ID="<directus_role_id>"
|
||||
AUTH_ADOBE_ICON="adobe"
|
||||
```
|
||||
|
||||
### OAuth 2.0 and OpenID
|
||||
|
||||
Directus' OAuth 2.0 and OpenID integrations provide powerful alternative ways to authenticate into your project.
|
||||
Directus will ask you to login on the external service, and return authenticated with a Directus account linked to that
|
||||
service.
|
||||
|
||||
Directus supports hundreds of OAuth 2.0 and OpenID services, but requires some configuration to authenticate users
|
||||
correctly. For example, enabling authentication through GitHub requires creating an
|
||||
[OAuth 2.0 app in GitHub](https://github.com/settings/developers) and adding the following configuration to Directus:
|
||||
|
||||
```
|
||||
AUTH_PROVIDERS="github"
|
||||
AUTH_GITHUB_CLIENT_ID="99d3...c3c4"
|
||||
AUTH_GITHUB_CLIENT_SECRET="34ae...f963"
|
||||
AUTH_GITHUB_AUTHORIZE_URL="https://github.com/login/oauth/authorize"
|
||||
AUTH_GITHUB_ACCESS_URL="https://github.com/login/oauth/access_token"
|
||||
AUTH_GITHUB_PROFILE_URL="https://api.github.com/user"
|
||||
```
|
||||
|
||||
::: warning PUBLIC_URL
|
||||
|
||||
The oAuth flow relies on the `PUBLIC_URL` variable for it's redirecting. Make sure that variable is configured
|
||||
correctly.
|
||||
These flows rely on the `PUBLIC_URL` variable for redirecting. Make sure that variable is configured correctly.
|
||||
|
||||
:::
|
||||
|
||||
#### Multiple Providers
|
||||
|
||||
`OAUTH_PROVIDERS` accepts a CSV of providers, allowing you to specify multiple at the same time:
|
||||
|
||||
```
|
||||
OAUTH_PROVIDERS ="google,microsoft"
|
||||
|
||||
OAUTH_GOOGLE_KEY = "<google_application_id>"
|
||||
OAUTH_GOOGLE_SECRET= "<google_application_secret_key>"
|
||||
OAUTH_GOOGLE_SCOPE="openid email"
|
||||
|
||||
OAUTH_MICROSOFT_KEY = "<microsoft_application_id>"
|
||||
OAUTH_MICROSOFT_SECRET = "<microsoft_application_secret_key>"
|
||||
OAUTH_MICROSOFT_SCOPE = "openid email"
|
||||
OAUTH_MICROSOFT_AUTHORIZE_URL = "https://login.microsoftonline.com/<microsoft_application_id>/oauth2/v2.0/authorize"
|
||||
OAUTH_MICROSOFT_ACCESS_URL = "https://login.microsoftonline.com/<microsoft_application_id>/oauth2/v2.0/token"
|
||||
|
||||
PUBLIC_URL = "<public_url_of_directus_instance>"
|
||||
```
|
||||
|
||||
### Provider Specific Configuration
|
||||
|
||||
If you use one of the many supported providers, you often don't have to configure any more than just the key and secret
|
||||
for the service. That being said, if you use a more tailored service (like the specific Microsoft application in the
|
||||
example above), you might have to provide more configuration values yourself. Please see
|
||||
https://github.com/simov/grant#configuration-description for a list of all available configuration flags.
|
||||
|
||||
## File Storage
|
||||
|
||||
By default, Directus stores every file you upload locally on disk. Instead of local file storage, you can configure
|
||||
|
||||
@@ -72,39 +72,37 @@ module.exports = function registerHook({ exceptions }) {
|
||||
|
||||
### Event Format Options
|
||||
|
||||
| Scope | Actions | Before |
|
||||
| ------------------------------- | ------------------------------------------------------------------ | ---------------- |
|
||||
| `cron()` | [See below for configuration](#interval-cron) | No |
|
||||
| `cli.init` | `before` and `after` | No |
|
||||
| `server` | `start` and `stop` | Optional |
|
||||
| `init` | | Optional |
|
||||
| `routes.init` | `before` and `after` | No |
|
||||
| `routes.custom.init` | `before` and `after` | No |
|
||||
| `middlewares.init` | `before` and `after` | No |
|
||||
| `request` | `not_found` | No |
|
||||
| `response` | | No<sup>[1]</sup> |
|
||||
| `database.error` | When a database error is thrown | No |
|
||||
| `error` | | No |
|
||||
| `auth` | `login`, `logout`<sup>[1]</sup>, `jwt` and `refresh`<sup>[1]</sup> | Optional |
|
||||
| `oauth.:provider`<sup>[2]</sup> | `login` and `redirect` | Optional |
|
||||
| `items` | `read`<sup>[3]</sup>, `create`, `update` and `delete` | Optional |
|
||||
| `activity` | `create`, `update` and `delete` | Optional |
|
||||
| `collections` | `create`, `update` and `delete` | Optional |
|
||||
| `fields` | `create`, `update` and `delete` | Optional |
|
||||
| `files` | `upload`<sup>[3]</sup> | No |
|
||||
| `folders` | `create`, `update` and `delete` | Optional |
|
||||
| `permissions` | `create`, `update` and `delete` | Optional |
|
||||
| `presets` | `create`, `update` and `delete` | Optional |
|
||||
| `relations` | `create`, `update` and `delete` | Optional |
|
||||
| `revisions` | `create`, `update` and `delete` | Optional |
|
||||
| `roles` | `create`, `update` and `delete` | Optional |
|
||||
| `settings` | `create`, `update` and `delete` | Optional |
|
||||
| `users` | `create`, `update` and `delete` | Optional |
|
||||
| `webhooks` | `create`, `update` and `delete` | Optional |
|
||||
| Scope | Actions | Before |
|
||||
| -------------------- | ------------------------------------------------------------------ | ---------------- |
|
||||
| `cron()` | [See below for configuration](#interval-cron) | No |
|
||||
| `cli.init` | `before` and `after` | No |
|
||||
| `server` | `start` and `stop` | Optional |
|
||||
| `init` | | Optional |
|
||||
| `routes.init` | `before` and `after` | No |
|
||||
| `routes.custom.init` | `before` and `after` | No |
|
||||
| `middlewares.init` | `before` and `after` | No |
|
||||
| `request` | `not_found` | No |
|
||||
| `response` | | No<sup>[1]</sup> |
|
||||
| `database.error` | When a database error is thrown | No |
|
||||
| `error` | | No |
|
||||
| `auth` | `login`, `logout`<sup>[1]</sup>, `jwt` and `refresh`<sup>[1]</sup> | Optional |
|
||||
| `items` | `read`<sup>[2]</sup>, `create`, `update` and `delete` | Optional |
|
||||
| `activity` | `create`, `update` and `delete` | Optional |
|
||||
| `collections` | `create`, `update` and `delete` | Optional |
|
||||
| `fields` | `create`, `update` and `delete` | Optional |
|
||||
| `files` | `upload`<sup>[2]</sup> | No |
|
||||
| `folders` | `create`, `update` and `delete` | Optional |
|
||||
| `permissions` | `create`, `update` and `delete` | Optional |
|
||||
| `presets` | `create`, `update` and `delete` | Optional |
|
||||
| `relations` | `create`, `update` and `delete` | Optional |
|
||||
| `revisions` | `create`, `update` and `delete` | Optional |
|
||||
| `roles` | `create`, `update` and `delete` | Optional |
|
||||
| `settings` | `create`, `update` and `delete` | Optional |
|
||||
| `users` | `create`, `update` and `delete` | Optional |
|
||||
| `webhooks` | `create`, `update` and `delete` | Optional |
|
||||
|
||||
<sup>1</sup> Feature Coming Soon\
|
||||
<sup>2</sup> oAuth provider name can replaced with wildcard for any oauth providers `oauth.*.login`\
|
||||
<sup>3</sup> Doesn't support `.before` modifier
|
||||
<sup>2</sup> Doesn't support `.before` modifier
|
||||
|
||||
#### Interval (cron)
|
||||
|
||||
@@ -196,7 +194,7 @@ receive the primary key(s) of the items but the query used:
|
||||
|
||||
#### Auth
|
||||
|
||||
The `auth` and `oauth` hooks have the following context properties:
|
||||
The `auth` hooks have the following context properties:
|
||||
|
||||
- `event` — Full event string
|
||||
- `accountability` — Information about the current user
|
||||
@@ -204,9 +202,7 @@ The `auth` and `oauth` hooks have the following context properties:
|
||||
- `payload` — Payload of the request
|
||||
- `schema` - The current API schema in use
|
||||
- `status` - One of `pending`, `success`, `fail`
|
||||
- `user` <sup>†</sup> - ID of the user that tried logging in/has logged in
|
||||
|
||||
<sup>†</sup> Not available in `oauth`
|
||||
- `user` - ID of the user that tried logging in/has logged in
|
||||
|
||||
## 5. Restart the API
|
||||
|
||||
|
||||
@@ -80,6 +80,10 @@ The token's expiration time can be configured through
|
||||
POST /auth/login
|
||||
```
|
||||
|
||||
```
|
||||
POST /auth/login/:provider
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "admin@example.com",
|
||||
@@ -331,17 +335,16 @@ mutation {
|
||||
|
||||
---
|
||||
|
||||
## List oAuth providers
|
||||
## List Auth Providers
|
||||
|
||||
List all the configured oAuth providers.
|
||||
List all the configured auth providers.
|
||||
|
||||
<div class="two-up">
|
||||
<div class="left">
|
||||
|
||||
::: tip Configuring oAuth
|
||||
::: tip Configuring auth providers
|
||||
|
||||
To learn more about setting up oAuth providers, see
|
||||
[Configuring SSO through oAuth](/guides/api-config/#oauth-single-sign-on-sso-openid).
|
||||
To learn more about setting up auth providers, see [Configuring auth providers](/guides/api-config/#auth).
|
||||
|
||||
:::
|
||||
|
||||
@@ -350,7 +353,7 @@ To learn more about setting up oAuth providers, see
|
||||
<div class="definitions">
|
||||
|
||||
`data` **Array**\
|
||||
Array of configured oAuth providers.
|
||||
Array of configured auth providers.
|
||||
|
||||
</div>
|
||||
|
||||
@@ -358,12 +361,27 @@ Array of configured oAuth providers.
|
||||
<div class="right">
|
||||
|
||||
```
|
||||
GET /auth/oauth
|
||||
GET /auth
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"data": ["GitHub", "Google", "Okta"]
|
||||
"data": [
|
||||
{
|
||||
"name": "GitHub",
|
||||
"driver": "oauth2",
|
||||
"icon": "github"
|
||||
},
|
||||
{
|
||||
"name": "Google",
|
||||
"driver": "openid",
|
||||
"icon": "google"
|
||||
},
|
||||
{
|
||||
"name": "Okta",
|
||||
"driver": "openid"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -372,15 +390,15 @@ GET /auth/oauth
|
||||
|
||||
---
|
||||
|
||||
## Login using oAuth provider
|
||||
## Login Using SSO Providers
|
||||
|
||||
Will redirect to the configured oAuth provider for the user to login.
|
||||
Will redirect to the configured SSO provider for the user to login.
|
||||
|
||||
<div class="two-up">
|
||||
<div class="right">
|
||||
|
||||
```
|
||||
GET /auth/oauth/:provider
|
||||
GET /auth/login/:provider
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
@@ -196,40 +196,6 @@ Alternatively, you can provide the individual connection parameters:
|
||||
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `CACHE_MEMCACHE` | Location of your memcache instance. You can use [`array:` syntax](#environment-syntax-prefix), eg: `array:<instance-1>,<instance-2>` for multiple memcache instances. | --- |
|
||||
|
||||
## Sessions
|
||||
|
||||
Sessions are only used in the oAuth authentication flow.
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
| --------------- | ------------------------------------------------------------------------------------ | ------------- |
|
||||
| `SESSION_STORE` | Where to store the session data. Either `memory`, `redis`, `memcache` or `database`. | `memory` |
|
||||
|
||||
Based on the `SESSION_STORE` used, you must also provide the following configurations:
|
||||
|
||||
### Memory
|
||||
|
||||
No additional configuration required.
|
||||
|
||||
### Redis
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
| --------------- | --------------------------------------------------------------------- | ------------- |
|
||||
| `SESSION_REDIS` | Redis connection string, eg: `redis://:authpassword@127.0.0.1:6380/4` | --- |
|
||||
|
||||
Alternatively, you can provide the individual connection parameters:
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
| ------------------------ | -------------------------------- | ------------- |
|
||||
| `SESSION_REDIS_HOST` | Hostname of the Redis instance | -- |
|
||||
| `SESSION_REDIS_PORT` | Port of the Redis instance | -- |
|
||||
| `SESSION_REDIS_PASSWORD` | Password for your Redis instance | -- |
|
||||
|
||||
### Memcache
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `SESSION_MEMCACHE_HOSTS` | Location of your memcache instance. You can use [`array:` syntax](#environment-syntax-prefix), eg: `array:<instance-1>,<instance-2>` for multiple memcache instances. | --- |
|
||||
|
||||
### Database
|
||||
|
||||
No additional configuration required.
|
||||
@@ -301,28 +267,57 @@ STORAGE_LOCAL_ROOT="./uploads"
|
||||
Image transformations can be fairly heavy on memory usage. If you're using a system with 1GB or less available memory,
|
||||
we recommend lowering the allowed concurrent transformations to prevent you from overflowing your server.
|
||||
|
||||
## OAuth
|
||||
## Auth
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
| ----------------- | --------------------------------------- | ------------- |
|
||||
| `OAUTH_PROVIDERS` | CSV of oAuth providers you want to use. | -- |
|
||||
| Variable | Description | Default Value |
|
||||
| ---------------- | -------------------------------------- | ------------- |
|
||||
| `AUTH_PROVIDERS` | CSV of auth providers you want to use. | -- |
|
||||
|
||||
For each of the OAuth providers you list, you must also provide a number of extra variables. These differ per external
|
||||
service. The following is a list of common required configuration options:
|
||||
For each of the auth providers you list, you must provide the following configuration:
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------- |
|
||||
| `OAUTH_<PROVIDER>_KEY` | oAuth key (a.k.a. application id) for the external service. | -- |
|
||||
| `OAUTH_<PROVIDER>_SECRET` | oAuth secret for the external service. | -- |
|
||||
| `OAUTH_<PROVIDER>_SCOPE` | A white-space separated list of privileges directus should ask for. A common value is: `openid email`. | -- |
|
||||
| `OAUTH_<PROVIDER>_AUTHORIZE_URL` | The authorize page URL of the external service | -- |
|
||||
| `OAUTH_<PROVIDER>_ACCESS_URL` | The access URL of the external service | -- |
|
||||
| `OAUTH_<PROVIDER>_PROFILE_URL` | Where Directus can fetch the profile information of the authenticated user. | -- |
|
||||
| Variable | Description | Default Value |
|
||||
| ------------------------ | ------------------------------------------------------- | ------------- |
|
||||
| `AUTH_<PROVIDER>_DRIVER` | Which driver to use, either `local`, `oauth2`, `openid` | -- |
|
||||
|
||||
Directus relies on [`grant`](https://www.npmjs.com/package/grant) for the handling of the oAuth flow. Grant includes
|
||||
[a lot of default values](https://github.com/simov/grant/blob/master/config/oauth.json) for popular services. For
|
||||
example, if you use `apple` as one of your providers, you only have to specify the key and secret, as Grant has the rest
|
||||
covered. Checkout [the grant repo](https://github.com/simov/grant) for more information.
|
||||
You must also provide a number of extra variables. These differ per auth driver service. The following is a list of
|
||||
common required configuration options:
|
||||
|
||||
### Local (`local`)
|
||||
|
||||
No additional configuration required.
|
||||
|
||||
### OAuth 2.0 (`oauth2`)
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
| ------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------- |
|
||||
| `AUTH_<PROVIDER>_CLIENT_ID` | OAuth identifier for the external service. | -- |
|
||||
| `AUTH_<PROVIDER>_CLIENT_SECRET` | OAUth secret for the external service. | -- |
|
||||
| `AUTH_<PROVIDER>_SCOPE` | A white-space separated list of privileges Directus will request. | `email` |
|
||||
| `AUTH_<PROVIDER>_AUTHORIZE_URL` | The authorize page URL of the external service. | -- |
|
||||
| `AUTH_<PROVIDER>_ACCESS_URL` | The token access URL of the external service. | -- |
|
||||
| `AUTH_<PROVIDER>_PROFILE_URL` | Where Directus can fetch the profile information of the authenticated user. | -- |
|
||||
| `AUTH_<PROVIDER>_EMAIL_KEY` | OAuth profile email key used to verify the user. | `email` |
|
||||
| `AUTH_<PROVIDER>_IDENTIFIER_KEY` | OAuth profile identifier key used to verify the user. Can be used in place of `EMAIL_KEY`. | -- |
|
||||
| `AUTH_<PROVIDER>_ALLOW_PUBLIC_REGISTRATION` | Whether to allow public registration of authenticating users. | `false` |
|
||||
| `AUTH_<PROVIDER>_DEFAULT_ROLE_ID` | Directus role ID to assign to users. | -- |
|
||||
| `AUTH_<PROVIDER>_ICON` | SVG icon to display with the login link. | `account_circle` |
|
||||
|
||||
If possible, OpenID is preferred over OAuth 2.0 as it provides better verification and consistent user information,
|
||||
allowing more complete user registrations.
|
||||
|
||||
### OpenID (`openid`)
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
| ------------------------------------------- | ----------------------------------------------------------------- | ---------------------- |
|
||||
| `AUTH_<PROVIDER>_CLIENT_ID` | OpenID identifier for the external service. | -- |
|
||||
| `AUTH_<PROVIDER>_CLIENT_SECRET` | OpenID secret for the external service. | -- |
|
||||
| `AUTH_<PROVIDER>_SCOPE` | A white-space separated list of privileges Directus will request. | `openid profile email` |
|
||||
| `AUTH_<PROVIDER>_ISSUER_URL` | The OpenID `.well-known` Discovery Document URL. | -- |
|
||||
| `AUTH_<PROVIDER>_IDENTIFIER_KEY` | OpenID profile identifier key used to verify the user. | `sub` |
|
||||
| `AUTH_<PROVIDER>_ALLOW_PUBLIC_REGISTRATION` | Whether to allow public registration of authenticating users. | `false` |
|
||||
| `AUTH_<PROVIDER>_REQUIRE_VERIFIED_EMAIL` | Require users to have a verified email address. | `false` |
|
||||
| `AUTH_<PROVIDER>_DEFAULT_ROLE_ID` | Directus role ID to assign to users. | -- |
|
||||
| `AUTH_<PROVIDER>_ICON` | SVG icon to display with the login link. | `account_circle` |
|
||||
|
||||
## Extensions
|
||||
|
||||
|
||||
576
package-lock.json
generated
576
package-lock.json
generated
@@ -96,10 +96,8 @@
|
||||
"execa": "^5.1.1",
|
||||
"exifr": "^7.1.2",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.2",
|
||||
"flat": "^5.0.2",
|
||||
"fs-extra": "^10.0.0",
|
||||
"grant": "^5.4.14",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql-compose": "^9.0.1",
|
||||
"inquirer": "^8.1.1",
|
||||
@@ -122,6 +120,7 @@
|
||||
"nodemailer": "^6.6.1",
|
||||
"object-hash": "^2.2.0",
|
||||
"openapi3-ts": "^2.0.0",
|
||||
"openid-client": "^4.9.0",
|
||||
"ora": "^5.4.0",
|
||||
"otplib": "^12.0.1",
|
||||
"pino": "6.13.3",
|
||||
@@ -303,6 +302,8 @@
|
||||
"@directus/extensions-sdk": "9.0.0-rc.98",
|
||||
"@directus/format-title": "9.0.0-rc.98",
|
||||
"@directus/shared": "9.0.0-rc.98",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fullcalendar/core": "5.10.0",
|
||||
"@fullcalendar/daygrid": "5.10.0",
|
||||
"@fullcalendar/interaction": "5.10.0",
|
||||
@@ -3269,6 +3270,42 @@
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "0.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
|
||||
"integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "1.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
|
||||
"integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-f1witbwycL9cTENJegcmcZRYyawAFbm8+c6IirLmwbbpqz46wyjbQYLuxOc7weXFXfB7QR8/Vd2u5R3q6JYD9g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/common": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/common/-/common-5.10.0.tgz",
|
||||
@@ -6529,6 +6566,14 @@
|
||||
"@otplib/plugin-thirty-two": "^12.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@panva/asn1.js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz",
|
||||
"integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@phc/format": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||
@@ -11930,7 +11975,7 @@
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bn.js": "^4.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
@@ -12772,7 +12817,7 @@
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.19.0",
|
||||
@@ -12905,7 +12950,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
|
||||
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/browser-process-hrtime": {
|
||||
"version": "1.0.0",
|
||||
@@ -18123,7 +18168,7 @@
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
@@ -19822,72 +19867,6 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.17.2",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz",
|
||||
"integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==",
|
||||
"dependencies": {
|
||||
"cookie": "0.4.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-headers": "~1.0.2",
|
||||
"parseurl": "~1.3.3",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "~2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"node_modules/express-session/node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/express/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -24681,43 +24660,6 @@
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
|
||||
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg=="
|
||||
},
|
||||
"node_modules/grant": {
|
||||
"version": "5.4.18",
|
||||
"resolved": "https://registry.npmjs.org/grant/-/grant-5.4.18.tgz",
|
||||
"integrity": "sha512-5rw0RkbpmzgM1Q4n8FSasnlqud2NcyxFaEMjA++JQ77MGbRL8VnUZ3JeBoF0HTyfI+xzJbHn68LhtLdYnBz3rA==",
|
||||
"dependencies": {
|
||||
"qs": "^6.10.1",
|
||||
"request-compose": "^2.1.4",
|
||||
"request-oauth": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cookie": "^0.4.1",
|
||||
"cookie-signature": "^1.1.0",
|
||||
"jwk-to-pem": "^2.0.5",
|
||||
"jws": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/grant/node_modules/cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/grant/node_modules/cookie-signature": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.1.0.tgz",
|
||||
"integrity": "sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graphlib": {
|
||||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
|
||||
@@ -25207,7 +25149,7 @@
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"minimalistic-assert": "^1.0.1"
|
||||
@@ -25296,7 +25238,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
|
||||
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"hash.js": "^1.0.3",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
@@ -27799,6 +27741,20 @@
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz",
|
||||
"integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==",
|
||||
"dependencies": {
|
||||
"@panva/asn1.js": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0 < 13 || >=13.7.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -28265,17 +28221,6 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jwk-to-pem": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz",
|
||||
"integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"asn1.js": "^5.3.0",
|
||||
"elliptic": "^6.5.4",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
@@ -32260,7 +32205,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
|
||||
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.0.4",
|
||||
@@ -34163,6 +34108,7 @@
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
@@ -34463,6 +34409,14 @@
|
||||
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/oidc-token-hash": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz",
|
||||
"integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==",
|
||||
"engines": {
|
||||
"node": "^10.13.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
@@ -34542,6 +34496,94 @@
|
||||
"opencollective-postinstall": "index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.9.1.tgz",
|
||||
"integrity": "sha512-DYUF07AHjI3QDKqKbn2F7RqozT4hyi4JvmpodLrq0HHoNP7t/AjeG/uqiBK1/N2PZSAQEThVjDLHSmJN4iqu/w==",
|
||||
"dependencies": {
|
||||
"aggregate-error": "^3.1.0",
|
||||
"got": "^11.8.0",
|
||||
"jose": "^2.0.5",
|
||||
"lru-cache": "^6.0.0",
|
||||
"make-error": "^1.3.6",
|
||||
"object-hash": "^2.0.1",
|
||||
"oidc-token-hash": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.19.0 || >=12.0.0 < 13 || >=13.7.0 < 14 || >= 14.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client/node_modules/@sindresorhus/is": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz",
|
||||
"integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client/node_modules/cacheable-lookup": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
|
||||
"engines": {
|
||||
"node": ">=10.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client/node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client/node_modules/got": {
|
||||
"version": "11.8.2",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
|
||||
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.0.0",
|
||||
"@szmarczak/http-timer": "^4.0.5",
|
||||
"@types/cacheable-request": "^6.0.1",
|
||||
"@types/responselike": "^1.0.0",
|
||||
"cacheable-lookup": "^5.0.3",
|
||||
"cacheable-request": "^7.0.1",
|
||||
"decompress-response": "^6.0.0",
|
||||
"http2-wrapper": "^1.0.0-beta.5.2",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client/node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/opentracing": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.5.tgz",
|
||||
@@ -37966,14 +38008,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.24.1.tgz",
|
||||
"integrity": "sha1-w7d1UZfzW43DUCIoJixMkd22uFc="
|
||||
},
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@@ -39254,35 +39288,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/request-compose": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/request-compose/-/request-compose-2.1.4.tgz",
|
||||
"integrity": "sha512-F8xik9Dxd5i2aHZ0/L/oIrCM1kKSgvp9BKYxGXk91lSWF9TbicWpnuxdOchxIhEWwvLdSBWZIAbCOeXfGfqaqA==",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/request-oauth": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/request-oauth/-/request-oauth-1.0.1.tgz",
|
||||
"integrity": "sha512-85THTg1RgOYtqQw42JON6AqvHLptlj1biw265Tsq4fD4cPdUvhDB2Qh9NTv17yCD322ROuO9aOmpc4GyayGVBA==",
|
||||
"dependencies": {
|
||||
"oauth-sign": "^0.9.0",
|
||||
"qs": "^6.9.6",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/request-oauth/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/request/node_modules/form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
@@ -45063,17 +45068,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"dependencies": {
|
||||
"random-bytes": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/umask": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz",
|
||||
@@ -51193,6 +51187,8 @@
|
||||
"@directus/extensions-sdk": "9.0.0-rc.98",
|
||||
"@directus/format-title": "9.0.0-rc.98",
|
||||
"@directus/shared": "9.0.0-rc.98",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fullcalendar/core": "5.10.0",
|
||||
"@fullcalendar/daygrid": "5.10.0",
|
||||
"@fullcalendar/interaction": "5.10.0",
|
||||
@@ -51898,6 +51894,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "0.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
|
||||
"integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==",
|
||||
"dev": true
|
||||
},
|
||||
"@fortawesome/fontawesome-svg-core": {
|
||||
"version": "1.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
|
||||
"integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-brands-svg-icons": {
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-f1witbwycL9cTENJegcmcZRYyawAFbm8+c6IirLmwbbpqz46wyjbQYLuxOc7weXFXfB7QR8/Vd2u5R3q6JYD9g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
}
|
||||
},
|
||||
"@fullcalendar/common": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/common/-/common-5.10.0.tgz",
|
||||
@@ -54629,6 +54649,11 @@
|
||||
"@otplib/plugin-thirty-two": "^12.0.1"
|
||||
}
|
||||
},
|
||||
"@panva/asn1.js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz",
|
||||
"integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw=="
|
||||
},
|
||||
"@phc/format": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||
@@ -59232,7 +59257,7 @@
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bn.js": "^4.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
@@ -59927,7 +59952,7 @@
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.19.0",
|
||||
@@ -60045,7 +60070,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
|
||||
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"browser-process-hrtime": {
|
||||
"version": "1.0.0",
|
||||
@@ -63961,10 +63986,8 @@
|
||||
"execa": "^5.1.1",
|
||||
"exifr": "^7.1.2",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.2",
|
||||
"flat": "^5.0.2",
|
||||
"fs-extra": "^10.0.0",
|
||||
"grant": "^5.4.14",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql-compose": "^9.0.1",
|
||||
"inquirer": "^8.1.1",
|
||||
@@ -63993,6 +64016,7 @@
|
||||
"nodemailer-mailgun-transport": "^2.1.3",
|
||||
"object-hash": "^2.2.0",
|
||||
"openapi3-ts": "^2.0.0",
|
||||
"openid-client": "^4.9.0",
|
||||
"ora": "^5.4.0",
|
||||
"otplib": "^12.0.1",
|
||||
"pg": "^8.6.0",
|
||||
@@ -64371,7 +64395,7 @@
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
@@ -65664,51 +65688,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"express-session": {
|
||||
"version": "1.17.2",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz",
|
||||
"integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==",
|
||||
"requires": {
|
||||
"cookie": "0.4.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-headers": "~1.0.2",
|
||||
"parseurl": "~1.3.3",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "~2.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ext": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz",
|
||||
@@ -69400,34 +69379,6 @@
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
|
||||
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg=="
|
||||
},
|
||||
"grant": {
|
||||
"version": "5.4.18",
|
||||
"resolved": "https://registry.npmjs.org/grant/-/grant-5.4.18.tgz",
|
||||
"integrity": "sha512-5rw0RkbpmzgM1Q4n8FSasnlqud2NcyxFaEMjA++JQ77MGbRL8VnUZ3JeBoF0HTyfI+xzJbHn68LhtLdYnBz3rA==",
|
||||
"requires": {
|
||||
"cookie": "^0.4.1",
|
||||
"cookie-signature": "^1.1.0",
|
||||
"jwk-to-pem": "^2.0.5",
|
||||
"jws": "^4.0.0",
|
||||
"qs": "^6.10.1",
|
||||
"request-compose": "^2.1.4",
|
||||
"request-oauth": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
|
||||
"optional": true
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.1.0.tgz",
|
||||
"integrity": "sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"graphlib": {
|
||||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
|
||||
@@ -69804,7 +69755,7 @@
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"minimalistic-assert": "^1.0.1"
|
||||
@@ -69874,7 +69825,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
|
||||
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"hash.js": "^1.0.3",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
@@ -71809,6 +71760,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"jose": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz",
|
||||
"integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==",
|
||||
"requires": {
|
||||
"@panva/asn1.js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -72187,17 +72146,6 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"jwk-to-pem": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz",
|
||||
"integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"asn1.js": "^5.3.0",
|
||||
"elliptic": "^6.5.4",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
@@ -75161,7 +75109,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
|
||||
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
@@ -76764,7 +76712,8 @@
|
||||
"oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
|
||||
"dev": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -76979,6 +76928,11 @@
|
||||
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
|
||||
"devOptional": true
|
||||
},
|
||||
"oidc-token-hash": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz",
|
||||
"integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
@@ -77037,6 +76991,63 @@
|
||||
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
|
||||
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q=="
|
||||
},
|
||||
"openid-client": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.9.1.tgz",
|
||||
"integrity": "sha512-DYUF07AHjI3QDKqKbn2F7RqozT4hyi4JvmpodLrq0HHoNP7t/AjeG/uqiBK1/N2PZSAQEThVjDLHSmJN4iqu/w==",
|
||||
"requires": {
|
||||
"aggregate-error": "^3.1.0",
|
||||
"got": "^11.8.0",
|
||||
"jose": "^2.0.5",
|
||||
"lru-cache": "^6.0.0",
|
||||
"make-error": "^1.3.6",
|
||||
"object-hash": "^2.0.1",
|
||||
"oidc-token-hash": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz",
|
||||
"integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw=="
|
||||
},
|
||||
"cacheable-lookup": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"requires": {
|
||||
"mimic-response": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"got": {
|
||||
"version": "11.8.2",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
|
||||
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
|
||||
"requires": {
|
||||
"@sindresorhus/is": "^4.0.0",
|
||||
"@szmarczak/http-timer": "^4.0.5",
|
||||
"@types/cacheable-request": "^6.0.1",
|
||||
"@types/responselike": "^1.0.0",
|
||||
"cacheable-lookup": "^5.0.3",
|
||||
"cacheable-request": "^7.0.1",
|
||||
"decompress-response": "^6.0.0",
|
||||
"http2-wrapper": "^1.0.0-beta.5.2",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"responselike": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"opentracing": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.5.tgz",
|
||||
@@ -79800,11 +79811,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
|
||||
},
|
||||
"randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@@ -80842,28 +80848,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"request-compose": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/request-compose/-/request-compose-2.1.4.tgz",
|
||||
"integrity": "sha512-F8xik9Dxd5i2aHZ0/L/oIrCM1kKSgvp9BKYxGXk91lSWF9TbicWpnuxdOchxIhEWwvLdSBWZIAbCOeXfGfqaqA=="
|
||||
},
|
||||
"request-oauth": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/request-oauth/-/request-oauth-1.0.1.tgz",
|
||||
"integrity": "sha512-85THTg1RgOYtqQw42JON6AqvHLptlj1biw265Tsq4fD4cPdUvhDB2Qh9NTv17yCD322ROuO9aOmpc4GyayGVBA==",
|
||||
"requires": {
|
||||
"oauth-sign": "^0.9.0",
|
||||
"qs": "^6.9.6",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -85402,14 +85386,6 @@
|
||||
"integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=",
|
||||
"dev": true
|
||||
},
|
||||
"uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"requires": {
|
||||
"random-bytes": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"umask": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz",
|
||||
|
||||
Reference in New Issue
Block a user