branch: externals/url-http-oauth
commit 8601d89d438f80ae83e4532497b5239bd7ad47a2
Author: Thomas Fitzsimmons <fitz...@fitzsim.org>
Commit: Thomas Fitzsimmons <fitz...@fitzsim.org>

    Complete regexp, list and token refresh design
    
    * url-http-oauth.el (url-http-oauth--interposed): Update
    docstring.
    (url-http-oauth--interposed-regexp): New variable.
    (url-http-oauth-url-string): Update docstring.
    (url-http-oauth-url-object): Likewise.
    (url-http-oauth-url-no-query): New function.
    (url-http-oauth-settings): Change implementation to be list-based.
    (url-http-oauth-update-regexp): New function.
    (url-http-oauth-interpose): Expand docstring.  Change
    implementation to be list-based.
    (url-http-oauth-uninterpose): Likewise.
    (url-http-oauth-interposed-p): New function.
    (url-http-oauth-port): Allow URL string arguments.
    (url-http-oauth-auth-source-search): Reimplement to put entire
    non-query URL string in :host field.
    (url-http-oauth--parse-grant): New function.
    (url-http-oauth-refresh-token-string): New function.
    (url-http-oauth-url-build-refresh): Likewise.
    (url-http-oauth--netrc-delete): Likewise.
    (url-http-oauth-save-bearer): Likewise.
    (url-http-oauth-refresh-access-token-grant): Likewise.
    (url-http-oauth-retrieve-and-save-bearer): Likewise.
    (url-http-oauth-get-bearer): Reimplement using new function.
    (url-oauth-auth): Check URL argument against regexp before
    proceeding.
---
 url-http-oauth.el | 529 ++++++++++++++++++++++++++++++++++++++----------------
 1 file changed, 370 insertions(+), 159 deletions(-)

diff --git a/url-http-oauth.el b/url-http-oauth.el
index bf1bf785a6..5d141674c2 100644
--- a/url-http-oauth.el
+++ b/url-http-oauth.el
@@ -37,30 +37,60 @@
 (require 'url-util)
 (require 'json)
 
+;; FIXME: make functions private.
+
 (defvar url-http-oauth--interposed nil
-  "A hash table mapping URL strings to lists of OAuth 2.0 settings.")
+  "A list of OAuth 2.0 settings association lists.")
+
+(defvar url-http-oauth--interposed-regexp nil
+  "A regular expression matching URLs.
+If a URL matches this regular expression, `url' will use this
+`url-http-oauth' to access resources at the URL via OAuth 2.0.")
 
 (defun url-http-oauth-url-string (url)
-  "Ensure URL is a string."
+  "Return URL as a string.
+URL is string or an object."
   (if (stringp url) url (url-recreate-url url)))
 
 (defun url-http-oauth-url-object (url)
-  "Ensure URL is a parsed URL object."
+  "Return URL as a parsed URL object.
+URL is a string or an object."
   (if (stringp url) (url-generic-parse-url url) url))
 
+(defun url-http-oauth-url-no-query (url)
+  "Return an object representing URL with no query components.
+URL is a string or an object."
+  (let ((url (url-http-oauth-url-object url)))
+    (url-parse-make-urlobj
+     (url-type url)
+     nil nil
+     (url-host url)
+     (url-portspec url)
+     (car (url-path-and-query url))
+     nil nil t)))
+
 (defun url-http-oauth-settings (url)
   "Return a settings list if URL needs OAuth 2.0, nil otherwise.
-URL is either a URL object."
-  (when url-http-oauth--interposed
-    (let* ((url-no-query (url-parse-make-urlobj
-                          (url-type url)
-                          nil nil
-                          (url-host url)
-                          (url-portspec url)
-                          (car (url-path-and-query url))
-                          nil nil t))
-           (key (url-http-oauth-url-string url-no-query)))
-      (gethash key url-http-oauth--interposed))))
+URL is an object or a string."
+  (let* ((url (url-http-oauth-url-string url)))
+    (catch 'found
+      (dolist (settings url-http-oauth--interposed)
+        (when (or (string-prefix-p (cdr (assoc "resource-url" settings)) url)
+                  (catch 'match
+                    (dolist (prefix (cdr (assoc "resource-url-prefixes"
+                                                settings)))
+                      (when (string-prefix-p prefix url)
+                        (throw 'match t)))))
+          (throw 'found settings))))))
+
+(defun url-http-oauth-update-regexp ()
+  "Update `url-http-oauth--interposed-regexp'."
+  (let (all-urls)
+    (dolist (settings url-http-oauth--interposed)
+      (push (cdr (assoc "resource-url" settings)) all-urls)
+      (dolist (prefix (cdr (assoc "resource-url-prefixes" settings)))
+        (push prefix all-urls)))
+    (setq url-http-oauth--interposed-regexp (regexp-opt all-urls))))
 
 ;; Maybe if RFC 8414, "OAuth 2.0 Authorization Server Metadata",
 ;; catches on, authorization-url and access-token-url can be made
@@ -68,75 +98,79 @@ URL is either a URL object."
 ;; 2023, RFC 8414 is not consistently implemented yet.
 (defun url-http-oauth-interpose (url-settings)
   "Arrange for Emacs to use OAuth 2.0 to access a URL using URL-SETTINGS.
-URL-SETTINGS is an alist with fields whose descriptions follow.
-URL will be accessed by Emacs's `url' library with a suitable
-\"Authorization\" header containing \"Bearer <token>\".
-AUTHORIZATION-URL and ACCESS-TOKEN-URL will be used to acquire
-<token> and save it to the user's `auth-source' file.  URL,
-AUTHORIZATION-URL and ACCESS-TOKEN-URL are either objects or
-strings.  CLIENT-IDENTIFIER is a string identifying an Emacs
-library or mode to the server.  SCOPE is a string defining the
--permissions that the Emacs library or mode is requesting.
+URL-SETTINGS is an association list (alist) with fields whose
+descriptions follow.  URL will be accessed by Emacs's `url'
+library with a suitable \"Authorization\" header containing
+\"Bearer <token>\".
+
+RESOURCE-URL is a string representing the main URL at which
+resources will be accessed.  RESOURCE-URL-PREFIXES is a list of
+strings.  The same bearer token that is used to access resources
+at RESOURCE-URL will be used for URLs that match a prefix string
+in RESOURCE-URL-PREFIXES.
+
+AUTHORIZATION-ENDPOINT and ACCESS-TOKEN-ENDPOINT are strings
+representing URLs that will be used to acquire <token>.
+Retrieved tokens will be saved it to the user's `auth-sources'
+file.
+
+CLIENT-IDENTIFIER is a string identifying an Emacs library or
+mode to the server.  SCOPE is a string defining the -permissions
+that the Emacs library or mode is requesting.
+
 CLIENT-SECRET-METHOD is the symbol `prompt' if a client secret is
-required, nil otherwise."
-  (unless url-http-oauth--interposed
-    (setq url-http-oauth--interposed (make-hash-table :test #'equal)))
-  (let* ((urls (cdr (assoc "urls" url-settings)))
-         (client-secret-method
+required, nil otherwise.  The client secret will be saved to the
+user's `auth-sources' file.
+
+SCOPE is a string, a space delimited list of requested permission
+scopes.  These scopes are not standardized, but they may be
+required or recommended by the OAuth 2.0 provider.
+
+AUTHORIZATION-EXTRA-ARGUMENTS is an alist of URL query key/value
+pairs that will be appended to the authorization URL.  Specific
+pairs in this list are not standardized but may be required or
+recommended by the OAuth 2.0 provider.  Examples of string types
+include RESOURCE, RESPONSE_MODE, LOGIN_HINT, PROMPT and
+REDIRECT_URI."
+  (let* ((client-secret-method
           (cdr (assoc "client-secret-method" url-settings))))
     (unless (or (eq client-secret-method 'prompt) (eq client-secret-method 
nil))
       (error "Unrecognized client-secret-method value"))
-    (dolist (url urls)
-      (puthash (url-http-oauth-url-string url) url-settings
-               url-http-oauth--interposed))))
+    (prog1
+        (add-to-list 'url-http-oauth--interposed url-settings)
+      (url-http-oauth-update-regexp))))
 
 (defun url-http-oauth-uninterpose (url-settings)
   "Arrange for Emacs not to use OAuth 2.0 when accessing URL in URL-SETTINGS.
 This function does the opposite of `url-http-oauth-interpose'."
-  (when url-http-oauth--interposed
-    (let* ((urls (cdr (assoc "urls" url-settings))))
-      (dolist (url urls)
-        (remhash (url-http-oauth-url-string url)
-                 url-http-oauth--interposed)))))
+  (prog1
+      (setq url-http-oauth--interposed
+            (delete url-settings url-http-oauth--interposed))
+    (url-http-oauth-update-regexp)))
+
+(defun url-http-oauth-interposed-p (url)
+  "Return non-nil if `url' will use OAuth 2.0 to access URL.
+URL is an object."
+  (string-match-p url-http-oauth--interposed-regexp
+                  (url-http-oauth-url-string url)))
 
 (defvar url-http-response-status)
 (defvar auth-source-creation-prompts)
+
 ;; FIXME: if anything goes wrong during the authentication steps,
 ;; `url-http-end-of-document-sentinel' calls back into
 ;; `url-oauth-auth' somehow.  Maybe `url-http-no-retry' can help here?
 (defvar url-http-no-retry)
 
 (defun url-http-oauth-port (url)
-  "Return port of URL object.
-Assume an HTTPS URL that does not specify a port uses 443."
-  (let ((port-number (url-port url)))
+  "Return port of URL.
+Assume an HTTPS URL that does not specify a port uses 443.  URL
+is a string or an object."
+  (let ((port-number (url-port (url-http-oauth-url-object url))))
     (if port-number
         (number-to-string port-number)
       (when (string= "https" (url-type url)) "443"))))
 
-(defun url-http-oauth-auth-source-search (&rest spec)
-  "Like `auth-source-search' but search for all of SPEC in all backends.
-Filter out nil spec entries prior to searching."
-  (let* ((auth-source-do-cache nil) ; do not cache nil result.
-         (all (apply #'auth-source-search :max 5001 spec)) ; hmm, no :max 'all.
-         (spec (cl-loop for i below (length spec) by 2
-                        unless (null (nth (1+ i) spec))
-                        collect (nth i spec)
-                        unless (null (nth (1+ i) spec))
-                        collect (nth (1+ i) spec)))
-         (result (cl-loop for entry in all
-                          when (auth-source-specmatchp spec entry)
-                          collect entry)))
-    (unless (or (eq (length result) 0)
-                (eq (length result) 1))
-      (warn "url-http-oauth-auth-source-search produced multiple results for 
%s"
-            spec))
-    result))
-
-(defun url-http-oauth-encode-scope (scope)
-  "Replace spaces in SCOPE with plus signs."
-  (replace-regexp-in-string " " "+" scope))
-
 ;; Backport of `auth-info-password'.
 (defun url-http-oauth-auth-info-password (auth-info)
   "Return the :secret password from the AUTH-INFO."
@@ -152,30 +186,77 @@ Filter out nil spec entries prior to searching."
     (json-read-from-string
      (buffer-substring (point) (point-max)))))
 
-(defun url-http-oauth-get-access-token-grant (url code)
-  "Get an access token for URL using CODE."
+(defun url-http-oauth-auth-source-search (url &optional user secret prompt
+                                              expiry refresh-token)
+  "Find the `auth-source' entry for USER and URL.
+Arrange for the entry to be created if it is not already saved in
+on of `auth-sources'.  URL is a string or an object.  USER is a
+string.  SECRET is a string if the password is already known and
+needs to be saved, or nil meaning to prompt for the password.  If
+SECRET is nil, PROMPT should be a string with which the user will
+be prompted to enter the password.  EXPIRY is a string
+representing the epoch-time at which SECRET becomes invalid.
+REFRESH-TOKEN is a string that can be sent to the authorization
+server to receive a new access token."
+  (let* ((auth-source-creation-prompts (when prompt `((secret . ,prompt))))
+         (create (when (or secret prompt)
+                   (if (or expiry refresh-token)
+                       `(,@(when expiry (list 'expiry))
+                         ,@(when refresh-token (list 'refresh-token)))
+                     t)))
+         (spec `(:user ,(or user "") ; "" => omit "user" field from authinfo.
+                       ;; Misuse the host field: insert the full URL.
+                       ;; This allows different authentication for
+                       ;; different URL paths on the same host.  The
+                       ;; `auth-source' netrc backend does not have
+                       ;; search support for arbitrary fields, like a
+                       ;; hypothetical :path that would be desirable
+                       ;; in this case.  Introducing support for
+                       ;; arbitrary fields would have too many forward
+                       ;; and backward compatibility implications for
+                       ;; netrc-formatted authinfo files.
+                       :host ,(url-http-oauth-url-string
+                               (url-http-oauth-url-no-query url))
+                       :port ,(url-http-oauth-port url)
+                       ,@(when secret (list :secret secret))
+                       ,@(when expiry (list :expiry expiry))
+                       ,@(when refresh-token
+                           (list :refresh-token refresh-token))
+                       ,@(when create (list :create create)))))
+    (car ; First result always wins.
+     (let ((auth-source-do-cache nil)) ; Do not cache nil result.
+       (apply #'auth-source-search spec)))))
+
+;; This monstrosity is required because the `auth-source' netrc
+;; backend does not support deletion, yet we need to refresh the
+;; bearer token.
+
+(defun url-http-oauth--parse-grant ()
+  "Parse the JSON grant structure in the current buffer.
+Return the parsed JSON object."
+  (progn
+    (goto-char (point-min))
+    (re-search-forward "\n\n")
+    (let* ((grant (url-http-oauth-json-parse-buffer))
+           (type (gethash "token_type" grant)))
+      (unless (equal (dowcase type) "bearer" )
+        (error "Unrecognized token type %s for %s" type url-settings))
+      ;; Return grant object.
+      grant)))
+
+(defun url-http-oauth-get-access-token-grant (url-settings code)
+  "Get an access token for using CODE.
+URL-SETTINGS are OAuth 2.0 settings needed by URL."
+  ;; (message "url-http-oauth-get-access-token-grant: %S, %S" url-settings 
code)
   (let* ((url-request-method "POST")
-         (url-settings (url-http-oauth-settings url))
-         (access-token-object
-          (url-http-oauth-url-object
-           (cdr (assoc "access-token-endpoint" url-settings))))
+         (access-token-url (cdr (assoc "access-token-endpoint" url-settings)))
          (client-identifier (cdr (assoc "client-identifier" url-settings)))
-         (scope (cdr (assoc "scope" url-settings)))
          (client-secret-method (cdr (assoc "client-secret-method"
                                            url-settings)))
          (auth-result
           (when client-secret-method
-            (car (let* ((auth-source-creation-prompts
-                         '((secret . "Client secret for %u at %h: ")))
-                        (spec (list :user client-identifier
-                                    :host (url-host access-token-object)
-                                    :port (url-http-oauth-port
-                                           access-token-object)
-                                    :path (url-filename access-token-object)
-                                    :scope
-                                    (url-http-oauth-encode-scope scope))))
-                   (or (apply #'url-http-oauth-auth-source-search spec)
-                       (apply #'auth-source-search :create '(path scope) 
spec))))))
+            (url-http-oauth-auth-source-search
+             url client-identifier "Client secret for %u at %h: ")))
          (client-secret (url-http-oauth-auth-info-password auth-result))
          (save-function (plist-get auth-result :save-function))
          (authorization (when client-secret
@@ -189,31 +270,24 @@ Filter out nil spec entries prior to searching."
                  (cons "Content-Type" "application/x-www-form-urlencoded")
                  (when authorization (cons "Authorization" authorization))))
          (redirect-uri
-          (cdr (assoc "redirect_uri"
-                      (cdr (assoc "authorization-extra-arguments"
-                                  url-settings)))))
+          (cdr (assoc "redirect_uri" (cdr (assoc 
"authorization-extra-arguments"
+                                                 url-settings)))))
          (url-request-data
           (url-build-query-string
            (apply #'list (list "code" code)
-                  (list "client_id" client-identifier)
-                  (list "grant_type" "authorization_code")
-                  (when redirect-uri
-                    (list (list "redirect_uri" redirect-uri)))))))
+                 (list "client_id" client-identifier)
+                 (list "grant_type" "authorization_code")
+                 (when redirect-uri
+                   (list (list "redirect_uri" redirect-uri)))))))
+    ;; (message "URL: %S\nAUTH: %S\nDAT: %S" url-settings authorization 
url-request-data)
     (with-current-buffer (url-retrieve-synchronously access-token-object)
+      ;; (message "GRANT BUFFER: %S" (buffer-string))
       (if (eq 'OK (car (alist-get url-http-response-status url-http-codes)))
-          (progn
-            (goto-char (point-min))
-            (re-search-forward "\n\n")
-            (let* ((grant (url-http-oauth-json-parse-buffer))
-                   (type (gethash "token_type" grant)))
-              (unless (equal (dowcase type) "bearer")
-                (error "Unrecognized token type %s for %s at %s" type
-                       client-identifier (url-http-oauth-url-string url)))
-              ;; Success, so save client secret, if necessary.
-              (when (functionp save-function)
-                (funcall save-function))
-              ;; Return grant object.
-              grant))
+          (prog1
+              (url-http-oauth--parse-grant)
+            ;; Success, so save client secret, if necessary.
+            (when (functionp save-function)
+              (funcall save-function))
         (error "url-http-oauth: Failed to get access token with %s"
                (buffer-string))))))
 
@@ -222,6 +296,11 @@ Filter out nil spec entries prior to searching."
 The time is in seconds since the epoch."
   (format-time-string "%s" (time-add nil (gethash "expires_in" grant))))
 
+(defun url-http-oauth-refresh-token-string (grant)
+  "Return the refresh token from GRANT.
+The refresh token is an opaque string."
+  (format-time-string "%s" (time-add nil (gethash "refresh_token" grant))))
+
 (defun url-http-oauth-extract-authorization-code (url)
   "Extract the value of the code parameter in URL."
   (let ((query (cdr (url-path-and-query (url-generic-parse-url url)))))
@@ -245,70 +324,200 @@ The time is in seconds since the epoch."
     (concat base "?" (url-build-query-string
                       (apply #'list client response-type scope extra)))))
 
+(defun url-http-oauth-url-build-refresh (url-settings)
+  "Build a refresh token URL query string from URL-SETTINGS."
+  (let* ((client-identifier (cdr (assoc "client-identifier" url-settings)))
+         (authorization-extra-arguments (cdr (assoc 
"authorization-extra-arguments" url-settings)))
+         (resource (cdr (assoc "resource" authorization-extra-arguments)))
+         (redirect-uri (cdr (assoc "redirect_uri" 
authorization-extra-arguments))))
+    (url-build-query-string
+     (apply #'list
+            (let ((resource-url
+                   (cdr (assoc "resource-url" url-settings))))
+              (list "refresh_token"
+                    (or (plist-get
+                         (url-http-oauth-auth-source-search resource-url)
+                         :refresh-token)
+                        (error "Failed to retrieve refresh token for %s"
+                               resource-url))))
+            (list "client_id" client-identifier)
+            (list "grant_type" "refresh_token")
+            (list "resource" resource)
+            (when redirect-uri
+              (list (list "redirect_uri" redirect-uri)))))))
+
+(defun url-http-oauth--netrc-delete (host &optional user port)
+  "Delete a netrc entry matching HOST, USER and PORT.
+Delete the first matching line from any `auth-source' backend.
+The entry is cleared from the `password-data' cache after the
+`auth-source' file is saved.  Respects
+`auth-source-save-behavior'."
+  (dolist (backend (mapcar #'auth-source-backend-parse auth-sources))
+    (when (eq (slot-value backend 'type) 'netrc)
+      (let* ((file (oref backend source))
+             (results (auth-source-netrc-normalize
+                       (auth-source-netrc-parse
+                        :max 1
+                        :file (oref backend source)
+                        :host (or host t)
+                        :user (or user t)
+                        :port (or port t))
+                       file)))
+        (when results
+          (with-temp-buffer
+            (when (file-exists-p file)
+              (insert-file-contents file))
+            (when auth-source-gpg-encrypt-to
+              ;; (see bug#7487) making `epa-file-encrypt-to' local to
+              ;; this buffer lets epa-file skip the key selection query
+              ;; (see the `local-variable-p' check in
+              ;; `epa-file-write-region').
+              (unless (local-variable-p 'epa-file-encrypt-to (current-buffer))
+                (make-local-variable 'epa-file-encrypt-to))
+              (if (listp auth-source-gpg-encrypt-to)
+                  (setq epa-file-encrypt-to auth-source-gpg-encrypt-to)))
+            ;; we want the new data to be found first, so insert at beginning
+            (goto-char (point-min))
+
+            ;; Ask AFTER we've successfully opened the file.
+            (let* ((allow-null t)
+                   (start-point (point-min))
+                   (prior-start-point
+                    (catch 'point
+                      (auth-source-netrc-parse-entries
+                       (lambda (alist)
+                         (let ((end-point (point)))
+                           (if (and alist
+                                    (or
+                                     (and allow-null (null host))
+                                     (auth-source-search-collection
+                                      host
+                                      (or
+                                       (auth-source--aget alist "machine")
+                                       (auth-source--aget alist "host")
+                                       t)))
+                                    (or
+                                     (and allow-null (null user))
+                                     (auth-source-search-collection
+                                      user
+                                      (or
+                                       (auth-source--aget alist "login")
+                                       (auth-source--aget alist "account")
+                                       (auth-source--aget alist "user")
+                                       t)))
+                                    (or
+                                     (and allow-null (null port))
+                                     (auth-source-search-collection
+                                      port
+                                      (or
+                                       (auth-source--aget alist "port")
+                                       (auth-source--aget alist "protocol")
+                                       t))))
+                               (throw 'point start-point)
+                             (progn
+                               (setq start-point end-point)
+                               nil))))
+                       1))))
+              (when prior-start-point
+                (message "prior start point: %s" prior-start-point)
+                (goto-char prior-start-point)
+                (auth-source-netrc-parse-next-interesting)
+                (goto-char (point-at-bol))
+                (let ((extents
+                       (if (bobp)
+                           (progn
+                             (goto-char (point-at-eol))
+                             (if (eobp)
+                                 (cons (point-at-bol) (point-at-eol))
+                               (cons (point-at-bol) (1+ (point-at-eol)))))
+                         (progn
+                           (goto-char (point-at-eol))
+                           (cons (1- (point-at-bol)) (point-at-eol))))))
+                  (let ((region-to-delete (buffer-substring (car extents)
+                                                            (cdr extents))))
+                    (when (or (not (eq auth-source-save-behavior 'ask))
+                              (y-or-n-p (format "Delete region %S and save? "
+                                                region-to-delete)))
+                      (delete-region (car extents) (cdr extents))
+                      (write-region (point-min) (point-max) file nil 'silent)
+                     ;; Make the .authinfo file non-world-readable.
+                     (set-file-modes file #o600)
+                      (auth-source-do-debug
+                       "auth-source-netrc-create: deleted region %S from %s"
+                       region-to-delete file)
+                      (auth-source-forget+ (list :host (or host t)
+                                                 :user (or user t)
+                                                 :port (or port t)))
+                      nil)))))))))))
+
+(defun url-http-oauth-save-bearer (url grant)
+  "Save bearer access token for URL from GRANT.
+URL is a string or an object.  GRANT is a parsed JSON object.
+Save the bearer token to `auth-sources' then return it."
+  (url-http-oauth--netrc-delete url)
+  (let* ((bearer-retrieved (gethash "access_token" grant))
+         (auth-result
+          (url-http-oauth-auth-source-search
+           url nil bearer-retrieved nil
+           (url-http-oauth-expiry-string grant)
+           (url-http-oauth-refresh-token-string grant)))
+         (save-function (plist-get auth-result :save-function)))
+    (when (functionp save-function)
+      (funcall save-function))
+    bearer-retrieved))
+
+;; FIXME: If a refresh token fails then maybe look for status = 401
+;; response with: WWW-Authenticate: Bearer
+;; client_id="00000000-0000-0000-0000-000000000000",
+;; trusted_issuers="00000000-0000-0000-0000-000000000000@*",
+;; token_types="app_asserted_user_v1 service_asserted_app_v1",
+(defun url-http-oauth-refresh-access-token-grant (url-settings)
+  "Refresh the access token for URL."
+;; authorization_uri=
+;; "https://login.microsoftonline.com/common/oauth2/authorize";,
+;; error="invalid_token",Basic Realm="" in which case, call refresh on
+;; URL before proceeding.
+  (let* ((url-request-method "POST")
+         (access-token-url (cdr (assoc "access-token-endpoint" url-settings)))
+         (url-request-data (url-http-oauth-url-build-refresh url-settings)))
+    ;; (message "URL: %S\nREQ: %S" url url-request-data)
+    (with-current-buffer (url-retrieve-synchronously access-token-url)
+      (if (eq 'OK (car (alist-get url-http-response-status url-http-codes)))
+          (url-http-oauth--parse-grant)
+        (error "url-http-oauth: Failed to get access token with %s"
+               (buffer-string))))))
+
+(defun url-http-oauth-retrieve-and-save-bearer (url url-settings)
+  "Retrieve the bearer token required to access resources needing URL-SETTINGS.
+Save the bearer token to `auth-sources' upon success."
+  (let* ((response-url
+          ;; FIXME: Make this a per-provider function.
+          (read-from-minibuffer
+           (format "Browse to %s and paste the redirected code URL: "
+                   (url-http-oauth-authorization-url url-settings))))
+         (code
+          (url-http-oauth-extract-authorization-code response-url))
+         (grant (url-http-oauth-get-access-token-grant url-settings code)))
+    (url-http-oauth-save-bearer url grant)))
+
 (defun url-http-oauth-get-bearer (url)
   "Prompt the user with the authorization endpoint for URL.
 URL is a parsed object."
-  (let* ((url (url-http-oauth-url-object url))
-         (url-settings (url-http-oauth-settings url))
+  (let* ((url-settings (url-http-oauth-settings url))
+         (url (url-http-oauth-url-object url))
          (path-and-query (url-path-and-query url))
-         (path (car path-and-query))
-         (scope (url-http-oauth-encode-scope (cdr (assoc "scope" 
url-settings))))
-         (bearer-current (url-http-oauth-auth-info-password
-                          (car
-                           (let ((auth-source-do-cache nil))
-                             (url-http-oauth-auth-source-search
-                              :user "BEARER"
-                              :host (url-host url)
-                              :port (url-http-oauth-port url)
-                              :path path
-                              :scope scope))))))
+         (path (car path-and-query)))
     (unless url-settings
       (error "%s is not interposed by url-http-oauth"
              (url-http-oauth-url-string url)))
-    (or bearer-current
-        (let* ((response-url
-                (read-from-minibuffer
-                 (format "Browse to %s and paste the redirected code URL: "
-                         (url-http-oauth-authorization-url url-settings))))
-               (code
-                (url-http-oauth-extract-authorization-code response-url))
-               (grant (url-http-oauth-get-access-token-grant url code))
-               (bearer-retrieved (gethash "access_token" grant))
-               (auth-result (auth-source-search
-                             :create '(path scope expiry)
-                             ;; If :user is nil, then
-                             ;; (auth-source-search :create ...) will
-                             ;; find the client-identifier username.
-                             ;; :user isn't used for bearer tokens
-                             ;; anyway, so use this dummy name to
-                             ;; differentiate the bearer token
-                             ;; authinfo line from the
-                             ;; client-identifier/client-secret
-                             ;; authinfo line.
-                             :user "BEARER"
-                             :host (url-host url)
-                             :port (url-http-oauth-port url)
-                             :path path
-                             :scope
-                             (let ((returned-scope
-                                    (gethash "scope" grant)))
-                               (if (string=
-                                    (url-http-oauth-encode-scope
-                                     returned-scope)
-                                    scope)
-                                   scope
-                                 (error
-                                  (concat "url-http-oauth:"
-                                          " Returned scope %S did not"
-                                          " match requested scope"
-                                          returned-scope))))
-                             :expiry (url-http-oauth-expiry-string grant)
-                             :secret bearer-retrieved))
-               (save-function (plist-get (car auth-result) :save-function)))
-          ;; Success; save bearer.
-          (when (functionp save-function)
-            (funcall save-function))
-          bearer-retrieved))))
+    (let ((expiry (plist-get (url-http-oauth-auth-source-search url) :expiry)))
+      (when (and expiry (> (time-to-seconds) expiry))
+        (url-http-oauth-save-bearer
+         url (url-http-oauth-refresh-access-token-grant url-settings))))
+    (let ((bearer-current (url-http-oauth-auth-info-password
+                           (url-http-oauth-auth-source-search url))))
+      (or bearer-current
+          (url-http-oauth-retrieve-and-save-bearer url-settings)))))
 
 ;;; Public function called by `url-get-authentication'.
 ;;;###autoload
@@ -317,9 +526,11 @@ URL is a parsed object."
 URL is an object representing a parsed URL.  It should specify a
 user, and contain a \"scope\" query argument representing the
 permissions that the caller is requesting."
-  (when (url-http-oauth-settings url)
+  (when (url-http-oauth-interposed-p url)
     (let ((bearer (url-http-oauth-get-bearer url)))
-      (concat "Bearer " bearer))))
+      (if bearer
+          (concat "Bearer " bearer)
+        (error "Bearer retrieval failed for %s" url)))))
 
 ;;; Register `url-oauth-auth' HTTP authentication method.
 ;;;###autoload

Reply via email to