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))))
(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)
)))))
&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/")
nil "GET"
'google "https://mail.google.com/")
;; Reddit homepage
(t-api-oauth2-request "https://oauth.reddit.com/best"
nil "GET"
'reddit "read vote submit")
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
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")
nil "GET"
"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")
'("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))
@@ -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
Post a Comment