diff --git a/api/package.json b/api/package.json index 0d98063117..b365eaaa78 100644 --- a/api/package.json +++ b/api/package.json @@ -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", diff --git a/api/src/app.ts b/api/src/app.ts index b8c92ec11a..51293d8ca5 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -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 { @@ -74,6 +74,8 @@ export default async function createApp(): Promise { await flushCaches(); + await registerAuthProviders(); + const extensionManager = getExtensionManager(); await extensionManager.initialize(); @@ -148,9 +150,6 @@ export default async function createApp(): Promise { 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); diff --git a/api/src/auth.ts b/api/src/auth.ts index 96c97fa37a..5fa8ede991 100644 --- a/api/src/auth.ts +++ b/api/src/auth.ts @@ -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 = 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): AuthDriver | undefined { - switch (driver) { - case 'local': - return new LocalAuthDriver(getDatabase(), config); - } -} +export async function registerAuthProviders(): Promise { + 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 = {} +): 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); + } +} diff --git a/api/src/auth/auth.ts b/api/src/auth/auth.ts index f4fbabf7de..5121551d84 100644 --- a/api/src/auth/auth.ts +++ b/api/src/auth/auth.ts @@ -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) { - this.knex = knex; + constructor(options: AuthDriverOptions, _config: Record) { + 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 { + async refresh(_user: User, sessionData: SessionData): Promise { /* Optional */ + return sessionData; } /** diff --git a/api/src/auth/drivers/index.ts b/api/src/auth/drivers/index.ts index 0646cef75a..2b61ea8a64 100644 --- a/api/src/auth/drivers/index.ts +++ b/api/src/auth/drivers/index.ts @@ -1 +1,3 @@ export * from './local'; +export * from './oauth2'; +export * from './openid'; diff --git a/api/src/auth/drivers/local.ts b/api/src/auth/drivers/local.ts index ddffddfe9c..dfcb29a8aa 100644 --- a/api/src/auth/drivers/local.ts +++ b/api/src/auth/drivers/local.ts @@ -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): Promise { 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 { if (!user.password || !(await argon2.verify(user.password, password as string))) { throw new InvalidCredentialsException(); } } - async login(user: User, payload: Record): Promise { - if (payload.password) { - await this.verify(user, payload.password); - } + async login(user: User, payload: Record): Promise { + await this.verify(user, payload.password); return null; } } diff --git a/api/src/auth/drivers/oauth2.ts b/api/src/auth/drivers/oauth2.ts new file mode 100644 index 0000000000..f303844dfe --- /dev/null +++ b/api/src/auth/drivers/oauth2.ts @@ -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; + + constructor(options: AuthDriverOptions, config: Record) { + 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 { + 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): Promise { + 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 { + return this.refresh(user, sessionData); + } + + async refresh(user: User, sessionData: SessionData): Promise { + 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; +} diff --git a/api/src/auth/drivers/openid.ts b/api/src/auth/drivers/openid.ts new file mode 100644 index 0000000000..fd82e9e1ee --- /dev/null +++ b/api/src/auth/drivers/openid.ts @@ -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; + redirectUrl: string; + usersService: UsersService; + config: Record; + + constructor(options: AuthDriverOptions, config: Record) { + 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 { + 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 { + 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): Promise { + 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 { + return this.refresh(user, sessionData); + } + + async refresh(user: User, sessionData: SessionData): Promise { + 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; +} diff --git a/api/src/cli/utils/create-env/env-stub.liquid b/api/src/cli/utils/create-env/env-stub.liquid index d381a60185..7486f3c271 100644 --- a/api/src/cli/utils/create-env/env-stub.liquid +++ b/api/src/cli/utils/create-env/env-stub.liquid @@ -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 diff --git a/api/src/controllers/auth.ts b/api/src/controllers/auth.ts index 5f7f0dd7b4..9c82473446 100644 --- a/api/src/controllers/auth.ts +++ b/api/src/controllers/auth.ts @@ -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; diff --git a/api/src/database/migrations/20211009A-add-auth-data.ts b/api/src/database/migrations/20211009A-add-auth-data.ts new file mode 100644 index 0000000000..5082865ef4 --- /dev/null +++ b/api/src/database/migrations/20211009A-add-auth-data.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('directus_users', (table) => { + table.json('auth_data'); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('directus_users', (table) => { + table.dropColumn('auth_data'); + }); +} diff --git a/api/src/database/system-data/fields/users.yaml b/api/src/database/system-data/fields/users.yaml index b85a0d133a..478083f09b 100644 --- a/api/src/database/system-data/fields/users.yaml +++ b/api/src/database/system-data/fields/users.yaml @@ -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 diff --git a/api/src/env.ts b/api/src/env.ts index 3bfefd9bf4..e8ab5f093b 100644 --- a/api/src/env.ts +++ b/api/src/env.ts @@ -30,8 +30,6 @@ const defaults: Record = { 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 = { AUTH_PROVIDERS: '', - OAUTH_PROVIDERS: '', - EXTENSIONS_PATH: './extensions', EMAIL_FROM: 'no-reply@directus.io', diff --git a/api/src/grant.ts b/api/src/grant.ts deleted file mode 100644 index 7704ea3716..0000000000 --- a/api/src/grant.ts +++ /dev/null @@ -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; diff --git a/api/src/middleware/session.ts b/api/src/middleware/session.ts deleted file mode 100644 index 1229c2591a..0000000000 --- a/api/src/middleware/session.ts +++ /dev/null @@ -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 }); diff --git a/api/src/services/authentication.ts b/api/src/services/authentication.ts index 456978a04b..f996552597 100644 --- a/api/src/services/authentication.ts +++ b/api/src/services/authentication.ts @@ -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) diff --git a/api/src/services/users.ts b/api/src/services/users.ts index 9c977ff6f2..83bffa446e 100644 --- a/api/src/services/users.ts +++ b/api/src/services/users.ts @@ -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); } diff --git a/api/src/types/auth.ts b/api/src/types/auth.ts index f11f48eba3..5b1635b8f2 100644 --- a/api/src/types/auth.ts +++ b/api/src/types/auth.ts @@ -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 | null; } -export type SessionData = Record | null; +export type AuthData = Record | null; export interface Session { token: string; expires: Date; - data: SessionData; + data: string | null; } + +export type SessionData = Record | null; diff --git a/api/src/types/express-session.d.ts b/api/src/types/express-session.d.ts deleted file mode 100644 index 6a5866cbaa..0000000000 --- a/api/src/types/express-session.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { SessionData } from 'express-session'; - -export = SessionData; - -declare module 'express-session' { - interface SessionData { - redirect: string; - grant: any; - } -} diff --git a/api/src/utils/get-auth-providers.ts b/api/src/utils/get-auth-providers.ts index 901fd88863..fbe5b33504 100644 --- a/api/src/utils/get-auth-providers.ts +++ b/api/src/utils/get-auth-providers.ts @@ -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`], })); } diff --git a/api/src/utils/get-email-from-profile.ts b/api/src/utils/get-email-from-profile.ts deleted file mode 100644 index 2ecd4e1470..0000000000 --- a/api/src/utils/get-email-from-profile.ts +++ /dev/null @@ -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 = {}; - -/** - * Extract the email address from a given user profile coming from a providers API - * - * Falls back to OAUTH__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 { - 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; -} diff --git a/api/src/utils/track.ts b/api/src/utils/track.ts index c0bccc57c0..9ef2db220c 100644 --- a/api/src/utils/track.ts +++ b/api/src/utils/track.ts @@ -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), }, diff --git a/app/package.json b/app/package.json index d4bd672c76..625f2e664c 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/src/components/v-icon/v-icon.vue b/app/src/components/v-icon/v-icon.vue index 3aefe88576..434391c4e2 100644 --- a/app/src/components/v-icon/v-icon.vue +++ b/app/src/components/v-icon/v-icon.vue @@ -8,12 +8,15 @@ @click="emitClick" > + {{ name }} @@ -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); } & + & { diff --git a/app/src/types/index.ts b/app/src/types/index.ts index e816cfed02..8b22a63622 100644 --- a/app/src/types/index.ts +++ b/app/src/types/index.ts @@ -2,3 +2,4 @@ export * from './collections'; export * from './error'; export * from './insights'; export * from './notifications'; +export * from './login'; diff --git a/app/src/types/login.ts b/app/src/types/login.ts new file mode 100644 index 0000000000..2ee389c38c --- /dev/null +++ b/app/src/types/login.ts @@ -0,0 +1,5 @@ +export interface AuthProvider { + driver: string; + name: string; + icon?: string; +} diff --git a/docs/guides/api-config.md b/docs/guides/api-config.md index eee76bec58..cd4e6ddfab 100644 --- a/docs/guides/api-config.md +++ b/docs/guides/api-config.md @@ -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="" +AUTH_GOOGLE_CLIENT_SECRET= "" +AUTH_GOOGLE_ISSUER_URL="https://accounts.google.com" +AUTH_GOOGLE_ALLOW_PUBLIC_REGISTRATION="true" +AUTH_GOOGLE_DEFAULT_ROLE_ID="" +AUTH_GOOGLE_ICON="google" + +AUTH_ADOBE_DRIVER="oauth2" +AUTH_ADOBE_CLIENT_ID="" +AUTH_ADOBE_CLIENT_SECRET="" +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="" +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 = "" -OAUTH_GOOGLE_SECRET= "" -OAUTH_GOOGLE_SCOPE="openid email" - -OAUTH_MICROSOFT_KEY = "" -OAUTH_MICROSOFT_SECRET = "" -OAUTH_MICROSOFT_SCOPE = "openid email" -OAUTH_MICROSOFT_AUTHORIZE_URL = "https://login.microsoftonline.com//oauth2/v2.0/authorize" -OAUTH_MICROSOFT_ACCESS_URL = "https://login.microsoftonline.com//oauth2/v2.0/token" - -PUBLIC_URL = "" -``` - -### 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 diff --git a/docs/guides/api-hooks.md b/docs/guides/api-hooks.md index 2fe363fec2..8ebfa5205e 100644 --- a/docs/guides/api-hooks.md +++ b/docs/guides/api-hooks.md @@ -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[1] | -| `database.error` | When a database error is thrown | No | -| `error` | | No | -| `auth` | `login`, `logout`[1], `jwt` and `refresh`[1] | Optional | -| `oauth.:provider`[2] | `login` and `redirect` | Optional | -| `items` | `read`[3], `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`[3] | 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[1] | +| `database.error` | When a database error is thrown | No | +| `error` | | No | +| `auth` | `login`, `logout`[1], `jwt` and `refresh`[1] | Optional | +| `items` | `read`[2], `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`[2] | 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 | 1 Feature Coming Soon\ -2 oAuth provider name can replaced with wildcard for any oauth providers `oauth.*.login`\ -3 Doesn't support `.before` modifier +2 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` - ID of the user that tried logging in/has logged in - - Not available in `oauth` +- `user` - ID of the user that tried logging in/has logged in ## 5. Restart the API diff --git a/docs/reference/api/system/authentication.md b/docs/reference/api/system/authentication.md index 3209f0bf7f..e21aabb1b4 100644 --- a/docs/reference/api/system/authentication.md +++ b/docs/reference/api/system/authentication.md @@ -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.
-::: 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
`data` **Array**\ -Array of configured oAuth providers. +Array of configured auth providers.
@@ -358,12 +361,27 @@ Array of configured oAuth providers.
``` -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.
``` -GET /auth/oauth/:provider +GET /auth/login/:provider ```
diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index 3d648b591e..01f84666b1 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -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:,` 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:,` 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__KEY` | oAuth key (a.k.a. application id) for the external service. | -- | -| `OAUTH__SECRET` | oAuth secret for the external service. | -- | -| `OAUTH__SCOPE` | A white-space separated list of privileges directus should ask for. A common value is: `openid email`. | -- | -| `OAUTH__AUTHORIZE_URL` | The authorize page URL of the external service | -- | -| `OAUTH__ACCESS_URL` | The access URL of the external service | -- | -| `OAUTH__PROFILE_URL` | Where Directus can fetch the profile information of the authenticated user. | -- | +| Variable | Description | Default Value | +| ------------------------ | ------------------------------------------------------- | ------------- | +| `AUTH__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__CLIENT_ID` | OAuth identifier for the external service. | -- | +| `AUTH__CLIENT_SECRET` | OAUth secret for the external service. | -- | +| `AUTH__SCOPE` | A white-space separated list of privileges Directus will request. | `email` | +| `AUTH__AUTHORIZE_URL` | The authorize page URL of the external service. | -- | +| `AUTH__ACCESS_URL` | The token access URL of the external service. | -- | +| `AUTH__PROFILE_URL` | Where Directus can fetch the profile information of the authenticated user. | -- | +| `AUTH__EMAIL_KEY` | OAuth profile email key used to verify the user. | `email` | +| `AUTH__IDENTIFIER_KEY` | OAuth profile identifier key used to verify the user. Can be used in place of `EMAIL_KEY`. | -- | +| `AUTH__ALLOW_PUBLIC_REGISTRATION` | Whether to allow public registration of authenticating users. | `false` | +| `AUTH__DEFAULT_ROLE_ID` | Directus role ID to assign to users. | -- | +| `AUTH__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__CLIENT_ID` | OpenID identifier for the external service. | -- | +| `AUTH__CLIENT_SECRET` | OpenID secret for the external service. | -- | +| `AUTH__SCOPE` | A white-space separated list of privileges Directus will request. | `openid profile email` | +| `AUTH__ISSUER_URL` | The OpenID `.well-known` Discovery Document URL. | -- | +| `AUTH__IDENTIFIER_KEY` | OpenID profile identifier key used to verify the user. | `sub` | +| `AUTH__ALLOW_PUBLIC_REGISTRATION` | Whether to allow public registration of authenticating users. | `false` | +| `AUTH__REQUIRE_VERIFIED_EMAIL` | Require users to have a verified email address. | `false` | +| `AUTH__DEFAULT_ROLE_ID` | Directus role ID to assign to users. | -- | +| `AUTH__ICON` | SVG icon to display with the login link. | `account_circle` | ## Extensions diff --git a/package-lock.json b/package-lock.json index f211ec0b77..3efcd28615 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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",