Twitter OAuth in F#
I have a few F# demos which use the Twitter APIs as simple examples of accessing online data interactively and working with it in F#. Recently, Twitter moved to require OAuth for accessing Twitter APIs on behalf of a user. Below is the F# code I wrote to integrate OAuth, which should work for any other F# Twitter scripts and apps.
OAuth Implementation
open System open System.IO open System.Net open System.Security.Cryptography open System.Text // Twitter OAuth Constants let consumerKey : string = failwith "Must provide the consumerKey for an app registered at https://dev.twitter.com/apps/new" let consumerSecret : string = failwith "Must provide the consumerSecret for an app registered at https://dev.twitter.com/apps/new" let requestTokenURI = "https://api.twitter.com/oauth/request_token" let accessTokenURI = "https://api.twitter.com/oauth/access_token" let authorizeURI = "https://api.twitter.com/oauth/authorize" // Utilities let unreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"; let urlEncode str = String.init (String.length str) (fun i -> let symbol = str.[i] if unreservedChars.IndexOf(symbol) = -1 then "%" + String.Format("{0:X2}", int symbol) else string symbol) // Core Algorithms let hmacsha1 signingKey str = let converter = new HMACSHA1(Encoding.ASCII.GetBytes(signingKey : string)) let inBytes = Encoding.ASCII.GetBytes(str : string) let outBytes = converter.ComputeHash(inBytes) Convert.ToBase64String(outBytes) let compositeSigningKey consumerSecret tokenSecret = urlEncode(consumerSecret) + "&" + urlEncode(tokenSecret) let baseString httpMethod baseUri queryParameters = httpMethod + "&" + urlEncode(baseUri) + "&" + (queryParameters |> Seq.sortBy (fun (k,v) -> k) |> Seq.map (fun (k,v) -> urlEncode(k)+"%3D"+urlEncode(v)) |> String.concat "%26") let createAuthorizeHeader queryParameters = let headerValue = "OAuth " + (queryParameters |> Seq.map (fun (k,v) -> urlEncode(k)+"\x3D\""+urlEncode(v)+"\"") |> String.concat ",") headerValue let currentUnixTime() = floor (DateTime.UtcNow - DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalSeconds /// Request a token from Twitter and return: /// oauth_token, oauth_token_secret, oauth_callback_confirmed let requestToken() = let signingKey = compositeSigningKey consumerSecret "" let queryParameters = ["oauth_callback", "oob"; "oauth_consumer_key", consumerKey; "oauth_nonce", System.Guid.NewGuid().ToString().Substring(24); "oauth_signature_method", "HMAC-SHA1"; "oauth_timestamp", currentUnixTime().ToString(); "oauth_version", "1.0"] let signingString = baseString "POST" requestTokenURI queryParameters let oauth_signature = hmacsha1 signingKey signingString let realQueryParameters = ("oauth_signature", oauth_signature)::queryParameters let req = WebRequest.Create(requestTokenURI, Method="POST") let headerValue = createAuthorizeHeader realQueryParameters req.Headers.Add(HttpRequestHeader.Authorization, headerValue) let resp = req.GetResponse() let stream = resp.GetResponseStream() let txt = (new StreamReader(stream)).ReadToEnd() let parts = txt.Split('&') (parts.[0].Split('=').[1], parts.[1].Split('=').[1], parts.[2].Split('=').[1] = "true") /// Get an access token from Twitter and returns: /// oauth_token, oauth_token_secret let accessToken token tokenSecret verifier = let signingKey = compositeSigningKey consumerSecret tokenSecret let queryParameters = ["oauth_consumer_key", consumerKey; "oauth_nonce", System.Guid.NewGuid().ToString().Substring(24); "oauth_signature_method", "HMAC-SHA1"; "oauth_token", token; "oauth_timestamp", currentUnixTime().ToString(); "oauth_verifier", verifier; "oauth_version", "1.0"] let signingString = baseString "POST" accessTokenURI queryParameters let oauth_signature = hmacsha1 signingKey signingString let realQueryParameters = ("oauth_signature", oauth_signature)::queryParameters let req = WebRequest.Create(accessTokenURI, Method="POST") let headerValue = createAuthorizeHeader realQueryParameters req.Headers.Add(HttpRequestHeader.Authorization, headerValue) let resp = req.GetResponse() let stream = resp.GetResponseStream() let txt = (new StreamReader(stream)).ReadToEnd() let parts = txt.Split('&') (parts.[0].Split('=').[1], parts.[1].Split('=').[1]) /// Compute the 'Authorization' header for the given request data let authHeaderAfterAuthenticated url httpMethod token tokenSecret queryParams = let signingKey = compositeSigningKey consumerSecret tokenSecret let queryParameters = ["oauth_consumer_key", consumerKey; "oauth_nonce", System.Guid.NewGuid().ToString().Substring(24); "oauth_signature_method", "HMAC-SHA1"; "oauth_token", token; "oauth_timestamp", currentUnixTime().ToString(); "oauth_version", "1.0"] let signingQueryParameters = List.append queryParameters queryParams let signingString = baseString httpMethod url signingQueryParameters let oauth_signature = hmacsha1 signingKey signingString let realQueryParameters = ("oauth_signature", oauth_signature)::queryParameters let headerValue = createAuthorizeHeader realQueryParameters headerValue /// Add an Authorization header to an existing WebRequest let addAuthHeaderForUser (webRequest : WebRequest) token tokenSecret queryParams = let url = webRequest.RequestUri.ToString() let httpMethod = webRequest.Method let header = authHeaderAfterAuthenticated url httpMethod token tokenSecret queryParams webRequest.Headers.Add(HttpRequestHeader.Authorization, header) type System.Net.WebRequest with /// Add an Authorization header to the WebRequest for the provided user authorization tokens and query parameters member this.AddOAuthHeader(userToken, userTokenSecret, queryParams) = addAuthHeaderForUser this userToken userTokenSecret queryParams let testing() = // Compute URL to send user to to allow our app to connect with their credentials, // then open the browser to have them accept let oauth_token'', oauth_token_secret'', oauth_callback_confirmed = requestToken() let url = authorizeURI + "?oauth_token=" + oauth_token'' System.Diagnostics.Process.Start("iexplore.exe", url) // *******NOTE********: // Get the 7 digit number from the web page, pass it to the function below to get oauth_token // Sample result if things go okay: // val oauth_token_secret' : string = "lbll17CpNUlSt0FbfMKyfrzBwyHUrRrY8Ge2rEhs" // val oauth_token' : string = "17033946-8vqDO7foX0TUNpqNg9MyJpO3Qui3nunkZPixxLs" let oauth_token, oauth_token_secret = accessToken oauth_token'' oauth_token_secret'' ("1579754") // Test 1: let streamSampleUrl2 = "https://api.twitter.com/1/statuses/home_timeline.xml" let req = WebRequest.Create(streamSampleUrl2) req.AddOAuthHeader(oauth_token, oauth_token_secret, []) let resp = req.GetResponse() let strm = resp.GetResponseStream() let text = (new StreamReader(strm)).ReadToEnd() text // Test 2: System.Net.ServicePointManager.Expect100Continue <- false let statusUrl = "https://twitter.com/statuses/update.xml" let request = WebRequest.Create (statusUrl, Method="POST") let tweet = urlEncode("Hello!") request.AddOAuthHeader(oauth_token,oauth_token_secret,["status",tweet]) let bodyStream = request.GetRequestStream() let bodyWriter = new StreamWriter(bodyStream) bodyWriter.Write("status=" + tweet) bodyWriter.Close() let resp = request.GetResponse() let strm = resp.GetResponseStream() let text = (new StreamReader(strm)).ReadToEnd() text
Comments
Anonymous
January 11, 2011
Luke, I checked your code, oAuth returns a 401 error, I had retrieved the token secret and the other details, it still throws some exception. -FahadAnonymous
May 15, 2012
I'm seeing the same issue. Works when no query parameters are used. 401 errors when I use a query parameter.Anonymous
May 18, 2012
The comment has been removed