Delbridge.Org

The Seldom Updated Weblog of Dave Delbridge

Delbridge.Org

Tackling Twitter's OAuth with ColdFusion

October 26, 2010 · 27 Comments

In September of 2010, Twitter pulled the plug on Basic Authentication, imposing an open authorization (OAuth) security protocol on application developers.  The trouble with OAuth?  It's a wee bit complicated.  Worse still, as of this writing, ColdFusion is noticeably absent from Twitter's API wiki, offering no libraries or examples to work from.

On the upside, a handful of ColdFusion solutions have surfaced, like the impressive Twitter4J Java library and a component library adapted from PHP by Harry Klein.  But, if you're new to OAuth, then you may have found those solutions difficult to understand.  Or perhaps you'd rather roll your own.  Either way, I thought it worthwhile to compose an OAuth primer for ColdFusion developers.  For the purposes of education, the code samples are traditional and heavily commented.  I hope you find it helpful.

introduction to Twitter's oauth

Before we begin, please read Twitter's "Authenticating Requests with OAuth."  The page supplies important background information, preliminary requirements, a beautiful workflow diagram, and indispensable step-by-step instructions with sample data.  It is the blueprint from which this ColdFusion-specific document is built.  Each of the diagram's steps, A through G, are mirrored here.

If you're rolling your own code from scratch, I recommend the OAuth signature checker at http://quonos.nl/oauthTester/ and the OAuth signature generator at http://oauth.googlecode.com/svn/code/javascript/example/signature.html.  Composing a valid signature base string is perhaps OAuth's toughest puzzle and Twitter's only response to malformed base strings is the vague "Failed to validate oauth signature and token."  As such, these validators can be indispensible.

Things To Watch Out For

Before diving into the details, here are some stumbling blocks to be aware of:

  • ColdFusion's URLEncodedFormat function is not 100% compliant with RFC 3986 and will trip up OAuth's sensitive string calculations.  For details and a workaround, read "URL Encoding to RFC 3986" in Adobe's Developer Connection.
  • Contrary to ColdFusion's documentation, HMAC-SHA1 is not supported by the Encrypt (or Hash) functions.  The workaround is a simple HMAC-SHA1 CFFunction.
  • In accordance with the OAuth specification, the callback URL is twice URL encoded.  [In fact, all parameters in the base string are twice URL encoded, though often unaffected.]  This is a common source of confusion when analyzing base strings.
  • ColdFusion's URLEncodedFormat() function produces incompatible strings when international characters are introduced, even with the charset="utf-8" parameter.  To correct, use the <cfprocessingdirective pageEncoding="utf-8"> tag.
  • OAuth was made for structures.  [Or is it the other way around?]  Unfortunately, ColdFusion automatically converts structure keys to uppercase when using dot notation (e.g., MyStruct.MyKey).  To maintain case, use bracketed notation instead, a la MyStruct["MyKey"].

The Functions

For efficiency and clarity, this project employs four functions.  Put them in a "_functions" directory (or update the cfinclude statements in each template, accordingly).  They are:

hmac_sha1():  Generates the HMAC-SHA1 authentication code required by OAuth.

<!--- ************************************************************ --->
<!--- HMAC-SHA1 AUTHENTICATION CODE FUNCTION --->
<!--- --->
<!--- Contrary to ColdFusion's docs, the Encrypt() and Hash() --->
<!--- functions do not support HMAC-SHA1 as required by this --->
<!--- project. This function, provided by Dmitry Yakhnov of --->
<!--- Yakhnov Studio (http://www.coldfusiondeveloper.com.au/) --->
<!--- takes advantage of Java's native support for HMAC-SHA1. --->
<!--- Thank you for sharing, Dmitry! --->
<!--- --->
<!--- PARAMETERS --->
<!--- signKey (string) = Secret key --->
<!--- signMessage (string) = Message to be hashed --->
<!--- --->
<!--- RETURNS --->
<!--- (binary) The keyed authentication code --->
<!--- --->
<!--- ************************************************************ --->

<cffunction name="HMAC_SHA1" returntype="binary" access="public" output="no">

<cfargument name="signKey" type="string" required="true" />
<cfargument name="signMessage" type="string" required="true" />

<cfset var jMsg = JavaCast("string",arguments.signMessage).getBytes("iso-8859-1") />
<cfset var jKey = JavaCast("string",arguments.signKey).getBytes("iso-8859-1") />

<cfset var key = createObject("java","javax.crypto.spec.SecretKeySpec") />
<cfset var mac = createObject("java","javax.crypto.Mac") />

<cfset key = key.init(jKey,"HmacSHA1") />

<cfset mac = mac.getInstance(key.getAlgorithm()) />
<cfset mac.init(key) />
<cfset mac.update(jMsg) />

<cfreturn mac.doFinal() />

</cffunction>

oauth_base_string():  Generates the OAuth "base string" from the parameters of your OAuth request.  Technically speaking, this code belongs inside the oauth_request() function, described next, being that the oauth_base_string() code isn't reused anywhere.  However, for the purposes of debugging, I chose to break this pesky little step out.  And, for the purposes of education and clarity, I've left things this way.  For example, it does become clearer from this function why oauth parameters are twice URL-encoded in a signature base string.

<!--- ************************************************************ --->
<!--- OAUTH SIGNATURE BASE STRING FUNCTION --->
<!--- --->
<!--- In accordance with the OAuth specification, this function --->
<!--- takes three input values (http method, base uri, and a --->
<!--- list, er, 'structure' of "key = value" parameters) and --->
<!--- returns a single OAuth base string. --->
<!--- --->
<!--- AUTHOR --->
<!--- Dave Delbridge, Circa 3000 (http://circa3000.com) --->
<!--- --->
<!--- PARAMETERS --->
<!--- HTTP_METHOD (string) = "GET" or "POST" --->
<!--- BASE_URI (string) = address where request will be sent, --->
<!--- minus any URL request parameters --->
<!--- PARAMETERS (structure) = key/value parameter pairs --->
<!--- Example: --->
<!--- params[oauth_nonce] = 12345 --->
<!--- params[oauth_version] = "1.0" --->
<!--- --->
<!--- RETURNS --->
<!--- (string) The sorted, URL-encoded, concatenated values --->
<!--- (the "signature base string") per OAuth spec --->
<!--- --->
<!--- ************************************************************ --->

<cffunction name="OauthBaseString" returntype="string" access="public" output="no">

<!--- ************************************************************ --->
<!--- Required parameters (http_method, base_uri, values) --->
<!--- ************************************************************ --->

<cfargument name="http_method" type="string" required="true">
<cfargument name="base_uri" type="string" required="true">
<cfargument name="parameters" type="struct" required="true">

<!--- ************************************************************ --->
<!--- Concatenate http_method & URL-encoded base_uri --->
<!--- ************************************************************ --->

<cfset oauth_signature_base_string = http_method & "&" & URLEncodedFormat_3986(base_uri) & "&">

<!--- ************************************************************ --->
<!--- Create sorted list of parameter keys --->
<!--- ************************************************************ --->

<cfset keys_list = StructKeyList(parameters)>
<cfset keys_list_sorted = ListSort(keys_list,"textnocase")>

<cfset amp = ""> <!--- first iteration requires no ampersand --->

<!--- ************************************************************ --->
<!--- Repeat for each parameter --->
<!--- ************************************************************ --->

<cfloop list="#keys_list_sorted#" index="key">

<!--- ************************************************************ --->
<!--- Concatenate URL-encoded parameter (key/value pair) --->
<!--- ************************************************************ --->

<cfset oauth_signature_base_string = oauth_signature_base_string & URLEncodedFormat_3986(amp & LCase(key) & "=" & parameters[key])>

<cfset amp = "&"> <!--- successive iterations require a starting ampersand --->

</cfloop>

<!--- ************************************************************ --->
<!--- Return with OAuth signature base string --->
<!--- ************************************************************ --->

<cfreturn oauth_signature_base_string>

</cffunction>

oauth_request():  This is the engine of the OAuth car.  Generates the OAuth request header and signature, submits to the provider (e.g., Twitter) and returns the response in a string.

For the record, this template was updated on 10/03/2011, responding to comments that URL request parameter values containing ampersand (&) and equals (=) symbols created trouble.  To further complicate things, Twitter has since updated their instructions - they no longer recommend passing URL query parameters in what amounts to a malformed URL.  This is understandable, being that parsing name/value pairs from an unencoded, malformed URL is, well, really not possible.  Rather than rewrite this article to keep pace with their new JSON examples, my workaround is to simply allow for escaped symbols (e.g., "&&", "==") in the URL request parameter values.  Be sure to escape those values before passing them to these templates, or you will generate an error.

<!--- ************************************************************ --->
<!--- OAUTH REQUEST FUNCTION --->
<!--- --->
<!--- Per OAuth specification, sends specified request and --->
<!--- parameters to the specified provider (e.g., Twitter). --->
<!--- Response is returned in a string. --->
<!--- --->
<!--- AUTHOR --->
<!--- Dave Delbridge, Circa 3000 (http://circa3000.com) --->
<!--- --->
<!--- PARAMETERS --->
<!--- HTTP_METHOD (string) = "GET" or "POST" --->
<!--- REQUEST_URL (string) = unencoded address where request is --->
<!--- to be sent, including any URL request parameters. All --->
<!--- ampersand (&) and equals (=) symbols appearing in any --->
<!--- URL request parameter values must be escaped (e.g., --->
<!--- "&&", "=="). --->
<!--- OAUTH_CONSUMER_SECRET (string) = consumer secret provided --->
<!--- by provider (e.g., Twitter) --->
<!--- PARAMS = structure containing request parameters --->
<!--- Example: --->
<!--- params[oauth_nonce] = 12345 --->
<!--- params[oauth_version] = "1.0" --->
<!--- --->
<!--- RETURNS --->
<!--- (string) The provider's response --->
<!--- --->
<!--- ************************************************************ --->

<cffunction name="oauth_request" returntype="string" access="public" output="no">

<!--- ************************************************************ --->
<!--- Parameters --->
<!--- ************************************************************ --->

<cfargument name="consumer_secret" type="string" required="yes">
<cfargument name="token_secret" type="string" required="yes">
<cfargument name="http_method" type="string" required="yes">
<cfargument name="request_url" type="string" required="yes">
<cfargument name="params" type="struct" required="yes">

<!--- ************************************************************ --->
<!--- Load necessary functions --->
<!--- ************************************************************ --->

<cfinclude template = "../_functions/urlencodedformat_3986.cfm">
<cfinclude template = "../_functions/oauth_base_string.cfm">
<cfinclude template = "../_functions/hmac_sha1.cfm">

<!--- ************************************************************ --->
<!--- Backup parameters for later --->
<!--- ************************************************************ --->

<cfset params_backup = Duplicate(params)>

<!--- ************************************************************ --->
<!--- Copy URL variables (if any) to parameters --->
<!--- ************************************************************ --->

<!--- ************************************************************ --->
<!--- Parse address and parameters from request URL --->
<!--- ************************************************************ --->

<cfset request_url_address = request_url>
<cfset request_url_query_string = "">

<cfset question_mark = Find("?",request_url,1)>

<cfif question_mark neq 0>

<cfset request_url_address = Left(request_url,question_mark-1)>
<cfset request_url_query_string = Right(request_url,(len(request_url)-question_mark))>

<!--- ************************************************************ --->
<!--- Repeat for each key/value pair --->
<!--- ************************************************************ --->

<cfset request_url_query_string = Replace(request_url_query_string, "&&", "PLACEHOLDER_AMPERSAND", "ALL")> <!--- save escaped ampersand (&) symbols --->
<cfset request_url_query_string = Replace(request_url_query_string, "==", "PLACEHOLDER_EQUALS", "ALL")> <!--- save escaped equals (=) symbols --->

<cfset params_list = ListChangeDelims(request_url_query_string,",","&,=")>

<cfloop from="1" to="#ListLen(params_list)#" index="index" step="2">

<!--- ************************************************************ --->
<!--- Add parameter to Params structure --->
<!--- ************************************************************ --->

<cfset params[ListGetAt(params_list,index)] = ListGetAt(params_list,index+1)>

<cfset params[ListGetAt(params_list,index)] = Replace(params[ListGetAt(params_list,index)], "PLACEHOLDER_AMPERSAND", "&", "ALL")> <!--- restore escaped ampersand (&) symbols as non-escaped --->
<cfset params[ListGetAt(params_list,index)] = Replace(params[ListGetAt(params_list,index)], "PLACEHOLDER_EQUALS", "=", "ALL")> <!--- restore escaped equals (=) symbols as non-escaped --->

</cfloop>

</cfif>

<!--- ************************************************************ --->
<!--- Generate signature base string --->
<!--- ************************************************************ --->

<!--- ************************************************************ --->
<!--- All parameters must be URL-encoded --->
<!--- ************************************************************ --->

<cfloop list="#StructKeyList(params)#" index="key">

<cfset params[key] = URLEncodedFormat_3986(params[key])>

</cfloop>

<!--- ************************************************************ --->
<!--- Get the base string --->
<!--- ************************************************************ --->

<cfset signature_base_string = OauthBaseString(http_method,request_url_address,params)>

<!--- ************************************************************ --->
<!--- Generate composite signing key --->
<!--- ************************************************************ --->

<cfset composite_signing_key = consumer_secret & "&" & token_secret>

<!--- ************************************************************ --->
<!--- Generate the SHA1 hash --->
<!--- ************************************************************ --->

<cfset signature = ToBase64(HMAC_SHA1(composite_signing_key,signature_base_string))>

<!--- ************************************************************ --->
<!--- Hash (now that we have it) must also be URL encoded --->
<!--- ************************************************************ --->

<cfset signature = URLEncodedFormat_3986(signature)>

<!--- ************************************************************ --->
<!--- Submit request to provider (e.g., Twitter) --->
<!--- ************************************************************ --->

<!--- ************************************************************ --->
<!--- Generate header parameters string --->
<!--- ************************************************************ --->

<cfset oauth_header = "OAuth ">

<!--- ************************************************************ --->
<!--- Parameters (minus URL parameters) --->
<!--- ************************************************************ --->

<cfset comma = "">

<cfloop list="#StructKeyList(params_backup)#" index="key"> <!--- use backup list of parameter keys to remove query parameters --->

<cfset oauth_header = oauth_header & comma & key & "=""" & params[key] & """"> <!--- ...but use current (URL-encoded) parameter values --->

<cfset comma = ", ">

</cfloop>

<!--- ************************************************************ --->
<!--- Signature --->
<!--- ************************************************************ --->

<cfset oauth_header = oauth_header & ", oauth_signature=""" & signature & """">

<!--- ************************************************************ --->
<!--- Send request --->
<!--- ************************************************************ --->

<cfhttp method="post" url="#request_url_address#">

<!--- ************************************************************ --->
<!--- Header --->
<!--- ************************************************************ --->

<cfhttpparam type="header" name="Authorization" value="#oauth_header#" encoded="no">

<!--- ************************************************************ --->
<!--- Parameters --->
<!--- ************************************************************ --->

<cfloop list="#StructKeyList(params)#" index="key">

<cfif not StructKeyExists(params_backup,key)> <!--- just the query parameters --->

<cfhttpparam type="formfield" name="#key#" value="#params[key]#" encoded="no">

</cfif>

</cfloop>

</cfhttp>

<!--- ************************************************************ --->
<!--- Success? --->
<!--- ************************************************************ --->

<cfif cfhttp.Statuscode neq "200 OK">

<!--- ************************************************************ --->
<!--- Failure --->
<!--- ************************************************************ --->

<h1>Failure!</h1>

<cfdump var="#variables#">

<cfabort>

</cfif>

<cfreturn cfhttp.FileContent>

</cffunction>

urlencodedformat_3986():  Enhances ColdFusion's URLEncoded() function to produce strings compliant with RFC 3986.

<!--- ************************************************************ --->
<!--- RFC 3986-COMPLIANT URLENCODEDFORMAT() FUNCTION --->
<!--- --->
<!--- Per "URL Encoding to RFC 3986" in Adobe's Developer --->
<!--- Connection, this function corrects inconsistencies in --->
<!--- ColdFusion's URLEncodedFormat() function that are known --->
<!--- to break OAuth authentication attempts. --->
<!--- --->
<!--- AUTHOR --->
<!--- Dave Delbridge, Circa 3000 (http://circa3000.com) --->
<!--- --->
<!--- PARAMETERS --->
<!--- URL (string) = address to be url-encoded --->
<!--- --->
<!--- RETURNS --->
<!--- (string) Url-encoded address, per RFC 3986 --->
<!--- --->
<!--- ************************************************************ --->

<!--- ************************************************************ --->
<!--- Perform URL encoding and correct mistakes --->
<!--- ************************************************************ --->

<cffunction name="URLEncodedFormat_3986" returntype="string" access="public" output="no">

<cfargument name="url" type="string" required="true" />

<cfset rfc_3986_bad_chars = "%2D,%2E,%5F,%7E">
<cfset rfc_3986_good_chars = "-,.,_,~">

<cfset url = ReplaceList(URLEncodedFormat(url),rfc_3986_bad_chars,rfc_3986_good_chars)>

<cfreturn url />

</cffunction>

The Templates

This project also employs three templates.  Copy the templates into the same directory that holds your "_functions" subdirectory, introduced above.

The first template obtains a Request Token from Twitter and forwards the user to Twitter for authentication.  The second template, or "callback page," receives the user back from Twitter, obtains an Access Token for the user and stores it for future use, as you would a username and password with Basic Authentication.  In the third template, we send an API call to Twitter, along with the Access Token.

For each of these templates, you must supply your own Consumer Key, Consumer Secret (collectively, your "Consumer Token") and Callback URL (your location for template get_access_token.cfm).  In a perfect world, Twitter would provide a public Consumer Token for testing purposes, but alas, this is not the case.  You'll need to go it alone - that is, I can't tell you what your results should look like - and if things go wrong, use the aforementioned validators to locate any trouble in your base strings and request headers.

get_request_token.cfm:  As already explained, this is the first of two templates used to obtain an access token and fulfills steps A, B and C of Twitter's OAuth authentication flow diagram.

<!--- ************************************************************ --->
<!--- GET OAUTH REQUEST TOKEN --->
<!--- --->
<!--- Executes the first three steps of Twitter's OAuth dia- --->
<!--- gram - sends Consumer Key to Twitter (Step A), receives --->
<!--- a Request Token (Step B), and redirects the user to --->
<!--- Twitter for authentication (Step C). --->
<!--- --->
<!--- Once authenticated, Twitter will return the user to us, --->
<!--- to the callback template specified here, in parameter --->
<!--- OAUTH_CALLBACK. Our callback url employs CF session --->
<!--- tokens to preserve session state (with our new request --->
<!--- tokens); no cookies required. --->
<!--- --->
<!--- AUTHOR --->
<!--- Dave Delbridge, Circa 3000 (http://circa3000.com) --->
<!--- --->
<!--- INPUT --->
<!--- n/a --->
<!--- --->
<!--- OUTPUT --->
<!--- SESSION.OAUTH_REQUEST_TOKEN = OAuth request token --->
<!--- SESSION.OAUTH_REQUEST_TOKEN_SECRET = OAuth request token --->
<!--- secret --->
<!--- --->
<!--- ************************************************************ --->

<!--- ************************************************************ --->
<!--- Preserve session state --->
<!--- ************************************************************ --->

<cfapplication sessionmanagement="yes" name="oauth" sessiontimeout="10">

<!--- ************************************************************ --->
<!--- Load necessary functions --->
<!--- ************************************************************ --->

<cfinclude template = "_functions/oauth_request.cfm">

<!--- ************************************************************ --->
<!--- Variables --->
<!--- ************************************************************ --->

<cfset gmt_time_zone = "8"> <!--- Greenwich mean time offset at server --->

<cfset http_method = "POST">
<cfset request_url = "http://twitter.com/oauth/request_token">
<cfset oauth_consumer_secret = "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98">

<cfset params = StructNew()>

<cfset params["oauth_callback"] = "http://www.mysite.com/twitter/oauth/get_access_token.cfm?#session.URLToken#">
<cfset params["oauth_consumer_key"] = "GDdmIQH6jhtmLUypg82g">
<cfset params["oauth_nonce"] = DateFormat(Now(),'yymmdd') & TimeFormat (Now(),'hhmmssl')>
<cfset params["oauth_signature_method"] = "HMAC-SHA1">
<cfset params["oauth_timestamp"] = DateDiff("s", "January 1 1970 00:00", (Now()+(gmt_time_zone/24)))>
<cfset params["oauth_version"] = "1.0">

<!--- ************************************************************ --->
<!--- Submit OAuth request --->
<!--- ************************************************************ --->

<cfset oauth_response = oauth_request(oauth_consumer_secret,"",http_method,request_url,params)>

<!--- ************************************************************ --->
<!--- Parse and store the results --->
<!--- ************************************************************ --->

<!--- ************************************************************ --->
<!--- Request Token (variable-length) --->
<!--- ************************************************************ --->

<cfset oauth_token_start = Find("oauth_token=",oauth_response)+12>
<cfset oauth_token_end = Find("&",oauth_response,oauth_token_start)>
<cfset session.oauth_request_token = Mid(oauth_response,oauth_token_start,(oauth_token_end-oauth_token_start))>

<!--- ************************************************************ --->
<!--- Request Token secret (variable-length) --->
<!--- ************************************************************ --->

<cfset oauth_token_secret_start = Find("oauth_token_secret=",oauth_response)+19>
<cfset oauth_token_secret_end = Find("&",oauth_response,oauth_token_secret_start)>
<cfset session.oauth_request_token_secret = Mid(oauth_response,oauth_token_secret_start,(oauth_token_secret_end-oauth_token_secret_start))>

<!--- ************************************************************ --->
<!--- Callback confirmation flag (true/false) --->
<!--- ************************************************************ --->

<!--- ignored --->

<!--- ************************************************************ --->
<!--- Forward user to Twitter for authentication --->
<!--- ************************************************************ --->

<cflocation url="https://api.twitter.com/oauth/authorize?oauth_token=#session.oauth_request_token#">

get_access_token.cfm:  Our "callback page," users are sent here after successful authentication at TWITTER.COM (Step D).  Next, we obtain an Access Token from Twitter (Steps E and F) and store the token for future use.

<!--- ************************************************************ --->
<!--- OAUTH CALLBACK PAGE / GET OAUTH ACCESS TOKEN --->
<!--- --->
<!--- Fourth, fifth and sixth steps of OAuth procedure (in --->
<!--- Twitter's OAuth diagram). This is our "callback" page, --->
<!--- where the provider (e.g., Twitter) forwards users upon --->
<!--- successful authentication, along with a new verification --->
<!--- token (Step D). --->
<!--- --->
<!--- Next, the verification token is sent to the provider --->
<!--- (Step E) in exchange for an Access Token (Step F). This --->
<!--- is the final authentication request. Store the Access --->
<!--- Token in a database, for example, as you would a username --->
<!--- and password under Basic Authentication. --->
<!--- --->
<!--- AUTHOR --->
<!--- Dave Delbridge, Circa 3000 (http://circa3000.com) --->
<!--- --->
<!--- INPUT --->
<!--- URL.OAUTH_TOKEN = copy of Request Token, sent from --->
<!--- Twitter, should match session.oauth_token --->
<!--- URL.OAUTH_VERIFIER = verifier sent from Twitter --->
<!--- URL.CFID = CF session state var specified in Callback URL --->
<!--- URL.CFTOKEN = ditto --->
<!--- SESSION.OAUTH_REQUEST_TOKEN = Request Token received in --->
<!--- Step B --->
<!--- SESSION.OAUTH_REQUEST_TOKEN_SECRET = Request Token Secret --->
<!--- from Step B --->
<!--- --->
<!--- OUTPUT --->
<!--- OAUTH_ACCESS_TOKEN = access token returned from provider --->
<!--- OAUTH_ACCESS_TOKEN_SECRET = key returned from provider --->
<!--- --->
<!--- ************************************************************ --->


<!--- ************************************************************ --->
<!--- Recover session state from url --->
<!--- ************************************************************ --->

<cfapplication sessionmanagement="yes" name="oauth" sessiontimeout="10">

<!--- ************************************************************ --->
<!--- Load necessary functions --->
<!--- ************************************************************ --->

<cfinclude template = "_functions/oauth_request.cfm">

<!--- ************************************************************ --->
<!--- Variables --->
<!--- ************************************************************ --->

<cfset gmt_time_zone = "8"> <!--- Greenwich mean time offset at server --->

<cfset http_method = "POST">
<cfset request_url = "http://api.twitter.com/oauth/access_token">
<cfset oauth_consumer_secret = "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98">

<cfset params = StructNew()>

<cfset params["oauth_consumer_key"] = "GDdmIQH6jhtmLUypg82g">
<cfset params["oauth_nonce"] = DateFormat(Now(),'yymmdd') & TimeFormat (Now(),'hhmmssl')>
<cfset params["oauth_signature_method"] = "HMAC-SHA1">
<cfset params["oauth_timestamp"] = DateDiff("s", "January 1 1970 00:00", (Now()+(gmt_time_zone/24)))>
<cfset params["oauth_token"] = url.oauth_token>
<cfset params["oauth_verifier"] = url.oauth_verifier>
<cfset params["oauth_version"] = "1.0">

<!--- ************************************************************ --->
<!--- Submit OAuth request --->
<!--- ************************************************************ --->

<cfset oauth_response = oauth_request(oauth_consumer_secret,session.oauth_request_token_secret,http_method,request_url,params)>

<!--- ************************************************************ --->
<!--- Parse and store the results --->
<!--- ************************************************************ --->

<!--- ************************************************************ --->
<!--- Get token (variable-length) --->
<!--- ************************************************************ --->

<cfset oauth_token_start = Find("oauth_token=",oauth_response)+12>
<cfset oauth_token_end = Find("&",oauth_response,oauth_token_start)>
<cfset oauth_access_token = Mid(oauth_response,oauth_token_start,(oauth_token_end-oauth_token_start))>

<!--- ************************************************************ --->
<!--- Get token secret (variable-length) --->
<!--- ************************************************************ --->

<cfset oauth_token_secret_start = Find("oauth_token_secret=",oauth_response)+19>
<cfset oauth_token_secret_end = Find("&",oauth_response,oauth_token_secret_start)>
<cfset oauth_access_token_secret = Mid(oauth_response,oauth_token_secret_start,(oauth_token_secret_end-oauth_token_secret_start))>

<!--- ************************************************************ --->
<!--- Now that we have our access token, store it in a database --->
<!--- as you would a username and password under Basic Authenti- --->
<!--- cation. [For educational purposes, we will instead display --->
<!--- the Access Token with some instructions.] --->
<!--- ************************************************************ --->

<cfoutput>

<p>
Access Token: #oauth_access_token#<br />
Access Token Secret: #oauth_access_token_secret#
</p>

<p>Normally, you would now store these in a database, as you would a username and password under Basic Authentication. For the purposes of instruction, we will instead pass these variables to the final template, CALL_METHOD.CFM, in URL variables. To proceed, click this link: <a href="call_method.cfm?oauth_token=#oauth_access_token#&oauth_token_secret=#oauth_access_token_secret#">call_method.cfm?oauth_token=#oauth_access_token#&oauth_token_secret=#oauth_access_token_secret#</a></p>

</cfoutput>

call_method.cfm:  With our Access Token in hand, we can finally submit a Twitter API call.  Or two.  Or three...

<!--- ************************************************************ --->
<!--- ACCESS PROTECTED RESOURCES --->
<!--- --->
<!--- The final step of OAuth procedure (Step G), submits a --->
<!--- specified Twitter API call against a protected resource. --->
<!--- The target account must match the specified Access Token --->
<!--- (OAUTH_TOKEN and OAUTH_TOKEN_SECRET). --->
<!--- --->
<!--- AUTHOR --->
<!--- Dave Delbridge, Circa 3000 (http://circa3000.com) --->
<!--- --->
<!--- INPUT --->
<!--- URL.OAUTH_TOKEN = Access Token given to us in Step F --->
<!--- URL.OAUTH_TOKEN_SECRET = Access Token secret given to us --->
<!--- in Step F --->
<!--- --->
<!--- OUTPUT --->
<!--- OAUTH_RESPONSE = provider's response --->
<!--- --->
<!--- ************************************************************ --->

<!--- ************************************************************ --->
<!--- Make sure international characters are encoded properly --->
<!--- ************************************************************ --->

<cfprocessingdirective pageEncoding="utf-8">

<!--- ************************************************************ --->
<!--- Load necessary functions --->
<!--- ************************************************************ --->

<cfinclude template = "_functions/oauth_request.cfm">

<!--- ************************************************************ --->
<!--- Variables --->
<!--- ************************************************************ --->

<cfset gmt_time_zone = "8"> <!--- Greenwich mean time offset at server --->

<cfset http_method = "POST">
<cfset request_url = "http://api.twitter.com/1/statuses/update.xml?status=yet another test of my new twitter account 私のさえずりを設定する">
<cfset oauth_consumer_secret = "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98">

<cfset params = StructNew()>

<cfset params["oauth_consumer_key"] = "GDdmIQH6jhtmLUypg82g">
<cfset params["oauth_nonce"] = DateFormat(Now(),'yymmdd') & TimeFormat (Now(),'hhmmssl')>
<cfset params["oauth_signature_method"] = "HMAC-SHA1">
<cfset params["oauth_token"] = url.oauth_token>
<cfset params["oauth_timestamp"] = DateDiff("s", "January 1 1970 00:00", (Now()+(gmt_time_zone/24)))>
<cfset params["oauth_version"] = "1.0">

<!--- ************************************************************ --->
<!--- Submit OAuth request --->
<!--- ************************************************************ --->

<cfset oauth_response = oauth_request(oauth_consumer_secret,url.oauth_token_secret,http_method,request_url,params)>

<!--- ************************************************************ --->
<!--- Display the results --->
<!--- ************************************************************ --->

<cfoutput>

<h1>Twitter's Response</h1>

<p>#HTMLCodeFormat(oauth_response)#</p>

</cfoutput>

The Path Ahead

Now, all that's left to do is upgrade your own Twitter apps from Basic Authentication.  Using the first two templates as a guide, update your applications' login pages, storing the Access Tokens in place of username and password.  Then, using the call_method.cfm template as a guide, perhaps create a general-purpose twitter() function, or maybe individual functions for those secured Twitter API methods you use most (e.g., update_status(), follow_id()).  Have fun!

I hope this has been helpful.  If so, you can follow me on Twitter at @circa3000.  And, of course, please consider Circa 3000 for your ColdFusion hosting needs.  I look forward to hearing from you!

Tags: Computer Tech

27 responses so far ↓

  • 1 Richard // Mar 16, 2011 at 7:26 AM

    Fantastic tutorial, the only one I've found that actually does what I want it to. Just once question, once I've allowed access to my app how can I stop it asking for access every other time. I'd rather just allow access once rather than having to keep enabling it.
  • 2 Dave Delbridge // Mar 16, 2011 at 10:38 PM

    I'm not sure that I understand your question. Once your app has the user's Access Tokens, there should be no need to re-authenticate through Twitter's OAuth.

    Now, when a user returns to your application pages, the app must somehow identify the user and recall the accordant Access Tokens for subsequent operations against the user's Twitter account. This is a function of your application's native registration / security features and might offer an optional "remember me" checkbox to avoid future manual logins.

    Does this answer your question?
  • 3 Richard // Mar 23, 2011 at 4:48 AM

    Hi Dave, yep that's great thanks.
  • 4 ijwk // Jul 21, 2011 at 1:23 PM

    Implemented this in about 5 minutes. Only isue was line 72 of get_acess_token.cfm the sessiona variable: session.oauth_token_secret was not available but passed back in the url as oauth_token. I tested it on a few twitter ids and that seemed to resolve the problem.
  • 5 Dave Delbridge // Jul 22, 2011 at 9:19 AM

    Good catch, ijwk!

    Yes, in template get_access_token.cfm, all references to session.oauth_token and session.oauth_token_secret should read session.oauth_request_token and session.oauth_request_token_secret, respectively, as described in the comments of the preceding template, get_request_token.cfm. [I really should read my own comments.]

    To avoid confusion, I've repaired get_access_token.cfm, above, to reference the correct session variables.

    Thank you for the correction!
  • 6 ijwk // Sep 7, 2011 at 8:04 PM

    Dave, I have been using this for a while and it is working great. I have found if I pass an ampersand through my tweet this I get an error in the parse address and parameters from the request URL section. (I assume an = would do the same thing). I have tried to pass the ampersand as %26 and I can make it through the code and even submit to twitter however the %26 is never converted back when twitter shows the tweet in the end. Have you resolved this in your own work? Any suggestion direction would be helpful
  • 7 ijwk // Sep 9, 2011 at 7:27 PM

    I could be wrong so someone please tell me.....
    after some testing I think the issue is the call_method.cfm specifically the request_url. If there is an ampersand it messes up the params in the oauth_request making it think there is an additional variable. I wrapped the message with with urlencoded and even the urlencoded_3986() version in the code and unfortunately all that does is send %20 and %26 etc straight to the tweet. Any help is appreciates. Short term solution is replace & with and. :)
  • 8 Dave Delbridge // Sep 12, 2011 at 11:32 AM

    I see where the trouble is: In function oauth_request(), any url parameters supplied in the URL_REQUEST variable must be converted to key/value pairs and added to the OAuth request's parameters. I did this very simply - too simply - by converting everything after the question mark to a delimited list, with '&' and '=' symbols as delimiters. This works fine as long as there aren't extraneous 'delimiters' in your url parameters, as in your described tweet.

    I'm sorry to say that I can't afford the time to fix and test this right now but can tell you very quickly that the solution is to replace the section labeled "Repeat for each key/value pair" with code logic that more carefully parses the key value pairs out of the url parameters.

    This could be done any number of ways - escape your input (e.g., &&), for example or simply slug it out to determine which are literal ampersands vs. delimiters. Better still, there might be a freely available UDF to do this.

    I hope this is helpful.
  • 9 Aztral // Oct 2, 2011 at 7:55 PM

    After looking at several Coldfusion cfcs for doing Twitter stuff which ultimately didn't work-I found your page.

    Good Job!
  • 10 Dave Delbridge // Oct 3, 2011 at 3:08 PM

    Okay, "slugging it out" is not an option, after all. Having gnashed on this for a bit, the simplest solution is, indeed, to escape ampersand (&) and equals (=) symbols in the URL request parameter values BEFORE submitting them to the oauth_request() function.

    I have updated the oauth_request() function, above, to accept "&&" and "==" in your URL request parameter values. I've also documented the changes, being that Twitter is apparently updating their instructions. Can't blame 'em.

    Thank you for the comments and e-mails.

    Again, I hope this is helpful.
  • 11 Henry Ho // Nov 1, 2011 at 6:21 PM

    "Contrary to ColdFusion's documentation, HMAC-SHA1 is not supported by the Encrypt (or Hash) functions." Has this been reported to Adobe? Or is it because you're not using Enterprise Edition?
  • 12 Henry Ho // Nov 1, 2011 at 7:01 PM

    Oops, just realized they are different things.

    Please vote this up if you want a built-in hmac() function: http://cfbugs.adobe.com/cfbugreport/flexbugui/cfbugtracker/main.html#bugId=86654
  • 13 Adam // Aug 29, 2012 at 7:11 AM

    Hi All! Thanks so much for posting.

    I know the last update was about a year ago, so I was interested to know if this oAuth code still works? From reading Twitter's docs, they have not yet adopted oAuth 2.0 - which I wish they would...

    Do you guys still have this code in production? Thx
  • 14 Dave Delbridge // Aug 29, 2012 at 12:31 PM

    I don't presently have this code in production but would expect it to work if Twitter hasn't implemented any major oAuth changes since last year.

    FWIW, it's not too difficult to try. Just copy the functions and templates as instructed and replace all of the consumer key and secret values with your own. Execute each of the templates in the presented order and watch for this update to your Twitter feed - "yet another test of my new twitter account 私のさえずりを設定する." If it fails, note "Twitter's Response" on call_method.cfm. Debug. Rinse. Lather. Repeat. :)

    Good luck!
  • 15 Adam // Aug 29, 2012 at 12:58 PM

    Thanks so much Dave...

    YES - The example above works! I was able to follow your instructions and put a test post to Twitter using this code, a properly registered application and an oAuth authorized user... Thanks so much for that! A HUGE help!

    Once point for any others who are messing with this code - the above example is for a POST call - which works great. But if you try to modify the HTTP_METHOD to GET, you will hit a generic Twitter failure saying, basically, 'your signature does not match.'

    I spent a few hours tracing this issue down and believe it is because the ordering of the STRUCT that carries the params into the signature generation process is not acceptable to Twitter. I dug around and couldn't figure out how to order a struct alphabetically...

    Nonetheless, the building blocks Dave has here are mostly solid. I used Dave's URLEncoding and hmac_sha1 functions... Then I dug out parts of the oauth_base_string and made my own that doesn't have the param order issue...

    With that adjustment to make the GET calls work, and Dave's examples for POST (which work as outlined above perfectly), you should be off and running with Twitter...

    Thanks again! HUGE help Dave! :-)
  • 16 Dave Delbridge // Aug 30, 2012 at 3:28 PM

    Forgive me, Adam, but I don't understand the problem (or, more importantly, how you solved it). The structure containing the request parameters is processed in alphabetical order of the keys, necessarily just after the URL request parameters are appended to the structure.

    Now, I did notice that parameter OAUTH_TOKEN is specified out of alphabetical order in template GET_OAUTH_TOKEN.CFM. The struct is backed up (sans sorting) and later used to generate the header parameters string - again, without sorting. Could that perhaps be the problem?
  • 17 Adam // Aug 31, 2012 at 5:36 AM

    Hi Dave! Thanks for the response.

    Exactly... There is sorting in one place, but not in the other... It's my belief that the lack of sorting in the backup struct is fine for POST calls, but Twitter is more finicky on their GET requests and so the signature authentication fails...

    My solution was to completely re-write the base string function for GET calls.... If I had more skill, I would have ordered it, but I don't understand STRUCTS well enough. And from what I researched, you can't order them. I'm sure this would be a quick fix for you. :-)

    Thanks again for your code! It was a great help!
  • 18 Dave Delbridge // Aug 31, 2012 at 12:43 PM

    My pleasure, Adam!

    I suspect you're right re Twitter's handling of GET vs. POST. As mentioned earlier, the only bugaboo I could find was a parameter specified out of alphabetical order. If that's really the problem, one would expect it to affect both methods. <shrug>

    For the record, ColdFusion's STRUCTKEYLIST function returns a structure's keys in order of their creation - not alphabetically. Hence, the simplest way to "sort" a structure is to define the elements in alphabetical order up front. [When that structure is dynamic, as when appending the URL request parameters in function OAUTH_BASE_STRING, then we must create a sorted list of keys and loop over that instead.]

    Consequently, our fix here is simple: define OAUTH_TOKEN after OAUTH_TIMESTAMP and our params are presorted in the structure for when we create the header parameters string later on.

    I have implemented this update in the samples above but have not tested it. If anyone experiences trouble with the GET method, please post your findings and we'll try again.

    Thanks again, Adam!
  • 19 James // Nov 8, 2012 at 5:28 AM

    Hi Dave, Adam

    Thanks for the excellent tutorials. I can't however get any of the GET requests to work. Have you had success with this?

    If so could you post the modified code for us to look at?

    Thanks
  • 20 Adam // Nov 8, 2012 at 5:45 AM

    Sorry, I never got the GET to work either. So I ended up re-building the GET calls using the piece-parts here...
  • 21 Dave Delbridge // Nov 8, 2012 at 9:29 AM

    Gentlemen, if you don't mind my asking, which versions of ColdFusion are you running your OAuth templates on? I'm beginning to wonder if my understanding that StructKeyList returns keys in the order of their creation (FIFO) is specific to CF 7 (on which I developed and tested these templates 2+ years ago).

    If so, then yes, we must sort the parameters whenever looping over them. I'll be happy to post a code fix.
  • 22 Adam // Nov 8, 2012 at 10:18 AM

    Good question. We have, cough cough, CF 7 in Dev and CF 8 in prod. I know, I know. Nutty.
  • 23 James // Nov 8, 2012 at 11:47 AM

    We are using CF9. I think I remember reading somewhere that the order of the attributes in the header was important so I tried turning

    cfloop list="#StructKeyList(params_backup)#" index="key"

    into

    cfloop list="#listSort(StructKeyList(params_backup),'txt','asc')#" index="key"

    after changing this the attributes in the header were ordered in alphabetical order but the request still failed.
  • 24 Steve // Apr 18, 2013 at 8:08 AM

    Hi, I just came across your post and it was a good start. I dont know if anything has changed in the last few months but I initially put everything together and get a 301 error that the link has moved.

    I updated get_request_token from:

    <cfset request_url = "http://twitter.com/oauth/request_token">;
    to
    <cfset request_url = "https://api.twitter.com/oauth/request_token">;

    and that got me further. after I logged in and was authenticated and sent back, I tried to do the call_method.cfm and received:
    "Read-only application cannot POST"

    I tried adding:
    <cfset params["x_auth_access_type"] = "write">

    but that just gave me more errors....

    Any suggestions for someone new to this?
  • 25 Steve // Apr 18, 2013 at 9:10 AM

    seems that I had my application in read only mode on the dev.twitter.com settings... fixed and works. thanks.
  • 26 Sachin // Jun 18, 2013 at 8:26 AM

    Hello Dave,

    Thanks for your post, I am very near to the integration of getting my tweets displayed on my website and used your example.. which is great.

    I am just getting an error when making call to their respective function:
    https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=cfsachin&count=2

    Below is my app link:
    http://iwizards.net/tweeter/oauth/get_request_token.cfm

    Can you please suggest!

    Thanks
  • 27 Sie auf den Link // Jul 22, 2013 at 6:50 AM

    I was getting an error, thanks to your post I could resolved it, thanks for that.

Leave a Comment

Leave this field empty: