Category
Web App Development in Delphi
Delphi has had several ways to create web apps for a long time. Starting clear back in the late 1990s, you could use WebModules with action-based request handling to produce dynamic content using tag-based replacement events in TPageProducers; this was called WebBroker and is still a part of Delphi today. Other techniques sprang up, some expanding on the Indy components (such as IntraWeb), or providing tools or components to transpile Pascal to JavaScript (such as Elevate Software's Elevate Web Builder or FMSoft's UniGUI or TMS Software's TMS WebCore). Other products hijacked the output of VCL apps and turned them into HTML on-the-fly (such as Cybele Software's Thinfinity VirtualUI). There are also several popular open-source Delphi projects such as DelphiMVCFramework, mORMot, and Mars Curiosity. Jim McKeeth and Marco Cantu put out an excellent webinar back in 2019 detailing most of these; it was called, Evaluating Web Development Frameworks in Delphi and nicely categorizes them between client-side and server-side technologies.
If you wanted to stick with the built-in WebBroker technology, you could only write sites that did not need authentication unless you wanted to handle session management yourself with cookies or long URLs. When Delphi 6 came out in 2001, WebSnap was added incorporating session management; but it required the more expensive Enterprise edition.
When WebSnap was removed from Delphi XE6 in 2014, the replacement was supposed to be DataSnap but its session management was more complicated because it was designed for persistent connections, not stateless web requests.
Introducing Session Management
So, what is web session management anyway and why is it necessary? HTTP is inherently stateless; each request from a browser to your web server is independent, with no built-in memory of previous interactions. Session management bridges this gap by creating a persistent context for each user across multiple page requests. Without it, users would need to re-authenticate on every page and shopping carts would empty between clicks. Websites without proper session management that save the state of user access on the server affect all current and future connected sessions; for example, if the WebStencils Demo from last year were published and someone logged in, anyone who accessed the site from anywhere would now suddenly share the same login state and be able to see the customer list! No one would publish a website like this, of course.
Traditionally, web developers implement session management using a combination of cookies (or URL parameters) containing a unique session ID on the client side, paired with server-side storage (memory, database, or cache) to hold the actual session data. The challenge isn't just creating sessions, it's managing their lifecycle: secure generation of IDs, timeout handling, cleanup of abandoned sessions, preventing session hijacking, and scaling across multiple server instances. For Delphi developers coming from desktop applications where state is inherently maintained, or those who've only used WebBroker for stateless request-response APIs, implementing robust session management has been a significant hurdle that often pushed them toward third-party frameworks or other development tools.
At long last, Delphi 13 Florence added Session Management to its WebBroker technology and it's available for all editions, not just Enterprise. By now, most people using Delphi for serious web app development are likely entrenched in one of the solutions mentioned previously that solved the problem of user authentication and authorization. But if you're new to Delphi or just now needing to build a web server with it, read on!
Demo Projects
Embarcadero has a wonderful, rich demo using WebStencils with WebBroker's new session management capabilities as an open-source project on Github. (Also included in the repository is a Firemonkey desktop app to let you play around with the scripting and variable-subsitution techniques of WebStencils.) While it is beautiful and works well, the breadth and complexity of this web app was a little overwhelming to me. What I wanted was a simple, no-frills explanation of how to use the new session-management components when building my own WebBroker app. It took me a while to parse out just those pieces, match them up with the sparse documentation, and build my own tests just to understand how this technology works.
What I ended up with is my own WebStencils Session Demo project on Github, an an extension to the one I wrote a litte over a year ago that compared the new WebStencils framework with TPageProducers of the WebBroker base technology. As before, the project uses WebStencils and uses a form to request login information. But this time, session-management has been added to isolate the user state to a single browser. There are two types of authentication that will be discussed below; this project only uses form-based authentication. The same Chinook.db SQLite database and concepts of user authentication to display a list of customers is also still in place. In other words, it looks and feels nearly identical to last year's WebStencils Demo to keep things simple and focus the changes solely on session management.
The New Components
The new components are all non-visual components you add to your application's WebModule:
TWebSessionManagermanagesTWebSessionobjects, which are returned from the newSessionproperty of aTWebRequest. Each session containsDataVarsand an associatedUser. The manager component provides properties for configuring where sessions are located, their scope, and other details which will be described below. Only one of these manager components is allowed per web module.TWebBasicAuthenticatororTWebFormsAuthenticatorare two different ways to handle user authentication. The first one performs basic HTTP authentication which typically pops up a web browser window prompting for a username and password. The second allows you to create a custom HTML form with your own fields and verification methods to authenticate a user. This component contains links for theHomeURL,LoginURL,LogoutURL, andFailedURLto provide redirection based on the authorized state of the user. There are also event handlers that are triggered when a user is authenticated. Only one of these two authenticator components is allowed per web module.TWebAuthorizerprovides a list of authorization zones associated with the authenticated user's security roles. Each zone defines a web path and a list of roles that are allowed to access that path.
These components work together: the Manager maintains all sessions, the Authenticator validates users, and the Authorizer controls what authenticated users can access. Let's look at how these are used in the demos app, then we'll explain the components in greater detail.
Disclaimer!
I had significant difficulty getting authentication to work with the TWebAuthorizor component. Perhaps I didn't understand the documentation but in any case, I ended up removing the authorizer and implemented page access based solely on user roles and WebStencils session variables.
TWebSessionManager
This component introduces the concept of individually connected web sessions for WebBroker applications. Its properties allow you to define where session IDs are stored, how long they last, and their scope. I left them at defaults for this demo:
IdLocation: Where the session ID should be stored. The options areilCookie,ilHeader, andilQuery. The default isilCookieand is the most common practice for websites around the world today. If you have special circumstances, you can put it in the HTTP header or on the URL as a query parameter.IdName: A string to identify the session ID; the default issessionId.InvalidSessionURL: This is the path to a page to which you can send users if an invalid session is encountered (I never did in all my testing).Scope: The default isssUnlimitedwhich generates a new session for any request without a session ID. The other options aressUserandssUserAndIPand generate a new session for each user or each user and IP address combination.SharedSecret: This is a string that secures session IDs for user-scoped sessions; not used by this demo.Timeout: This specifies, in seconds, how long a session can last before it is discarded. The default is3600, or one hour.
TWebFormsAuthenticator
This authenticator expects you to provide your own HTML form that collects a username and password and posts it to the web server. Several properties specify key paths:
FailedURL: A simple error page to redirect the user to if authentication fails. In my demo, this is/failed.HomeURL: Where to send the user once authentication is successful. In my demo, this is/custlist.LoginURL: Where the form is located that asks for login credentials. In my demo, this is/login.LogoutURL: Ends the user session. This is/in my demo.
There are also two event handlers:
OnAuthenticate: This event gets called when the user posts the login form. You'll check the provided credentials against a database, hardcoded values, or other authentication system, settingSuccess := Trueand populating theRolesparameter with any security roles the user has.OnAuthenticated: This event is fired after successful authentication, allowing you to perform additional setup like loading user preferences or logging the login event.
In some demo programs, the login credentials would be hard coded; so this event would simply check to see if the username and password matched those values. In my demo, I access the Employees table of a local SQLite database. Please see the README of the demo project for details of how this was implemented and how Roles are established.
TWebBasicAuthenticator
If you're not writing your own form to collect user credentials, you can use this component and your web browser will pop up a small default form. Similar to the TWebFormsAuthenticator, it provides a LogoutURL for ending your session, an OnAuthenticate event for validating the entered user credentials, and an OnAuthenticated event for post login code. It also has a property to set the Realm of your session.
I could find no examples of how to use this and all my attempts failed to pop up the web browser's basic login form. Therefore, I have no example and no further information to share on this.
TWebAuthorizer
Once the user is logged in, you need to map which paths the user is allowed to access based on their security clearance. For example, a simple user might be able to browse public articles and access their own profile but would not be allowed to configure the system like an administrator would. The idea is that the Roles assigned by one of the aforementioned "Authenticator" components map to paths defined by the Authorizer to simplify secure navigation throughout your website.
You set up this security map with the Zones property of the authorizer which pops up a list of zones, each with a set of properties:
Kind:zkProtected,zkIgnore, orzkFree. I don't know what the difference is betweenzkIgnoreandzkFreebut both of them allow anyone to access the page without checking credentials.zkProtected(as its name indicates) protects the path by checking the user's roles.PathInfo: This specifies the path that is being allowed or protected.Roles: this is a comma-separated list of roles, one of which the user needs to have in order to access the path.
I wanted to use this component but every time I placed it on the web module and ran the program, it ignored the OnAuthenticate event of the TWebFormsAuthenticator; I could not figure out how to use this with authentication so, as mentioned above, I removed it and set up WebStencils variables for dynamic HTML building.
Web App Flow
When the web program starts up, it opens the database, establishes some variables, and adjusts the HTML paths of the TWebStencilsProcessor components. All paths and web action items are defined at design-time through the Object Inspector. All web action items use TWebStencilsProcessor components which point to HTML files that are included in the project.
The login page asks for a simple username and password, then makes a POST call to /login which triggers the TWebFormsAuthenticator's OnAuthenticate event. After checking the database for a valid user, the procedure's Success and Roles parameters are set which then tell the authenticator where to send the user next. If login was successful, the user sees a list of customers pulled from the database; otherwise, an error page with a link to try again. Customer IDs are linked to an "edit" page that lists the customer's details (no changes are saved to the database as this is just a simple demo illustrating WebStencils and session management).
Security
WebStencils has several ways to determine visibility of content. The first iteration of this demo used the keyword @LoginRequired. This keyword has been enhanced in Delphi 13 by adding an optional parameter, Role, which means that in addition to being authenticated, the user also has to have the listed role in order to access the page.
Another technique is using the Session.UserHasRole() WebStencils function. This is used in the custedit.html file because if @LoginRequired(<role>) fails, it gives an ugly "Internal Application Error" message; by allowing a "viewer" role to access the page, I can check to see if they have the "editor" role and if not, display a nicely formatted message instead.
Session Reset on Logout
As I was finishing this demo, I noticed that when I logged out of one user and in with another, it would retain the user roles internally which resulted in a "viewer"-only user getting links to view customer details after an "editor" or "manager" had logged out. I fixed that by replacing a "Logout" link with a small form button so that I could call /logout with the POST method and thus trigger the Authenticator's /LogoutURL and properly clear the session.
Conclusion
The documentation and initial demo from Embarcadero promised ease of use and drop-in components with little coding for flexible session management with mapped paths to user roles. What I experienced took me several weeks in my spare time of trying one thing then another, and a lot of trial-and-error to try and interpret the documentation.
Some of the things I learned include:
- [WebSessionMgr] Setting
Scopeto anything butssUnlimitedrequires deeper understanding of session and user management, and possibly some coding or other properties set properly; any other setting continually generated "Cannot generate session ID" errors for me. - [Authenticator] Calling the
LogoutURLwith a simplehreflink does not work; you must create a form-button with the POST method in order to properly clear a session. - [WebStencils] The
@ifexpression needs parenthesis if calling a function, like(session.UserHasRole(<role>))but not if accessing a simple object variable. - [WebStencils] The
@switchexpression always needs parenthesis. - [WebStencils] Setting variables in Delphi and then accessing them in WebStencils is much easier than calling functions and doing logic branching in HTML.
I hope the documentation improves in a future version of Delphi. And maybe we'll get more examples. Then again, I've seen no conversations about this on the popular Delphi forums, so perhaps this technology is too late for anyone to care about.
In any case, I'm glad it's been added to the WebBroker stack; I might use it some day.
Add new comment