Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SE's OAuth workflow in Emacs

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.

like image 943
Sean Allred Avatar asked Nov 03 '14 00:11

Sean Allred


2 Answers

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! :)

like image 181
Carl Groner Avatar answered Nov 20 '22 16:11

Carl Groner


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.
like image 35
Nathan Osman Avatar answered Nov 20 '22 14:11

Nathan Osman