Category
A few months ago when Delphi 13 was first released, I wrote about WebBroker's new Session Management capability. There were a few things I didn't quite understand. Since then, Embarcadero has broken out its big WebStencils demo into smaller, more digestible pieces. So, I've been playing around with my own demos some more and now have a better understanding of how it all works.
LoginRequired and UserHasRole
I started with one demo app on Github but have expanded it to three. The original program is pretty much the same as it was originally written (with a few minor fixes) to show one way to control user content access. As mentioned in the previous article, I used the @LoginRequired(<role>) and Session.UserHasRole(<role>) keywords in the HTML files which are processed through the WebStencils engine to control which pages and which content are accessible. This is a perfectly fine and reasonable way to do this and allows user content control to be defined in the HTML files, external to the web application.
Demo #2 - With TWebAuthorizer
A different approach is to handle access with the TWebAuthorizer component. For the second demo (in the /Authorizer folder), a copy of the HTML pages has been made with those WebStencils keywords removed and a TWebAuthorizer component added to the web module. Similar user access control is established but now, instead of embedded in various HTML templates, it's managed through the Zones property of the authorizer.
I found this to be simpler to implement; managing the WebModule's TWebActionItems and associating each with a path and TWebStencilsProcessor or coding the Response.Content of each OnAction event handler is tedious. However, if you need explicit control or have a unique situation to handle, those are still there and can be used in conjunction with the TWebAuthorizer.
Zones and Roles
The TWebAuthorizer's Zones property determines which web paths a user is allowed to view. The first one, /, is the root and is marked as zkFree indicating that by default, all paths and sub-paths are allowed access. This provides a way to view public pages, like static text, errors, or navigate to a login page. The TWebFormsAuthenticator's LoginURL is not listed specifically in the Zones, so it falls in the zkFree area and allows the user to login.
The next zone is /custlist which is marked zkProtected which means no access unless the user is logged in (authenticated). It has no Roles defined which allows any authenticated user to view that path and any sub-paths. /custedit is also zkProtected but restricts access to users with the viewer and editor roles. This means a user must have both the viewer and the editor roles in order to access the page.
(Originally, I thought the user had to have only one or the other of the roles but some testing and pondering, I realized that all roles listed for the authorizer zone are required. This makes sense if you consider an accounting program: perhaps a user has access to edit tax rates in the Accounts Payable module but no permission to view employee paychecks, so would only have an AP role, for example; another user might have both AP and payroll permissions and a Payroll page would require both roles.)
When the WebFormsAuthenticator authorizes a user (through its OnAuthenticate event, the user's roles are also assigned; when a user visits the /custedit page, the WebAuthorizer ensures that only users with all required roles can view the content.
Unauthorized
If a user is barred from visiting a page, they are redirected to the UnauthorizedURL of the TWebAuthorizer automatically; without this component, you have to manually redirect or, like I did in the first demo, use the aforementioned WebStencils keywords embedded in the HTML that adjusts content dynamically.
TWebFileDispatcher
Another change in this version of the demo is that I removed all the TWebStencilsProcessor components and the list of TWebActionItems for the TWebModule's Actions property and added a single TWebFileDispatch component and assigned it to the Dispatcher property of the TWebStencilsEngine. By configuring the WebFileDispatcher properly, it will pick up all HTML files and process them automatically; the default property values work for most cases so I was able to simply drop this on the form and have it work immediately, meaning I no longer have to provide a specific WebActionItem or WebStencilsProcessor for every single page; instead, it finds all "html" files in the specified folders and automatically processes them through the WebStencilsEngine.
This took care of all but two of web pages; the /custlist and /custedit pages needed data, so I hooked into the WebFileDispatcher's BeforeDispatch event and opened the appropriate queries, setting a web variable which the WebStencilsEngine uses as it processes the template HTML files.
If you still have some older-style "WebBroker" templates, you could mix the two using the WebActionItems and assigning various paths to specific processors, using either TPageProducer or TWebStencils components.
(The TWebFileDispatcher component is not a new one; it's been in Delphi at least a decade. But it's nice that the new WebStencilsEngine supports it.)
Demo #3 - Using TWebBasicAuthenticator
After thoroughly covering the form-based web authentication method in the first two demo projects, the third demo (in the /basic folder) uses the TWebBasicAuthenticator component. It is simpler because it doesn't require you to write your own login page but simply pops up a default web browser window asking for a username and password. It's not near as pretty but it's functional for a quick and dirty website.
Similar to the TWebFormsAuthenticator, there's an OnAuthenticate event to validate the UserName and Password passed in by your browser, and you can set Roles and web vars as before.
There is only one path to setup, LogoutURL (which I left blank, explained later). Basically, you don't call a login URL; instead, you simply try to navigate to a protected path and it automatically triggers a user credential prompt. So specifying the zones with a TWebAuthorizer is important. I left these the same as they were set in the previous demo.
Bug!
Before I move on, I should say that I had quite a bit of trouble getting this to work. I could not find any example or demo project using this component anywhere! After trying everything I could think of, I asked Claude Code to look through Delphi's run-time library code and teach me how to implement it. After quite a bit of reading through the WebBroker source code, it came back and said there was a bug in the Web.HTTPApp unit! (You can read what it said in the bug report I filed if you have an Embarcadero Quality Portal login.) It even suggested a fix but since I didn't want to modify the RTL source, I asked for a work-around. That work-around is in the web module's AfterDispatch event where if the status is 401 (unauthorized because I navigated to a protected page) it fixes the Response headers which triggers the username/password prompt.
Everything else is quite similar. I don't have need for login.html, logout.html, or loginfailed.html as that's all handled by the web browser, so there are fewer HTML files.
Logout
One of the drawbacks of using this concept is that it has a problem logging out. Initially, I set the LogoutURL property and sent a POST call to /logout (as is documented); while it looked like it logged me out, I could manually navigate back to the protected folder and it showed the protected data again, along with the cached authentication status!
It turns out, this is fairly standard among web browsers when using their HTTP authentication. The solution for this demo, was to leave the LogoutURL property of the TWebBasicAuthenticator component blank and add a TWebActionItem with the following two lines in its OnAction event:
Response.StatusCode := 401; Handled := True;
This caused the WebBroker server to invalidate the session which cleared its internal cache and left me truly logged out. Then, because it was left with a 401, as soon as I refreshed the page, I got the username prompt again (because that's what the code in the WebModuleAfterDispatch method does).
Conclusion
There was definitely a lot of research to reproduce these "simple" demos but I learned a lot in the process and with these notes and demos as reference, my "real life" project will go much more smoothly. Hopefully, what I've written will help someone else, too.
I'm glad I learned how to get the TWebBasicAuthenticator working, although I'll likely never use it. There's so much more flexibility and user interface control when using the TWebFormsAuthenticator and with AI's help, generating the few extra HTML pages is a no-brainer.
The last time I worked seriously with WebBroker, the TWebFileDispatcher didn't exist (that was a long time ago!); looking at the Embarcadero demos taught me how simple it is to use this instead of creating a bunch of TWebActionItems and hooking up either a TPageProducer (of yester-year) or the newer TWebStencilsProcessor components. And now, with all the pieces in place, session management, user authentication, WebStencils templates, and simplified path protection, creating a fully functional, modern web site with user security in Delphi is a breeze!
Add new comment