In my last entry, I walked us through the basic set up and design for the photo gallery application. In this entry we will start talking about the security and registration system. To be honest, this is something that I had trouble with when I was creating my first Model-Glue application. I'm not convinced I have the best answer for this, but this is what has worked for me so far, so I'm going to share it with others.
Like most sites, our application will have both a logon screen and a registration screen. But how do we handle security? Typically I'd use onRequestStart in an Application.cfc file to see if a request needs to be secured. So for example, I'd typically do something like this pseudo-code:
if the request is NOT for logon.cfm, or register.cfm,
the user needs to be logged on so let's push them there
and abort the request
But how do we handle this in Model-Glue? Let's cover some basic Model-Glue facts. Model-Glue defines events. These events simply define what your application can do. The default application template that we used has two events, Home and Exception. When we hit the application without an event, it is going to use a default event defined in the settings. In our case, it's the Home template. So let's start by securing the Home template.
Events can (and normally will) broadcast a message. When I first read this in the quick start guide, it didn't make much sense. The way I look at it is - sometimes events need additional information ("Am I logged in?" for example), and the event requests that information when the event is fired. Your event can even do something with the result.
So how can we use this? We've already said that we need to require people to logon or register. So we can modify the Home event to check if the user is logged in. If they aren't, we can force them to the logon page. Here is a modified version of the Home event:
<event-handler name="Home">
<broadcasts>
<message name="getAuthenticated" />
</broadcasts>
<views>
<include name="body" template="dspBody.cfm" />
<include name="main" template="dspTemplate.cfm" />
</views>
<results>
<result name="notAuthenticated" do="Logon" />
</results>
</event-handler>
What has changed here? I added a new message, getAuthenticated. I added a new result. The name of the result, notAuthenticated, means, only run this result if the notAuthenticated result was returned from the controller. As you can probably guess, my controller will either return authenticated or notAuthenticated. In my case, I only really care if the user is not authenticated.
Now we need to make the controller listen for the event. You may ask - why do I need to do this? What else is going to listen to my events? Well, the Model-Glue sample application only has one controller, but a Model-Glue production application may have many. You will also see that we can tell the controller to listen to event "foo" but call method "goo". This is nice in case our controller changes as it lets us keep our events using the older event name. So go up to your controllers block, go to myController, and after the onRequestEnd message, add a new one for getAuthenticated. The entire block will look like this now:
<controllers>
<controller name="myController" type="controller.Controller">
<message-listener message="OnRequestStart" function="OnRequestStart" />
<message-listener message="OnRequestEnd" function="OnRequestEnd" />
<message-listener message="getAuthenticated" function="getAuthenticated" />
<!-- Message-Listener Template
<message-listener message="BroadcastMessageName" function="ControllerFunctionToFire">
<argument name="AnEventArgument" value="aValue" />
</message-listener>
-->
</controller>
</controllers>
If you want, you can get rid of the comment in there. It is just there to help you learn the format. As you can see, we added the getAuthenticated listener and it calls the same method in the controller. Now let's add the method to our controller. Open up controller/Controller.cfc, and add this method, before OnRequestStart:
<cffunction name="getAuthenticated" access="public" returntype="void" output="false" hint="I return if the user is authenticated.">
<cfargument name="event" type="ModelGlue.Core.Event" required="true">
<cfset arguments.event.addResult("notAuthenticated") />
</cffunction>
So let's examine this line by line. All controller methods that listen to events are passed in a Model-Glue event. This is what you modify to pass results back and forth. It is a bit weird at first. I always want to return a value from methods, so this took some getting used to. But you can imagine this Event object as a bus making stops at all the events during your request. At each bus stop, a new value may get on or get off, or the bus driver may be given special instructions. In this case, I'm not adding any values per se, but adding a result to the event. As you can guess, I'm hard coding this now since we don't really have a Model yet, a back end. Therefore the result will always be that a user is not logged in.
Hopefully you are still with me. Since we are returning a result of notAuthenticated, and our Home event says to fire the Logon event, we now need to add that:
<event-handler name="Logon">
<broadcasts />
<views>
<include name="body" template="dspLogon.cfm" />
</views>
<results />
</event-handler>
This is pretty simular to how the original Home event was. I'm not broadcasting any events. I'm simply including a logon template. Notice that the Home event uses two views. One is the content (named body) and one is the display. Normally a logon page has slightly different layout than the rest of the site. Typically I'll create a view called "base" or "bare" for that and other pages (like the register page). For now though I'm going to keep it ugly and simple. The dspLogon.cfm file is included in the zip (which I didn't forget this time) and is nothing more than a simple form. There is one line of interest that I'll share here:
<form action="#viewstate.getValue("myself")#logonattempt" method="post">
The view state, as desribed in the Quick Start, is a collection of data passed to the view. So for example, if you were displaying a press release, the controller could ask the model for the data, and then set that data in the view state so that your view files could use it. I'll talk more about this later because, to be honest, it confused me at first. But for now, the thing I want to point out is that "myself" is a default value in the view state. You can use it to point to the root file of your Model-Glue application. All you need to add to the end is the event name. Since I'm using "Logon" to display the logon form, I'm using "logonattempt" for the "try to logon" event.
Summary
So we talked about a lot here. Let's do a quick summary of what changed:
- The first thing I did was add a new setting, DSN, to my ModelGlue.xml file. This setting will be available to my code. You can think of it like Application variables.
- I modified the Home event to broadcast an event. This event checks to see if we are logged in.
- I modified the controller to check and see if the user is logged on. For this release, I simply made it return false. I did this by adding a result to my event that the Home event will notice.
- I added the Logon event to handle displaying the logon form. Any request for the site's Home event will now force you to logon if you haven't done so.
As before, you can see this in action here.
Archived Comments
hey fyi your "last entry" link currently takes you back to this entry, or are you just trying to confuse us this morning? lol :)
Fixed, thanks!
no worries. thank you for your site and all your great code. i run a dream journal site which i'll be upgrading soon and members want the dream dictionary to be updatable by them, the more i think about it wiki is the way to go. i'm very glad you came out with Canvas, can't wait to check it out. Anyway keep up the great work, I love CF, got my start in it almost 9 years ago back in DC.
Do you add the:
<broadcasts>
<message name="getAuthenticated">
</broadcasts>
<results>
<result name="notAuthenticated" do="Logon" />
</results>
to each event handler?
Scott, yes. I didn't mention it in this entry since I'm trying hard to make each entry as simple as possible. Either the 3rd or 4th entry will cover this. Although now that I think about it, I should have mentioned it probably.
Joe has mentioned that MG 1.1 will allow you to add 'results' in the onRequestStart events, and have them acted upon.
Then, unless only some of your pages require login, you will be able to put the login logic in onRequestStart and add an appropraite event to trigger a login form.
Hey Ray. Do use the #viewstate.getValue("myself")# shortcut for login forms? I ask because your form action doesn't take in to account that it should be posting over SSL. Granted, I know this is an academic exercise, but would you just do:
action="https://www.mysite.com/#vie..."myself")#logonattempt"
in that case?
Also, what's your opinion on Sean's notion that events should not be named the same as the Controller method? So instead of "getAuthentication" being announced and mapping to getAuthentication(), you'd announce "needAuthentication" which maps to getAuthentication(). Again, I know this is academic and I'm not advocating a change in the code. I'm just curious as to your thoughts on it in general. I've personally taken to the "need" approach, as I've found that when bringing my team up to speed, it helps them more clearly separate (mentally) an event from a Controller method.
message tag needs to be terminated
<message name="getAuthenticated" />
Brendan, I fixed that in my code, I forgot to in the entry. Will edit in 2 minutes.
Dave:
Well, I typically only use SSL when doing ecommerce type stuff. For simple logons it would be overkill probably. But if I did want to forcve SSL, I'd do as you did.
I haven't heard Sean's views on that - so I can't comment. Do you know why? Personally I've never liked that. It feels weird, but as a MG newbie myself, I'm willing to admit I could be wrong.
Yeah, so you ask the logical question and of course I can't find the reference! (Aside: We really need to do something about Topica's atrocious search functionality for the MG list. Anyway...) I know I saw Jared use it in his great MG security tutorial he posted a while back:
http://www.web-relevant.com...
It was some comments in response to that from *somewhere* (wow, I'm being really helpful, aren't I?) that the discussion of "need" vs. "get" came up. Like everything else if ColdFusion, I certainly don't think there's a "right" and a "wrong" way to accomplish the same task, particularly in this case. Like I said, I've just found (for whatever reason) that when I have to show my developers the difference between the message broadcast and the controller handling, the distinct naming helps "solidify" the concept a bit quicker. That being said, you're approach is certainly reasonable and probably the most common.
Interesting. Well, let's leave it as an "open issue" for now. ;)
One of my plans is, post completion, is another entry on random crap I'd change. Ie, small updates here and there. One change would be multiple controllers. (As just an excample.)
Fair enough. Incidentally, a nice MG reference that answers some high-level questions can be found at:
http://awads.net/wp/2005/06...
I'm sure you're getting to most, if not all, of these in your future posts, but it's convenient to have a concise reference too.
Hey Ray,
This is excellent! Does your wish list include Amazon gift cards?
A quick typo: (2 paragraphs above the summary) "Notice that the Home event uses TWO views."
And a question: In this app, everyone needs to login regardless, so it makes sense that you're checking authentication at the home page. But in some apps, a login menu appears on every page for member logins. In that case, it would be nice to pass the originating page URL with the event and then return to that page once login is complete. Can that be done with MG?
Thanks a lot, this is great even though you've been pressed for time this week!
1) No gift cards, but you can send one to my snail mail if you want. No obligation for sure. I'm not writing this to get rich. ;)
2) Typo fixed thanks. I plan on offering a PDF 'book' when all done.
3) You can get the current event. So yes, you could return. A quick, off the cuff response is, I'd make my logon link (or form) pass the current event asn an arg, like oldEvent=XXXX. Therefore you can send the user back when done.
I think that oldEvent=xxxx will do exactly what I'm looking for. I believe that would even be retained through other events (as long as I didn't delete or overwrite it), in case the user ended up mistyping their password or needing to register (additional events) before continuing on with their original event.
Thanks again.
As a M-G newbie I would like to say thanks for example and keep them comming. I have one question. I have the Logon form displaying but how do you logon? What are the username and password? Is this functionality not currently part of this or is going to be added? Where do I go from here with this example.
Thanks.
Tim, the app, so far, is the most secure app ever, because, as you have guessed, no, you cannot logon. ;)
That will be in the next article.
"The first thing I did was add a new setting, DSN, to my ModelGlue.xml file. This setting will be available to my code. You can think of it like Application variables."
I don't actually see this step in your walk-through. Was it in a different post?
Not yet. In part 3 I start on the model, and it uses a DSN value, but you don't see it passed in. Part 4 will show me hooking up the control to the model and you will see it there.
My first question has nothing to do with Model-Glue (I think). I notice in your form you do not use cfform tags, only standard html tags. Is this because you're just used to it or is there a specific reason such as speed or simplicity?
Just a preference. I only use cfform when I do flash forms.
Melanie, I just realized that I dropped the ball on that setting there. I will get it into part 4. Sorry for the confusion!
What a great tutorial...I'm starting to catch on....thanks Ray!