signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
193 lines • 8.11 kB
JavaScript
;
/*
* Copyright 2025 Matti Airas
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseEnvConfig = parseEnvConfig;
exports.mergeConfigs = mergeConfigs;
exports.validateOIDCConfig = validateOIDCConfig;
exports.parseOIDCConfig = parseOIDCConfig;
exports.isOIDCEnabled = isOIDCEnabled;
const types_1 = require("./types");
/**
* Parse OIDC configuration from environment variables
*/
function parseEnvConfig() {
const config = {};
if (process.env.SIGNALK_OIDC_ENABLED !== undefined) {
config.enabled = process.env.SIGNALK_OIDC_ENABLED.toLowerCase() === 'true';
}
if (process.env.SIGNALK_OIDC_ISSUER) {
config.issuer = process.env.SIGNALK_OIDC_ISSUER;
}
if (process.env.SIGNALK_OIDC_CLIENT_ID) {
config.clientId = process.env.SIGNALK_OIDC_CLIENT_ID;
}
if (process.env.SIGNALK_OIDC_CLIENT_SECRET) {
config.clientSecret = process.env.SIGNALK_OIDC_CLIENT_SECRET;
}
if (process.env.SIGNALK_OIDC_REDIRECT_URI) {
config.redirectUri = process.env.SIGNALK_OIDC_REDIRECT_URI;
}
if (process.env.SIGNALK_OIDC_SCOPE) {
config.scope = process.env.SIGNALK_OIDC_SCOPE;
}
if (process.env.SIGNALK_OIDC_DEFAULT_PERMISSION) {
const perm = process.env.SIGNALK_OIDC_DEFAULT_PERMISSION.toLowerCase();
if (perm === 'readonly' || perm === 'readwrite' || perm === 'admin') {
config.defaultPermission = perm;
}
}
if (process.env.SIGNALK_OIDC_AUTO_CREATE_USERS !== undefined) {
config.autoCreateUsers =
process.env.SIGNALK_OIDC_AUTO_CREATE_USERS.toLowerCase() === 'true';
}
// Parse admin groups from comma-separated string
if (process.env.SIGNALK_OIDC_ADMIN_GROUPS) {
const groups = process.env.SIGNALK_OIDC_ADMIN_GROUPS.split(',')
.map((g) => g.trim())
.filter((g) => g.length > 0);
if (groups.length > 0) {
config.adminGroups = groups;
}
}
// Parse readwrite groups from comma-separated string
if (process.env.SIGNALK_OIDC_READWRITE_GROUPS) {
const groups = process.env.SIGNALK_OIDC_READWRITE_GROUPS.split(',')
.map((g) => g.trim())
.filter((g) => g.length > 0);
if (groups.length > 0) {
config.readwriteGroups = groups;
}
}
// Parse groups attribute (ID token claim key for groups)
if (process.env.SIGNALK_OIDC_GROUPS_ATTRIBUTE) {
config.groupsAttribute = process.env.SIGNALK_OIDC_GROUPS_ATTRIBUTE;
}
// Parse provider name (display name for login button)
if (process.env.SIGNALK_OIDC_PROVIDER_NAME) {
config.providerName = process.env.SIGNALK_OIDC_PROVIDER_NAME;
}
// Parse auto login (auto-redirect to OIDC when not authenticated)
if (process.env.SIGNALK_OIDC_AUTO_LOGIN !== undefined) {
config.autoLogin =
process.env.SIGNALK_OIDC_AUTO_LOGIN.toLowerCase() === 'true';
}
return config;
}
/**
* Merge configuration from security.json and environment variables
* Priority: env vars > security.json > defaults
*/
function mergeConfigs(securityJsonConfig, envConfig) {
return {
enabled: envConfig.enabled ?? securityJsonConfig.enabled ?? types_1.OIDC_DEFAULTS.enabled,
issuer: envConfig.issuer ?? securityJsonConfig.issuer ?? '',
clientId: envConfig.clientId ?? securityJsonConfig.clientId ?? '',
clientSecret: envConfig.clientSecret ?? securityJsonConfig.clientSecret ?? '',
redirectUri: envConfig.redirectUri ?? securityJsonConfig.redirectUri ?? '',
scope: envConfig.scope ?? securityJsonConfig.scope ?? types_1.OIDC_DEFAULTS.scope,
defaultPermission: envConfig.defaultPermission ??
securityJsonConfig.defaultPermission ??
types_1.OIDC_DEFAULTS.defaultPermission,
autoCreateUsers: envConfig.autoCreateUsers ??
securityJsonConfig.autoCreateUsers ??
types_1.OIDC_DEFAULTS.autoCreateUsers,
adminGroups: envConfig.adminGroups ?? securityJsonConfig.adminGroups,
readwriteGroups: envConfig.readwriteGroups ?? securityJsonConfig.readwriteGroups,
groupsAttribute: envConfig.groupsAttribute ?? securityJsonConfig.groupsAttribute,
providerName: envConfig.providerName ??
securityJsonConfig.providerName ??
types_1.OIDC_DEFAULTS.providerName,
autoLogin: envConfig.autoLogin ??
securityJsonConfig.autoLogin ??
types_1.OIDC_DEFAULTS.autoLogin
};
}
/**
* Validate OIDC configuration
* @throws OIDCError if configuration is invalid
*/
function validateOIDCConfig(config) {
// If OIDC is disabled, no validation needed
if (config.enabled === false || config.enabled === undefined) {
return;
}
// Validate required fields when enabled
if (!config.issuer) {
throw new types_1.OIDCError('OIDC issuer is required when enabled', 'CONFIG_INVALID');
}
if (!config.clientId) {
throw new types_1.OIDCError('OIDC clientId is required when enabled', 'CONFIG_INVALID');
}
if (!config.clientSecret) {
throw new types_1.OIDCError('OIDC clientSecret is required when enabled', 'CONFIG_INVALID');
}
// Validate redirectUri is required and is a valid URL
if (!config.redirectUri) {
throw new types_1.OIDCError('OIDC redirectUri is required when enabled. Set it via SIGNALK_OIDC_REDIRECT_URI or in security.json.', 'CONFIG_INVALID');
}
let redirectUrl;
try {
redirectUrl = new URL(config.redirectUri);
}
catch {
throw new types_1.OIDCError(`OIDC redirectUri must be a valid URL: ${config.redirectUri}`, 'CONFIG_INVALID');
}
if (redirectUrl.hash) {
throw new types_1.OIDCError('OIDC redirectUri must not contain a fragment (RFC 6749 §3.1.2)', 'CONFIG_INVALID');
}
const LOCALHOST_HOSTS = ['localhost', '127.0.0.1', '[::1]'];
if (redirectUrl.protocol !== 'https:' &&
!LOCALHOST_HOSTS.includes(redirectUrl.hostname)) {
throw new types_1.OIDCError('OIDC redirectUri must use https (http is only allowed for localhost)', 'CONFIG_INVALID');
}
// Validate issuer is a valid URL
try {
new URL(config.issuer);
}
catch {
throw new types_1.OIDCError(`OIDC issuer must be a valid URL: ${config.issuer}`, 'CONFIG_INVALID');
}
// Validate scope contains 'openid'
if (config.scope && !config.scope.split(' ').includes('openid')) {
throw new types_1.OIDCError('OIDC scope must contain "openid"', 'CONFIG_INVALID');
}
// Validate defaultPermission
if (config.defaultPermission &&
!['readonly', 'readwrite', 'admin'].includes(config.defaultPermission)) {
throw new types_1.OIDCError(`OIDC defaultPermission must be one of: readonly, readwrite, admin`, 'CONFIG_INVALID');
}
}
/**
* Parse OIDC configuration from security.json and environment variables
* @param securityConfig The security configuration object (may contain oidc section)
* @returns Complete OIDC configuration
* @throws OIDCError if configuration is invalid
*/
function parseOIDCConfig(securityConfig) {
const securityJsonConfig = securityConfig.oidc ?? {};
const envConfig = parseEnvConfig();
const merged = mergeConfigs(securityJsonConfig, envConfig);
validateOIDCConfig(merged);
return merged;
}
/**
* Check if OIDC is enabled
*/
function isOIDCEnabled(config) {
return config.enabled === true;
}
//# sourceMappingURL=config.js.map