Skip to content
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,21 @@ You can customize the client by using the options below:
| httpTimeout | `number` | Integer value for the HTTP timeout in milliseconds for authentication requests. Defaults to `5000` milliseconds |
| 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`. |

## Configuration Validation

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:

- `domain` (or `AUTH0_DOMAIN` environment variable)
- `clientId` (or `AUTH0_CLIENT_ID` environment variable)
- `clientSecret` (or `AUTH0_CLIENT_SECRET` environment variable)
- `appBaseUrl` (or `APP_BASE_URL` environment variable)
- `secret` (or `AUTH0_SECRET` environment variable)

If any of these required options are missing, the SDK will throw a `ConfigurationError` with the code `MISSING_REQUIRED_OPTIONS` and details about which specific options are missing. The error includes:

- A list of missing options
- Instructions on how to provide each missing option (via environment variable or constructor parameter)

## Routes

The SDK mounts 6 routes:
Expand Down
36 changes: 36 additions & 0 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,39 @@ export class AccessTokenForConnectionError extends SdkError {
this.cause = cause;
}
}

/**
* Enum representing error codes related to configuration.
*/
export enum ConfigurationErrorCode {
/**
* Missing required configuration options.
*/
MISSING_REQUIRED_OPTIONS = "missing_required_options"
}

/**
* Error class representing a configuration error.
* Extends the `SdkError` class.
*/
export class ConfigurationError extends SdkError {
/**
* The error code associated with the configuration error.
*/
public code: string;
public missingOptions?: string[];

/**
* Constructs a new `ConfigurationError` instance.
*
* @param code - The error code.
* @param message - The error message.
* @param missingOptions - Optional array of missing configuration option names.
*/
constructor(code: string, message: string, missingOptions?: string[]) {
super(message);
this.name = "ConfigurationError";
this.code = code;
this.missingOptions = missingOptions;
}
}
112 changes: 112 additions & 0 deletions src/server/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { ConfigurationError, ConfigurationErrorCode } from "../errors";
import { Auth0Client } from "./client";

describe("Auth0Client", () => {
// Store original env vars
const originalEnv = { ...process.env };

// Clear env vars before each test
beforeEach(() => {
vi.resetModules();
// Clear all environment variables that might affect the tests
delete process.env.AUTH0_DOMAIN;
delete process.env.AUTH0_CLIENT_ID;
delete process.env.AUTH0_CLIENT_SECRET;
delete process.env.APP_BASE_URL;
delete process.env.AUTH0_SECRET;
});

// Restore env vars after each test
afterEach(() => {
process.env = { ...originalEnv };
});

describe("constructor validation", () => {
it("should throw ConfigurationError when some required options are missing", () => {
// Provide some but not all required options
const options = {
domain: "example.auth0.com",
clientId: "client_123"
};

try {
new Auth0Client(options);
} catch (error) {
const configError = error as ConfigurationError;
expect(configError).toBeInstanceOf(ConfigurationError);
expect(configError.code).toBe(
ConfigurationErrorCode.MISSING_REQUIRED_OPTIONS
);
// These should be missing
expect(configError.missingOptions).toContain("clientSecret");
expect(configError.missingOptions).toContain("appBaseUrl");
expect(configError.missingOptions).toContain("secret");
// These should not be in the missing list
expect(configError.missingOptions).not.toContain("domain");
expect(configError.missingOptions).not.toContain("clientId");
}
});

it("should use environment variables when options are not provided", () => {
// Set environment variables
process.env.AUTH0_DOMAIN = "env.auth0.com";
process.env.AUTH0_CLIENT_ID = "env_client_id";
process.env.AUTH0_CLIENT_SECRET = "env_client_secret";
process.env.APP_BASE_URL = "https://myapp.com";
process.env.AUTH0_SECRET = "env_secret";

// Should not throw
const client = new Auth0Client();

// The client should be instantiated successfully
expect(client).toBeInstanceOf(Auth0Client);
});

it("should prioritize options over environment variables", () => {
// Set environment variables
process.env.AUTH0_DOMAIN = "env.auth0.com";
process.env.AUTH0_CLIENT_ID = "env_client_id";
process.env.AUTH0_CLIENT_SECRET = "env_client_secret";
process.env.APP_BASE_URL = "https://myapp.com";
process.env.AUTH0_SECRET = "env_secret";

// Provide conflicting options
const options = {
domain: "options.auth0.com",
clientId: "options_client_id",
clientSecret: "options_client_secret",
appBaseUrl: "https://options-app.com",
secret: "options_secret"
};

// Mock the validateAndExtractRequiredOptions to verify which values are used
const mockValidateAndExtractRequiredOptions = vi
.fn()
.mockReturnValue(options);
const originalValidateAndExtractRequiredOptions =
Auth0Client.prototype["validateAndExtractRequiredOptions"];
Auth0Client.prototype["validateAndExtractRequiredOptions"] =
mockValidateAndExtractRequiredOptions;

try {
new Auth0Client(options);

// Check that validateAndExtractRequiredOptions was called with our options
expect(mockValidateAndExtractRequiredOptions).toHaveBeenCalledWith(
options
);
// The first argument of the first call should be our options object
const passedOptions =
mockValidateAndExtractRequiredOptions.mock.calls[0][0];
expect(passedOptions.domain).toBe("options.auth0.com");
expect(passedOptions.clientId).toBe("options_client_id");
} finally {
// Restore the original method
Auth0Client.prototype["validateAndExtractRequiredOptions"] =
originalValidateAndExtractRequiredOptions;
}
});
});
});
67 changes: 52 additions & 15 deletions src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {
AccessTokenErrorCode,
AccessTokenForConnectionError,
AccessTokenForConnectionErrorCode,
ConfigurationError,
ConfigurationErrorCode
} from "../errors";
import {
AuthorizationParameters,
AccessTokenForConnectionOptions,
AuthorizationParameters,
SessionData,
SessionDataStore,
StartInteractiveLoginOptions
Expand Down Expand Up @@ -181,15 +183,9 @@ export class Auth0Client {
private authClient: AuthClient;

constructor(options: Auth0ClientOptions = {}) {
const domain = (options.domain || process.env.AUTH0_DOMAIN) as string;
const clientId = (options.clientId ||
process.env.AUTH0_CLIENT_ID) as string;
const clientSecret = (options.clientSecret ||
process.env.AUTH0_CLIENT_SECRET) as string;

const appBaseUrl = (options.appBaseUrl ||
process.env.APP_BASE_URL) as string;
const secret = (options.secret || process.env.AUTH0_SECRET) as string;
// Extract and validate required options
const { domain, clientId, clientSecret, appBaseUrl, secret } =
this.validateAndExtractRequiredOptions(options);

const clientAssertionSigningKey =
options.clientAssertionSigningKey ||
Expand Down Expand Up @@ -261,7 +257,7 @@ export class Auth0Client {
allowInsecureRequests: options.allowInsecureRequests,
httpTimeout: options.httpTimeout,
enableTelemetry: options.enableTelemetry,
enableAccessTokenEndpoint: options.enableAccessTokenEndpoint,
enableAccessTokenEndpoint: options.enableAccessTokenEndpoint
});
}

Expand Down Expand Up @@ -473,10 +469,7 @@ export class Auth0Client {
: tokenSet
);
} else {
tokenSets = [
...(session.connectionTokenSets || []),
retrievedTokenSet
];
tokenSets = [...(session.connectionTokenSets || []), retrievedTokenSet];
}

await this.saveToSession(
Expand Down Expand Up @@ -652,4 +645,48 @@ export class Auth0Client {
}
}
}

/**
* Validates and extracts required configuration options.
* @param options The client options
* @returns The validated required options
* @throws ConfigurationError if any required option is missing
*/
private validateAndExtractRequiredOptions(options: Auth0ClientOptions) {
const requiredOptions = {
domain: options.domain ?? process.env.AUTH0_DOMAIN,
clientId: options.clientId ?? process.env.AUTH0_CLIENT_ID,
clientSecret: options.clientSecret ?? process.env.AUTH0_CLIENT_SECRET,
appBaseUrl: options.appBaseUrl ?? process.env.APP_BASE_URL,
secret: options.secret ?? process.env.AUTH0_SECRET
};

// Check for missing options and prepare error message in one operation
const missing: string[] = [];
let errorMsg = "";

for (const [key, value] of Object.entries(requiredOptions)) {
if (!value) {
missing.push(key);
errorMsg += `- ${key}: Set AUTH0_${key.toUpperCase()} env var or pass ${key} in options\n`;
}
}

if (missing.length) {
throw new ConfigurationError(
ConfigurationErrorCode.MISSING_REQUIRED_OPTIONS,
`Missing mandatory configuration: ${missing.join(", ")}\n` +
"Provide via constructor options or environment variables:\n" +
errorMsg.trim(),
missing
);
}

// Type-safe assignment after validation
return requiredOptions as {
[K in keyof typeof requiredOptions]: NonNullable<
(typeof requiredOptions)[K]
>;
};
}
}