Skip to content

Commit 6370b96

Browse files
Feature: Auth0Client Configuration Validation (#2026)
2 parents 51deff0 + 0f57478 commit 6370b96

File tree

4 files changed

+328
-18
lines changed

4 files changed

+328
-18
lines changed

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,20 @@ You can customize the client by using the options below:
147147
| httpTimeout | `number` | Integer value for the HTTP timeout in milliseconds for authentication requests. Defaults to `5000` milliseconds |
148148
| enableTelemetry | `boolean` | Boolean value to opt-out of sending the library name and version to your authorization server via the `Auth0-Client` header. Defaults to `true`. |
149149

150+
## Configuration Validation
151+
152+
The SDK performs validation of required configuration options when initializing the `Auth0Client`. The following options are mandatory and must be provided either through constructor options or environment variables:
153+
154+
- `domain` (or `AUTH0_DOMAIN` environment variable)
155+
- `clientId` (or `AUTH0_CLIENT_ID` environment variable)
156+
- `appBaseUrl` (or `APP_BASE_URL` environment variable)
157+
- `secret` (or `AUTH0_SECRET` environment variable)
158+
- Either:
159+
- `clientSecret` (or `AUTH0_CLIENT_SECRET` environment variable), OR
160+
- `clientAssertionSigningKey` (or `AUTH0_CLIENT_ASSERTION_SIGNING_KEY` environment variable)
161+
162+
If any of these required options are missing, the SDK will throw a `ConfigurationError` with the code `MISSING_REQUIRED_OPTIONS` and a detailed error message explaining which options are missing and how to provide them.
163+
150164
## Routes
151165

152166
The SDK mounts 6 routes:

src/errors/index.ts

+55
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,58 @@ export class AccessTokenForConnectionError extends SdkError {
144144
this.cause = cause;
145145
}
146146
}
147+
148+
/**
149+
* Enum representing error codes related to configuration.
150+
*/
151+
export enum ConfigurationErrorCode {
152+
/**
153+
* Missing required configuration options.
154+
*/
155+
MISSING_REQUIRED_OPTIONS = "missing_required_options"
156+
}
157+
158+
/**
159+
* Error class representing a configuration error.
160+
* Extends the `SdkError` class.
161+
*/
162+
export class ConfigurationError extends SdkError {
163+
/**
164+
* The error code associated with the configuration error.
165+
*/
166+
public code: string;
167+
public missingOptions?: string[];
168+
169+
/**
170+
* Constructs a new `ConfigurationError` instance.
171+
*
172+
* @param code - The error code.
173+
* @param missingOptions - Array of missing configuration option names.
174+
* @param envVarMapping - Optional mapping of option names to their environment variable names.
175+
*/
176+
constructor(
177+
code: string,
178+
missingOptions: string[] = [],
179+
envVarMapping: Record<string, string> = {}
180+
) {
181+
// Standard intro message explaining the issue
182+
let errorMessage =
183+
"Not all required options where provided when creating an instance of Auth0Client. Ensure to provide all missing options, either by passing it to the Auth0Client constructor, or by setting the corresponding environment variable.\n\n";
184+
185+
// Add specific details for each missing option
186+
missingOptions.forEach((key) => {
187+
if (key === "clientAuthentication") {
188+
errorMessage += `Missing: clientAuthentication: Set either AUTH0_CLIENT_SECRET env var or AUTH0_CLIENT_ASSERTION_SIGNING_KEY env var, or pass clientSecret or clientAssertionSigningKey in options\n`;
189+
} else if (envVarMapping[key]) {
190+
errorMessage += `Missing: ${key}: Set ${envVarMapping[key]} env var or pass ${key} in options\n`;
191+
} else {
192+
errorMessage += `Missing: ${key}\n`;
193+
}
194+
});
195+
196+
super(errorMessage.trim());
197+
this.name = "ConfigurationError";
198+
this.code = code;
199+
this.missingOptions = missingOptions;
200+
}
201+
}

src/server/client.test.ts

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
import { ConfigurationError, ConfigurationErrorCode } from "../errors/index.js";
4+
import { Auth0Client } from "./client.js";
5+
6+
describe("Auth0Client", () => {
7+
// Store original env vars
8+
const originalEnv = { ...process.env };
9+
10+
// Define correct environment variable names
11+
const ENV_VARS = {
12+
DOMAIN: "AUTH0_DOMAIN",
13+
CLIENT_ID: "AUTH0_CLIENT_ID",
14+
CLIENT_SECRET: "AUTH0_CLIENT_SECRET",
15+
CLIENT_ASSERTION_SIGNING_KEY: "AUTH0_CLIENT_ASSERTION_SIGNING_KEY",
16+
APP_BASE_URL: "APP_BASE_URL",
17+
SECRET: "AUTH0_SECRET",
18+
SCOPE: "AUTH0_SCOPE"
19+
};
20+
21+
// Clear env vars before each test
22+
beforeEach(() => {
23+
vi.resetModules();
24+
// Clear all environment variables that might affect the tests
25+
delete process.env[ENV_VARS.DOMAIN];
26+
delete process.env[ENV_VARS.CLIENT_ID];
27+
delete process.env[ENV_VARS.CLIENT_SECRET];
28+
delete process.env[ENV_VARS.CLIENT_ASSERTION_SIGNING_KEY];
29+
delete process.env[ENV_VARS.APP_BASE_URL];
30+
delete process.env[ENV_VARS.SECRET];
31+
delete process.env[ENV_VARS.SCOPE];
32+
});
33+
34+
// Restore env vars after each test
35+
afterEach(() => {
36+
process.env = { ...originalEnv };
37+
});
38+
39+
describe("constructor validation", () => {
40+
it("should throw ConfigurationError when all required options are missing", () => {
41+
expect(() => new Auth0Client()).toThrow(ConfigurationError);
42+
43+
try {
44+
new Auth0Client();
45+
} catch (error) {
46+
const configError = error as ConfigurationError;
47+
expect(configError).toBeInstanceOf(ConfigurationError);
48+
expect(configError.code).toBe(
49+
ConfigurationErrorCode.MISSING_REQUIRED_OPTIONS
50+
);
51+
expect(configError.missingOptions).toContain("domain");
52+
expect(configError.missingOptions).toContain("clientId");
53+
expect(configError.missingOptions).toContain("clientAuthentication");
54+
expect(configError.missingOptions).toContain("appBaseUrl");
55+
expect(configError.missingOptions).toContain("secret");
56+
57+
// Check that error message contains specific text
58+
expect(configError.message).toContain(
59+
"Not all required options where provided"
60+
);
61+
expect(configError.message).toContain(ENV_VARS.DOMAIN);
62+
expect(configError.message).toContain(ENV_VARS.CLIENT_ID);
63+
expect(configError.message).toContain(ENV_VARS.CLIENT_SECRET);
64+
expect(configError.message).toContain(
65+
ENV_VARS.CLIENT_ASSERTION_SIGNING_KEY
66+
);
67+
expect(configError.message).toContain(ENV_VARS.APP_BASE_URL);
68+
expect(configError.message).toContain(ENV_VARS.SECRET);
69+
}
70+
});
71+
72+
it("should throw ConfigurationError when some required options are missing", () => {
73+
// Provide some but not all required options
74+
const options = {
75+
domain: "example.auth0.com",
76+
clientId: "client_123"
77+
};
78+
79+
try {
80+
new Auth0Client(options);
81+
} catch (error) {
82+
const configError = error as ConfigurationError;
83+
expect(configError).toBeInstanceOf(ConfigurationError);
84+
expect(configError.code).toBe(
85+
ConfigurationErrorCode.MISSING_REQUIRED_OPTIONS
86+
);
87+
// These should be missing
88+
expect(configError.missingOptions).toContain("clientAuthentication");
89+
expect(configError.missingOptions).toContain("appBaseUrl");
90+
expect(configError.missingOptions).toContain("secret");
91+
// These should not be in the missing list
92+
expect(configError.missingOptions).not.toContain("domain");
93+
expect(configError.missingOptions).not.toContain("clientId");
94+
95+
// Error message should only contain instructions for missing options
96+
expect(configError.message).toContain(ENV_VARS.CLIENT_SECRET);
97+
expect(configError.message).toContain(
98+
ENV_VARS.CLIENT_ASSERTION_SIGNING_KEY
99+
);
100+
expect(configError.message).toContain(ENV_VARS.APP_BASE_URL);
101+
expect(configError.message).toContain(ENV_VARS.SECRET);
102+
expect(configError.message).not.toContain(`Set ${ENV_VARS.DOMAIN}`);
103+
expect(configError.message).not.toContain(`Set ${ENV_VARS.CLIENT_ID}`);
104+
}
105+
});
106+
107+
it("should accept clientSecret as authentication method", () => {
108+
// Set required environment variables with clientSecret
109+
process.env[ENV_VARS.DOMAIN] = "env.auth0.com";
110+
process.env[ENV_VARS.CLIENT_ID] = "env_client_id";
111+
process.env[ENV_VARS.CLIENT_SECRET] = "env_client_secret";
112+
process.env[ENV_VARS.APP_BASE_URL] = "https://myapp.com";
113+
process.env[ENV_VARS.SECRET] = "env_secret";
114+
115+
// Should not throw
116+
const client = new Auth0Client();
117+
118+
// The client should be instantiated successfully
119+
expect(client).toBeInstanceOf(Auth0Client);
120+
});
121+
122+
it("should accept clientAssertionSigningKey as authentication method", () => {
123+
// Set required environment variables with clientAssertionSigningKey instead of clientSecret
124+
process.env[ENV_VARS.DOMAIN] = "env.auth0.com";
125+
process.env[ENV_VARS.CLIENT_ID] = "env_client_id";
126+
process.env[ENV_VARS.CLIENT_ASSERTION_SIGNING_KEY] = "some-signing-key";
127+
process.env[ENV_VARS.APP_BASE_URL] = "https://myapp.com";
128+
process.env[ENV_VARS.SECRET] = "env_secret";
129+
130+
// Should not throw
131+
const client = new Auth0Client();
132+
133+
// The client should be instantiated successfully
134+
expect(client).toBeInstanceOf(Auth0Client);
135+
});
136+
137+
it("should prioritize options over environment variables", () => {
138+
// Set environment variables
139+
process.env[ENV_VARS.DOMAIN] = "env.auth0.com";
140+
process.env[ENV_VARS.CLIENT_ID] = "env_client_id";
141+
process.env[ENV_VARS.CLIENT_SECRET] = "env_client_secret";
142+
process.env[ENV_VARS.APP_BASE_URL] = "https://myapp.com";
143+
process.env[ENV_VARS.SECRET] = "env_secret";
144+
145+
// Provide conflicting options
146+
const options = {
147+
domain: "options.auth0.com",
148+
clientId: "options_client_id",
149+
clientSecret: "options_client_secret",
150+
appBaseUrl: "https://options-app.com",
151+
secret: "options_secret"
152+
};
153+
154+
// Mock the validateAndExtractRequiredOptions to verify which values are used
155+
const mockValidateAndExtractRequiredOptions = vi
156+
.fn()
157+
.mockReturnValue(options);
158+
const originalValidateAndExtractRequiredOptions =
159+
Auth0Client.prototype["validateAndExtractRequiredOptions"];
160+
Auth0Client.prototype["validateAndExtractRequiredOptions"] =
161+
mockValidateAndExtractRequiredOptions;
162+
163+
try {
164+
new Auth0Client(options);
165+
166+
// Check that validateAndExtractRequiredOptions was called with our options
167+
expect(mockValidateAndExtractRequiredOptions).toHaveBeenCalledWith(
168+
options
169+
);
170+
// The first argument of the first call should be our options object
171+
const passedOptions =
172+
mockValidateAndExtractRequiredOptions.mock.calls[0][0];
173+
expect(passedOptions.domain).toBe("options.auth0.com");
174+
expect(passedOptions.clientId).toBe("options_client_id");
175+
} finally {
176+
// Restore the original method
177+
Auth0Client.prototype["validateAndExtractRequiredOptions"] =
178+
originalValidateAndExtractRequiredOptions;
179+
}
180+
});
181+
});
182+
});

src/server/client.ts

+77-18
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import {
88
AccessTokenErrorCode,
99
AccessTokenForConnectionError,
1010
AccessTokenForConnectionErrorCode,
11+
ConfigurationError,
12+
ConfigurationErrorCode
1113
} from "../errors/index.js";
1214
import {
13-
AuthorizationParameters,
1415
AccessTokenForConnectionOptions,
16+
AuthorizationParameters,
1517
SessionData,
1618
SessionDataStore,
1719
StartInteractiveLoginOptions
@@ -181,19 +183,16 @@ export class Auth0Client {
181183
private authClient: AuthClient;
182184

183185
constructor(options: Auth0ClientOptions = {}) {
184-
const domain = (options.domain || process.env.AUTH0_DOMAIN) as string;
185-
const clientId = (options.clientId ||
186-
process.env.AUTH0_CLIENT_ID) as string;
187-
const clientSecret = (options.clientSecret ||
188-
process.env.AUTH0_CLIENT_SECRET) as string;
189-
190-
const appBaseUrl = (options.appBaseUrl ||
191-
process.env.APP_BASE_URL) as string;
192-
const secret = (options.secret || process.env.AUTH0_SECRET) as string;
186+
// Extract and validate required options
187+
const {
188+
domain,
189+
clientId,
190+
clientSecret,
191+
appBaseUrl,
192+
secret,
193+
clientAssertionSigningKey
194+
} = this.validateAndExtractRequiredOptions(options);
193195

194-
const clientAssertionSigningKey =
195-
options.clientAssertionSigningKey ||
196-
process.env.AUTH0_CLIENT_ASSERTION_SIGNING_KEY;
197196
const clientAssertionSigningAlg =
198197
options.clientAssertionSigningAlg ||
199198
process.env.AUTH0_CLIENT_ASSERTION_SIGNING_ALG;
@@ -261,7 +260,7 @@ export class Auth0Client {
261260
allowInsecureRequests: options.allowInsecureRequests,
262261
httpTimeout: options.httpTimeout,
263262
enableTelemetry: options.enableTelemetry,
264-
enableAccessTokenEndpoint: options.enableAccessTokenEndpoint,
263+
enableAccessTokenEndpoint: options.enableAccessTokenEndpoint
265264
});
266265
}
267266

@@ -473,10 +472,7 @@ export class Auth0Client {
473472
: tokenSet
474473
);
475474
} else {
476-
tokenSets = [
477-
...(session.connectionTokenSets || []),
478-
retrievedTokenSet
479-
];
475+
tokenSets = [...(session.connectionTokenSets || []), retrievedTokenSet];
480476
}
481477

482478
await this.saveToSession(
@@ -652,4 +648,67 @@ export class Auth0Client {
652648
}
653649
}
654650
}
651+
652+
/**
653+
* Validates and extracts required configuration options.
654+
* @param options The client options
655+
* @returns The validated required options
656+
* @throws ConfigurationError if any required option is missing
657+
*/
658+
private validateAndExtractRequiredOptions(options: Auth0ClientOptions) {
659+
// Base required options that are always needed
660+
const requiredOptions = {
661+
domain: options.domain ?? process.env.AUTH0_DOMAIN,
662+
clientId: options.clientId ?? process.env.AUTH0_CLIENT_ID,
663+
appBaseUrl: options.appBaseUrl ?? process.env.APP_BASE_URL,
664+
secret: options.secret ?? process.env.AUTH0_SECRET
665+
};
666+
667+
// Check client authentication options - either clientSecret OR clientAssertionSigningKey must be provided
668+
const clientSecret =
669+
options.clientSecret ?? process.env.AUTH0_CLIENT_SECRET;
670+
const clientAssertionSigningKey =
671+
options.clientAssertionSigningKey ??
672+
process.env.AUTH0_CLIENT_ASSERTION_SIGNING_KEY;
673+
const hasClientAuthentication = !!(
674+
clientSecret || clientAssertionSigningKey
675+
);
676+
677+
const missing = Object.entries(requiredOptions)
678+
.filter(([, value]) => !value)
679+
.map(([key]) => key);
680+
681+
// Add client authentication error if neither option is provided
682+
if (!hasClientAuthentication) {
683+
missing.push("clientAuthentication");
684+
}
685+
686+
if (missing.length) {
687+
// Map of option keys to their exact environment variable names
688+
const envVarNames: Record<string, string> = {
689+
domain: "AUTH0_DOMAIN",
690+
clientId: "AUTH0_CLIENT_ID",
691+
appBaseUrl: "APP_BASE_URL",
692+
secret: "AUTH0_SECRET"
693+
};
694+
695+
throw new ConfigurationError(
696+
ConfigurationErrorCode.MISSING_REQUIRED_OPTIONS,
697+
missing,
698+
envVarNames
699+
);
700+
}
701+
702+
// Prepare the result object with all validated options
703+
const result = {
704+
...requiredOptions,
705+
clientSecret,
706+
clientAssertionSigningKey
707+
};
708+
709+
// Type-safe assignment after validation
710+
return result as {
711+
[K in keyof typeof result]: NonNullable<(typeof result)[K]>;
712+
};
713+
}
655714
}

0 commit comments

Comments
 (0)