Category: "Symfony"

Symfony 3 - Legacy Authentication Bridge - straight md5

If you are building a system which needs a fallback authentication system to welcome existing users into a new application architecture, you will need to support the authentication approach of the legacy application.

One very common method is to md5 the password and store it in the database. Symfony supports md5 password encoders, but needs a little extra configuration to disable the salt.

This post shows how to configure a connection into the legacy database, chain the authentication providers, and configure the password encoder.

I chose to put all the code that is strictly operating on the legacy system in its own bundle with the rationale that it may be removed in the future. If it isn't removed, it should be easier to set the context of tasks when working in the different bundles.

If you are transitioning from a legacy application to a new architecture, you will probably have two databases - one for the old system and one for the new one. The first step is to set up Symfony and Doctrine to connect to both.

app/config/config.yml

    dbal:
        default_connection: default
        connections:
            default:
                driver:   "%database_driver%"
                host:     "%database_host%"
                port:     "%database_port%"
                dbname:   "%database_name%"
                user:     "%database_user%"
                password: "%database_password%"
                charset:  UTF8
                
            legacy:
                driver:   '%legacy_database_driver%'
                host:     '%legacy_database_host%'
                port:     '%legacy_database_port%'
                dbname:   '%legacy_database_name%'
                user:     '%legacy_database_user%'
                password: '%legacy_database_password%'
                charset:  UTF8

     ...

    orm:
        auto_generate_proxy_classes: "%kernel.debug%"
        default_entity_manager: default

        entity_managers:
            default:        
                auto_mapping: true
                mappings:
                    AppBundle: ~
            legacy:
                connection: legacy
                mappings:                    
                    LegacyBridgeBundle: ~

Then you need to create the user provider as described here http://symfony.com/doc/current/cookbook/security/entity_provider.html. Be sure to make any column name updates and ensure they are reflected in the config files as well.

This is a fallback configuration, with the FOS User Bundle as the first provider (Ref: http://symfony.com/doc/current/cookbook/security/multiple_user_providers.html) and the legacy provider running next.

Add the legacy provider into your chain as follows:

app/config/security.yml


imports:
    - { resource: "@LegacyBridgeBundle/Resources/config/security.yml" }

security:

    ...

    providers:
        chain_provider:
            chain:
                providers: [ fos_userbundle, legacy_user_provider ]

    ...

        main:
            pattern: ^/
            form_login:
                provider: chain_provider

And set up the encoder the bundle security.yml

security.yml


security:
    encoders:
        LegacyBridgeBundle\Entity\LegacyUser: 
            algorithm: md5
            encode_as_base64: false
            iterations: 0
       
    providers:
        legacy_user_provider:
            entity: 
                class: LegacyBridgeBundle:LegacyUser
                property: username
                manager_name: legacy

Please note that I'm not recommending unsalted md5 (I do recommend unsalted butter), but sharing an approach that allows the authentication to run against an existing database.

Presenting KnpMenus with Dojo

This Dojo module will traverse a menu delivered by the KnpMenuBundle and present it as a Dijitized menu bar.

There may be a few extraneous modules included, but this code will likely be extended.

define([
    "dojo/_base/declare",
    "dojo/dom",
    "dojo/dom-attr",
    "dojo/dom-construct",
    "dojo/on",
    "dojo/query",
    "dijit/registry",
    "dijit/MenuBar",
    "dijit/MenuBarItem",
    "dijit/PopupMenuItem",
    "dijit/PopupMenuBarItem",
    "dijit/MenuItem",
    "dijit/DropDownMenu",
    "dijit/form/Button",
    "dijit/Dialog",
    "app/lib/common",
    "dojo/i18n!app/nls/core",
    "dojo/NodeList-traverse",
    "dojo/domReady!"
], function (declare, dom, domAttr, domConstruct, on, query, registry,
        MenuBar, MenuBarItem, PopupMenuItem, PopupMenuBarItem, MenuItem, DropDownMenu, Dialog,
        lib, libGrid, core) {

    function run() {
        var menuBar = new MenuBar({}, "admin-top-menu");

        function createMenuItem(widget, parent, depth) {
            var children, node, item, i, label, nextNode;
            var popupMenuObj, labelObj, link;
            children = query(parent).children();
            for( i = 0; i < children.length; i++ ) {
                node = children[i];
                nextNode = (typeof children[i + 1] !== "undefined") ? children[i + 1] : null;
                switch( node.tagName ) {
                    case "SPAN":
                    case "A":
                        label = node.textContent.trim();
                        if( typeof node.href !== "undefined") {
                            link = node.href;
                        } else {
                            link = null;
                        }
                        if( nextNode !== null && nextNode.tagName === "UL" ) {
                            popup = new DropDownMenu();
                            popupMenuObj = {label: label, popup: popup};
                            if( depth <= 1 ) {
                                item = new PopupMenuBarItem(popupMenuObj);
                            } else {
                                item = new PopupMenuItem(popupMenuObj);
                            }
                            createMenuItem(popup, nextNode);
                        } else {
                            labelObj = {label: label};
                            if( depth <= 1 ) {
                                item = new MenuBarItem(labelObj);
                            } else {
                                item = new MenuItem(labelObj);
                            }
                        }
                        if( link !== null ) {
                            item.on("click", function () {
                                location.href = link
                            });
                        }
                        widget.addChild(item);
                        break;
                    case "LI":
                        createMenuItem(widget, node, depth + 1);
                        break;
                }
            }
        }

        var menuElements = query("#admin-top-menu ul");
        if( menuElements.length > 0 ) {
            createMenuItem(menuBar, menuElements[0], 0);
            domConstruct.destroy(menuElements[0]);
        }
        menuBar.startup();
    }
    return {
        run: run
    };
});

Symfony / Dojo - Prod and Dev environment management

Dojo has a great build process which allows you to create a optimized and minimized files for the client side (and more!).

In a production environment, this greatly improves performance.

However, building the code after every change will slow development significantly.

Since Dojo can load the required modules dynamically, you can load the source files and work with them directly, and maintain your profile file as you work. Running a build at the end of each development session will help to ensure the code and profile stay in sync.

admin.base.html.twig

This template provides the foundation page layout for all admin pages. If the application is running in a dev environment, it includes a page_footer_script, but in production, it includes dojo.js

{% if app.environment == 'dev' %}
    {% include 'admin/parts/page_footer_script.html.twig' %}
{% else %}
    <script data-dojo-config="async:1" src="/release/dojo/dojo.js"></script>
{% endif %}

{% block javascripts %}
{% endblock %}

{% if omit_menu is not defined %}
    <script>
        require([
            "app/admin/menu",
            "dojo/domReady!"
        ], function (menu) {
            menu.run();
        });
    </script>
{% endif %}

page_footer_script.html.twig

Used only in the dev environment


<script>
    var dojoConfig = {
        async: true,
        baseUrl: '/release',
        paths: {"lib": "/app/lib",
            "nls": "/app/nls"},
        packages: [
            {"name": 'app', "location": '/app'},
            'dojo',
            'dijit',
            'dojox',
            'dgrid',
            'dstore',
            'put-selector',
            'xstyle'
        ],
        selectorEngine: 'lite',
        tlmSiblingOfDojo: false,
        has: {
            "dojo-trace-api": false
        }
    };
</script>
<script data-dojo-config="async:1" src="/vendor/dojo/dojo/dojo.js"></script>

example-page.html.twig

Each page template has a javascripts block which includes the require call to bring in the client side code.


{% block javascripts %}
    <script>
        require(["app/admin/asset/brand"], function (brand) {
            brand.run();
        });
    </script>
{% endblock %}

Symfony / Dojo - Prod and Dev environment management
1 3