在.NET 中使用验证的 OAuth

我想创造一个。基于 NET 的客户端应用程序(在 WPF 中——尽管目前我只是把它作为一个控制台应用程序)集成到一个支持 OAuth 的应用程序中,特别是 Mendeley (http://dev.mendeley.com) ,它显然使用了3条腿的 OAuth。

这是我第一次使用 OAuth,我在开始使用它时遇到了很多困难。我找到了几个。NET OAuth 库或者助手,但是它们似乎比我想象的要复杂得多。我想要做的就是能够向 Mendeley API 发出 REST 请求并得到响应!

到目前为止,我试过:

第一个(DotNetOpenAuth)似乎可以满足我的需要,如果我花费大量的时间试图解决这个问题。第二个和第三个,据我所知,不支持 Mendeley 发回的验证代码——尽管我可能错了:)

我有一个来自 Mendeley 的用户密钥和机密文件,通过 DotNetOpenAuth,我成功地启动了一个浏览器,其中 Mendeley 页面为用户输入应用程序提供了验证代码。然而,在这一点上,我迷失了方向,不知道如何明智地将它提供给应用程序。

我非常愿意承认,我不知道从哪里开始(虽然它似乎有一个相当陡峭的学习曲线)-如果有人可以指出我在正确的方向,我会很感激!

80882 次浏览

I agree with you. The open-source OAuth support classes available for .NET apps are hard to understand, overly complicated (how many methods are exposed by DotNetOpenAuth?), poorly designed (look at the methods with 10 string parameters in the OAuthBase.cs module from that google link you provided - there's no state management at all), or otherwise unsatisfactory.

It doesn't need to be this complicated.

I'm not an expert on OAuth, but I have produced an OAuth client-side manager class, that I use successfully with Twitter and TwitPic. It's relatively simple to use. It's open source and available here: Oauth.cs

For review, in OAuth 1.0a...kinda funny, there's a special name and it looks like a "standard" but as far as I know the only service that implements "OAuth 1.0a" is Twitter. I guess that's standard enough. ok, anyway in OAuth 1.0a, the way it works for desktop apps is this:

  1. You, the developer of the app, register the app and get a "consumer key" and "consumer secret". On Arstechnica, there's a well written analysis of why this model isn't the best, but as they say, it is what it is.

  2. Your app runs. The first time it runs, it needs to get the user to explicitly grant approval for the app to make oauth-authenticated REST requests to Twitter and its sister services (like TwitPic). To do this you must go through an approval process, involving explicit approval by the user. This happens only the first time the app runs. Like this:

    • request a "request token". Aka temporary token.
    • pop a web page, passing that request token as a query param. This web page presents UI to the user, asking "do you want to grant access to this app?"
    • the user logs in to the twitter web page, and grants or denies access.
    • the response html page appears. If the user has granted access, there's a PIN displayed in a 48-pt font
    • the user now needs to cut/paste that pin into a windows form box, and click "Next" or something similar.
    • the desktop app then does an oauth-authenticated request for an "Access token". Another REST request.
    • the desktop app receives the "access token" and "access secret".

After the approval dance, the desktop app can just use the user-specific "access token" and "access secret" (along with the app-specific "consumer key" and "consumer secret") to do authenticated requests on behalf of the user to Twitter. These don't expire, although if the user de-authorizes the app, or if Twitter for some reason de-authorizes your app, or if you lose your access token and/or secret, you'd need to do the approval dance again.


If you're not clever, the UI flow can sort of mirror the multi-step OAuth message flow. There is a better way.

Use a WebBrowser control, and open the authorize web page within the desktop app. When the user clicks "Allow", grab the response text from that WebBrowser control, extract the PIN automatically, then get the access tokens. You send 5 or 6 HTTP requests but the user needs to see only a single Allow/Deny dialog. Simple.

Like this:
alt text


If you've got the UI sorted, the only challenge that remains is to produce oauth-signed requests. This trips up lots of people because the oauth signing requirements are sort of particular. That's what the simplified OAuth Manager class does.

Example code to request a token:

var oauth = new OAuth.Manager();
// the URL to obtain a temporary "request token"
var rtUrl = "https://api.twitter.com/oauth/request_token";
oauth["consumer_key"] = MY_APP_SPECIFIC_KEY;
oauth["consumer_secret"] = MY_APP_SPECIFIC_SECRET;
oauth.AcquireRequestToken(rtUrl, "POST");

THAT'S IT. Simple. As you can see from the code, the way to get to oauth parameters is via a string-based indexer, something like a dictionary. The AcquireRequestToken method sends an oauth-signed request to the URL of the service that grants request tokens, aka temporary tokens. For Twitter, this URL is "https://api.twitter.com/oauth/request_token". The oauth spec says you need to pack up the set of oauth parameters (token, token_secret, nonce, timestamp, consumer_key, version, and callback), in a certain way (url-encoded and joined by ampersands), and in a lexicographically-sorted order, generate a signature on that result, then pack up those same parameters along with the signature, stored in the new oauth_signature parameter, in a different way (joined by commas). The OAuth manager class does this for you automatically. It generates nonces and timestamps and versions and signatures automatically - your app doesn't need to care or be aware of that stuff. Just set the oauth parameter values and make a simple method call. the manager class sends out the request and parses the response for you.

Ok, then what? Once you get the request token, you pop the web browser UI in which the user will explicitly grant approval. If you do it right, you'll pop this in an embedded browser. For Twitter, the URL for this is "https://api.twitter.com/oauth/authorize?oauth_token=" with the oauth_token appended. Do this in code like so:

var url = SERVICE_SPECIFIC_AUTHORIZE_URL_STUB + oauth["token"];
webBrowser1.Url = new Uri(url);

(If you were doing this in an external browser you'd use System.Diagnostics.Process.Start(url).)

Setting the Url property causes the WebBrowser control to navigate to that page automatically.

When the user clicks the "Allow" button a new page will be loaded. It's an HTML form and it works the same as in a full browser. In your code, register a handler for the DocumentedCompleted event of the WebBrowser control, and in that handler, grab the pin:

var divMarker = "<div id=\"oauth_pin\">"; // the div for twitter's oauth pin
var index = webBrowser1.DocumentText.LastIndexOf(divMarker) + divMarker.Length;
var snip = web1.DocumentText.Substring(index);
var pin = RE.Regex.Replace(snip,"(?s)[^0-9]*([0-9]+).*", "$1").Trim();

That's a bit of HTML screen scraping.

After grabbing the pin, you don't need the web browser any more, so:

webBrowser1.Visible = false; // all done with the web UI

...and you might want to call Dispose() on it as well.

The next step is getting the access token, by sending another HTTP message along with that pin. This is another signed oauth call, constructed with the oauth ordering and formatting I described above. But once again this is really simple with the OAuth.Manager class:

oauth.AcquireAccessToken(URL_ACCESS_TOKEN,
"POST",
pin);

For Twitter, that URL is "https://api.twitter.com/oauth/access_token".

Now you have access tokens, and you can use them in signed HTTP requests. Like this:

var authzHeader = oauth.GenerateAuthzHeader(url, "POST");

...where url is the resource endpoint. To update the user's status, it would be "http://api.twitter.com/1/statuses/update.xml?status=Hello".

Then set that string into the HTTP Header named Authorization.

To interact with third-party services, like TwitPic, you need to construct a slightly different OAuth header, like this:

var authzHeader = oauth.GenerateCredsHeader(URL_VERIFY_CREDS,
"GET",
AUTHENTICATION_REALM);

For Twitter, the values for the verify creds url and realm are "https://api.twitter.com/1/account/verify_credentials.json", and "http://api.twitter.com/" respectively.

...and put that authorization string in an HTTP header called X-Verify-Credentials-Authorization. Then send that to your service, like TwitPic, along with whatever request you're sending.

That's it.

All together, the code to update twitter status might be something like this:

// the URL to obtain a temporary "request token"
var rtUrl = "https://api.twitter.com/oauth/request_token";
var oauth = new OAuth.Manager();
// The consumer_{key,secret} are obtained via registration
oauth["consumer_key"] = "~~~CONSUMER_KEY~~~~";
oauth["consumer_secret"] = "~~~CONSUMER_SECRET~~~";
oauth.AcquireRequestToken(rtUrl, "POST");
var authzUrl = "https://api.twitter.com/oauth/authorize?oauth_token=" + oauth["token"];
// here, should use a WebBrowser control.
System.Diagnostics.Process.Start(authzUrl);  // example only!
// instruct the user to type in the PIN from that browser window
var pin = "...";
var atUrl = "https://api.twitter.com/oauth/access_token";
oauth.AcquireAccessToken(atUrl, "POST", pin);


// now, update twitter status using that access token
var appUrl = "http://api.twitter.com/1/statuses/update.xml?status=Hello";
var authzHeader = oauth.GenerateAuthzHeader(appUrl, "POST");
var request = (HttpWebRequest)WebRequest.Create(appUrl);
request.Method = "POST";
request.PreAuthenticate = true;
request.AllowWriteStreamBuffering = true;
request.Headers.Add("Authorization", authzHeader);


using (var response = (HttpWebResponse)request.GetResponse())
{
if (response.StatusCode != HttpStatusCode.OK)
MessageBox.Show("There's been a problem trying to tweet:" +
Environment.NewLine +
response.StatusDescription);
}

OAuth 1.0a is sort of complicated under the covers, but using it doesn't need to be. The OAuth.Manager handles the generation of outgoing oauth requests, and the receiving and processing of oauth content in the responses. When the Request_token request gives you an oauth_token, your app doesn't need to store it. The Oauth.Manager is smart enough to do that automatically. Likewise when the access_token request gets back an access token and secret, you don't need to explicitly store those. The OAuth.Manager handles that state for you.

In subsequent runs, when you already have the access token and secret, you can instantiate the OAuth.Manager like this:

var oauth = new OAuth.Manager();
oauth["consumer_key"] = CONSUMER_KEY;
oauth["consumer_secret"] = CONSUMER_SECRET;
oauth["token"] = your_stored_access_token;
oauth["token_secret"] = your_stored_access_secret;

...and then generate authorization headers as above.

// now, update twitter status using that access token
var appUrl = "http://api.twitter.com/1/statuses/update.xml?status=Hello";
var authzHeader = oauth.GenerateAuthzHeader(appUrl, "POST");
var request = (HttpWebRequest)WebRequest.Create(appUrl);
request.Method = "POST";
request.PreAuthenticate = true;
request.AllowWriteStreamBuffering = true;
request.Headers.Add("Authorization", authzHeader);


using (var response = (HttpWebResponse)request.GetResponse())
{
if (response.StatusCode != HttpStatusCode.OK)
MessageBox.Show("There's been a problem trying to tweet:" +
Environment.NewLine +
response.StatusDescription);
}

You can download a DLL containing the OAuth.Manager class here. There is also a helpfile in that download. Or you can view the helpfile online.

See an example of a Windows Form that uses this manager here.


WORKING EXAMPLE

Download a working example of a command-line tool that uses the class and technique described here: