10

Progress is being made rapidly on StackMode, an Emacs client for StackExchange, and now we need to be able to make authenticated requests to the API for continued testing. (The 300-request limit is starting to limit how much testing I can do in a day.)

Disclaimer: I know very little about web development; it's one of the areas I'm working on professionally. Please excuse me if I misuse any terms and feel free to correct me in the comments. Thanks!

The StackExchange API uses OAuth 2.0 authentication. Since this is a local client application with client authorization. I have the following pieces of information provided to me by StackExchange:

  • Client ID
  • Client Secret (mustn't share, so it shouldn't be necessary in this flow)
  • Key
  • Description (not OAuth related)
  • OAuth Domain
  • Application Website (not OAuth related)
  • Application Icon (not OAuth related)
  • Stack Apps Post (not OAuth related)

with the following extra pieces of information:

  • Client Side Flow Is Enabled
  • Desktop OAuth Redirect Uri Is Enabled

In order to keep any answer both general and explicit, you can use my-client-id (etc.) for values. Actual values—those I think I'm OK to share, are available on GitHub.


I've been researching this for half the day, but I'm not very much closer to a solution than when I started. The closest I've gotten is this little snippet of code:

(require 'oauth2) ; available via GNU ELPA
(defconst stack-auth-token
  (make-oauth2-token
   :client-id stack-auth--client-id
   :client-secret stack-auth--key))

;; this doesn't use the above, but it does open an auth page on SE
(oauth2-auth-and-store
 "https://stackexchange.com/oauth/dialog"
 nil nil
 stack-auth--client-id
 stack-auth--key
 "https://stackexchange.com/oauth/login_success")

The only things I have to offer an OAuth2 request (from above) are apparently

  • Client ID
  • Key
  • OAuth Domain

How can I implement this flow in Elisp?


Current 'Flow'

  1. Execute oauth2-auth-and-store with proper variables set.
  2. Opens

    auth

  3. Click "Approve"
  4. Opens

    page

    with this URL

    url

  5. The application is successfully added

    added

  6. But I have no code to provide oauth2

    prompt

In addition to answers, PRs are also welcome, of course.

Community
  • 1
  • 1
Sean Allred
  • 3,558
  • 3
  • 32
  • 71

2 Answers2

3

Here's a quick example. In short, this will open the auth url in the client browser, ask the user to allow the app, and then redirect to the /oauth/login_success url as described in the docs (implicit auth).

This code prompts the user to paste the login_success URL complete, then parses and saves the access_token which can then be used for subsequent calls to the api. Two interactive function are defined: so-authenticate which performs the auth steps described above, and so-read-inbox which fetches the api data for the authenticated users inbox and dumps it to the messages buffer.


Warning, this example has no error handling!

At the very least you'll want to add checks for authentication failure, api request failures and token expiration. You can see an example api error by attempting to call so-read-inbox before calling so-authenticate.


To run, paste the following into a buffer, set the so--client-id and so--client-key variables then M-x eval-buffer.

You can then use M-x so-authenticate to authenticate and M-x so-read-inbox to dump the inbox response.

(require 'json)

(defvar so--client-id "")  ; SET THIS
(defvar so--client-key "") ; AND THIS

(defvar so--auth-url "https://stackexchange.com/oauth/dialog?")
(defvar so--redirect-url "https://stackexchange.com/oauth/login_success")
(defvar so--api-inbox-url "https://api.stackexchange.com/inbox?")

(defvar so--current-token nil) ; this will get set after authentication

(defun so-authenticate ()
  (interactive)
  (so--open-auth))

(defun so-read-inbox()
  (interactive)
  (so--retrieve-inbox))

;; Open auth url in browser and call so--get-save-token.
(defun so--open-auth ()
  (let ((auth-url
     (concat so--auth-url (url-build-query-string
               `((client_id ,so--client-id)
                 (scope "read_inbox")
                 (redirect_uri ,so--redirect-url))))))
(browse-url auth-url))
  (so--get-save-token))

;; Prompt user for callback URL, extract token and save in so--current-token
(defun so--get-save-token ()
  (let* ((post-auth-url-string (read-string "Enter URL from your browser: "))
     (token (nth 2 (split-string post-auth-url-string "[[#=&]"))))
(setq so--current-token token)
(message "Saved token: %S" token)))

;; Make a request for our inbox data
(defun so--retrieve-inbox()
  (let ((inbox-url (concat so--api-inbox-url
               (url-build-query-string
            `((access_token ,so--current-token) ; the token from auth
              (key ,so--client-key))))))        ; your client key
(url-retrieve inbox-url 'so--retrieve-inbox-cb)))

;; Parse json response for inbox request.
;; This simply dumps the parsed data to your messages buffer.
(defun so--retrieve-inbox-cb (status)
  (goto-char (point-min))
  (re-search-forward "^$")
  (let ((inbox-data (json-read)))
(message "inbox data: %S" inbox-data)))

Now have fun parsing the response! :)

Carl Groner
  • 4,149
  • 1
  • 19
  • 20
2

I'll try to answer as much of this as I can. I know absolutely nothing about Lisp, but I am very familiar with the Stack Exchange API and authorization flows.

"The 300-request limit is starting to limit how much testing I can do in a day."

You can upgrade this limit to 10,000 queries/day by appending your API key to the query string of method URLs (&key=...).

"Actual values—those I think I'm OK to share, are available on GitHub."

Yup, you're safe to share those since any application shipping with those values can easily be reverse-engineered or decompiled to extract the values anyway.

"4. Opens [...] page [...] with this URL"

That is intended behavior. In your screenshot, authorization was successful and the URL's hash contains the access token. You will need this token to access certain methods, such as /inbox.

What you probably want to do looks something like this:

  1. Continue as you have been doing until you reach the end of step #4 in your example.
  2. Prompt the user in Emacs for the URL currently displayed. They will copy and paste it as-is.
  3. Extract the hash (everything after the rightmost '#') and parse it as you would a query string. The access_token parameter contains the value you need.
  4. Use the access_token and key (the API key) parameters whenever invoking protected methods.
Nathan Osman
  • 71,149
  • 71
  • 256
  • 361
  • Re (3): Due to the asynchronous nature of the flow, this isn't possible. I don't believe it is possible (or reasonable, really) to extract the hash from the browser's URL using purely elisp. Additionally, extracting the `access_token` from the URL manually and entering it into the image for (6) yields an error. My only recourse is to stand up a website (possibly a page on the project's site) that extracts this information and displays it on the page as HTML (or something visible in the source) and extracting it that way. Is this the only option you see? – Sean Allred Nov 05 '14 at 04:23
  • @SeanAllred Another thought, does `oauth2-auth-and-store` do implicit auth or is it doing explicit auth? If it does explicit auth, then the prompt is probably expecting an auth _code_ instead of an access token. – Nathan Osman Nov 05 '14 at 04:25
  • Not even the source says, and I don't know the difference to be able to tell you. I do know that it is the library `twittering-mode` uses, which is (unsurprisingly) a Twitter client. A quick google search actually yields [this Twitter documentation](https://dev.twitter.com/oauth/pin-based) that sounds exactly what `oauth2` is doing. – Sean Allred Nov 05 '14 at 04:27
  • @SeanAllred Stack Exchange's API supports two OAuth 2 flows: explicit (which involves an extra step and can only be done server-side since the client secret must be included) and implicit (which involves one less step and directly provides an access token once auth succeeds). Unfortunately, Stack Exchange does not support a PIN-based flow. I'll see if I can figure out if your library provides support for the implicit flow. – Nathan Osman Nov 05 '14 at 04:38
  • Assuming that (1) [this is the library](http://elpa.gnu.org/packages/oauth2.html) you're using and (2) I can understand what the Lisp code is doing, then I'm afraid I have bad news. The library only seems to support the explicit flow. – Nathan Osman Nov 05 '14 at 04:49
  • It is indeed, and boy, that is bad news. If all else fails and it isn't possible in pure elisp, do you think the idea of standing up a website to parse the information is worth pursuing? I've no idea if JS (or similar tech) can be used to access the hash (though I suppose Google will) or if this information can be inserted in a way that would be transparent to, say, `curl`. – Sean Allred Nov 05 '14 at 04:53
  • @SeanAllred: Well, that could work. JavaScript can easily access the page hash and it would not be difficult to set up a page that simply grabbed the hash and displayed it somewhere on the page with instructions. I'm not sure how easy it is to launch the default browser with a specific URL in Lisp, but the implicit flow would be pretty easy to implement by hand. Basically (1) put the appropriate parameters in a URL (2) launch the browser with the URL (3) the user gets redirected to the page you set up once auth is complete (4) they see the access_token from the hash printed on the page... – Nathan Osman Nov 05 '14 at 04:57
  • ... (5) they enter it into Emacs and you can then use it in future requests. The hash also conveys an error if the user does not grant access to your application. – Nathan Osman Nov 05 '14 at 04:58