Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Supports any standard OAuth 2.0 provider through configuration.
*/

import { initializeProviders } from './lib/config.ts';
import { initializeProviders, expandEnvVar } from './lib/config.ts';
import { createOAuthResource } from './lib/resource.ts';
import { validateAndRefreshSession } from './lib/sessionValidator.ts';
import { clearOAuthSession } from './lib/handlers.ts';
Expand Down Expand Up @@ -83,11 +83,7 @@ export async function handleApplication(scope: Scope): Promise<void> {
const rawOptions = (scope.options.getAll() || {}) as OAuthPluginConfig;

// Expand environment variables in plugin-level options
let debugValue = rawOptions.debug;
if (typeof debugValue === 'string' && debugValue.startsWith('${') && debugValue.endsWith('}')) {
const envVar = debugValue.slice(2, -1);
debugValue = process.env[envVar] || debugValue;
}
const debugValue = expandEnvVar(rawOptions.debug);

const options = { ...rawOptions, debug: debugValue };
const previousDebugMode = debugMode;
Expand Down
35 changes: 26 additions & 9 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ import { OAuthProvider } from './OAuthProvider.ts';
import { getProvider } from './providers/index.ts';
import type { OAuthProviderConfig, OAuthPluginConfig, ProviderRegistry, Logger } from '../types.ts';

/**
* Expand environment variable in a string value
*
* If the value is a string in the format `${VAR_NAME}`, it will be replaced
* with the value of the environment variable. Non-string values are returned unchanged.
*
* @example
* expandEnvVar('${MY_VAR}') // Returns process.env.MY_VAR or '${MY_VAR}' if undefined
* expandEnvVar('literal') // Returns 'literal'
* expandEnvVar(123) // Returns 123
* expandEnvVar(true) // Returns true
*/
export function expandEnvVar(value: any): any {
if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
// Extract environment variable name
const envVar = value.slice(2, -1);
const envValue = process.env[envVar];
// Only use env value if it exists (even if empty string)
return envValue !== undefined ? envValue : value;
}
return value;
}

/**
* Build configuration for a specific provider
*/
Expand All @@ -21,13 +44,7 @@ export function buildProviderConfig(
// Expand environment variables in config values
const expandedOptions: Record<string, any> = {};
for (const [key, value] of Object.entries(options)) {
if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
// Extract environment variable name
const envVar = value.slice(2, -1);
expandedOptions[key] = process.env[envVar] || value;
} else {
expandedOptions[key] = value;
}
expandedOptions[key] = expandEnvVar(value);
}

// Check for known provider presets
Expand Down Expand Up @@ -99,10 +116,10 @@ export function buildProviderConfig(
export function extractPluginDefaults(options: OAuthPluginConfig): Partial<OAuthProviderConfig> {
const pluginDefaults: Partial<OAuthProviderConfig> = {};

// Copy all non-provider config to defaults
// Copy all non-provider config to defaults, expanding environment variables
for (const [key, value] of Object.entries(options)) {
if (key !== 'providers' && key !== 'debug') {
pluginDefaults[key as keyof OAuthProviderConfig] = value as any;
pluginDefaults[key as keyof OAuthProviderConfig] = expandEnvVar(value);
}
}

Expand Down
132 changes: 130 additions & 2 deletions test/lib/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

import { describe, it, before, after, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { buildProviderConfig, extractPluginDefaults, initializeProviders } from '../../dist/lib/config.js';
import {
buildProviderConfig,
extractPluginDefaults,
initializeProviders,
expandEnvVar,
} from '../../dist/lib/config.js';

describe('OAuth Configuration', () => {
let originalEnv;
Expand All @@ -23,7 +28,7 @@ describe('OAuth Configuration', () => {
beforeEach(() => {
// Reset environment for each test
Object.keys(process.env).forEach((key) => {
if (key.startsWith('OAUTH_')) {
if (key.startsWith('OAUTH_') || key.startsWith('TEST_')) {
delete process.env[key];
}
});
Expand All @@ -36,6 +41,46 @@ describe('OAuth Configuration', () => {
};
});

describe('expandEnvVar', () => {
it('should expand environment variable references', () => {
process.env.TEST_VAR = 'test-value';
const result = expandEnvVar('${TEST_VAR}');
assert.equal(result, 'test-value');
});

it('should return original value when env var does not exist', () => {
const result = expandEnvVar('${NONEXISTENT_VAR}');
assert.equal(result, '${NONEXISTENT_VAR}');
});

it('should return non-string values unchanged', () => {
assert.equal(expandEnvVar(123), 123);
assert.equal(expandEnvVar(true), true);
assert.equal(expandEnvVar(null), null);
assert.deepEqual(expandEnvVar({ key: 'value' }), { key: 'value' });
});

it('should return literal strings unchanged', () => {
const result = expandEnvVar('literal-string');
assert.equal(result, 'literal-string');
});

it('should not expand partial matches', () => {
const result1 = expandEnvVar('${MISSING_CLOSE');
const result2 = expandEnvVar('MISSING_OPEN}');
const result3 = expandEnvVar('text ${VAR} text');
assert.equal(result1, '${MISSING_CLOSE');
assert.equal(result2, 'MISSING_OPEN}');
assert.equal(result3, 'text ${VAR} text');
});

it('should handle empty environment variable values', () => {
process.env.EMPTY_VAR = '';
const result = expandEnvVar('${EMPTY_VAR}');
assert.equal(result, '');
});
});

describe('buildProviderConfig', () => {
it('should build basic provider config', () => {
const providerConfig = {
Expand Down Expand Up @@ -318,6 +363,62 @@ describe('OAuth Configuration', () => {
assert.equal(defaults.providers, undefined);
assert.equal(defaults.debug, undefined);
});

it('should expand environment variables in plugin defaults', () => {
process.env.TEST_REDIRECT_URI = 'https://example.com/oauth/callback';
process.env.TEST_DEFAULT_ROLE = 'admin';

const options = {
redirectUri: '${TEST_REDIRECT_URI}',
defaultRole: '${TEST_DEFAULT_ROLE}',
scope: 'openid profile',
providers: { github: {} },
};

const defaults = extractPluginDefaults(options);

assert.equal(defaults.redirectUri, 'https://example.com/oauth/callback');
assert.equal(defaults.defaultRole, 'admin');
assert.equal(defaults.scope, 'openid profile');
});

it('should preserve literal values when not env vars', () => {
const options = {
redirectUri: 'https://literal.com/oauth',
scope: 'openid profile email',
};

const defaults = extractPluginDefaults(options);

assert.equal(defaults.redirectUri, 'https://literal.com/oauth');
assert.equal(defaults.scope, 'openid profile email');
});

it('should handle missing environment variables in defaults', () => {
const options = {
redirectUri: '${NONEXISTENT_REDIRECT_URI}',
postLoginRedirect: '/home',
};

const defaults = extractPluginDefaults(options);

// Should preserve the original value when env var doesn't exist
assert.equal(defaults.redirectUri, '${NONEXISTENT_REDIRECT_URI}');
assert.equal(defaults.postLoginRedirect, '/home');
});

it('should handle non-string values in defaults', () => {
const options = {
redirectUri: 'https://example.com/oauth',
timeout: 5000,
enabled: true,
};

const defaults = extractPluginDefaults(options);

assert.equal(defaults.timeout, 5000);
assert.equal(defaults.enabled, true);
});
});

describe('initializeProviders', () => {
Expand Down Expand Up @@ -453,5 +554,32 @@ describe('OAuth Configuration', () => {
const providers = initializeProviders(options, mockLogger);
assert.ok(providers.bad || !providers.bad); // Either initialized or skipped
});

it('should expand environment variables in plugin-level redirectUri', () => {
process.env.TEST_OAUTH_REDIRECT = 'https://test.com/oauth';
process.env.TEST_GOOGLE_CLIENT_ID = 'google-client-123';
process.env.TEST_GOOGLE_SECRET = 'google-secret-456';

const options = {
redirectUri: '${TEST_OAUTH_REDIRECT}',
defaultRole: 'user',
providers: {
google: {
provider: 'google',
clientId: '${TEST_GOOGLE_CLIENT_ID}',
clientSecret: '${TEST_GOOGLE_SECRET}',
},
},
};

const providers = initializeProviders(options, mockLogger);

assert.ok(providers.google);
assert.equal(providers.google.config.clientId, 'google-client-123');
assert.equal(providers.google.config.clientSecret, 'google-secret-456');
// The redirectUri should use the expanded value from plugin defaults
assert.ok(providers.google.config.redirectUri.startsWith('https://test.com/oauth'));
assert.ok(providers.google.config.redirectUri.includes('google'));
});
});
});