JavaScript Mapping Library
This will be my third ColdFusion post in the past year. I’m not saying I’m going to continue the trend, but as I find interesting use cases, I’m going to share. Today, that involves the <cfoauth> tag that I recently had a chance to play with.
<cfoauth>
About two weeks ago, an old client of mine reached out asking for help adding an OAuth flow to their CF app. I’ve covered CF and OAuth in a few posts from ten-plus years ago (part one, covering Facebook, part two, covering LinkedIn, and part three, covering Google).
That code demonstrated the basics of using OAuth, which entailed:
Now, that flow hasn’t changed, and I could have simply copied over the existing code that would still work fine, but I decided I’d take a quick look at the <cfoauth> tag to see if it would help.
While the docs are clear about the syntax, it doesn’t really explain how it works practically. The most important aspect is that the tag handles the redirect flow for you automatically. What that means is – on a page with <cfoauth>, the user is automatically redirected. This is important to know as you’ll want to use the tag on a page with no UI, one that is loaded when a user wants to login. The tag will also handle responding to the return from the auth provider by automatically getting the access token (on a good result of course).
In a simple CF app, you could use this like so. First, a basic Application.cfc:
Application.cfc
component { this.name = "cfoauthdemo"; this.sessionManagement = true; public function onRequestStart(page) { if(structKeyExists(url, "refresh")) { applicationStop(); structClear(session); cflocation(url="/index.cfm",addToken=false); } } public function onApplicationStart() { application.googleAuth = { clientId="client id from google", clientSecret="client secret from google" } return true; }}
All I’ve done here is define my Google credentials in the application scope and enabled session management. Now, on an index.cfm page, I can do this:
index.cfm
<h2>cfoauth Demo</h2><cfif not structKeyExists(session, "auth")> <p> <a href="login.cfm">Login with Google</a> </p><cfelse> <cfoutput> <img src="#session.auth.other.picture#"> <p> You are #session.auth.name# </p> </cfoutput></cfif>
Basically, if not authenticated with Google, link to the page that will handle it, otherwise, print out basic information from the user profile.
Here’s login.cfm:
login.cfm
<cfoauth type="google" clientId="#application.googleAuth.clientId#" secretkey="#application.googleAuth.clientSecret#" result="result" scope="https://www.googleapis.com/auth/calendar" ><cfif structKeyExists(variables, "result")> <cfset session.auth = result> <cflocation url="/index.cfm" addtoken="false"></cfif>
I’ve provided a type, my credentials, and told the tag how to store the result. I’ve also requested an additional scope for Google Calendar data.
So to be clear – when the user enters this page, the tag is going to generate the URL at Google to handle auth, and automatically redirect. That’s the important bit and what I found lacking in the docs.
When, and if, the user logs in, they are automatically redirected back to this URL, and the result variable will be populated. Here’s an example of that result. I didn’t bother obscuring the access token as it’s expired:
result
And here’s the result back on index.cfm:
With that access token, you can now make ad hoc queries against what you have access to, and as we asked for calendar access in the scope, we can fetch it. So for example (and I wouldn’t include a UDF on the page like this, but it works for a simple sample):
<cfscript> function getEvents(accesstoken, data=[], page="") { var theUrl = "https://www.googleapis.com/calendar/v3/calendars/primary/events"; if(arguments.page != "") { theUrl &= "&pageToken=#page#"; } cfhttp(url="#theUrl#") { cfhttpparam(type="header", name="Authorization", value="OAuth #arguments.accesstoken#"); } var result = deserializeJSON(cfhttp.fileContent).items; return result; }</cfscript><h3>Events</h3><cfset events = getEvents(session.auth.access_token)>
Simple, and useful… but…
Access tokens returned from an OAuth provider have an expiration, and here we see the first problem. If you look at the cfdump of the result above, you’ll note I got the access_token, but the expires_in value which I know (well, 99% know) is returned by Google is not in this result set for some odd reason.
cfdump
expires_in
Next, it’s possible to ask Google (and I assume other providers) to return an additional refresh_token. That token can be used to refresh an access token when it expires.
refresh_token
In my research, I found that if you added access_type=offline&prompt=consent to the URL when authenticating, you should get the values returned. The <cfoauth> supports this via the urlparams attribute, so I tried this:
access_type=offline&prompt=consent
urlparams
<cfoauth type="google" clientId="#application.googleAuth.clientId#" secretkey="#application.googleAuth.clientSecret#" result="result" scope="https://www.googleapis.com/auth/calendar" urlparams="access_type=offline&prompt=consent">
Easy, right? And while it changed the ‘auth’ flow (ie, Google noticed the change in the request), when I back to the result variable, the refresh_token wasn’t there. If I had to guess, I’d say the tag is looking for a set of named variables and ignoring the rest of the result, which doesn’t make sense to me.
Unfortunately, I’d say this one flaw makes the tag unusable unless you want to force users to log in every 60 minutes (the value Google uses for its tokens). To be fair, that’s probably not too big of a hurdle as folks probably won’t be sticking around that long, but it’s something you would need to ensure you properly handle.
I filed a bug report for this behavior issue here: https://tracker.adobe.com/#/view/CF-4221899
Raymond Camden
You must be logged in to post a comment.
This site uses Akismet to reduce spam. Learn how your comment data is processed.