Oauth2 sample flow in Elisp

Here's a modified version of oauth2.el . This Elisp code works for oauth2 flow using Twitter, Gmail, Reddit and Tumblr. To add more services, add the provider URLs to the variable oauth2-service-conf.

To make API calls, set the service configuration values (client-id, secret-key and  redirect-url) in the variable t-service-conf. Then use the examples in the usage section.

(require 'oauth2) 

(setq t-token nil)
(setq t-service-conf
  '((service . (client-id secret-key redirect-url))))
 
(defun t-api-oauth2-request (url params method service auth-scope
                       &optional headers)
  (let* ((oauth2-conf (assoc service oauth2-service-conf))
     (conf (assoc service t-service-conf))
     (t-consumer-key (nth 1 conf))
     (t-secret-key (nth 2 conf))
     (t-callback-url (nth 3 conf))
     (data nil))
    (unless (assoc service t-token)
      (push (cons service
          (oauth2-auth (nth 1 oauth2-conf)
                   (nth 2 oauth2-conf)
                   t-consumer-key t-secret-key
                   auth-scope "nil" t-callback-url)
          )
        t-token))
    (setq data (if headers
           ;; Use content type as is e.g. JSON
           params
         (mapconcat
          #'(lambda (x)
              (and (cdr x)
               (concat "&" (url-hexify-string (format "%s" (car x)))
                   "=" (url-hexify-string (format "%s" (cdr x))))))
          params "")))
    (if (string= method "GET")
    (setq url (concat url "?" data)))
    (with-current-buffer
    (oauth2-url-retrieve-synchronously
     (cdr (assoc service t-token))
     url method data headers)
      (t-api-process-response))))

(defun t-api-process-response (&optional service)
  "Process Tumblr's response in the current buffer,
returning JSON or signaling an error for other requests."
  (decode-coding-region (point-min) (point-max) 'utf-8-dos)
  ;; the following copied from url.el
  (goto-char (point-min))
  (skip-chars-forward " \t\n")        ; Skip any blank crap
  (skip-chars-forward "HTTP/")        ; Skip HTTP Version
  (skip-chars-forward "[0-9].")
  (let ((pointpos (point))
        (code (read (current-buffer))))
    (cond
     ((= code 100) ;; Gotta clean up the buffer and try again
      (search-forward-regexp "^$" nil t)
      (delete-region (point-min) (point))
      (t-api-process-response service))
     ((not (and (<= 200 code) (<= code 299)))
      (if (= code 401)
      (delq (assoc service t-token) t-token))
      (error (buffer-substring pointpos
                               (line-end-position))))
     (t
      (search-forward-regexp "^$" nil t)
      ;; body
      (let* ((json-response (buffer-substring (1+ (point)) (point-max)))
             (json-object-type 'plist)
             (json-array-type 'list)
             (json-false nil))
    ;; Uncomment for builtin JSON parser
        ;; (json-read-from-string json-response)
    ;; Use with libjansson
    (json-parse-string json-response)
    )))))
 

In case you want to permanently store tokens in an encrypted file, use (oauth2-auth-and-store) instead of (oauth2-auth).

Usage

;; Get list of messages from GMail 
(t-api-oauth2-request "https://gmail.googleapis.com/gmail/v1/users/me/messages"
                nil "GET"
                'google "https://mail.google.com/")
 
;; Reddit homepage
(t-api-oauth2-request "https://oauth.reddit.com/best"
              nil "GET"
              'reddit "read vote submit")

;; Tumblr blog info
(t-api-oauth2-request "https://api.tumblr.com/v2/blog/david/info"
              nil "GET"
              'tumblr "write offline_access")

;; Twitter logged in user profile
(t-api-oauth2-request "https://api.twitter.com/2/users/me"
              nil "GET"
              'twitter
              "tweet.read users.read tweet.write like.write offline.access")
 
;; Instagram user profile
(t-api-oauth2-request "https://graph.instagram.com/me"
              '("fields" "id,username") "GET"
              'instagram "user_profile,user_media")
 

 

REST API Testing

Oauth2 access token has to be refreshed after periodic intervals. If you apply the following patch, url API in general will refresh the token automatically if you set appropriate oauth2-token and oauth2-service. This means you can also use eww to test GET APIs. You can use sample function (oauth2-login-test) with appropriate changes for generating the token.

modified   lisp/url/url-http.el
@@ -446,6 +446,7 @@ url-http-handle-authentication
             (mail-fetch-field
              (if proxy "proxy-authenticate" "www-authenticate")
              nil nil t))
+                   (mapcar #'car url-registered-auth-schemes)
           '("basic")))
     (type nil)
     (url (url-recreate-url url-current-object))
 
 
 
While generating a URL request, Emacs tries to generate (calls (url-get-authentication)) an authentication header by all the registered providers; then it picks up the top one based on weight. A provider is registered using (url-register-auth-scheme) .e.g.
(url-register-auth-scheme "oauth2" nil 8)
 
If the authorization fails (401 response is received), (url-get-authentication) is called again with args. Default just calls basic authentication. The patch forces it to retry the initial authentication mechanism based on weight.
 

Code

https://gitlab.com/atamariya/emacs/-/blob/dev/lisp/net/oauth2.el (commit: 5d54ed16363d27b14f533b6e2755ccacc46b8038)
 

Key points

  • Twitter implements code_verifier in Oauth2 specs. You might want to upgrade the algorithm to SHA256.
  • Reddit needs authorization header in token-request. Twitter needs this only for "confidential client".
  • Strip trailing _ & # from code value, if any.
  • Oauth (v1.0) needs  both oauth_token and oauth_verifier.
  • If you want to store and reuse tokens across sessions, you might have to look for "offline access" scope.
  • If you get "Too many requests" error, try changing the User-Agent header via oauth2-user-agent-string variable.
  • Twitter refresh_token can only be used once.
  • Out-of-band or OOB not supported typically means you need to provide a proper URL as redirect URL instead of previous default oob. 
  • If you need a public redirect URL, you can use https://www.example.com

 

Comments

Popular posts from this blog

GNU Emacs as a Comic Book Reader

Data Visualization with GNU Emacs

Mozilla Readability in GNU Emacs