Handling 401 Unauthorized Responses In Turbo Drive And ColdFusion
For the past few months, I've been digging into the use of the Hotwire framework in a ColdFusion context. And, for the most part, everything I've looked at so far has represented the "happy path" - all that which happens when everything goes right. Now, I want to start looking at some failure cases - the so-called "sad path." In today's post, I'm considering ways to respond to an unauthorized request in a ColdFusion application.
View this code in my ColdFusion + Hotwire Demos project on GitHub.
Handling errors in a Hotwire application is more complicated than a vanilla ColdFusion application because requests can be made in different contexts. Some requests are made as top-level page requests; and, can seamlessly follow 301
/ 302
responses. Other requests are made in a Turbo Frame context; and, don't provide any built-in mechanism for breaking out of the frame on error.
Thankfully, Turbo Drive includes a special HTTP header when it is making requests in a Turbo Frame context: Turbo-Frame
. If this HTTP header is present (the id
of the contextual frame), we can respond with a frame-specific error; and, if this HTTP header is absent, we can respond with a traditional location()
based redirect.
To demonstrate this bifurcation, I've created a hard-coded oops.cfm
file to represent our 401 Unauthorized
request. If we're not in a Turbo Frame when this is called, I'll redirect to index.cfm
- our login page. And, if we are in a Turbo Frame, I'm going to respond with a meaningful message to the user.
<cfscript>
// If the UNAUTHORIZED request is being made OUTSIDE OF ANY TURBO FRAME, then we can
// simply redirect the user back to the login page, the same way that we might for any
// other ColdFusion application using an authentication / authorization wall.
if ( ! request.turbo.isFrame ) {
location( url = "./index.htm", addToken = false );
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// If the UNAUTHORIZED request is being made INSIDE A TURBO FRAME context, then
// returning a redirect gets a bit tricky. The redirect will apply to the Turbo Frame
// itself, not to the entire page. I'm not sure that there is a "right way" to do
// this. For this demo, I'm going to return a static value (indicating the logged-out
// state) with the option to also render a custom Turbo Stream element that performs
// an automatic redirect.
param name="url.useStream" type="boolean" default=false;
header
statusCode = 401
statusText = "Unauthorized"
;
</cfscript>
<cfoutput>
<!--- Make sure to echo the correct frame ID. --->
<turbo-frame id="#encodeForHtmlAttribute( request.turbo.frame )#">
<p>
You've been logged-out.
<a href="./index.htm" data-turbo="false">Please login</a>
to continue using the app.
</p>
<!---
If the stream flag is enabled, this custom action will perform an automatic
redirect of the top-level page.
--->
<cfif url.useStream>
<turbo-stream
action="visit"
data-url="./index.htm">
</turbo-stream>
</cfif>
</turbo-frame>
</cfoutput>
As you can see, if the unauthorized request is being made in the context of a Turbo Frame, I'm doing several things:
I'm making sure to echo the
id
of the Turbo Frame as provided by theTurbo-Frame
HTTP header (extraction not shown in this code).I'm providing a message about being logged-out, complete with an explicit link back to the login page.
I'm optionally including an inline Turbo Stream object to perform an automatic page-navigation to the login page. The
visit
action is a custom Turbo Stream action. You can think of this as invokinglocation()
in a Turbo Frame context.
To try triggering this 401 Unauthorized
request in both a top-level and frame-based context, I created another ColdFusion page - authorized.cfm
- which allows for navigation actions to take place both at the top-level and at the frame-level. Both set of links are (essentially) the same; but, the latter set is scoped to a <turbo-frame>
element. Both sets of links contain an oops.cfm
target for the above error handling:
<cfscript>
// For the sake of simplicity, all the "logged-in" pages will be rendered as this
// page, using the "v" value to differentiate.
param name="url.v" type="string" default="home";
</cfscript>
<cfmodule template="./tags/page.cfm">
<cfoutput>
<h2>
Page For #encodeForHtml( url.v.ucfirst() )#
</h2>
<p>
<a href="authenticated.htm?v=home">Home</a> —
<a href="authenticated.htm?v=activity">Activity</a> —
<a href="authenticated.htm?v=profile">Profile</a> —
<a href="oops.htm">Oops Page</a>
</p>
<p>
This is the <strong>page content</strong> for
<mark>[ #encodeForHtml( url.v )# ]</mark>.
</p>
<!---
FRAME LEVEL page navigation options. These are all the same links; however,
since they are defined inside a Turbo Frame, the are automatically scoped to
the Turbo Frame instead of using a top-level navigation.
--->
<turbo-frame id="my-frame">
<h3>
Inside A Turbo Frame
</h3>
<p>
<a href="authenticated.htm?v=home">Home</a> —
<a href="authenticated.htm?v=activity">Activity</a> —
<a href="authenticated.htm?v=profile">Profile</a> —
<a href="oops.htm">Oops Page</a>
( <a href="oops.htm?useStream=true">with Stream</a> )
</p>
<p>
This is the <strong>frame content</strong> for
<mark>[ #encodeForHtml( url.v )# ]</mark>.
</p>
</turbo-frame>
</cfoutput>
</cfmodule>
Ok, let's try this out. First, we're going to log into the application and perform all of the navigation actions at the top-level:
As you can see, once we navigate to the oops.cfm
page (our unauthorized request), we are automatically redirected to the Login page. This mirrors what we might do in a traditional ColdFusion application.
Now, let's try navigating in the context of the Turbo Frame:
This time, once we hit the oops.cfm
page, we can't just perform a location()
call, otherwise it would be scoped to the Turbo Frame. Instead, we tell the user that they are logged-out and we provide them with a link back to the Login page.
As our last example, we're going to navigate in the context of the Turbo Frame; but, this time we'll ask for the inline Turbo Stream action to be included:
This time, with the inline Turbo Stream action, we can see a behavior that is more akin to the first demo with the location()
call. There is a flash of content since we're still rendering a response to the user. But, it's only there for a moment before the user is whisked away to the Login page.
Handling an unauthorized request at the top-level in a Hotwire application is pretty natural - it's the same thing we might do in a vanilla ColdFusion application. It's the Turbo-Frame-based request that adds complexity. But, I think this approach - with a message and an inline Turbo Stream action - feels like a nice solution.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →