import dotenv from 'dotenv';
import axios from 'axios';
// Load environment variables
dotenv.config();
console.log('Environment variables loaded.');
// Configuration constants
const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com';
const ADMIN_USERNAME = process.env.KEYCLOAK_ADMIN_USER;
const ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD;
const REALM_NAME = 'office-automation';
console.log('Configuration constants set:', { KEYCLOAK_URL, ADMIN_USERNAME, REALM_NAME });
// Client IDs and their configuration
const CLIENTS = {
[process.env.NEXTCLOUD_CLIENT_ID || 'nextcloud']: {
redirectUris: [
`https://cloud.mrx8086.com/apps/sociallogin/custom_oidc/keycloak`,
`https://cloud.mrx8086.com/apps/user_oidc/code`
],
postLogoutRedirectUris: ["https://cloud.mrx8086.com/*"]
},
[process.env.PAPERLESS_CLIENT_ID || 'paperless']: {
redirectUris: ["https://docs.mrx8086.com/*"]
},
[process.env.NODERED_CLIENT_ID || 'nodered']: {
redirectUris: ["https://automate.mrx8086.com/*"]
}
};
console.log('CLIENTS configuration:', CLIENTS);
// Helper function for API error handling
const handleAxiosError = (error, operation, config, response) => {
console.error(`Error during ${operation}:`);
if (config) {
console.error('Request:', {
method: config.method,
url: config.url,
headers: config.headers,
data: config.data,
});
}
if (error.response) {
console.error('Response:', {
status: error.response.status,
data: error.response.data
});
} else {
console.error('Error Message:', error.message);
}
throw error;
};
// Get Admin Token
async function getAdminToken() {
console.log('Getting admin token...');
try {
const response = await axios.post(
`${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`,
new URLSearchParams({
'client_id': 'admin-cli',
'username': ADMIN_USERNAME,
'password': ADMIN_PASSWORD,
'grant_type': 'password'
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
console.log('Admin token received.');
return response.data.access_token;
} catch (error) {
handleAxiosError(error, 'getting admin token');
}
}
// Check if Realm exists
async function checkRealmExists(token) {
console.log(`Checking if realm ${REALM_NAME} exists...`);
try {
await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
console.log(`Realm ${REALM_NAME} exists.`);
return true;
} catch (error) {
if (error.response?.status === 404) {
console.log(`Realm ${REALM_NAME} does not exist.`);
return false;
}
handleAxiosError(error, 'checking realm existence');
}
}
// Function to get client information by clientId
async function getClientByClientId(token, clientId) {
console.log(`Getting client information for ${clientId}...`);
try {
const response = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`, {
headers: { 'Authorization': `Bearer ${token}` },
params: { clientId }
});
if (response.data.length === 0) {
console.log(`Client ${clientId} not found`);
return null;
}
console.log(`Client ${clientId} found.`);
return response.data[0];
} catch (error) {
handleAxiosError(error, `getting client ${clientId}`);
return null;
}
}
// Check if client exists
const checkClientExists = async (token, clientId) => !!await getClientByClientId(token, clientId);
// Get client mappers by client ID
async function getClientMappers(token, clientId) {
console.log(`Getting client mappers for ${clientId}...`);
const client = await getClientByClientId(token, clientId);
if (!client) {
console.log(`Client ${clientId} not found, no mappers to get.`);
return [];
}
try {
const response = await axios.get(
`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
console.log(`Client mappers for ${clientId} retrieved.`);
return response.data;
} catch (error) {
handleAxiosError(error, `getting client mappers for ${clientId}`, error.config, error.response);
return [];
}
}
// Get client scopes for a client
async function getClientScopes(token, clientId) {
console.log(`Getting client scopes for ${clientId}...`);
const client = await getClientByClientId(token, clientId);
if (!client) {
console.log(`Client ${clientId} not found, no client scopes to get.`);
return [];
}
try {
const response = await axios.get(
`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/client-scopes`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
console.log(`Client scopes for ${clientId} retrieved.`);
return response.data;
} catch (error) {
handleAxiosError(error, `getting client scopes for ${clientId}`, error.config, error.response);
return [];
}
}
// Get a specific client scope by name
async function getClientScope(token, scopeName) {
console.log(`Getting client scope "${scopeName}"...`);
try {
const response = await axios.get(
`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
const foundScope = response.data.find(scope => scope.name === scopeName);
if (!foundScope) {
console.log(`Client Scope "${scopeName}" not found`);
return null;
}
console.log(`Client scope "${scopeName}" found:`, foundScope);
return foundScope;
} catch (error) {
handleAxiosError(error, `getting client scope ${scopeName}`, error.config, error.response);
return null;
}
}
// Add a default client scope to a client
async function addDefaultClientScope(token, clientId, scopeName) {
console.log(`Adding client scope "${scopeName}" as default for client "${clientId}"...`);
const client = await getClientByClientId(token, clientId);
const scope = await getClientScope(token, scopeName);
if (!client || !scope) {
console.log(`Client "${clientId}" or scope "${scopeName}" not found, cannot add as default scope.`);
return;
}
try {
await axios.put(
`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/default-client-scopes/${scope.id}`,
null,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.log(`Client scope "${scopeName}" added as default scope for client "${clientId}"`);
} catch (error) {
handleAxiosError(error, `adding client scope "${scopeName}" as default for client "${clientId}"`);
}
}
// Create Realm
async function createRealm(token) {
console.log(`Creating realm ${REALM_NAME}...`);
const realmConfig = {
realm: REALM_NAME,
enabled: true,
displayName: "Office Automation",
displayNameHtml: "
Office Automation
",
sslRequired: "external",
registrationAllowed: false,
loginWithEmailAllowed: true,
duplicateEmailsAllowed: false,
resetPasswordAllowed: true,
editUsernameAllowed: false,
bruteForceProtected: true,
permanentLockout: false,
maxFailureWaitSeconds: 900,
minimumQuickLoginWaitSeconds: 60,
waitIncrementSeconds: 60,
quickLoginCheckMilliSeconds: 1000,
maxDeltaTimeSeconds: 43200,
failureFactor: 3,
defaultSignatureAlgorithm: "RS256",
offlineSessionMaxLifespan: 5184000,
offlineSessionMaxLifespanEnabled: true,
webAuthnPolicySignatureAlgorithms: ["ES256"],
webAuthnPolicyAttestationConveyancePreference: "none",
webAuthnPolicyAuthenticatorAttachment: "cross-platform",
webAuthnPolicyRequireResidentKey: "not specified",
webAuthnPolicyUserVerificationRequirement: "preferred",
webAuthnPolicyCreateTimeout: 0,
webAuthnPolicyAvoidSameAuthenticatorRegister: false,
defaultDefaultClientScopes: ["email", "profile", "roles", "web-origins"],
defaultOptionalClientScopes: ["address", "phone", "offline_access", "microprofile-jwt"]
};
try {
await axios.post(`${KEYCLOAK_URL}/admin/realms`, realmConfig, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('Realm created successfully');
} catch (error) {
handleAxiosError(error, 'creating realm');
}
}
// Create client and manage mappers
async function createClient(token, clientId, clientName, redirectUris) {
console.log(`Creating client "${clientId}"...`);
let client = await getClientByClientId(token, clientId);
if (!client) {
const clientConfig = {
clientId: clientId,
name: clientName,
enabled: true,
protocol: "openid-connect",
publicClient: false,
authorizationServicesEnabled: false,
serviceAccountsEnabled: false,
standardFlowEnabled: true,
implicitFlowEnabled: false,
directAccessGrantsEnabled: true,
redirectUris: redirectUris,
webOrigins: ["+"],
defaultClientScopes: ["roles"],
optionalClientScopes: ["address", "phone", "offline_access", "microprofile-jwt"],
rootUrl: process.env.NEXTCLOUD_URL,
baseUrl: process.env.NEXTCLOUD_URL,
adminUrl: process.env.NEXTCLOUD_URL,
};
try {
const response = await axios.post(
`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`,
clientConfig,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.log(`Client "${clientId}" created successfully`);
client = response.data;
} catch (error) {
handleAxiosError(error, `creating client: ${clientId}`);
return;
}
} else {
console.log(`Client "${clientId}" already exists, checking mappers`);
}
if (client) {
try {
await axios.put(
`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}`,
{ ...client, secret: process.env[`KEYCLOAK_${clientId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()}_CLIENT_SECRET`] },
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.log(`Set client secret for client: ${clientId}`);
} catch (error) {
handleAxiosError(error, `setting client secret for client: ${clientId}`, error.config, error.response);
}
const existingMappers = await getClientMappers(token, clientId);
const requiredMappers = [
{
name: "groups",
protocol: "openid-connect",
protocolMapper: "oidc-group-membership-mapper",
config: {
"full.path": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true",
"claim.name": "groups"
}
},
{
name: "realm roles",
protocol: "openid-connect",
protocolMapper: "oidc-usermodel-realm-role-mapper",
config: {
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true",
"claim.name": "roles",
"jsonType.label": "String",
"multivalued": "true"
}
}
];
for (const mapper of requiredMappers) {
const existingMapper = existingMappers.find(m => m.name === mapper.name);
try {
if (existingMapper) {
await axios.put(
`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models/${existingMapper.id}`,
{ ...existingMapper, ...mapper },
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.log(`Mapper "${mapper.name}" updated for client "${clientId}"`);
} else {
await axios.post(
`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`,
mapper,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.log(`Mapper "${mapper.name}" created for client "${clientId}"`);
}
} catch (error) {
handleAxiosError(error, `managing mapper "${mapper.name}" for client "${clientId}"`, error.config, error.response);
}
}
if (clientId.includes("nextcloud")) {
await addDefaultClientScope(token, clientId, "openid");
await addDefaultClientScope(token, clientId, "profile");
await addDefaultClientScope(token, clientId, "groups-nextcloud");
try {
await axios.put(
`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}`,
{ ...client, defaultClientScopes: client.defaultClientScopes.filter(c => c !== "nextcloud-dedicated") },
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.log(`Removed client scope nextcloud-dedicated from client: ${clientId}`);
} catch (error) {
handleAxiosError(error, `removing client scope nextcloud-dedicated from client: ${clientId}`, error.config, error.response);
}
}
}
}
// Create default groups
async function createDefaultGroups(token) {
console.log('Creating default groups...');
const groups = [
{ name: "nextcloud-admins", path: "/nextcloud-admins", attributes: { "description": ["Nextcloud administrators"] } },
{ name: "nextcloud-users", path: "/nextcloud-users", attributes: { "description": ["Nextcloud regular users"] } },
{ name: "nextcloud-youpi", path: "/nextcloud-youpi", attributes: { "description": ["Nextcloud youpi"] } },
{ name: "nextcloud-service", path: "/nextcloud-service", attributes: { "description": ["Nextcloud service user"] } }
];
for (const group of groups) {
try {
const existingGroups = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, {
headers: { 'Authorization': `Bearer ${token}` },
params: { search: group.name, exact: true } // Added exact: true for precise matching
});
if (existingGroups.data.length > 0) {
console.log(`Group "${group.name}" already exists`);
continue;
}
await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, group, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log(`Group "${group.name}" created successfully`);
} catch (error) {
if (error.response?.status === 409) {
console.log(`Group "${group.name}" already exists`);
} else {
handleAxiosError(error, `creating group: ${group.name}`);
}
}
}
}
// Create a test token for a user
async function createTestToken(token, username) {
console.log(`Creating test token for user "${username}"...`);
try {
const nextcloudClientId = Object.keys(CLIENTS).find(key => key.includes('nextcloud')) || 'nextcloud';
const client = await getClientByClientId(token, nextcloudClientId);
if (!client) {
console.log(`Client "${nextcloudClientId}" not found, cannot create test token.`);
return null;
}
const response = await axios.post(
`${KEYCLOAK_URL}/realms/${REALM_NAME}/protocol/openid-connect/token`,
new URLSearchParams({
'client_id': nextcloudClientId,
'client_secret': process.env[`KEYCLOAK_${nextcloudClientId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()}_CLIENT_SECRET`],
'username': username,
'password': process.env.TESTADMIN_PASSWORD || "initial123!",
'grant_type': 'password'
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
console.log(`Test token for user "${username}" created.`);
return response.data.access_token;
} catch (error) {
handleAxiosError(error, `getting test token for ${username}`, error.config, error.response);
return null;
}
}
// Function to decode a JWT token
function decodeToken(token) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(c =>
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
).join(''));
return JSON.parse(jsonPayload);
} catch (error) {
console.error("Error decoding token:", error.message);
return null;
}
}
// Create initial users
async function createInitialUsers(token) {
console.log('Creating initial users...');
const users = [
{ username: "testadmin", enabled: true, emailVerified: true, firstName: "Test", lastName: "Admin", email: "testadmin@mrx8086.com", credentials: [{ type: "password", value: process.env.TESTADMIN_PASSWORD || "initial123!", temporary: false }], groups: ["nextcloud-admins", "nextcloud-users"] },
{ username: "testuser", enabled: true, emailVerified: true, firstName: "Test", lastName: "User", email: "testuser@mrx8086.com", credentials: [{ type: "password", value: process.env.TESTUSER_PASSWORD || "initial123!", temporary: false }], groups: ["nextcloud-users", "nextcloud-youpi"] },
{ username: "testserviceuser", enabled: true, emailVerified: true, firstName: "Test", lastName: "Service User", email: "testserviceuser@mrx8086.com", credentials: [{ type: "password", value: process.env.TESTSERVICEUSER_PASSWORD || "initial123!", temporary: false }], groups: ["nextcloud-service"] }
];
for (const user of users) {
try {
const existingUsers = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`, {
headers: { 'Authorization': `Bearer ${token}` },
params: { username: user.username, exact: true } // Added exact: true for precise matching
});
if (existingUsers.data.length > 0) {
console.log(`User "${user.username}" already exists`);
continue;
}
await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`, user, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log(`User "${user.username}" created successfully`);
} catch (error) {
handleAxiosError(error, `creating user: ${user.username}`, error.config, error.response);
}
}
}
async function createGroupsNextcloudScope(token) {
const scopeName = "groups-nextcloud";
const mapperName = "groups-mapper";
console.log(`Starting createGroupsNextcloudScope`);
let clientScope = await getClientScope(token, scopeName);
if (!clientScope) {
try {
console.log(`Creating client scope "${scopeName}"`);
clientScope = await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`,
{
"name": scopeName,
"protocol": "openid-connect",
"description": "Provides access to user group information for Nextcloud.", // Hinzugefügt: Beschreibung
"attributes": {},
"consentScreenText": "Grant access to user groups in Nextcloud",
"includeInTokenScope": true
},
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log(`Client scope "${scopeName}" created successfully`);
clientScope = response.data;
} catch (error) {
console.error(`Error creating client scope "${scopeName}":`, error);
handleAxiosError(error, `creating ${scopeName} client scope`, error.config, error.response);
return;
}
} else {
console.log(`Client scope "${scopeName}" exists, skipping creation`);
}
console.log("Client scope creation step finished");
if (clientScope) {
console.log(`Check for mapper "${mapperName}" in scope "${scopeName}"`);
try {
const mappersResponse = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
if (!mappersResponse.data.find(m => m.name === mapperName)) {
try {
await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`,
{
"name": mapperName,
"protocol": "openid-connect",
"protocolMapper": "oidc-group-membership-mapper",
"config": {
"full.path": "false",
"id.token.claim": "false",
"access.token.claim": "true",
"userinfo.token.claim": "true",
"claim.name": "groups",
"add.to.introspection": "false"
}
},
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log(`Mapper "${mapperName}" created for client scope "${scopeName}"`);
} catch (error) {
console.error(`Error creating mapper "${mapperName}" for scope "${scopeName}":`, error);
handleAxiosError(error, `creating mapper for ${scopeName}`, error.config, error.response);
}
} else {
console.log(`Mapper "${mapperName}" exists in client scope "${scopeName}", skipping creation`);
}
} catch (error) {
console.error("Error checking for mappers:", error);
handleAxiosError(error, `checking mappers for ${scopeName}`, error.config, error.response);
}
}
console.log("Finished createGroupsNextcloudScope");
}
// Main function
async function setupRealm() {
try {
console.log('Starting Keycloak setup...');
const token = await getAdminToken();
// Check if realm exists
const realmExists = await checkRealmExists(token);
if (!realmExists) {
console.log('Creating new realm...');
await createRealm(token);
} else {
console.log('Realm already exists, skipping base setup');
}
// Create client scope groups-nextcloud
await createGroupsNextcloudScope(token);
// Create clients
for (const clientId in CLIENTS) {
await createClient(token, clientId, clientId, CLIENTS[clientId].redirectUris);
}
// Create groups
await createDefaultGroups(token);
// Create test users
await createInitialUsers(token);
// Read the configuration of the Office-Automation realm with Admin Token
if (token) {
console.log("Master Realm Admin Token:", token);
try {
const realmConfig = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
console.log("Office Automation Realm Configuration:", realmConfig.data);
} catch (error) {
handleAxiosError(error, 'getting office realm configuration', error.config, error.response);
}
} else {
console.error("Error getting Master Realm admin token");
}
// Create Test Token
const testToken = await createTestToken(token, "testadmin");
if (testToken) {
console.log("Test Token generated successfully!");
const decodedToken = decodeToken(testToken);
if (decodedToken)
console.log("Token:", decodedToken);
} else {
console.error("Error generating Test Token");
}
console.log('Setup completed successfully');
} catch (error) {
console.error('Setup failed:', error);
process.exit(1);
}
}
// Execute the script
setupRealm();