config: Improve error messages

Add labels to most Joi objects
Convert all auth variable insert names to upper case to prevent inconsistent naming of the variables
Rewrite auth errors to correctly point out the problematic variable
Add tests for the config utils functions

Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Philip Molares 2021-01-19 15:47:05 +01:00
parent 5cb1f29a2c
commit 454a883f17
8 changed files with 372 additions and 168 deletions

View file

@ -7,6 +7,7 @@
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import { Loglevel } from './loglevel.enum';
import { buildErrorMessage } from './utils';
export interface AppConfig {
domain: string;
@ -15,12 +16,13 @@ export interface AppConfig {
}
const schema = Joi.object({
domain: Joi.string(),
port: Joi.number().default(3000).optional(),
domain: Joi.string().label('HD_DOMAIN'),
port: Joi.number().default(3000).optional().label('PORT'),
loglevel: Joi.string()
.valid(...Object.values(Loglevel))
.default(Loglevel.WARN)
.optional(),
.optional()
.label('HD_LOGLEVEL'),
});
export default registerAs('appConfig', async () => {
@ -36,7 +38,10 @@ export default registerAs('appConfig', async () => {
},
);
if (appConfig.error) {
throw new Error(appConfig.error.toString());
const errorMessages = await appConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
return appConfig.value;
});

View file

@ -6,7 +6,11 @@
import * as Joi from 'joi';
import { GitlabScope, GitlabVersion } from './gitlab.enum';
import { toArrayConfig } from './utils';
import {
buildErrorMessage,
replaceAuthErrorsWithEnvironmentVariables,
toArrayConfig,
} from './utils';
import { registerAs } from '@nestjs/config';
export interface AuthConfig {
@ -102,38 +106,50 @@ export interface AuthConfig {
const authSchema = Joi.object({
email: {
enableLogin: Joi.boolean().default(false).optional(),
enableRegister: Joi.boolean().default(false).optional(),
enableLogin: Joi.boolean()
.default(false)
.optional()
.label('HD_AUTH_EMAIL_ENABLE_LOGIN'),
enableRegister: Joi.boolean()
.default(false)
.optional()
.label('HD_AUTH_EMAIL_ENABLE_REGISTER'),
},
facebook: {
clientID: Joi.string().optional(),
clientSecret: Joi.string().optional(),
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(),
consumerSecret: Joi.string().optional(),
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(),
clientSecret: Joi.string().optional(),
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(),
clientSecret: Joi.string().optional(),
appKey: Joi.string().optional(),
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(),
clientSecret: Joi.string().optional(),
apiKey: Joi.string().optional(),
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({
providerName: Joi.string().default('Gitlab').optional(),
baseURL: Joi.string().optional(),
clientID: Joi.string().optional(),
clientSecret: Joi.string().optional(),
baseURL: Joi.string(),
clientID: Joi.string(),
clientSecret: Joi.string(),
scope: Joi.string()
.valid(...Object.values(GitlabScope))
.default(GitlabScope.READ_USER)
@ -142,7 +158,7 @@ const authSchema = Joi.object({
.valid(...Object.values(GitlabVersion))
.default(GitlabVersion.V4)
.optional(),
}),
}).optional(),
)
.optional(),
// ToDo: should searchfilter have a default?
@ -150,70 +166,83 @@ const authSchema = Joi.object({
.items(
Joi.object({
providerName: Joi.string().default('LDAP').optional(),
url: Joi.string().optional(),
url: Joi.string(),
bindDn: Joi.string().optional(),
bindCredentials: Joi.string().optional(),
searchBase: Joi.string().optional(),
searchBase: Joi.string(),
searchFilter: Joi.string().default('(uid={{username}})').optional(),
searchAttributes: Joi.array().items(Joi.string()),
usernameField: Joi.string().default('userid').optional(),
useridField: Joi.string().optional(),
tlsCa: Joi.array().items(Joi.string()),
}),
searchAttributes: Joi.array()
.items(Joi.string())
.default(['displayName', 'mail'])
.optional(),
usernameField: Joi.string().optional(),
useridField: Joi.string(),
tlsCa: Joi.array().items(Joi.string()).optional(),
}).optional(),
)
.optional(),
saml: Joi.array()
.items(
Joi.object({
providerName: Joi.string().default('SAML').optional(),
idpSsoUrl: Joi.string().optional(),
idpCert: Joi.string().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(), //.default().optional(),
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()),
externalGroups: Joi.array().items(Joi.string()),
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(),
email: Joi.string().default('NameId').optional(),
},
}),
}).optional(),
)
.optional(),
oauth2: Joi.array()
.items(
Joi.object({
providerName: Joi.string().default('OAuth2').optional(),
baseURL: Joi.string().optional(),
userProfileURL: Joi.string().optional(),
baseURL: Joi.string(),
userProfileURL: Joi.string(),
userProfileIdAttr: Joi.string().optional(),
userProfileUsernameAttr: Joi.string().optional(),
userProfileDisplayNameAttr: Joi.string().optional(),
userProfileEmailAttr: Joi.string().optional(),
tokenURL: Joi.string().optional(),
authorizationURL: Joi.string().optional(),
clientID: Joi.string().optional(),
clientSecret: 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', async () => {
// ToDo: Validate these with Joi to prevent duplicate entries?
const gitlabNames = toArrayConfig(process.env.HD_AUTH_GITLABS, ',');
const ldapNames = toArrayConfig(process.env.HD_AUTH_LDAPS, ',');
const samlNames = toArrayConfig(process.env.HD_AUTH_SAMLS, ',');
const oauth2Names = toArrayConfig(process.env.HD_AUTH_OAUTH2S, ',');
const gitlabNames = toArrayConfig(
process.env.HD_AUTH_GITLABS,
',',
).map((name) => name.toUpperCase());
const ldapNames = toArrayConfig(process.env.HD_AUTH_LDAPS, ',').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 {
@ -221,7 +250,7 @@ const gitlabs = gitlabNames.map((gitlabName) => {
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}_GITLAB_SCOPE`],
scope: process.env[`HD_AUTH_GITLAB_${gitlabName}_SCOPE`],
version: process.env[`HD_AUTH_GITLAB_${gitlabName}_GITLAB_VERSION`],
};
});
@ -251,7 +280,8 @@ const samls = samlNames.map((samlName) => {
idpCert: process.env[`HD_AUTH_SAML_${samlName}_IDPCERT`],
clientCert: process.env[`HD_AUTH_SAML_${samlName}_CLIENTCERT`],
issuer: process.env[`HD_AUTH_SAML_${samlName}_ISSUER`],
identifierFormat: process.env[`HD_AUTH_SAML_${samlName}_IDENTIFIERFORMAT`],
identifierFormat:
process.env[`HD_AUTH_SAML_${samlName}_IDENTIFIERFORMAT`],
disableRequestedAuthnContext:
process.env[`HD_AUTH_SAML_${samlName}_DISABLEREQUESTEDAUTHNCONTEXT`],
groupAttribute: process.env[`HD_AUTH_SAML_${samlName}_GROUPATTRIBUTE`],
@ -274,7 +304,7 @@ const samls = samlNames.map((samlName) => {
const oauth2s = oauth2Names.map((oauth2Name) => {
return {
providerName: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_PROVIDER_NAME`],
baseURL: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_BASEURL`],
baseURL: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_BASE_URL`],
userProfileURL:
process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_URL`],
userProfileIdAttr:
@ -293,12 +323,11 @@ const oauth2s = oauth2Names.map((oauth2Name) => {
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}_ROLES_CLAIM`],
rolesClaim: process.env[`HD_AUTH_OAUTH2_${oauth2Name}`],
accessRole: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_ACCESS_ROLE`],
};
});
export default registerAs('authConfig', async () => {
const authConfig = authSchema.validate(
{
email: {
@ -338,7 +367,36 @@ export default registerAs('authConfig', async () => {
},
);
if (authConfig.error) {
throw new Error(authConfig.error.toString());
const errorMessages = await authConfig.error.details
.map((detail) => detail.message)
.map((error) => {
error = replaceAuthErrorsWithEnvironmentVariables(
error,
'gitlab',
'HD_AUTH_GITLAB_',
gitlabNames,
);
error = replaceAuthErrorsWithEnvironmentVariables(
error,
'ldap',
'HD_AUTH_LDAP_',
ldapNames,
);
error = replaceAuthErrorsWithEnvironmentVariables(
error,
'saml',
'HD_AUTH_SAML_',
samlNames,
);
error = replaceAuthErrorsWithEnvironmentVariables(
error,
'oauth2',
'HD_AUTH_OAUTH2_',
oauth2Names,
);
return error;
});
throw new Error(buildErrorMessage(errorMessages));
}
return authConfig.value;
});

View file

@ -6,17 +6,16 @@
import * as Joi from 'joi';
import { registerAs } from '@nestjs/config';
import { buildErrorMessage } from './utils';
export interface CspConfig {
enable: boolean;
maxAgeSeconds: number;
includeSubdomains: boolean;
preload: boolean;
reportURI: string;
}
const cspSchema = Joi.object({
enable: Joi.boolean().default(true).optional(),
reportURI: Joi.string().optional(),
enable: Joi.boolean().default(true).optional().label('HD_CSP_ENABLE'),
reportURI: Joi.string().optional().label('HD_CSP_REPORTURI'),
});
export default registerAs('cspConfig', async () => {
@ -31,7 +30,10 @@ export default registerAs('cspConfig', async () => {
},
);
if (cspConfig.error) {
throw new Error(cspConfig.error.toString());
const errorMessages = await cspConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
return cspConfig.value;
});

View file

@ -7,6 +7,7 @@
import * as Joi from 'joi';
import { DatabaseDialect } from './database-dialect.enum';
import { registerAs } from '@nestjs/config';
import { buildErrorMessage } from './utils';
export interface DatabaseConfig {
username: string;
@ -23,33 +24,35 @@ const databaseSchema = Joi.object({
is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}),
}).label('HD_DATABASE_USER'),
password: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}),
}).label('HD_DATABASE_PASS'),
database: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}),
}).label('HD_DATABASE_NAME'),
host: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}),
}).label('HD_DATABASE_HOST'),
port: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.number(),
otherwise: Joi.optional(),
}),
}).label('HD_DATABASE_PORT'),
storage: Joi.when('dialect', {
is: Joi.valid(DatabaseDialect.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}),
dialect: Joi.string().valid(...Object.values(DatabaseDialect)),
}).label('HD_DATABASE_STORAGE'),
dialect: Joi.string()
.valid(...Object.values(DatabaseDialect))
.label('HD_DATABASE_DIALECT'),
});
export default registerAs('databaseConfig', async () => {
@ -69,7 +72,10 @@ export default registerAs('databaseConfig', async () => {
},
);
if (databaseConfig.error) {
throw new Error(databaseConfig.error.toString());
const errorMessages = await databaseConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
return databaseConfig.value;
});

View file

@ -6,6 +6,7 @@
import * as Joi from 'joi';
import { registerAs } from '@nestjs/config';
import { buildErrorMessage } from './utils';
export interface HstsConfig {
enable: boolean;
@ -15,12 +16,16 @@ export interface HstsConfig {
}
const hstsSchema = Joi.object({
enable: Joi.boolean().default(true).optional(),
enable: Joi.boolean().default(true).optional().label('HD_HSTS_ENABLE'),
maxAgeSeconds: Joi.number()
.default(60 * 60 * 24 * 365)
.optional(),
includeSubdomains: Joi.boolean().default(true).optional(),
preload: Joi.boolean().default(true).optional(),
.optional()
.label('HD_HSTS_MAX_AGE'),
includeSubdomains: Joi.boolean()
.default(true)
.optional()
.label('HD_HSTS_INCLUDE_SUBDOMAINS'),
preload: Joi.boolean().default(true).optional().label('HD_HSTS_PRELOAD'),
});
export default registerAs('hstsConfig', async () => {
@ -37,7 +42,10 @@ export default registerAs('hstsConfig', async () => {
},
);
if (hstsConfig.error) {
throw new Error(hstsConfig.error.toString());
const errorMessages = await hstsConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
return hstsConfig.value;
});

View file

@ -7,6 +7,7 @@
import * as Joi from 'joi';
import { BackendType } from '../media/backends/backend-type.enum';
import { registerAs } from '@nestjs/config';
import { buildErrorMessage } from './utils';
export interface MediaConfig {
backend: {
@ -33,37 +34,41 @@ export interface MediaConfig {
const mediaSchema = Joi.object({
backend: {
use: Joi.string().valid(...Object.values(BackendType)),
use: Joi.string()
.valid(...Object.values(BackendType))
.label('HD_MEDIA_BACKEND'),
filesystem: {
uploadPath: Joi.when('...use', {
is: Joi.valid(BackendType.FILESYSTEM),
then: Joi.string(),
otherwise: Joi.optional(),
}),
}).label('HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH'),
},
s3: Joi.when('...use', {
is: Joi.valid(BackendType.S3),
then: Joi.object({
accessKey: Joi.string(),
secretKey: Joi.string(),
endPoint: Joi.string(),
secure: Joi.boolean(),
port: Joi.number(),
accessKey: Joi.string().label('HD_MEDIA_BACKEND_S3_ACCESS_KEY'),
secretKey: Joi.string().label('HD_MEDIA_BACKEND_S3_SECRET_KEY'),
endPoint: Joi.string().label('HD_MEDIA_BACKEND_S3_ENDPOINT'),
secure: Joi.boolean().label('HD_MEDIA_BACKEND_S3_SECURE'),
port: Joi.number().label('HD_MEDIA_BACKEND_S3_PORT'),
}),
otherwise: Joi.optional(),
}),
azure: Joi.when('...use', {
is: Joi.valid(BackendType.AZURE),
then: Joi.object({
connectionString: Joi.string(),
container: Joi.string(),
connectionString: Joi.string().label(
'HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING',
),
container: Joi.string().label('HD_MEDIA_BACKEND_AZURE_CONTAINER'),
}),
otherwise: Joi.optional(),
}),
imgur: Joi.when('...use', {
is: Joi.valid(BackendType.IMGUR),
then: Joi.object({
clientID: Joi.string(),
clientID: Joi.string().label('HD_MEDIA_BACKEND_IMGUR_CLIENTID'),
}),
otherwise: Joi.optional(),
}),
@ -80,7 +85,7 @@ export default registerAs('mediaConfig', async () => {
},
s3: {
accessKey: process.env.HD_MEDIA_BACKEND_S3_ACCESS_KEY,
secretKey: process.env.HD_MEDIA_BACKEND_S3_ACCESS_KEY,
secretKey: process.env.HD_MEDIA_BACKEND_S3_SECRET_KEY,
endPoint: process.env.HD_MEDIA_BACKEND_S3_ENDPOINT,
secure: process.env.HD_MEDIA_BACKEND_S3_SECURE,
port: parseInt(process.env.HD_MEDIA_BACKEND_S3_PORT) || undefined,
@ -101,7 +106,10 @@ export default registerAs('mediaConfig', async () => {
},
);
if (mediaConfig.error) {
throw new Error(mediaConfig.error.toString());
const errorMessages = await mediaConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
return mediaConfig.value;
});

43
src/config/utils.spec.ts Normal file
View file

@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
replaceAuthErrorsWithEnvironmentVariables,
toArrayConfig,
} from './utils';
describe('config utils', () => {
describe('toArrayConfig', () => {
it('empty', () => {
expect(toArrayConfig('')).toEqual([]);
});
it('one element', () => {
expect(toArrayConfig('one')).toEqual(['one']);
});
it('multiple elements', () => {
expect(toArrayConfig('one, two, three')).toEqual(['one', 'two', 'three']);
});
it('non default seperator', () => {
expect(toArrayConfig('one ; two ; three', ';')).toEqual([
'one',
'two',
'three',
]);
});
});
describe('toArrayConfig', () => {
it('"gitlab[0].scope', () => {
expect(
replaceAuthErrorsWithEnvironmentVariables(
'"gitlab[0].scope',
'gitlab',
'HD_AUTH_GITLAB_',
['test'],
),
).toEqual('"HD_AUTH_GITLAB_test_SCOPE');
});
});
});

View file

@ -15,3 +15,77 @@ export const toArrayConfig = (configValue: string, separator = ',') => {
return configValue.split(separator).map((arrayItem) => arrayItem.trim());
};
export const buildErrorMessage = (errorMessages: string[]): string => {
let totalErrorMessage = 'There were some errors with your configuration:';
for (const message of errorMessages) {
totalErrorMessage += '\n - ';
totalErrorMessage += message;
}
totalErrorMessage +=
'\nFor further information, have a look at our configuration docs at https://docs.hedgedoc.org/configuration';
return totalErrorMessage;
};
export const replaceAuthErrorsWithEnvironmentVariables = (
message: string,
name: string,
replacement: string,
arrayOfNames: string[],
): string => {
// this builds a regex like /"gitlab\[(\d+)]\./ to extract the position in the arrayOfNames
const regex = new RegExp('"' + name + '\\[(\\d+)]\\.', 'g');
message = message.replace(
regex,
(_, index) => `"${replacement}${arrayOfNames[index]}.`,
);
message = message.replace('.providerName', '_PROVIDER_NAME');
message = message.replace('.baseURL', '_BASE_URL');
message = message.replace('.clientID', '_CLIENT_ID');
message = message.replace('.clientSecret', '_CLIENT_SECRET');
message = message.replace('.scope', '_SCOPE');
message = message.replace('.version', '_GITLAB_VERSION');
message = message.replace('.url', '_URL');
message = message.replace('.bindDn', '_BIND_DN');
message = message.replace('.bindCredentials', '_BIND_CREDENTIALS');
message = message.replace('.searchBase', '_SEARCH_BASE');
message = message.replace('.searchFilter', '_SEARCH_FILTER');
message = message.replace('.searchAttributes', '_SEARCH_ATTRIBUTES');
message = message.replace('.usernameField', '_USERNAME_FIELD');
message = message.replace('.useridField', '_USERID_FIELD');
message = message.replace('.tlsCa', '_TLS_CA');
message = message.replace('.idpSsoUrl', '_IDPSSOURL');
message = message.replace('.idpCert', '_IDPCERT');
message = message.replace('.clientCert', '_CLIENTCERT');
message = message.replace('.issuer', '_ISSUER');
message = message.replace('.identifierFormat', '_IDENTIFIERFORMAT');
message = message.replace(
'.disableRequestedAuthnContext',
'_DISABLEREQUESTEDAUTHNCONTEXT',
);
message = message.replace('.groupAttribute', '_GROUPATTRIBUTE');
message = message.replace('.requiredGroups', '_REQUIREDGROUPS');
message = message.replace('.externalGroups', '_EXTERNALGROUPS');
message = message.replace('.attribute.id', '_ATTRIBUTE_ID');
message = message.replace('.attribute.username', '_ATTRIBUTE_USERNAME');
message = message.replace('.attribute.email', '_ATTRIBUTE_USERNAME');
message = message.replace('.userProfileURL', '_USER_PROFILE_URL');
message = message.replace('.userProfileIdAttr', '_USER_PROFILE_ID_ATTR');
message = message.replace(
'.userProfileUsernameAttr',
'_USER_PROFILE_USERNAME_ATTR',
);
message = message.replace(
'.userProfileDisplayNameAttr',
'_USER_PROFILE_DISPLAY_NAME_ATTR',
);
message = message.replace(
'.userProfileEmailAttr',
'_USER_PROFILE_EMAIL_ATTR',
);
message = message.replace('.tokenURL', '_TOKEN_URL');
message = message.replace('.authorizationURL', '_AUTHORIZATION_URL');
message = message.replace('.rolesClaim', '_ROLES_CLAIM');
message = message.replace('.accessRole', '_ACCESS_ROLE');
return message;
};