CVE-2024-28239 (published 2024-03-12T21:15Z)

Directus is a real-time API and App dashboard for managing SQL database content. The authentication API has a `redirect` parameter that can be exploited as an open redirect vulnerability as the user tries to log in via the API URL. There\'s a redirect that is done after successful login via the Auth API GET request to `directus/auth/login/google?redirect=http://malicious-fishing-site.com`. While credentials don\'t seem to be passed to the attacker site, the user can be phished into clicking a legitimate directus site and be taken to a malicious site made to look like a an error message "Your password needs to be updated" to phish out the current password. Users who login via OAuth2 into Directus may be at risk. This issue has been addressed in version 10.10.0. Users are advised to upgrade. There are no known workarounds for this vulnerability.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/directus/directus
commit_details: repo_url: https://github.com/directus/directus
author: Brainslug
author_date: 2024-03-04 05:43:48+01:00
msg: Fix URL Redirection in OAuth2/OpenID/SAML (#21238)

Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com>
repository: repo_url: https://github.com/directus/directus
repo_name: directus/directus
description: The Modern Data Stack 🐰 — Directus is an instant REST+GraphQL API and intuitive no-code data collaboration app for any SQL database.
repo_language: TypeScript
owner: directus
file_changes: filename: calm-tables-destroy.md
change_type: ModificationType.ADD
diff: @@ -0,0 +1,6 @@
+---
+"@directus/api": patch
+"@directus/env": patch
+---
+
+Fixed potential Open Redirect vulnerability with OAuth2/OpenID/SAML SSO providers (via Directus redirect query parameter), by requiring redirect URLs to be enabled via allow list

programming_language: Markdown
filename: oauth2.ts
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,7 @@ import { useEnv } from '@directus/env';
 import {
 	ErrorCode,
 	InvalidCredentialsError,
+	InvalidPayloadError,
 	InvalidProviderConfigError,
 	InvalidProviderError,
 	InvalidTokenError,
@@ -16,6 +17,7 @@ import jwt from 'jsonwebtoken';
 import type { Client } from 'openid-client';
 import { errors, generators, Issuer } from 'openid-client';
 import { getAuthProvider } from '../../auth.js';
+import { REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from '../../constants.js';
 import getDatabase from '../../database/index.js';
 import emitter from '../../emitter.js';
 import { useLogger } from '../../logger.js';
@@ -26,9 +28,9 @@ 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 { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
 import { Url } from '../../utils/url.js';
 import { LocalAuthDriver } from './local.js';
-import { REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from '../../constants.js';
 
 export class OAuth2AuthDriver extends LocalAuthDriver {
 	client: Client;
@@ -298,15 +300,16 @@ export function createOAuth2AuthRouter(providerName: string): Router {
 			const provider = getAuthProvider(providerName) as OAuth2AuthDriver;
 			const codeVerifier = provider.generateCodeVerifier();
 			const prompt = !!req.query['prompt'];
+			const redirect = req.query['redirect'];
 
-			const token = jwt.sign(
-				{ verifier: codeVerifier, redirect: req.query['redirect'], prompt },
-				env['SECRET'] as string,
-				{
-					expiresIn: '5m',
-					issuer: 'directus',
-				},
-			);
+			if (isLoginRedirectAllowed(redirect, providerName) === false) {
+				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, {
+				expiresIn: '5m',
+				issuer: 'directus',
+			});
 
 			res.cookie(`oauth2.${providerName}`, token, {
 				httpOnly: true,

programming_language: TypeScript
filename: openid.ts
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,7 @@ import { useEnv } from '@directus/env';
 import {
 	ErrorCode,
 	InvalidCredentialsError,
+	InvalidPayloadError,
 	InvalidProviderConfigError,
 	InvalidProviderError,
 	InvalidTokenError,
@@ -16,6 +17,7 @@ import jwt from 'jsonwebtoken';
 import type { Client } from 'openid-client';
 import { errors, generators, Issuer } from 'openid-client';
 import { getAuthProvider } from '../../auth.js';
+import { REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from '../../constants.js';
 import getDatabase from '../../database/index.js';
 import emitter from '../../emitter.js';
 import { useLogger } from '../../logger.js';
@@ -26,9 +28,9 @@ 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 { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
 import { Url } from '../../utils/url.js';
 import { LocalAuthDriver } from './local.js';
-import { REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from '../../constants.js';
 
 export class OpenIDAuthDriver extends LocalAuthDriver {
 	client: Promise<Client>;
@@ -320,7 +322,6 @@ const handleError = (e: any) => {
 
 export function createOpenIDAuthRouter(providerName: string): Router {
 	const env = useEnv();
-
 	const router = Router();
 
 	router.get(
@@ -329,15 +330,16 @@ export function createOpenIDAuthRouter(providerName: string): Router {
 			const provider = getAuthProvider(providerName) as OpenIDAuthDriver;
 			const codeVerifier = provider.generateCodeVerifier();
 			const prompt = !!req.query['prompt'];
+			const redirect = req.query['redirect'];
 
-			const token = jwt.sign(
-				{ verifier: codeVerifier, redirect: req.query['redirect'], prompt },
-				env['SECRET'] as string,
-				{
-					expiresIn: '5m',
-					issuer: 'directus',
-				},
-			);
+			if (isLoginRedirectAllowed(redirect, providerName) === false) {
+				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, {
+				expiresIn: '5m',
+				issuer: 'directus',
+			});
 
 			res.cookie(`openid.${providerName}`, token, {
 				httpOnly: true,

programming_language: TypeScript
filename: saml.ts
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,12 @@
 import * as validator from '@authenio/samlify-node-xmllint';
 import { useEnv } from '@directus/env';
-import { ErrorCode, InvalidCredentialsError, InvalidProviderError, isDirectusError } from '@directus/errors';
+import {
+	ErrorCode,
+	InvalidCredentialsError,
+	InvalidPayloadError,
+	InvalidProviderError,
+	isDirectusError,
+} from '@directus/errors';
 import express, { Router } from 'express';
 import * as samlify from 'samlify';
 import { getAuthProvider } from '../../auth.js';
@@ -15,6 +21,7 @@ import type { AuthDriverOptions, User } from '../../types/index.js';
 import asyncHandler from '../../utils/async-handler.js';
 import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
 import { LocalAuthDriver } from './local.js';
+import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
 
 // Register the samlify schema validator
 samlify.setSchemaValidator(validator);
@@ -126,7 +133,13 @@ export function createSAMLAuthRouter(providerName: string) {
 			const parsedUrl = new URL(url);
 
 			if (req.query['redirect']) {
-				parsedUrl.searchParams.append('RelayState', req.query['redirect'] as string);
+				const redirect = req.query['redirect'] as string;
+
+				if (isLoginRedirectAllowed(redirect, providerName) === false) {
+					throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
+				}
+
+				parsedUrl.searchParams.append('RelayState', redirect);
 			}
 
 			return res.redirect(parsedUrl.toString());

programming_language: TypeScript
filename: is-login-redirect-allowed.test.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,78 @@
+import { vi, expect, test, afterEach } from 'vitest';
+import { useEnv } from '@directus/env';
+import { isLoginRedirectAllowed } from './is-login-redirect-allowed.js';
+
+vi.mock('@directus/env');
+
+afterEach(() => {
+	vi.clearAllMocks();
+});
+
+test('isLoginRedirectAllowed returns true with no redirect', () => {
+	const redirect = undefined;
+	const provider = 'local';
+
+	expect(isLoginRedirectAllowed(redirect, provider)).toBe(true);
+});
+
+test('isLoginRedirectAllowed returns false with invalid redirect', () => {
+	const redirect = 123456;
+	const provider = 'local';
+
+	expect(isLoginRedirectAllowed(redirect, provider)).toBe(false);
+});
+
+test('isLoginRedirectAllowed returns true for allowed URL', () => {
+	const provider = 'local';
+
+	vi.mocked(useEnv).mockReturnValue({
+		[`AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`]:
+			'http://external.example.com,https://external.example.com,http://external.example.com:8055/test',
+		PUBLIC_URL: 'http://public.example.com',
+	});
+
+	expect(isLoginRedirectAllowed('http://public.example.com', provider)).toBe(true);
+	expect(isLoginRedirectAllowed('http://external.example.com', provider)).toBe(true);
+	expect(isLoginRedirectAllowed('https://external.example.com', provider)).toBe(true);
+	expect(isLoginRedirectAllowed('http://external.example.com:8055/test', provider)).toBe(true);
+});
+
+test('isLoginRedirectAllowed returns false for denied URL', () => {
+	const provider = 'local';
+
+	vi.mocked(useEnv).mockReturnValue({
+		[`AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`]: 'http://external.example.com',
+		PUBLIC_URL: 'http://public.example.com',
+	});
+
+	expect(isLoginRedirectAllowed('https://external.example.com', provider)).toBe(false);
+	expect(isLoginRedirectAllowed('http://external.example.com:8055', provider)).toBe(false);
+	expect(isLoginRedirectAllowed('http://external.example.com/test', provider)).toBe(false);
+});
+
+test('isLoginRedirectAllowed returns true for relative paths', () => {
+	const provider = 'local';
+
+	vi.mocked(useEnv).mockReturnValue({
+		[`AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`]: 'http://external.example.com',
+		PUBLIC_URL: 'http://public.example.com',
+	});
+
+	expect(isLoginRedirectAllowed('/admin/content', provider)).toBe(true);
+	expect(isLoginRedirectAllowed('../admin/content', provider)).toBe(true);
+	expect(isLoginRedirectAllowed('./admin/content', provider)).toBe(true);
+
+	expect(isLoginRedirectAllowed('http://public.example.com/admin/content', provider)).toBe(true);
+});
+
+test('isLoginRedirectAllowed returns false if missing protocol', () => {
+	const provider = 'local';
+
+	vi.mocked(useEnv).mockReturnValue({
+		[`AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`]: 'http://example.com',
+		PUBLIC_URL: 'http://example.com',
+	});
+
+	expect(isLoginRedirectAllowed('//example.com/admin/content', provider)).toBe(false);
+	expect(isLoginRedirectAllowed('//user@password:example.com/', provider)).toBe(false);
+});

programming_language: TypeScript
filename: is-login-redirect-allowed.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,41 @@
+import { useEnv } from '@directus/env';
+import { toArray } from '@directus/utils';
+import isUrlAllowed from './is-url-allowed.js';
+
+/**
+ * Checks if the defined redirect after successful SSO login is in the allow list
+ */
+export function isLoginRedirectAllowed(redirect: unknown, provider: string): boolean {
+	if (!redirect) return true; // empty redirect
+	if (typeof redirect !== 'string') return false; // invalid type
+
+	const env = useEnv();
+	const publicUrl = env['PUBLIC_URL'] as string;
+
+	if (URL.canParse(redirect) === false) {
+		if (redirect.startsWith('//') === false) {
+			// should be a relative path like `/admin/test`
+			return true;
+		}
+
+		// domain without protocol `//example.com/test`
+		return false;
+	}
+
+	const { protocol: redirectProtocol, hostname: redirectDomain } = new URL(redirect);
+
+	const envKey = `AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`;
+
+	if (envKey in env) {
+		if (isUrlAllowed(redirect, [...toArray(env[envKey] as string), publicUrl])) return true;
+	}
+
+	if (URL.canParse(publicUrl) === false) {
+		return false;
+	}
+
+	// allow redirects to the defined PUBLIC_URL
+	const { protocol: publicProtocol, hostname: publicDomain } = new URL(publicUrl);
+
+	return `${redirectProtocol}//${redirectDomain}` === `${publicProtocol}//${publicDomain}`;
+}

programming_language: TypeScript
filename: is-url-allowed.test.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,37 @@
+import { expect, test } from 'vitest';
+import isUrlAllowed from './is-url-allowed.js';
+
+test('isUrlAllowed should allow matching domain', () => {
+	const checkUrl = 'https://directus.io';
+	const allowedUrls = ['https://directus.io/'];
+
+	expect(isUrlAllowed(checkUrl, allowedUrls)).toBe(true);
+});
+
+test('isUrlAllowed should allow matching path', () => {
+	const checkUrl = 'https://directus.io/tv';
+	const allowedUrls = ['https://directus.io/tv'];
+
+	expect(isUrlAllowed(checkUrl, allowedUrls)).toBe(true);
+});
+
+test('isUrlAllowed should block different paths', () => {
+	const checkUrl = 'http://example.com/test1';
+	const allowedUrls = ['http://example.com/test2', 'http://example.com/test3', 'http://example.com/'];
+
+	expect(isUrlAllowed(checkUrl, allowedUrls)).toBe(false);
+});
+
+test('isUrlAllowed should block different domains', () => {
+	const checkUrl = 'http://directus.io/';
+	const allowedUrls = ['http://example.com/', 'http://directus.chat'];
+
+	expect(isUrlAllowed(checkUrl, allowedUrls)).toBe(false);
+});
+
+test('isUrlAllowed blocks varying protocols', () => {
+	const checkUrl = 'http://example.com/';
+	const allowedUrls = ['ftp://example.com/', 'https://example.com/'];
+
+	expect(isUrlAllowed(checkUrl, allowedUrls)).toBe(false);
+});

programming_language: TypeScript
filename: is-url-allowed.ts
change_type: ModificationType.MODIFY
diff: @@ -3,7 +3,7 @@ import { URL } from 'url';
 import { useLogger } from '../logger.js';
 
 /**
- * Check if url matches allow list either exactly or by domain+path
+ * Check if URL matches allow list either exactly or by origin (protocol+domain+port) + pathname
  */
 export default function isUrlAllowed(url: string, allowList: string | string[]): boolean {
 	const logger = useLogger();
@@ -15,8 +15,8 @@ export default function isUrlAllowed(url: string, allowList: string | string[]):
 	const parsedWhitelist = urlAllowList
 		.map((allowedURL) => {
 			try {
-				const { hostname, pathname } = new URL(allowedURL);
-				return hostname + pathname;
+				const { origin, pathname } = new URL(allowedURL);
+				return origin + pathname;
 			} catch {
 				logger.warn(`Invalid URL used "${url}"`);
 			}
@@ -26,8 +26,8 @@ export default function isUrlAllowed(url: string, allowList: string | string[]):
 		.filter((f) => f) as string[];
 
 	try {
-		const { hostname, pathname } = new URL(url);
-		return parsedWhitelist.includes(hostname + pathname);
+		const { origin, pathname } = new URL(url);
+		return parsedWhitelist.includes(origin + pathname);
 	} catch {
 		return false;
 	}

programming_language: TypeScript
filename: breaking-changes.md
change_type: ModificationType.MODIFY
diff: @@ -134,6 +134,17 @@ AuthenticationService.login('email', 'password', { otp: 'otp-code', session: tru
 
 :::
 
+### Introduced Allow List for OAuth2/OpenID/SAML Redirects
+
+Due to an Open Redirect vulnerability with the OAuth2, OpenID and SAML SSO providers, we have introduced an allow list
+for these redirects.
+
+If your current workflow depends on redirecting to an external domain after successful SSO login using the
+`?redirect=http://example.com/login` query parameter, then you'll need to add this URL to the
+`AUTH_<PROVIDER>_REDIRECT_ALLOW_LIST` config option.
+
+`AUTH_<PROVIDER>_REDIRECT_ALLOW_LIST` accepts a comma-separated list of URLs (path is included in comparison).
+
 ## Version 10.9.0
 
 ### Updated Exif Tags

programming_language: Markdown
filename: config-options.md
change_type: ModificationType.MODIFY
diff: @@ -747,23 +747,24 @@ These flows rely on the `PUBLIC_URL` variable for redirecting. Ensure the variab
 
 ### OAuth 2.0
 
-| Variable                                    | Description                                                                                       | Default Value    |
-| ------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------- |
-| `AUTH_<PROVIDER>_CLIENT_ID`                 | Client identifier for the OAuth provider.                                                         | --               |
-| `AUTH_<PROVIDER>_CLIENT_SECRET`             | Client secret for the OAuth provider.                                                             | --               |
-| `AUTH_<PROVIDER>_SCOPE`                     | A white-space separated list of permissions to request.                                           | `email`          |
-| `AUTH_<PROVIDER>_AUTHORIZE_URL`             | Authorization page URL of the OAuth provider.                                                     | --               |
-| `AUTH_<PROVIDER>_ACCESS_URL`                | Access token URL of the OAuth provider.                                                           | --               |
-| `AUTH_<PROVIDER>_PROFILE_URL`               | User profile URL of the OAuth provider.                                                           | --               |
-| `AUTH_<PROVIDER>_IDENTIFIER_KEY`            | User profile identifier key <sup>[1]</sup>. Will default to `EMAIL_KEY`.                          | --               |
-| `AUTH_<PROVIDER>_EMAIL_KEY`                 | User profile email key.                                                                           | `email`          |
-| `AUTH_<PROVIDER>_FIRST_NAME_KEY`            | User profile first name key.                                                                      | --               |
-| `AUTH_<PROVIDER>_LAST_NAME_KEY`             | User profile last name key.                                                                       | --               |
-| `AUTH_<PROVIDER>_ALLOW_PUBLIC_REGISTRATION` | Automatically create accounts for authenticating users.                                           | `false`          |
-| `AUTH_<PROVIDER>_DEFAULT_ROLE_ID`           | A Directus role ID to assign created users.                                                       | --               |
-| `AUTH_<PROVIDER>_ICON`                      | SVG icon to display with the login link. [See options here](/user-guide/overview/glossary#icons). | `account_circle` |
-| `AUTH_<PROVIDER>_LABEL`                     | Text to be presented on SSO button within App.                                                    | `<PROVIDER>`     |
-| `AUTH_<PROVIDER>_PARAMS`                    | Custom query parameters applied to the authorization URL.                                         | --               |
+| Variable                                    | Description                                                                                               | Default Value    |
+| ------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------- |
+| `AUTH_<PROVIDER>_CLIENT_ID`                 | Client identifier for the OAuth provider.                                                                 | --               |
+| `AUTH_<PROVIDER>_CLIENT_SECRET`             | Client secret for the OAuth provider.                                                                     | --               |
+| `AUTH_<PROVIDER>_SCOPE`                     | A white-space separated list of permissions to request.                                                   | `email`          |
+| `AUTH_<PROVIDER>_AUTHORIZE_URL`             | Authorization page URL of the OAuth provider.                                                             | --               |
+| `AUTH_<PROVIDER>_ACCESS_URL`                | Access token URL of the OAuth provider.                                                                   | --               |
+| `AUTH_<PROVIDER>_PROFILE_URL`               | User profile URL of the OAuth provider.                                                                   | --               |
+| `AUTH_<PROVIDER>_IDENTIFIER_KEY`            | User profile identifier key <sup>[1]</sup>. Will default to `EMAIL_KEY`.                                  | --               |
+| `AUTH_<PROVIDER>_EMAIL_KEY`                 | User profile email key.                                                                                   | `email`          |
+| `AUTH_<PROVIDER>_FIRST_NAME_KEY`            | User profile first name key.                                                                              | --               |
+| `AUTH_<PROVIDER>_LAST_NAME_KEY`             | User profile last name key.                                                                               | --               |
+| `AUTH_<PROVIDER>_ALLOW_PUBLIC_REGISTRATION` | Automatically create accounts for authenticating users.                                                   | `false`          |
+| `AUTH_<PROVIDER>_DEFAULT_ROLE_ID`           | A Directus role ID to assign created users.                                                               | --               |
+| `AUTH_<PROVIDER>_ICON`                      | SVG icon to display with the login link. [See options here](/user-guide/overview/glossary#icons).         | `account_circle` |
+| `AUTH_<PROVIDER>_LABEL`                     | Text to be presented on SSO button within App.                                                            | `<PROVIDER>`     |
+| `AUTH_<PROVIDER>_PARAMS`                    | Custom query parameters applied to the authorization URL.                                                 | --               |
+| `AUTH_<PROVIDER>_REDIRECT_ALLOW_LIST`       | A comma-separated list of external URLs (including paths) allowed for redirecting after successful login. | --               |
 
 <sup>[1]</sup> When authenticating, Directus will match the identifier value from the external user profile to a
 Directus users "External Identifier".
@@ -772,19 +773,20 @@ Directus users "External Identifier".
 
 OpenID is an authentication protocol built on OAuth 2.0, and should be preferred over standard OAuth 2.0 where possible.
 
-| Variable                                    | Description                                                                                       | Default Value          |
-| ------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------- |
-| `AUTH_<PROVIDER>_CLIENT_ID`                 | Client identifier for the external service.                                                       | --                     |
-| `AUTH_<PROVIDER>_CLIENT_SECRET`             | Client secret for the external service.                                                           | --                     |
-| `AUTH_<PROVIDER>_SCOPE`                     | A white-space separated list of permissions to request.                                           | `openid profile email` |
-| `AUTH_<PROVIDER>_ISSUER_URL`                | OpenID `.well-known` discovery document URL of the external service.                              | --                     |
-| `AUTH_<PROVIDER>_IDENTIFIER_KEY`            | User profile identifier key <sup>[1]</sup>. Will default to `EMAIL_KEY`.                          | `sub`<sup>[2]</sup>    |
-| `AUTH_<PROVIDER>_ALLOW_PUBLIC_REGISTRATION` | Automatically create accounts for authenticating users.                                           | `false`                |
-| `AUTH_<PROVIDER>_REQUIRE_VERIFIED_EMAIL`    | Require created users to have a verified email address.                                           | `false`                |
-| `AUTH_<PROVIDER>_DEFAULT_ROLE_ID`           | A Directus role ID to assign created users.                                                       | --                     |
-| `AUTH_<PROVIDER>_ICON`                      | SVG icon to display with the login link. [See options here](/user-guide/overview/glossary#icons). | `account_circle`       |
-| `AUTH_<PROVIDER>_LABEL`                     | Text to be presented on SSO button within App.                                                    | `<PROVIDER>`           |
-| `AUTH_<PROVIDER>_PARAMS`                    | Custom query parameters applied to the authorization URL.                                         | --                     |
+| Variable                                    | Description                                                                                               | Default Value          |
+| ------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------- |
+| `AUTH_<PROVIDER>_CLIENT_ID`                 | Client identifier for the external service.                                                               | --                     |
+| `AUTH_<PROVIDER>_CLIENT_SECRET`             | Client secret for the external service.                                                                   | --                     |
+| `AUTH_<PROVIDER>_SCOPE`                     | A white-space separated list of permissions to request.                                                   | `openid profile email` |
+| `AUTH_<PROVIDER>_ISSUER_URL`                | OpenID `.well-known` discovery document URL of the external service.                                      | --                     |
+| `AUTH_<PROVIDER>_IDENTIFIER_KEY`            | User profile identifier key <sup>[1]</sup>. Will default to `EMAIL_KEY`.                                  | `sub`<sup>[2]</sup>    |
+| `AUTH_<PROVIDER>_ALLOW_PUBLIC_REGISTRATION` | Automatically create accounts for authenticating users.                                                   | `false`                |
+| `AUTH_<PROVIDER>_REQUIRE_VERIFIED_EMAIL`    | Require created users to have a verified email address.                                                   | `false`                |
+| `AUTH_<PROVIDER>_DEFAULT_ROLE_ID`           | A Directus role ID to assign created users.                                                               | --                     |
+| `AUTH_<PROVIDER>_ICON`                      | SVG icon to display with the login link. [See options here](/user-guide/overview/glossary#icons).         | `account_circle`       |
+| `AUTH_<PROVIDER>_LABEL`                     | Text to be presented on SSO button within App.                                                            | `<PROVIDER>`           |
+| `AUTH_<PROVIDER>_PARAMS`                    | Custom query parameters applied to the authorization URL.                                                 | --                     |
+| `AUTH_<PROVIDER>_REDIRECT_ALLOW_LIST`       | A comma-separated list of external URLs (including paths) allowed for redirecting after successful login. | --                     |
 
 <sup>[1]</sup> When authenticating, Directus will match the identifier value from the external user profile to a
 Directus users "External Identifier".
@@ -848,16 +850,17 @@ without a password.
 - Identity provider (IdP) authenticates users and provides to service providers an authentication assertion that
   indicates a user has been authenticated.
 
-| Variable                                    | Description                                             | Default Value                                                          |
-| ------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------- |
-| `AUTH_<PROVIDER>_SP_metadata`               | String containing XML metadata for service provider     | --                                                                     |
-| `AUTH_<PROVIDER>_IDP_metadata`              | String containing XML metadata for identity provider    | --                                                                     |
-| `AUTH_<PROVIDER>_ALLOW_PUBLIC_REGISTRATION` | Automatically create accounts for authenticating users. | `false`                                                                |
-| `AUTH_<PROVIDER>_DEFAULT_ROLE_ID`           | A Directus role ID to assign created users.             | --                                                                     |
-| `AUTH_<PROVIDER>_IDENTIFIER_KEY`            | User profile identifier key <sup>[1]</sup>.             | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier` |
-| `AUTH_<PROVIDER>_EMAIL_KEY`                 | User profile email key.                                 | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`   |
-| `AUTH_<PROVIDER>_GIVEN_NAME_KEY`            | User first name attribute.                              | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname`      |
-| `AUTH_<PROVIDER>_FAMILY_NAME_KEY`           | User last name attribute.                               | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname`        |
+| Variable                                    | Description                                                                                               | Default Value                                                          |
+| ------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
+| `AUTH_<PROVIDER>_SP_metadata`               | String containing XML metadata for service provider                                                       | --                                                                     |
+| `AUTH_<PROVIDER>_IDP_metadata`              | String containing XML metadata for identity provider                                                      | --                                                                     |
+| `AUTH_<PROVIDER>_ALLOW_PUBLIC_REGISTRATION` | Automatically create accounts for authenticating users.                                                   | `false`                                                                |
+| `AUTH_<PROVIDER>_DEFAULT_ROLE_ID`           | A Directus role ID to assign created users.                                                               | --                                                                     |
+| `AUTH_<PROVIDER>_IDENTIFIER_KEY`            | User profile identifier key <sup>[1]</sup>.                                                               | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier` |
+| `AUTH_<PROVIDER>_EMAIL_KEY`                 | User profile email key.                                                                                   | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`   |
+| `AUTH_<PROVIDER>_GIVEN_NAME_KEY`            | User first name attribute.                                                                                | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname`      |
+| `AUTH_<PROVIDER>_FAMILY_NAME_KEY`           | User last name attribute.                                                                                 | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname`        |
+| `AUTH_<PROVIDER>_REDIRECT_ALLOW_LIST`       | A comma-separated list of external URLs (including paths) allowed for redirecting after successful login. | --                                                                     |
 
 <sup>[1]</sup> When authenticating, Directus will match the identifier value from the external user profile to a
 Directus users "External Identifier".

programming_language: Markdown
filename: directus-variables.ts
change_type: ModificationType.MODIFY
diff: @@ -153,6 +153,7 @@ export const DIRECTUS_VARIABLES = [
 	'AUTH_.+_GROUP_SCOPE',
 	'AUTH_.+_IDP.+',
 	'AUTH_.+_SP.+',
+	'AUTH_.+_REDIRECT_ALLOW_LIST',
 	// extensions
 	'PACKAGE_FILE_LOCATION',
 	'EXTENSIONS_LOCATION',

programming_language: PHP