Partager via


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. -Fahad

  • Anonymous
    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