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();