From e52ba22450bec9b77afdd943a03d79b2f89870da Mon Sep 17 00:00:00 2001 Message-Id: In-Reply-To: References: From: Aleksandr Vityazev Date: Thu, 25 Nov 2021 10:39:08 +0300 Subject: [PATCH 01/11] * oauth2: Update version to 0.17 * oauth2: Update version to 0.17, require Emacs version 27.1. (oauth2-request-authorization, oauth2-request-access): use cl-defun, update doc string. (oauth2-token): make read-only. (oauth2-request-access): update accordingly to changes (oauth2-auth, oauth2-compute-id, oauth2-url-append-access-token): Update doc string. (oauth2-auth-and-store): change order of args and update doc string. (oauth2--url-advice): Rename. --- oauth2.el | 244 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 145 insertions(+), 99 deletions(-) diff --git a/oauth2.el b/oauth2.el index 0099e06..1444890 100644 --- a/oauth2.el +++ b/oauth2.el @@ -1,11 +1,13 @@ ;;; oauth2.el --- OAuth 2.0 Authorization Protocol -*- lexical-binding:t -*- +;; Copyright © 2021 Aleksandr Vityazev ;; Copyright (C) 2011-2021 Free Software Foundation, Inc ;; Author: Julien Danjou -;; Version: 0.16 +;; Version: 0.17 ;; Keywords: comm -;; Package-Requires: ((cl-lib "0.5") (nadvice "0.3")) +;; Package-Requires: ((emacs "27.1")) +;; Homepage: https://git.sr.ht/~akagi/oauth2 ;; This file is part of GNU Emacs. @@ -36,11 +38,19 @@ ;;; Code: -(eval-when-compile (require 'cl-lib)) +(eval-when-compile + (require 'cl-lib) + (require 'subr-x)) + (require 'plstore) (require 'json) (require 'url-http) +(defcustom oauth2-token-file (concat user-emacs-directory "oauth2.plstore") + "File path where store OAuth tokens." + :type 'file + :group 'oauth2) + (defvar url-http-data) (defvar url-http-method) (defvar url-http-extra-headers) @@ -53,15 +63,22 @@ :link '(url-link :tag "Savannah" "http://git.savannah.gnu.org/cgit/emacs/elpa.git/tree/?h=externals/oauth2") :link '(url-link :tag "ELPA" "https://elpa.gnu.org/packages/oauth2.html")) -(defun oauth2-request-authorization (auth-url client-id &optional scope state redirect-uri) +(cl-defun oauth2-request-authorization (auth-url client-id &optional scope state + (redirect-uri "urn:ietf:wg:oauth:2.0:oob")) "Request OAuth authorization at AUTH-URL by launching `browse-url'. CLIENT-ID is the client id provided by the provider. -It returns the code provided by the service." +The REDIRECT-URI is the registered redirect_uri for your CLIENT-ID, +the default for the desktop applications is \"urn:ietf:wg:oauth:2.0:oob\". +SCOPE identify the resources that your application could access +on the user's behalf. STATE it is a string that your application +uses to maintain state between the request and redirect response. + +Return the code provided by the service." (browse-url (concat auth-url (if (string-match-p "\?" auth-url) "&" "?") "client_id=" (url-hexify-string client-id) "&response_type=code" - "&redirect_uri=" (url-hexify-string (or redirect-uri "urn:ietf:wg:oauth:2.0:oob")) + "&redirect_uri=" (url-hexify-string redirect-uri) (if scope (concat "&scope=" (url-hexify-string scope)) "") (if state (concat "&state=" (url-hexify-string state)) ""))) (read-string "Enter the code your browser displayed: ")) @@ -84,21 +101,27 @@ It returns the code provided by the service." data)))) (cl-defstruct oauth2-token - plstore - plstore-id - client-id - client-secret - access-token - refresh-token - token-url - access-response) - -(defun oauth2-request-access (token-url client-id client-secret code &optional redirect-uri) + (plstore "" :read-only t) + (plstore-id "" :read-only t) + (client-id "" :read-only t) + (client-secret "" :read-only t) + (access-token "" :read-only t) + (refresh-token "" :read-only t) + (token-url "" :read-only t) + (access-response "" :read-only t)) + +(cl-defun oauth2-request-access (token-url client-id client-secret code &optional + (redirect-uri "urn:ietf:wg:oauth:2.0:oob")) "Request OAuth access at TOKEN-URL. +CLIENT-ID is the client id provided by the provider. +CLIENT-SECRET is the client secret associated with your CLIENT-ID. +The REDIRECT-URI is the registered redirect_uri for your CLIENT-ID, +the default for the desktop applications is \"urn:ietf:wg:oauth:2.0:oob\". The CODE should be obtained with `oauth2-request-authorization'. -Return an `oauth2-token' structure." + +Return an response from requested service." (when code - (let ((result + (let ((response (oauth2-make-access-request token-url (concat @@ -106,102 +129,125 @@ Return an `oauth2-token' structure." (when client-secret (concat "&client_secret=" client-secret)) "&code=" code - "&redirect_uri=" (url-hexify-string (or redirect-uri "urn:ietf:wg:oauth:2.0:oob")) + "&redirect_uri=" (url-hexify-string redirect-uri) "&grant_type=authorization_code")))) - (make-oauth2-token :client-id client-id - :client-secret client-secret - :access-token (cdr (assoc 'access_token result)) - :refresh-token (cdr (assoc 'refresh_token result)) - :token-url token-url - :access-response result)))) + response))) ;;;###autoload (defun oauth2-refresh-access (token) "Refresh OAuth access TOKEN. TOKEN should be obtained with `oauth2-request-access'." - (setf (oauth2-token-access-token token) - (cdr (assoc 'access_token - (oauth2-make-access-request - (oauth2-token-token-url token) - (concat "client_id=" (oauth2-token-client-id token) - (when (oauth2-token-client-secret token) - (concat "&client_secret=" (oauth2-token-client-secret token))) - "&refresh_token=" (oauth2-token-refresh-token token) - "&grant_type=refresh_token"))))) - ;; If the token has a plstore, update it - (let ((plstore (oauth2-token-plstore token))) - (when plstore - (plstore-put plstore (oauth2-token-plstore-id token) - nil `(:access-token - ,(oauth2-token-access-token token) - :refresh-token - ,(oauth2-token-refresh-token token) - :access-response - ,(oauth2-token-access-response token) - )) - (plstore-save plstore))) - token) + (let ((plstore (oauth2-token-plstore token)) + (plstore-id (oauth2-token-plstore-id token)) + (client-id (oauth2-token-client-id token)) + (client-secret (oauth2-token-client-secret token)) + (token-url (oauth2-token-token-url token))) + (let* ((response (oauth2-make-access-request + (oauth2-token-token-url token) + (concat "client_id=" client-id + (when client-secret + (concat "&client_secret=" client-secret)) + "&refresh_token=" (oauth2-token-refresh-token token) + "&grant_type=refresh_token"))) + (new-token (make-oauth2-token :plstore plstore + :plstore-id plstore-id + :client-id client-id + :client-secret client-secret + :access-token (cdr (assoc 'access_token response)) + :refresh-token (cdr (assoc 'refresh_token response)) + :token-url token-url + :access-response response))) + (when plstore + (plstore-put plstore plstore-id + nil `(:access-token + ,(oauth2-token-access-token new-token) + :refresh-token + ,(oauth2-token-refresh-token new-token) + :access-response + ,(oauth2-token-access-response new-token))) + (plstore-save plstore)) + new-token))) ;;;###autoload (defun oauth2-auth (auth-url token-url client-id client-secret &optional scope state redirect-uri) - "Authenticate application via OAuth2." - (oauth2-request-access - token-url - client-id - client-secret - (oauth2-request-authorization - auth-url client-id scope state redirect-uri) - redirect-uri)) + "Authenticate application via OAuth2 at AUTH-URL and TOKEN-URL. +CLIENT-ID is the client id provided by the provider. +CLIENT-SECRET is the client secret associated with your CLIENT-ID. +The REDIRECT-URI is the registered redirect_uri for your CLIENT-ID. +SCOPE identify the resources that your application could access +on the user's behalf. STATE it is a string that your application +uses to maintain state between the request and redirect response. -(defcustom oauth2-token-file (concat user-emacs-directory "oauth2.plstore") - "File path where store OAuth tokens." - :group 'oauth2 - :type 'file) +Return an `oauth2-token' structure." + + (let ((plstore (plstore-open oauth2-token-file)) + (id (oauth2-compute-id auth-url token-url scope)) + (response (oauth2-request-access + token-url + client-id + client-secret + (oauth2-request-authorization + auth-url client-id scope state redirect-uri) + redirect-uri))) + (make-oauth2-token :plstore plstore + :plstore-id id + :client-id client-id + :client-secret client-secret + :access-token (cdr (assoc 'access_token response)) + :refresh-token (cdr (assoc 'refresh_token response)) + :token-url token-url + :access-response response))) (defun oauth2-compute-id (auth-url token-url scope) - "Compute an unique id based on URLs. + "Compute an unique id based on AUTH-URL, TOKEN-URL and SCOPE. This allows to store the token in an unique way." (secure-hash 'md5 (concat auth-url token-url scope))) ;;;###autoload -(defun oauth2-auth-and-store (auth-url token-url scope client-id client-secret &optional redirect-uri state) - "Request access to a resource and store it using `plstore'." - ;; We store a MD5 sum of all URL - (let* ((plstore (plstore-open oauth2-token-file)) - (id (oauth2-compute-id auth-url token-url scope)) - (plist (cdr (plstore-get plstore id)))) - ;; Check if we found something matching this access - (if plist - ;; We did, return the token object - (make-oauth2-token :plstore plstore - :plstore-id id - :client-id client-id - :client-secret client-secret - :access-token (plist-get plist :access-token) - :refresh-token (plist-get plist :refresh-token) - :token-url token-url - :access-response (plist-get plist :access-response)) - (let ((token (oauth2-auth auth-url token-url - client-id client-secret scope state redirect-uri))) - ;; Set the plstore - (setf (oauth2-token-plstore token) plstore) - (setf (oauth2-token-plstore-id token) id) - (plstore-put plstore id nil `(:access-token - ,(oauth2-token-access-token token) - :refresh-token - ,(oauth2-token-refresh-token token) - :access-response - ,(oauth2-token-access-response token))) - (plstore-save plstore) - token)))) +(defun oauth2-auth-and-store (auth-url token-url client-id client-secret scope &optional state redirect-uri) + "Request access to a resource and store it using `plstore'. +If the token has not yet been saved in plsote, then authenticate +the application via OAuth2 using the AUTH-URL, TOKEN-URL and after that +save the token in the `oauth2-token-file'. +CLIENT-ID is the client id provided by the provider. +CLIENT-SECRET is the client secret associated with your CLIENT-ID. +The REDIRECT-URI is the registered redirect_uri for your CLIENT-ID. +SCOPE identify the resources that your application could access +on the user's behalf. STATE it is a string that your application +uses to maintain state between the request and redirect response. + +Return an `oauth2-token' structure." + (if-let ((plstore (plstore-open oauth2-token-file)) + (id (oauth2-compute-id auth-url token-url scope)) + (plist (cdr (plstore-get plstore id)))) + ;; Check if we found something matching this access + ;; We did, return the token object + (make-oauth2-token :plstore plstore + :plstore-id id + :client-id client-id + :client-secret client-secret + :access-token (plist-get plist :access-token) + :refresh-token (plist-get plist :refresh-token) + :token-url token-url + :access-response (plist-get plist :access-response)) + (let ((token (oauth2-auth auth-url token-url + client-id client-secret scope state redirect-uri))) + (plstore-put plstore id nil `(:access-token + ,(oauth2-token-access-token token) + :refresh-token + ,(oauth2-token-refresh-token token) + :access-response + ,(oauth2-token-access-response token))) + (plstore-save plstore) + token))) (defun oauth2-url-append-access-token (token url) - "Append access token to URL." + "Append access TOKEN to URL." (concat url (if (string-match-p "\?" url) "&" "?") "access_token=" (oauth2-token-access-token token))) -(defvar oauth--url-advice nil) +(defvar oauth2--url-advice nil) (defvar oauth--token-data) (defun oauth2-authz-bearer-header (token) @@ -216,7 +262,7 @@ This allows to store the token in an unique way." ;; FIXME: We should change URL so that this can be done without an advice. (defun oauth2--url-http-handle-authentication-hack (orig-fun &rest args) - (if (not oauth--url-advice) + (if (not oauth2--url-advice) (apply orig-fun args) (let ((url-request-method url-http-method) (url-request-data url-http-data) @@ -227,8 +273,8 @@ This allows to store the token in an unique way." url-callback-function url-callback-arguments) ;; This is to make `url' think it's done. - (when (boundp 'success) (setq success t)) ;For URL library in Emacs<24.4. - t))) ;For URL library in Emacs≥24.4. + t))) + (advice-add 'url-http-handle-authentication :around #'oauth2--url-http-handle-authentication-hack) @@ -237,7 +283,7 @@ This allows to store the token in an unique way." "Retrieve an URL synchronously using TOKEN to access it. TOKEN can be obtained with `oauth2-auth'." (let* ((oauth--token-data (cons token url))) - (let ((oauth--url-advice t) ;Activate our advice. + (let ((oauth2--url-advice t) ;Activate our advice. (url-request-method request-method) (url-request-data request-data) (url-request-extra-headers @@ -246,14 +292,14 @@ TOKEN can be obtained with `oauth2-auth'." ;;;###autoload (defun oauth2-url-retrieve (token url callback &optional - cbargs - request-method request-data request-extra-headers) + cbargs + request-method request-data request-extra-headers) "Retrieve an URL asynchronously using TOKEN to access it. TOKEN can be obtained with `oauth2-auth'. CALLBACK gets called with CBARGS when finished. See `url-retrieve'." ;; TODO add support for SILENT and INHIBIT-COOKIES. How to handle this in `url-http-handle-authentication'. (let* ((oauth--token-data (cons token url))) - (let ((oauth--url-advice t) ;Activate our advice. + (let ((oauth2--url-advice t) ;Activate our advice. (url-request-method request-method) (url-request-data request-data) (url-request-extra-headers -- 2.34.0