Integrating Matrix client functionality into ConcreteCMS

ConcreteCMS Matrix Chat Client: Project Outline

What It Is

The ConcreteCMS Matrix Chat Client is a Currently a single-page application that connects ConcreteCMS with the Matrix communication protocol. It lets users log in to their Matrix accounts from within ConcreteCMS using JWT authentication. The server-side logic is handled by PHP, while the client-side communication uses JavaScript with the Matrix JS SDK. It’s built to provide a simple, integrated chat experience without leaving the ConcreteCMS platform.

Planned Main Features

  • Secure Login: Authenticates users with JWT tokens for a safe and efficient sign-in process.
  • Real-Time Messaging: Supports sending and receiving text, images, video, and files in a live chat feed.
  • Room and Member Tools: Allows users to view and join rooms, check member lists, and start direct chats.
  • Typing Indicators: Shows when others are typing to improve real-time interaction.
  • Simple Controls: Messages can be sent with a button or the Enter key, and room lists can be refreshed easily.
  • Server Federation: Federate multiple ConcreteCMS websites together, allowing a shared social media experience between multiple web properties.

Purpose for ConcreteCMS

This project aims to bring secure, accessible communication to ConcreteCMS users. By using Matrix’s decentralized protocol, it gives users control over their data while offering a practical tool for community discussions, team collaboration, or support channels. It’s being designed to fit seamlessly into ConcreteCMS, enhancing how users connect and interact.

###Current Proof of Concept:

1 Like

Oh nice! I have been fiddling with a package to automatically create Useraccounts on the matrix client from the concreteCMS user data. But nothing working now.

Great if you want to integrate that! I’ll be happy to help!

Yeah so with this thing there are really 3 components too it, there is:

  1. The Matrix Synapse server itself thats setup with JWT authentication, with my current script if you try authenticate with a user that does not exist on the matrix server, it just makes them a passwordless account based off their username in concrete

  2. The SSO provider I made for concrete cms, that generally just gives logged in concretecms users a JWT session token signed with their username and the secret

  3. The Client itself, which is just a single page application at the moment I have a few different versions of.

Recently, I discovered that if you AI code your concretecms single pages and blocks to use Web Components, the LLM’s out there like ChatGPT or Grok are able to give you way more functionality.

So I have been converting my projects too use the standard with alot of success.

This is the custom_auth.py I wrote for my matrix server docker instances

To use it, you add a password provider too your homeserver.yaml

password_providers:
  - module: "custom_auth.ConcreteCMSAuthProvider"
    config:
      secret_key: "Your secret key"  # Replace with a 256 bit secure key

This custom_auth.py will securely authenticate any user based on their username and jwt token, if they dont already have a user account already, its enough for them to have a valid jwt token signed with their username and the secret; and an account will be made for them.

import jwt
import logging
from synapse.module_api import ModuleApi

logger = logging.getLogger("ConcreteCMSAuthProvider")

class ConcreteCMSAuthProvider:
    def __init__(self, config, account_handler: ModuleApi):
        self.account_handler = account_handler
        self.secret_key = config.get("secret_key")
        if not self.secret_key:
            raise ValueError("Missing 'secret_key' in config")
        logger.info("Initialized with server_name: %s, secret_key (first 10 chars): %s", 
                    self.account_handler.server_name, self.secret_key[:10])

    async def check_auth(self, username, login_type, login_dict):
        logger.debug("check_auth called - username: %s, login_type: %s, login_dict: %s", 
                     username, login_type, login_dict)
        
        if login_type != "org.concretecms.sso":
            logger.debug("Unsupported login type: %s", login_type)
            return None

        token = login_dict.get("token")
        device_id = login_dict.get("device_id")  # Extract device_id if provided
        if not token:
            logger.warning("No token provided")
            return None

        # Extract username from identifier if provided, fallback to token
        identifier = login_dict.get("identifier", {})
        if identifier.get("type") == "m.id.user":
            expected_username = identifier.get("user")
        else:
            expected_username = None

        try:
            payload = jwt.decode(token, self.secret_key, algorithms=["HS256"])
            logger.debug("JWT payload: %s", payload)

            jwt_username = payload.get("sub") or payload.get("username")
            if not jwt_username:
                logger.warning("No 'sub' or 'username' in JWT")
                return None

            # Ensure username matches identifier if provided
            if expected_username and jwt_username != expected_username:
                logger.warning("JWT username (%s) doesn’t match identifier (%s)", 
                               jwt_username, expected_username)
                return None

            matrix_id = f"@{jwt_username}:{self.account_handler.server_name}"
            logger.info("Generated Matrix ID: %s", matrix_id)

            if not await self.account_handler.check_user_exists(matrix_id):
                logger.info("Registering new user: %s with device_id: %s", matrix_id, device_id)
                await self.account_handler.register_user(
                    localpart=jwt_username,
                    password=None,
                    admin=False,
                    device_id=device_id if device_id else None  # Use provided device_id
                )
                logger.info("User %s registered", matrix_id)

            logger.info("Authentication successful for %s", matrix_id)
            return matrix_id

        except jwt.ExpiredSignatureError:
            logger.warning("JWT token expired")
            return None
        except jwt.InvalidTokenError:
            logger.warning("Invalid JWT token")
            return None
        except Exception as e:
            logger.error("Unexpected error: %s", str(e), exc_info=True)
            return None

    @staticmethod
    def get_supported_login_types():
        logger.debug("get_supported_login_types called")
        return {"org.concretecms.sso": ["token"]}

This is the SSO provider I wrote:

To get it working you need to use composer to install firebase/php-jwt into a vendor directory at root of your concretecms site (NOT the concretecms vendor directory in /concrete/)

Like this:


/application/src/matrix.php

<?php
namespace Application\Src;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Concrete\Core\User\User;

class Matrix {
    private $secretKey = "your super secret jwt shared 256 bit key";
	private $session;
    private $app;

    public function __construct() {
        $this->app = \Core::make('app');
        $this->session = $this->app->make('session');
    }

    public function generateJWToken(User $user) {
        $username = $user->getUserName();
        $now = time();
        $payload = [
            'sub' => $username,
            'iat' => $now,
            'exp' => $now + 3600
        ];

        $jwtString = JWT::encode($payload, $this->secretKey, 'HS256');
        $this->session->set('matrix_jwt_token', $jwtString);
        return $jwtString;
    }

    public function getJWToken() {
        $token = $this->session->get('matrix_jwt_token');
        if ($token) {
            try {
                $decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
                if ($decoded->exp >= time()) {
                    return $token; // Token is valid and not expired
                }
            } catch (\Exception $e) {
                // Token is invalid, proceed to generate a new one
            }
        }
        // Token is expired, invalid, or doesn't exist
        $user = $this->app->make('user');
        if ($user->isLoggedIn()) {
            return $this->generateJWToken($user); // Generate and return a new token
        }
        return null; // No valid token and user not logged in
    }
}

/controllers/single_page/dashboard/matrix/client.php

<?php
namespace Application\Controller\SinglePage\Dashboard\Matrix;

use Concrete\Core\Page\Controller\DashboardPageController;
use Application\Src\Matrix;

class Client extends DashboardPageController {
    public function view() {
        $sso = new Matrix($this->app->make('session')); // Use app from controller
        $this->set('jwt_token', $sso->getJWToken());
    }
}

Then you need too add the following lines too:
/application/bootstrap/app.php

require_once __DIR__ . '/../src/matrix.php';
// Load the root-level autoloader for application-specific dependencies
require_once __DIR__ . '/../../vendor/autoload.php';

// on_user_login listener for JWT generation
Events::addListener('on_user_login', function($event) use ($app) {
    $user = $event->getUserObject();
    $sso = new \Application\Src\Matrix($app->make('session'));
    $token = $sso->generateJWToken($user);
    \Log::addEntry("Generated JWT token for user {$user->getUserName()}: $token");
});

Later I will share the code for the client I have made, it still needs a little bit of work, but the auth provider part is 100% sorted out.

You can pretty much just turn registration on, on your concretecms site… and thats enough for it to function as the auth provider for a matrix server.
It will take complete control.

This is what the current client looks like though, just needs more functions added too it for federation and such: