0

I'm trying to implement the newest yahoo weather API in a .NET application (the one we were using was discontinued in favor of this new one): https://developer.yahoo.com/weather/documentation.html#commercial

Their only examples are in PHP and Java. I've done my best to convert the Java example to .NET but I still am getting a "401 - Unauthorized" response. I've gone over my code several times and cannot find any problems so I'm hoping someone else will be able to see where I went wrong. Code is below.

    Private Sub _WeatherLoader_DoWork(sender As Object, e As DoWorkEventArgs)

        Try

            Dim oauth As New OAuth.OAuthBase
            Dim forecastRssResponse As query
            Dim appId As String = My.Settings.YahooAppID
            Dim consumerKey As String = My.Settings.YahooAPIConsumerKey
            Dim yahooUri As String = String.Format("{0}?location=billings,mt&format=xml", YAHOO_WEATHER_API_BASE_ENDPOINT)
            Dim oAuthTimestamp As Integer = oauth.GenerateTimeStamp()
            Dim oAuthNonce As String = oauth.GenerateNonce()
            Dim parameters As New List(Of String)

            Try

                parameters.Add(String.Format("oauth_consumer_key={0}", consumerKey))
                parameters.Add(String.Format("oauth_nonce={0}", oAuthNonce))
                parameters.Add("oauth_signature_method=HMAC-SHA1")
                parameters.Add(String.Format("oauth_timestamp={0}", oAuthTimestamp.ToString()))
                parameters.Add("oauth_version=1.0")

                ' Encode the location
                parameters.Add(String.Format("location={0}", HttpUtility.UrlEncode("billings,mt", Encoding.UTF8)))
                parameters.Add("format=xml")

                ' Sort parameters ascending
                parameters = parameters.OrderBy(Function(item) item).ToList()

                Dim i As Integer = 0
                Dim builder As New StringBuilder()

                Do While (i < parameters.Count())

                    builder.Append(String.Format("{0}{1}", If(i > 0, "&", String.Empty), parameters(i)))
                    i += 1

                Loop

                Dim signatureString As String = String.Format("GET&{0}&{1}", HttpUtility.UrlEncode(YAHOO_WEATHER_API_BASE_ENDPOINT, Encoding.UTF8), HttpUtility.UrlEncode(builder.ToString(), Encoding.UTF8))
                Dim oAuthSignature As String = _CreateOauthSignature(signatureString)
                Dim authorizationLine As String = String.Format("OAuth oauth_consumer_key={0}, oauth_nonce={1}, oauth_timestamp={2}, oauth_signature_method=HMAC-SHA1, oauth_signature={3}, oauth_version=1.0", consumerKey, oAuthNonce, oAuthTimestamp, oAuthSignature)
                Dim forecastRequest As WebRequest = WebRequest.Create(yahooUri)

                forecastRequest.Headers.Add("Authorization", authorizationLine)
                forecastRequest.Headers.Add("Yahoo-App-Id", appId)

                ' Cast to HttpWebRequest to set ContentType through property
                CType(forecastRequest, HttpWebRequest).ContentType = "text/xml"

                Dim forecastResponse As WebResponse = forecastRequest.GetResponse()

                If forecastResponse IsNot Nothing Then

                    Using responseStream As Stream = forecastResponse.GetResponseStream()

                        Dim rssDoc As New XmlDocument()

                        rssDoc.Load(responseStream)
                        forecastRssResponse = rssDoc.OuterXml().FromXml(Of query)()

                    End Using

                    e.Result = forecastRssResponse

                End If

            Catch ex As Exception

                e.Result = Nothing
                LoadingManually = False

            End Try

        Catch ex As Exception
            modMain.SendDevErrorEmail(ex, "_WeatherLoader_DoWork in WeatherWidget", "Catch around dowork code in fired from refresh timer event in wether widget")

            e.Result = Nothing
            LoadingManually = False
        End Try

    End Sub

Private Function _CreateOauthSignature(baseInfo As String) As String

    Dim secretKey As String = String.Format("{0}&", My.Settings.YahooAPIConsumerSecretKey)
    Dim encoding As New System.Text.ASCIIEncoding()
    Dim keyBytes As Byte() = encoding.GetBytes(secretKey)
    Dim messageBytes As Byte() = encoding.GetBytes(baseInfo)
    Dim hashMessage As Byte()

    Using hmac As New HMACSHA1(keyBytes)

    hashMessage = hmac.ComputeHash(messageBytes)

    End Using

    Return Convert.ToBase64String(hashMessage)

End Function
DonnieDarko
  • 93
  • 1
  • 14
  • We have followed the instructions to get access, have sent request to get our app whitelisted and have received response from Yahoo saying we are good to go. I've also verified our app_id, consumer key and secret key are all correct. – DonnieDarko Jan 22 '19 at 18:19
  • Here is the response I got back from the "Yahoo weather team": "Hi, We are not familiar with .net code. We would recommend using an oauth library for signature generation." Seriously...that is what they call support? I've used "oauthbase" to generate a signature and I get the same response. 401, Unauthorized. – DonnieDarko Jan 22 '19 at 21:55

1 Answers1

0

After painstakingly creating a Java app, pasting in the Java example and stepping through it I found that the issue is in a poorly implemented URL Decode function on the receiving end.

In the Java app, URL Encode uses upper case characters while in .NET HTTPUtility.URLEncode uses lower case characters. This is enough to throw off your signature and cause a 401 - Unauthorized error.

My solution was to create a string extension method that will URL Encode in upper case:

  <Extension>
    Public Function UppercaseURLEncode(ByVal sourceString As String) As String

        Dim temp As Char() = HttpUtility.UrlEncode(sourceString).ToCharArray()

        For i As Integer = 0 To temp.Length - 2

            If temp(i).ToString().Equals("%", StringComparison.OrdinalIgnoreCase) Then

                temp(i + 1) = Char.ToUpper(temp(i + 1))
                temp(i + 2) = Char.ToUpper(temp(i + 2))

            End If

        Next

        Return New String(temp)

    End Function

Using this extension method my signature gets created exactly like the one in the Java app and I am able to retrieve the response.

Hope this helps other .net programmers with this issue!

DonnieDarko
  • 93
  • 1
  • 14