Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't require KEY or SECRET to be set on startup #22320

Merged
merged 11 commits into from
May 6, 2024
6 changes: 6 additions & 0 deletions .changeset/weak-chefs-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---

Check warning on line 1 in .changeset/weak-chefs-sniff.md

View workflow job for this annotation

GitHub Actions / Lint

File ignored by default.
'@directus/env': patch
'@directus/api': patch
---

Deprecated KEY env var, made SECRET optional (for test environments)
1 change: 0 additions & 1 deletion api/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ vi.mock('./utils/validate-env.js');

beforeEach(() => {
vi.mocked(useEnv).mockReturnValue({
KEY: 'xxxxxxx-xxxxxx-xxxxxxxx-xxxxxxxxxx',
SECRET: 'abcdef',
SERVE_APP: 'true',
PUBLIC_URL: 'http://localhost:8055/directus',
Expand Down
23 changes: 13 additions & 10 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ import schema from './middleware/schema.js';
import { initTelemetry } from './telemetry/index.js';
import { getConfigFromEnv } from './utils/get-config-from-env.js';
import { Url } from './utils/url.js';
import { validateEnv } from './utils/validate-env.js';
import { validateStorage } from './utils/validate-storage.js';

const require = createRequire(import.meta.url);
Expand All @@ -75,16 +74,7 @@ export default async function createApp(): Promise<express.Application> {
const logger = useLogger();
const helmet = await import('helmet');

validateEnv(['KEY', 'SECRET']);

if (!new Url(env['PUBLIC_URL'] as string).isAbsolute()) {
logger.warn('PUBLIC_URL should be a full URL');
}

await validateStorage();

await validateDatabaseConnection();
await validateDatabaseExtensions();

if ((await isInstalled()) === false) {
logger.error(`Database doesn't have Directus tables installed.`);
Expand All @@ -95,6 +85,19 @@ export default async function createApp(): Promise<express.Application> {
logger.warn(`Database migrations have not all been run`);
}

if (!env['SECRET']) {
logger.warn(
`"SECRET" env variable is missing. Using a random value instead. Tokens will not persist between restarts. This is not appropriate for production usage.`,
);
}

if (!new Url(env['PUBLIC_URL'] as string).isAbsolute()) {
logger.warn('"PUBLIC_URL" should be a full URL');
}

await validateDatabaseExtensions();
await validateStorage();

await registerAuthProviders();

const extensionManager = getExtensionManager();
Expand Down
5 changes: 3 additions & 2 deletions api/src/auth/drivers/oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { getIPFromReq } from '../../utils/get-ip-from-req.js';
import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
import { Url } from '../../utils/url.js';
import { LocalAuthDriver } from './local.js';
import { getSecret } from '../../utils/get-secret.js';

export class OAuth2AuthDriver extends LocalAuthDriver {
client: Client;
Expand Down Expand Up @@ -306,7 +307,7 @@ export function createOAuth2AuthRouter(providerName: string): Router {
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
}

const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, env['SECRET'] as string, {
const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, getSecret(), {
expiresIn: '5m',
issuer: 'directus',
});
Expand Down Expand Up @@ -338,7 +339,7 @@ export function createOAuth2AuthRouter(providerName: string): Router {
let tokenData;

try {
tokenData = jwt.verify(req.cookies[`oauth2.${providerName}`], env['SECRET'] as string, {
tokenData = jwt.verify(req.cookies[`oauth2.${providerName}`], getSecret(), {
issuer: 'directus',
}) as {
verifier: string;
Expand Down
5 changes: 3 additions & 2 deletions api/src/auth/drivers/openid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { AuthData, AuthDriverOptions, User } from '../../types/index.js';
import asyncHandler from '../../utils/async-handler.js';
import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
import { getSecret } from '../../utils/get-secret.js';
import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
import { Url } from '../../utils/url.js';
import { LocalAuthDriver } from './local.js';
Expand Down Expand Up @@ -336,7 +337,7 @@ export function createOpenIDAuthRouter(providerName: string): Router {
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
}

const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, env['SECRET'] as string, {
const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, getSecret(), {
expiresIn: '5m',
issuer: 'directus',
});
Expand Down Expand Up @@ -368,7 +369,7 @@ export function createOpenIDAuthRouter(providerName: string): Router {
let tokenData;

try {
tokenData = jwt.verify(req.cookies[`openid.${providerName}`], env['SECRET'] as string, {
tokenData = jwt.verify(req.cookies[`openid.${providerName}`], getSecret(), {
issuer: 'directus',
}) as {
verifier: string;
Expand Down
3 changes: 0 additions & 3 deletions api/src/cli/utils/create-env/env-stub.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,6 @@ STORAGE_LOCAL_ROOT="./uploads"

{{ security }}

# Unique identifier for the project
# KEY="xxxxxxx-xxxxxx-xxxxxxxx-xxxxxxxxxx"

# Secret string for the project
# SECRET="abcdef"

Expand Down
2 changes: 0 additions & 2 deletions api/src/cli/utils/create-env/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import fs from 'fs';
import { Liquid } from 'liquidjs';
import { randomUUID } from 'node:crypto';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import path from 'path';
Expand Down Expand Up @@ -28,7 +27,6 @@ export default async function createEnv(

const config: Record<string, any> = {
security: {
KEY: randomUUID(),
SECRET: nanoid(32),
},
database: {
Expand Down
9 changes: 5 additions & 4 deletions api/src/controllers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import { useEnv } from '@directus/env';
import { ErrorCode, InvalidPayloadError, isDirectusError } from '@directus/errors';
import type { Accountability } from '@directus/types';
import { Router } from 'express';
import type { Request } from 'express';
import { Router } from 'express';
import {
createLDAPAuthRouter,
createLocalAuthRouter,
createOAuth2AuthRouter,
createOpenIDAuthRouter,
createSAMLAuthRouter,
} from '../auth/drivers/index.js';
import { REFRESH_COOKIE_OPTIONS, DEFAULT_AUTH_PROVIDER, SESSION_COOKIE_OPTIONS } from '../constants.js';
import { DEFAULT_AUTH_PROVIDER, REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from '../constants.js';
import { useLogger } from '../logger.js';
import { respond } from '../middleware/respond.js';
import { AuthenticationService } from '../services/authentication.js';
import { UsersService } from '../services/users.js';
import type { AuthenticationMode } from '../types/auth.js';
import asyncHandler from '../utils/async-handler.js';
import { getAuthProviders } from '../utils/get-auth-providers.js';
import { getIPFromReq } from '../utils/get-ip-from-req.js';
import { getSecret } from '../utils/get-secret.js';
import isDirectusJWT from '../utils/is-directus-jwt.js';
import { verifyAccessJWT } from '../utils/jwt.js';
import type { AuthenticationMode } from '../types/auth.js';

const router = Router();
const env = useEnv();
Expand Down Expand Up @@ -90,7 +91,7 @@ function getCurrentRefreshToken(req: Request, mode: AuthenticationMode): string
const token = req.cookies[env['SESSION_COOKIE_NAME'] as string];

if (isDirectusJWT(token)) {
const payload = verifyAccessJWT(token, env['SECRET'] as string);
const payload = verifyAccessJWT(token, getSecret());
return payload.session;
}
}
Expand Down
5 changes: 3 additions & 2 deletions api/src/services/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import emitter from '../emitter.js';
import { RateLimiterRes, createRateLimiter } from '../rate-limiter.js';
import type { AbstractServiceOptions, DirectusTokenPayload, LoginResult, Session, User } from '../types/index.js';
import { getMilliseconds } from '../utils/get-milliseconds.js';
import { getSecret } from '../utils/get-secret.js';
import { stall } from '../utils/stall.js';
import { ActivityService } from './activity.js';
import { SettingsService } from './settings.js';
Expand Down Expand Up @@ -226,7 +227,7 @@ export class AuthenticationService {

const TTL = env[options?.session ? 'SESSION_COOKIE_TTL' : 'ACCESS_TOKEN_TTL'] as string;

const accessToken = jwt.sign(customClaims, env['SECRET'] as string, {
const accessToken = jwt.sign(customClaims, getSecret(), {
expiresIn: TTL,
issuer: 'directus',
});
Expand Down Expand Up @@ -403,7 +404,7 @@ export class AuthenticationService {

const TTL = env[options?.session ? 'SESSION_COOKIE_TTL' : 'ACCESS_TOKEN_TTL'] as string;

const accessToken = jwt.sign(customClaims, env['SECRET'] as string, {
const accessToken = jwt.sign(customClaims, getSecret(), {
expiresIn: TTL,
issuer: 'directus',
});
Expand Down
5 changes: 3 additions & 2 deletions api/src/services/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import getDatabase from '../../database/index.js';
import type { AbstractServiceOptions, AuthenticationMode, GraphQLParams } from '../../types/index.js';
import { generateHash } from '../../utils/generate-hash.js';
import { getGraphQLType } from '../../utils/get-graphql-type.js';
import { getSecret } from '../../utils/get-secret.js';
import { getService } from '../../utils/get-service.js';
import isDirectusJWT from '../../utils/is-directus-jwt.js';
import { verifyAccessJWT } from '../../utils/jwt.js';
Expand Down Expand Up @@ -2311,7 +2312,7 @@ export class GraphQLService {
const token = req?.cookies[env['SESSION_COOKIE_NAME'] as string];

if (isDirectusJWT(token)) {
const payload = verifyAccessJWT(token, env['SECRET'] as string);
const payload = verifyAccessJWT(token, getSecret());
currentRefreshToken = payload.session;
}
}
Expand Down Expand Up @@ -2378,7 +2379,7 @@ export class GraphQLService {
const token = req?.cookies[env['SESSION_COOKIE_NAME'] as string];

if (isDirectusJWT(token)) {
const payload = verifyAccessJWT(token, env['SECRET'] as string);
const payload = verifyAccessJWT(token, getSecret());
currentRefreshToken = payload.session;
}
}
Expand Down
2 changes: 1 addition & 1 deletion api/src/services/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export class ServerService {
const data: HealthData = {
status: 'ok',
releaseId: version,
serviceId: env['KEY'] as string,
serviceId: env['PUBLIC_URL'] as string,
checks: merge(
...(await Promise.all([
testDatabase(),
Expand Down
3 changes: 2 additions & 1 deletion api/src/services/shares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
ShareData,
} from '../types/index.js';
import { getMilliseconds } from '../utils/get-milliseconds.js';
import { getSecret } from '../utils/get-secret.js';
import { md } from '../utils/md.js';
import { Url } from '../utils/url.js';
import { userName } from '../utils/user-name.js';
Expand Down Expand Up @@ -106,7 +107,7 @@ export class SharesService extends ItemsService {

const TTL = env[options?.session ? 'SESSION_COOKIE_TTL' : 'ACCESS_TOKEN_TTL'] as string;

const accessToken = jwt.sign(tokenPayload, env['SECRET'] as string, {
const accessToken = jwt.sign(tokenPayload, getSecret(), {
expiresIn: TTL,
issuer: 'directus',
});
Expand Down
9 changes: 5 additions & 4 deletions api/src/services/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { performance } from 'perf_hooks';
import getDatabase from '../database/index.js';
import { useLogger } from '../logger.js';
import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
import { getSecret } from '../utils/get-secret.js';
import isUrlAllowed from '../utils/is-url-allowed.js';
import { verifyJWT } from '../utils/jwt.js';
import { stall } from '../utils/stall.js';
Expand Down Expand Up @@ -159,7 +160,7 @@ export class UsersService extends ItemsService {
private inviteUrl(email: string, url: string | null): string {
const payload = { email, scope: 'invite' };

const token = jwt.sign(payload, env['SECRET'] as string, { expiresIn: '7d', issuer: 'directus' });
const token = jwt.sign(payload, getSecret(), { expiresIn: '7d', issuer: 'directus' });
const inviteURL = url ? new Url(url) : new Url(env['PUBLIC_URL'] as string).addPath('admin', 'accept-invite');
inviteURL.setQuery('token', token);

Expand Down Expand Up @@ -429,7 +430,7 @@ export class UsersService extends ItemsService {
}

async acceptInvite(token: string, password: string): Promise<void> {
const { email, scope } = verifyJWT(token, env['SECRET'] as string) as {
const { email, scope } = verifyJWT(token, getSecret()) as {
email: string;
scope: string;
};
Expand Down Expand Up @@ -473,7 +474,7 @@ export class UsersService extends ItemsService {
});

const payload = { email: user.email, scope: 'password-reset', hash: getSimpleHash('' + user.password) };
const token = jwt.sign(payload, env['SECRET'] as string, { expiresIn: '1d', issuer: 'directus' });
const token = jwt.sign(payload, getSecret(), { expiresIn: '1d', issuer: 'directus' });

const acceptURL = url
? new Url(url).setQuery('token', token).toString()
Expand Down Expand Up @@ -501,7 +502,7 @@ export class UsersService extends ItemsService {
}

async resetPassword(token: string, password: string): Promise<void> {
const { email, scope, hash } = jwt.verify(token, env['SECRET'] as string, { issuer: 'directus' }) as {
const { email, scope, hash } = jwt.verify(token, getSecret(), { issuer: 'directus' }) as {
email: string;
scope: string;
hash: string;
Expand Down
6 changes: 2 additions & 4 deletions api/src/utils/get-accountability-for-token.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { useEnv } from '@directus/env';
import { InvalidCredentialsError } from '@directus/errors';
import type { Accountability } from '@directus/types';
import getDatabase from '../database/index.js';
import { getSecret } from './get-secret.js';
import isDirectusJWT from './is-directus-jwt.js';
import { verifyAccessJWT } from './jwt.js';

export async function getAccountabilityForToken(
token?: string | null,
accountability?: Accountability,
): Promise<Accountability> {
const env = useEnv();

if (!accountability) {
accountability = {
user: null,
Expand All @@ -22,7 +20,7 @@ export async function getAccountabilityForToken(

if (token) {
if (isDirectusJWT(token)) {
const payload = verifyAccessJWT(token, env['SECRET'] as string);
const payload = verifyAccessJWT(token, getSecret());

accountability.role = payload.role;
accountability.admin = payload.admin_access === true || payload.admin_access == 1;
Expand Down
20 changes: 20 additions & 0 deletions api/src/utils/get-secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEnv } from '@directus/env';
import { nanoid } from 'nanoid';

export const _cache: { secret: string | null } = { secret: null };

export const getSecret = () => {
if (_cache.secret) {
return _cache.secret;
}

const env = useEnv();

if (env['SECRET']) {
return env['SECRET'] as string;
paescuj marked this conversation as resolved.
Show resolved Hide resolved
}

_cache.secret = nanoid(32);

return _cache.secret;
};
4 changes: 2 additions & 2 deletions docs/contributing/running-locally.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ Create an `.env` file under the `api` folder using vars from the online

::: tip Config Values

The `KEY`& `SECRET` config options from [Security](https://docs.directus.io/self-hosted/config-options.html#security)
are mandatory.
The `SECRET` config option from [Security](https://docs.directus.io/self-hosted/config-options.html#security) is
mandatory in production.

Also the [Database Configuration](https://docs.directus.io/self-hosted/config-options.html#database) must be specified.
You might want to use the [docker-compose.yml](https://github.com/directus/directus/blob/main/docker-compose.yml) file
Expand Down
3 changes: 1 addition & 2 deletions docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ readTime: 7 min read
```shell
docker run \
-p 8055:8055 \
-e KEY=replace-with-random-value \
-e SECRET=replace-with-random-value \
-e SECRET=replace-with-secure-random-value \
directus/directus
```

Expand Down
4 changes: 2 additions & 2 deletions docs/reference/system/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ return more in-depth information about the current health status of the system.
{
"status": "ok",
"releaseId": "10.0.0",
"serviceId": "3292c816-ae02-43b4-ba91-f0bb549f040c",
"serviceId": "https://directus.example.com",
"checks": {
"pg:responseTime": [
{
Expand Down Expand Up @@ -479,7 +479,7 @@ Authenticated admin users also get the following information:
Directus version in use.

`serviceId` **string**\
UUID of the current Directus instance.
Public URL of the current Directus instance.

`checks` **array**\
Array with the status of all individually connected services.
Expand Down