mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-16 16:14:43 -04:00

This was done as LDAPS us both the plural of LDAP and the common abbreviation for secure LDAP connections. Fixes #4460 Signed-off-by: Philip Molares <philip.molares@udo.edu>
452 lines
14 KiB
TypeScript
452 lines
14 KiB
TypeScript
/*
|
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
import { registerAs } from '@nestjs/config';
|
|
import * as fs from 'fs';
|
|
import * as Joi from 'joi';
|
|
|
|
import { GitlabScope, GitlabVersion } from './gitlab.enum';
|
|
import {
|
|
buildErrorMessage,
|
|
parseOptionalNumber,
|
|
replaceAuthErrorsWithEnvironmentVariables,
|
|
toArrayConfig,
|
|
} from './utils';
|
|
|
|
export interface LDAPConfig {
|
|
identifier: string;
|
|
providerName: string;
|
|
url: string;
|
|
bindDn?: string;
|
|
bindCredentials?: string;
|
|
searchBase: string;
|
|
searchFilter: string;
|
|
searchAttributes: string[];
|
|
userIdField: string;
|
|
displayNameField: string;
|
|
profilePictureField: string;
|
|
tlsCaCerts?: string[];
|
|
}
|
|
|
|
export interface AuthConfig {
|
|
session: {
|
|
secret: string;
|
|
lifetime: number;
|
|
};
|
|
local: {
|
|
enableLogin: boolean;
|
|
enableRegister: boolean;
|
|
minimalPasswordStrength: number;
|
|
};
|
|
facebook: {
|
|
clientID: string;
|
|
clientSecret: string;
|
|
};
|
|
twitter: {
|
|
consumerKey: string;
|
|
consumerSecret: string;
|
|
};
|
|
github: {
|
|
clientID: string;
|
|
clientSecret: string;
|
|
};
|
|
dropbox: {
|
|
clientID: string;
|
|
clientSecret: string;
|
|
appKey: string;
|
|
};
|
|
google: {
|
|
clientID: string;
|
|
clientSecret: string;
|
|
apiKey: string;
|
|
};
|
|
gitlab: {
|
|
identifier: string;
|
|
providerName: string;
|
|
baseURL: string;
|
|
clientID: string;
|
|
clientSecret: string;
|
|
scope: GitlabScope;
|
|
version: GitlabVersion;
|
|
}[];
|
|
// ToDo: tlsOptions exist in config.json.example. See https://nodejs.org/api/tls.html#tls_tls_connect_options_callback
|
|
ldap: LDAPConfig[];
|
|
saml: {
|
|
identifier: string;
|
|
providerName: string;
|
|
idpSsoUrl: string;
|
|
idpCert: string;
|
|
clientCert: string;
|
|
issuer: string;
|
|
identifierFormat: string;
|
|
disableRequestedAuthnContext: string;
|
|
groupAttribute: string;
|
|
requiredGroups?: string[];
|
|
externalGroups?: string[];
|
|
attribute: {
|
|
id: string;
|
|
username: string;
|
|
email: string;
|
|
};
|
|
}[];
|
|
oauth2: {
|
|
identifier: string;
|
|
providerName: string;
|
|
baseURL: string;
|
|
userProfileURL: string;
|
|
userProfileIdAttr: string;
|
|
userProfileUsernameAttr: string;
|
|
userProfileDisplayNameAttr: string;
|
|
userProfileEmailAttr: string;
|
|
tokenURL: string;
|
|
authorizationURL: string;
|
|
clientID: string;
|
|
clientSecret: string;
|
|
scope: string;
|
|
rolesClaim: string;
|
|
accessRole: string;
|
|
}[];
|
|
}
|
|
|
|
const authSchema = Joi.object({
|
|
session: {
|
|
secret: Joi.string().label('HD_SESSION_SECRET'),
|
|
lifetime: Joi.number()
|
|
.default(1209600000) // 14 * 24 * 60 * 60 * 1000ms = 14 days
|
|
.optional()
|
|
.label('HD_SESSION_LIFETIME'),
|
|
},
|
|
local: {
|
|
enableLogin: Joi.boolean()
|
|
.default(false)
|
|
.optional()
|
|
.label('HD_AUTH_LOCAL_ENABLE_LOGIN'),
|
|
enableRegister: Joi.boolean()
|
|
.default(false)
|
|
.optional()
|
|
.label('HD_AUTH_LOCAL_ENABLE_REGISTER'),
|
|
minimalPasswordStrength: Joi.number()
|
|
.default(2)
|
|
.min(0)
|
|
.max(4)
|
|
.optional()
|
|
.label('HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH'),
|
|
},
|
|
facebook: {
|
|
clientID: Joi.string().optional().label('HD_AUTH_FACEBOOK_CLIENT_ID'),
|
|
clientSecret: Joi.string()
|
|
.optional()
|
|
.label('HD_AUTH_FACEBOOK_CLIENT_SECRET'),
|
|
},
|
|
twitter: {
|
|
consumerKey: Joi.string().optional().label('HD_AUTH_TWITTER_CONSUMER_KEY'),
|
|
consumerSecret: Joi.string()
|
|
.optional()
|
|
.label('HD_AUTH_TWITTER_CONSUMER_SECRET'),
|
|
},
|
|
github: {
|
|
clientID: Joi.string().optional().label('HD_AUTH_GITHUB_CLIENT_ID'),
|
|
clientSecret: Joi.string().optional().label('HD_AUTH_GITHUB_CLIENT_SECRET'),
|
|
},
|
|
dropbox: {
|
|
clientID: Joi.string().optional().label('HD_AUTH_DROPBOX_CLIENT_ID'),
|
|
clientSecret: Joi.string()
|
|
.optional()
|
|
.label('HD_AUTH_DROPBOX_CLIENT_SECRET'),
|
|
appKey: Joi.string().optional().label('HD_AUTH_DROPBOX_APP_KEY'),
|
|
},
|
|
google: {
|
|
clientID: Joi.string().optional().label('HD_AUTH_GOOGLE_CLIENT_ID'),
|
|
clientSecret: Joi.string().optional().label('HD_AUTH_GOOGLE_CLIENT_SECRET'),
|
|
apiKey: Joi.string().optional().label('HD_AUTH_GOOGLE_APP_KEY'),
|
|
},
|
|
gitlab: Joi.array()
|
|
.items(
|
|
Joi.object({
|
|
identifier: Joi.string(),
|
|
providerName: Joi.string().default('Gitlab').optional(),
|
|
baseURL: Joi.string(),
|
|
clientID: Joi.string(),
|
|
clientSecret: Joi.string(),
|
|
scope: Joi.string()
|
|
.valid(...Object.values(GitlabScope))
|
|
.default(GitlabScope.READ_USER)
|
|
.optional(),
|
|
version: Joi.string()
|
|
.valid(...Object.values(GitlabVersion))
|
|
.default(GitlabVersion.V4)
|
|
.optional(),
|
|
}).optional(),
|
|
)
|
|
.optional(),
|
|
// ToDo: should searchfilter have a default?
|
|
ldap: Joi.array()
|
|
.items(
|
|
Joi.object({
|
|
identifier: Joi.string(),
|
|
providerName: Joi.string().default('LDAP').optional(),
|
|
url: Joi.string(),
|
|
bindDn: Joi.string().optional(),
|
|
bindCredentials: Joi.string().optional(),
|
|
searchBase: Joi.string(),
|
|
searchFilter: Joi.string().default('(uid={{username}})').optional(),
|
|
searchAttributes: Joi.array().items(Joi.string()).optional(),
|
|
userIdField: Joi.string().default('uid').optional(),
|
|
displayNameField: Joi.string().default('displayName').optional(),
|
|
profilePictureField: Joi.string().default('jpegPhoto').optional(),
|
|
tlsCaCerts: Joi.array().items(Joi.string()).optional(),
|
|
}).optional(),
|
|
)
|
|
.optional(),
|
|
saml: Joi.array()
|
|
.items(
|
|
Joi.object({
|
|
identifier: Joi.string(),
|
|
providerName: Joi.string().default('SAML').optional(),
|
|
idpSsoUrl: Joi.string(),
|
|
idpCert: Joi.string(),
|
|
clientCert: Joi.string().optional(),
|
|
// ToDo: (default: config.serverURL) will be build on-the-fly in the config/index.js from domain, urlAddPort and urlPath.
|
|
issuer: Joi.string().optional(),
|
|
identifierFormat: Joi.string()
|
|
.default('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')
|
|
.optional(),
|
|
disableRequestedAuthnContext: Joi.boolean().default(false).optional(),
|
|
groupAttribute: Joi.string().optional(),
|
|
requiredGroups: Joi.array().items(Joi.string()).optional(),
|
|
externalGroups: Joi.array().items(Joi.string()).optional(),
|
|
attribute: {
|
|
id: Joi.string().default('NameId').optional(),
|
|
username: Joi.string().default('NameId').optional(),
|
|
local: Joi.string().default('NameId').optional(),
|
|
},
|
|
}).optional(),
|
|
)
|
|
.optional(),
|
|
oauth2: Joi.array()
|
|
.items(
|
|
Joi.object({
|
|
identifier: Joi.string(),
|
|
providerName: Joi.string().default('OAuth2').optional(),
|
|
baseURL: Joi.string(),
|
|
userProfileURL: Joi.string(),
|
|
userProfileIdAttr: Joi.string().optional(),
|
|
userProfileUsernameAttr: Joi.string(),
|
|
userProfileDisplayNameAttr: Joi.string(),
|
|
userProfileEmailAttr: Joi.string(),
|
|
tokenURL: Joi.string(),
|
|
authorizationURL: Joi.string(),
|
|
clientID: Joi.string(),
|
|
clientSecret: Joi.string(),
|
|
scope: Joi.string().optional(),
|
|
rolesClaim: Joi.string().optional(),
|
|
accessRole: Joi.string().optional(),
|
|
}).optional(),
|
|
)
|
|
.optional(),
|
|
});
|
|
|
|
export default registerAs('authConfig', () => {
|
|
// ToDo: Validate these with Joi to prevent duplicate entries?
|
|
const gitlabNames = (
|
|
toArrayConfig(process.env.HD_AUTH_GITLABS, ',') ?? []
|
|
).map((name) => name.toUpperCase());
|
|
const ldapNames = (
|
|
toArrayConfig(process.env.HD_AUTH_LDAP_SERVERS, ',') ?? []
|
|
).map((name) => name.toUpperCase());
|
|
const samlNames = (toArrayConfig(process.env.HD_AUTH_SAMLS, ',') ?? []).map(
|
|
(name) => name.toUpperCase(),
|
|
);
|
|
const oauth2Names = (
|
|
toArrayConfig(process.env.HD_AUTH_OAUTH2S, ',') ?? []
|
|
).map((name) => name.toUpperCase());
|
|
|
|
const gitlabs = gitlabNames.map((gitlabName) => {
|
|
return {
|
|
identifier: gitlabName,
|
|
providerName: process.env[`HD_AUTH_GITLAB_${gitlabName}_PROVIDER_NAME`],
|
|
baseURL: process.env[`HD_AUTH_GITLAB_${gitlabName}_BASE_URL`],
|
|
clientID: process.env[`HD_AUTH_GITLAB_${gitlabName}_CLIENT_ID`],
|
|
clientSecret: process.env[`HD_AUTH_GITLAB_${gitlabName}_CLIENT_SECRET`],
|
|
scope: process.env[`HD_AUTH_GITLAB_${gitlabName}_SCOPE`],
|
|
version: process.env[`HD_AUTH_GITLAB_${gitlabName}_GITLAB_VERSION`],
|
|
};
|
|
});
|
|
|
|
const ldaps = ldapNames.map((ldapName) => {
|
|
const caFiles = toArrayConfig(
|
|
process.env[`HD_AUTH_LDAP_${ldapName}_TLS_CERT_PATHS`],
|
|
',',
|
|
);
|
|
let tlsCaCerts = undefined;
|
|
if (caFiles) {
|
|
tlsCaCerts = caFiles.map((fileName) => {
|
|
if (fs.existsSync(fileName)) {
|
|
return fs.readFileSync(fileName, 'utf8');
|
|
}
|
|
});
|
|
}
|
|
return {
|
|
identifier: ldapName,
|
|
providerName: process.env[`HD_AUTH_LDAP_${ldapName}_PROVIDER_NAME`],
|
|
url: process.env[`HD_AUTH_LDAP_${ldapName}_URL`],
|
|
bindDn: process.env[`HD_AUTH_LDAP_${ldapName}_BIND_DN`],
|
|
bindCredentials: process.env[`HD_AUTH_LDAP_${ldapName}_BIND_CREDENTIALS`],
|
|
searchBase: process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_BASE`],
|
|
searchFilter: process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_FILTER`],
|
|
searchAttributes: toArrayConfig(
|
|
process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_ATTRIBUTES`],
|
|
',',
|
|
),
|
|
userIdField: process.env[`HD_AUTH_LDAP_${ldapName}_USER_ID_FIELD`],
|
|
displayNameField:
|
|
process.env[`HD_AUTH_LDAP_${ldapName}_DISPLAY_NAME_FIELD`],
|
|
profilePictureField:
|
|
process.env[`HD_AUTH_LDAP_${ldapName}_PROFILE_PICTURE_FIELD`],
|
|
tlsCaCerts: tlsCaCerts,
|
|
};
|
|
});
|
|
|
|
const samls = samlNames.map((samlName) => {
|
|
return {
|
|
identifier: samlName,
|
|
providerName: process.env[`HD_AUTH_SAML_${samlName}_PROVIDER_NAME`],
|
|
idpSsoUrl: process.env[`HD_AUTH_SAML_${samlName}_IDP_SSO_URL`],
|
|
idpCert: process.env[`HD_AUTH_SAML_${samlName}_IDP_CERT`],
|
|
clientCert: process.env[`HD_AUTH_SAML_${samlName}_CLIENT_CERT`],
|
|
issuer: process.env[`HD_AUTH_SAML_${samlName}_ISSUER`],
|
|
identifierFormat:
|
|
process.env[`HD_AUTH_SAML_${samlName}_IDENTIFIER_FORMAT`],
|
|
disableRequestedAuthnContext:
|
|
process.env[`HD_AUTH_SAML_${samlName}_DISABLE_REQUESTED_AUTHN_CONTEXT`],
|
|
groupAttribute: process.env[`HD_AUTH_SAML_${samlName}_GROUP_ATTRIBUTE`],
|
|
requiredGroups: toArrayConfig(
|
|
process.env[`HD_AUTH_SAML_${samlName}_REQUIRED_GROUPS`],
|
|
'|',
|
|
),
|
|
externalGroups: toArrayConfig(
|
|
process.env[`HD_AUTH_SAML_${samlName}_EXTERNAL_GROUPS`],
|
|
'|',
|
|
),
|
|
attribute: {
|
|
id: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_ID`],
|
|
username: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`],
|
|
local: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_LOCAL`],
|
|
},
|
|
};
|
|
});
|
|
|
|
const oauth2s = oauth2Names.map((oauth2Name) => {
|
|
return {
|
|
identifier: oauth2Name,
|
|
providerName: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_PROVIDER_NAME`],
|
|
baseURL: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_BASE_URL`],
|
|
userProfileURL:
|
|
process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_URL`],
|
|
userProfileIdAttr:
|
|
process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_ID_ATTR`],
|
|
userProfileUsernameAttr:
|
|
process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_USERNAME_ATTR`],
|
|
userProfileDisplayNameAttr:
|
|
process.env[
|
|
`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_DISPLAY_NAME_ATTR`
|
|
],
|
|
userProfileEmailAttr:
|
|
process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_EMAIL_ATTR`],
|
|
tokenURL: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_TOKEN_URL`],
|
|
authorizationURL:
|
|
process.env[`HD_AUTH_OAUTH2_${oauth2Name}_AUTHORIZATION_URL`],
|
|
clientID: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_CLIENT_ID`],
|
|
clientSecret: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_CLIENT_SECRET`],
|
|
scope: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_SCOPE`],
|
|
rolesClaim: process.env[`HD_AUTH_OAUTH2_${oauth2Name}`],
|
|
accessRole: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_ACCESS_ROLE`],
|
|
};
|
|
});
|
|
|
|
const authConfig = authSchema.validate(
|
|
{
|
|
session: {
|
|
secret: process.env.HD_SESSION_SECRET,
|
|
lifetime: parseOptionalNumber(process.env.HD_SESSION_LIFETIME),
|
|
},
|
|
local: {
|
|
enableLogin: process.env.HD_AUTH_LOCAL_ENABLE_LOGIN,
|
|
enableRegister: process.env.HD_AUTH_LOCAL_ENABLE_REGISTER,
|
|
minimalPasswordStrength: parseOptionalNumber(
|
|
process.env.HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH,
|
|
),
|
|
},
|
|
facebook: {
|
|
clientID: process.env.HD_AUTH_FACEBOOK_CLIENT_ID,
|
|
clientSecret: process.env.HD_AUTH_FACEBOOK_CLIENT_SECRET,
|
|
},
|
|
twitter: {
|
|
consumerKey: process.env.HD_AUTH_TWITTER_CONSUMER_KEY,
|
|
consumerSecret: process.env.HD_AUTH_TWITTER_CONSUMER_SECRET,
|
|
},
|
|
github: {
|
|
clientID: process.env.HD_AUTH_GITHUB_CLIENT_ID,
|
|
clientSecret: process.env.HD_AUTH_GITHUB_CLIENT_SECRET,
|
|
},
|
|
dropbox: {
|
|
clientID: process.env.HD_AUTH_DROPBOX_CLIENT_ID,
|
|
clientSecret: process.env.HD_AUTH_DROPBOX_CLIENT_SECRET,
|
|
appKey: process.env.HD_AUTH_DROPBOX_APP_KEY,
|
|
},
|
|
google: {
|
|
clientID: process.env.HD_AUTH_GOOGLE_CLIENT_ID,
|
|
clientSecret: process.env.HD_AUTH_GOOGLE_CLIENT_SECRET,
|
|
apiKey: process.env.HD_AUTH_GOOGLE_APP_KEY,
|
|
},
|
|
gitlab: gitlabs,
|
|
ldap: ldaps,
|
|
saml: samls,
|
|
oauth2: oauth2s,
|
|
},
|
|
{
|
|
abortEarly: false,
|
|
presence: 'required',
|
|
},
|
|
);
|
|
if (authConfig.error) {
|
|
const errorMessages = authConfig.error.details
|
|
.map((detail) => detail.message)
|
|
.map((error) =>
|
|
replaceAuthErrorsWithEnvironmentVariables(
|
|
error,
|
|
'gitlab',
|
|
'HD_AUTH_GITLAB_',
|
|
gitlabNames,
|
|
),
|
|
)
|
|
.map((error) =>
|
|
replaceAuthErrorsWithEnvironmentVariables(
|
|
error,
|
|
'ldap',
|
|
'HD_AUTH_LDAP_',
|
|
ldapNames,
|
|
),
|
|
)
|
|
.map((error) =>
|
|
replaceAuthErrorsWithEnvironmentVariables(
|
|
error,
|
|
'saml',
|
|
'HD_AUTH_SAML_',
|
|
samlNames,
|
|
),
|
|
)
|
|
.map((error) =>
|
|
replaceAuthErrorsWithEnvironmentVariables(
|
|
error,
|
|
'oauth2',
|
|
'HD_AUTH_OAUTH2_',
|
|
oauth2Names,
|
|
),
|
|
);
|
|
throw new Error(buildErrorMessage(errorMessages));
|
|
}
|
|
return authConfig.value as AuthConfig;
|
|
});
|