[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/url-http-oauth 0bf4a7633d 08/24: Complete confidential
From: |
Thomas Fitzsimmons |
Subject: |
[elpa] externals/url-http-oauth 0bf4a7633d 08/24: Complete confidential client support |
Date: |
Mon, 8 May 2023 21:10:45 -0400 (EDT) |
branch: externals/url-http-oauth
commit 0bf4a7633d6ca1958323500c00fdd82efe4f11ff
Author: Thomas Fitzsimmons <fitzsim@fitzsim.org>
Commit: Thomas Fitzsimmons <fitzsim@fitzsim.org>
Complete confidential client support
* url-http-oauth.el: Remove mml-url requirement. Remove
development comments.
(url-http-oauth-configuration): Allow user and scope in URL
itself.
(url-http-oauth-register-resource): Remove scope argument. Rename
some variables.
(url-http-oauth-get-access-token-grant): Rename. Implement
auth-source saving.
(url-http-oauth-expiry-string): New function.
(url-http-oauth-extract-authorization-code): Reimplement using
url-parse-query-string.
(url-http-oauth-get-bearer): Do not cache auth-source queries.
(url-oauth-auth): Remove a debug message.
---
url-http-oauth.el | 233 ++++++++++++++++++++++++++++--------------------------
1 file changed, 122 insertions(+), 111 deletions(-)
diff --git a/url-http-oauth.el b/url-http-oauth.el
index 382d26b35f..4852bab2f3 100644
--- a/url-http-oauth.el
+++ b/url-http-oauth.el
@@ -31,11 +31,7 @@
(require 'url-auth)
(require 'url-http)
(require 'url-util)
-(require 'mm-url)
-;; For evaluation during development:
-;; (setq url-http-oauth--registered-oauth-urls nil)
-;; (message "%S" url-http-oauth--registered-oauth-urls)
(defvar url-http-oauth--registered-oauth-urls nil
"A hash table mapping URL strings to lists of OAuth 2.0 configuration.")
@@ -50,18 +46,25 @@
(defun url-http-oauth-configuration (url)
"Return a configuration list if URL needs OAuth 2.0, nil otherwise.
URL is either a URL object or a URL string."
- (let ((key (url-http-oauth-url-string url)))
- (gethash key url-http-oauth--registered-oauth-urls)))
+ (when url-http-oauth--registered-oauth-urls
+ (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--registered-oauth-urls))))
;; Maybe if RFC 8414, "OAuth 2.0 Authorization Server Metadata",
;; catches on, authorization-url and access-token-url can be made
-;; optional, and their values retrieved automatically. But from what
-;; I can tell RFC 8414 is not consistently implemented yet.
+;; optional and their values retrieved automatically. As of early
+;; 2023, RFC 8414 is not consistently implemented yet.
(defun url-http-oauth-register-resource (url
authorization-url
access-token-url
client-identifier
- scope
&optional
client-secret-required)
"Tell Emacs that to access URL, it needs to use OAuth 2.0.
@@ -69,19 +72,17 @@ 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 URL objects or
-URL 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.
-CLIENT-SECRET-REQUIRED is the symbol `prompt' if a client secret
-is required, nil otherwise."
+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. CLIENT-SECRET-REQUIRED is the
+symbol `prompt' if a client secret is required, nil otherwise."
(unless url-http-oauth--registered-oauth-urls
(setq url-http-oauth--registered-oauth-urls
(make-hash-table :test #'equal)))
(let ((key (url-http-oauth-url-string url))
(authorization (url-http-oauth-url-string authorization-url))
- (access-token (url-http-oauth-url-string access-token-url)))
- (puthash key (list authorization access-token client-identifier scope
+ (access-token-object (url-http-oauth-url-object access-token-url)))
+ (puthash key (list authorization access-token-object client-identifier
(cond
((eq client-secret-required 'prompt) 'prompt)
((eq client-secret-required nil) nil)
@@ -89,148 +90,158 @@ is required, nil otherwise."
"Unrecognized client-secret-required value"))))
url-http-oauth--registered-oauth-urls)))
-;; (car (auth-source-search :host "https://meta.sr.ht/oauth2/access-token";
:login "107ba4a9-2a96-4420-8818-84ec1f112405" :max 1))
+(defun url-http-oauth-unregister-resource (url)
+ "Tell Emacs not to use OAuth 2.0 when accessing URL.
+URL is either an objects or a string."
+ (when url-http-oauth--registered-oauth-urls
+ (remhash (url-http-oauth-url-string url)
+ url-http-oauth--registered-oauth-urls)))
(defvar url-http-response-status)
+(defvar auth-source-creation-prompts)
+
+(defun url-http-oauth-port (url)
+ "Return port of URL object.
+Assume an HTTPS URL that does not specify a port uses 443."
+ (or (url-port url) (when (string= "https" (url-type url)) 443)))
-(defun url-http-oauth-get-access-token (url code)
+(defun url-http-oauth-get-access-token-grant (url code)
"Get an access token for URL using CODE."
(let* ((url-request-method "POST")
- (key-url (url-http-oauth-url-string url))
- (url-list (url-http-oauth-configuration key-url))
- (access-token-url (nth 1 url-list))
+ (url-list (url-http-oauth-configuration url))
+ (access-token-object (nth 1 url-list))
(client-identifier (nth 2 url-list))
- (client-secret-required (nth 4 url-list))
- (client-secret-current (when client-secret-required
- (auth-info-password
- (car (auth-source-search
- :host access-token-url
- ;; FIXME: Why doesn't :user
- ;; work here, but :login
- ;; does?
- :login client-identifier
- :max 1)))))
- (client-secret-read (unless client-secret-current
- (when client-secret-required
- (read-from-minibuffer
- (format "Client secret for %s at %s: "
- client-identifier key-url)))))
+ (client-secret-required (nth 3 url-list))
+ (auth-result
+ (when client-secret-required
+ (car (let ((auth-source-creation-prompts
+ '((secret . "Client secret for %u at %h")))
+ ;; Do not cache nil result.
+ (auth-source-do-cache nil))
+ (auth-source-search
+ :user client-identifier
+ :host (url-host access-token-object)
+ :port (url-http-oauth-port access-token-object)
+ :path (url-filename access-token-object)
+ :create '(path)
+ :max 1)))))
+ (client-secret (auth-info-password auth-result))
+ (save-function (plist-get auth-result :save-function))
(authorization (concat
"Basic "
(base64-encode-string
- (format "%s:%s" client-identifier
- (or client-secret-current client-secret-read
- ;; FIXME what to do if not required?
- ""))
+ (if client-secret
+ (format "%s:%s" client-identifier client-secret)
+ ;; FIXME: what to do if client-secret not required?
+ (format "%s" client-identifier))
t)))
(url-request-extra-headers
(list (cons "Content-Type" "application/x-www-form-urlencoded")
(cons "Authorization" authorization)))
(url-request-data
- (mm-url-encode-www-form-urlencoded
- (list (cons "grant_type" "authorization_code")
- (cons "code" code)))))
- (with-current-buffer (url-retrieve-synchronously access-token-url)
+ (url-build-query-string
+ (list (list "grant_type" "authorization_code")
+ (list "code" code)))))
+ (with-current-buffer (url-retrieve-synchronously access-token-object)
(if (eq 'OK (car (alist-get url-http-response-status url-http-codes)))
(progn
- (message "BUFFER-STRING: %s" (buffer-string)) ; FIXME: remove
after testing.
(goto-char (point-min))
(re-search-forward "\n\n")
(let* ((grant (json-parse-buffer))
(type (gethash "token_type" grant)))
- (message "GRANT: %S" grant) ; FIXME: remove after testing.
(unless (equal type "bearer" )
(error "Unrecognized token type %s for %s at %s" type
- client-identifier key-url))
+ client-identifier (url-http-oauth-url-string url)))
;; Success, so save client secret, if necessary.
- (when (and (not client-secret-current)
- client-secret-read)
- (let* ((auth-result (auth-source-search
- :host access-token-url
- ;; FIXME: Why does :user here get
- ;; translated to "login" in
- ;; authinfo.gpg?
- :user client-identifier
- :secret client-secret-read
- :create t))
- (save-function (plist-get (car auth-result)
- :save-function)))
- (if (functionp save-function)
- (funcall save-function)
- (warn "Saving client secret for %s at %s failed"
- client-identifier key-url))))
- ;; Return access token string.
- (gethash "access_token" grant)))
+ (when (functionp save-function)
+ (funcall save-function))
+ ;; Return grant object.
+ grant))
(error "url-http-oauth: Failed to get access token with %s"
(buffer-string))))))
+(defun url-http-oauth-expiry-string (grant)
+ "Return as a string a number representing the expiry time of GRANT.
+The time is in seconds since the epoch."
+ (format-time-string "%s" (time-add nil (gethash "expires_in" grant))))
+
(defun url-http-oauth-extract-authorization-code (url)
"Extract the value of the code parameter in URL."
- (let* ((filename (url-filename (url-generic-parse-url url)))
- (query-index (string-search "?" filename)))
- (unless query-index
- (error "Expected a URL with a query component after a `?' character"))
- (let* ((query (substring filename (1+ query-index)))
- (code
- (catch 'found
- (dolist (parameter (string-split query "&" t))
- (let ((pair (split-string parameter "=")))
- (when (equal (car pair) "code")
- (throw 'found (cadr pair))))))))
+ (let ((query (cdr (url-path-and-query (url-generic-parse-url url)))))
+ (unless query
+ (error "url-http-oauth: Expected URL with query component"))
+ (let ((code (cadr (assoc "code" (url-parse-query-string query)))))
(unless code
- (error "Could not find code in pasted URL"))
+ (error "url-http-oauth: Failed to find code in query component"))
code)))
(defun url-http-oauth-get-bearer (url)
- "Prompt the user with the authorization endpoint for URL."
- (let* ((key-url (url-http-oauth-url-string url))
+ "Prompt the user with the authorization endpoint for URL.
+URL is a parsed object."
+ (let* ((path-and-query (url-path-and-query url))
+ (path (car path-and-query))
+ (query (cdr path-and-query))
+ (scope (cadr (assoc "scope" (url-parse-query-string query))))
(bearer-current (auth-info-password
- (car (auth-source-search
- :host key-url
- :user user-login-name
- :max 1)))))
+ (car
+ (let ((auth-source-do-cache nil))
+ (auth-source-search
+ :user (url-user url)
+ :host (url-host url)
+ :port (url-http-oauth-port url)
+ :path path
+ :scope scope
+ :max 1))))))
(or bearer-current
- (let ((url-list (url-http-oauth-configuration key-url)))
+ (let ((url-list (url-http-oauth-configuration url)))
(unless url-list
- (error "%s is not registered with url-http-oauth" key-url))
+ (error "%s is not registered with url-http-oauth"
+ (url-http-oauth-url-string url)))
(let* ((response-url
(read-from-minibuffer
(format "Browse to %s and paste the redirected code URL: "
(concat (nth 0 url-list)
"?"
- (mm-url-encode-www-form-urlencoded
- (list (cons "client_id" (nth 2 url-list))
- (cons "response_type" "code")
- ;; FIXME: Add :expiry support to
- ;; auth-source?
- (cons "scope" (nth 3 url-list))))))))
- (code (url-http-oauth-extract-authorization-code
response-url)))
- (let ((bearer-got (url-http-oauth-get-access-token url code)))
+ (url-build-query-string
+ (list (list "client_id" (nth 2 url-list))
+ (list "response_type" "code")
+ (list "scope" scope)))))))
+ (code
+ (url-http-oauth-extract-authorization-code response-url)))
+ (let* ((grant (url-http-oauth-get-access-token-grant url code))
+ (bearer-retrieved (gethash "access_token" grant))
+ (auth-result (let ((auth-source-do-cache nil))
+ (auth-source-search
+ :user (url-user url)
+ :host (url-host url)
+ :port (url-http-oauth-port url)
+ :path path
+ :scope (if (string= (gethash "scope" grant)
+ scope)
+ scope
+ (error
+ (concat "url-http-oauth:"
+ " Returned scope did not"
+ " match requested
scope")))
+ :expiry (url-http-oauth-expiry-string grant)
+ :secret bearer-retrieved
+ :create '(path scope expiry)
+ :max 1)))
+ (save-function (plist-get (car auth-result)
:save-function)))
;; Success, so save bearer.
- (message "BEARER GOT: %s" bearer-got)
- (let* ((auth-result (auth-source-search
- :host key-url
- ;; FIXME: Maybe support multiple
- ;; different :user values?
- :user user-login-name
- :secret bearer-got
- :create t))
- (save-function (plist-get (car auth-result)
- :save-function)))
- (if (functionp save-function)
- (funcall save-function)
- (warn "Saving bearer for %s failed" key-url)))
- bearer-got))))))
+ (when (functionp save-function)
+ (funcall save-function))
+ bearer-retrieved))))))
;;; Public function called by `url-get-authentication'.
;;;###autoload
(defun url-oauth-auth (url &optional _prompt _overwrite _realm _args)
"Return an OAuth 2.0 HTTP authorization header.
-URL is an object representing a parsed URL."
- ;; Do nothing for now.
+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-configuration url)
(let ((bearer (url-http-oauth-get-bearer url)))
- (message "BEARER: %s" bearer)
(concat "Bearer " bearer))))
;;; Register `url-oauth-auth' HTTP authentication method.
- [elpa] externals/url-http-oauth 972011f217 04/24: Shorten header line to fit into 80 columns, (continued)
- [elpa] externals/url-http-oauth 972011f217 04/24: Shorten header line to fit into 80 columns, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth 8719575647 10/24: Add scope argument to top-level interpose function, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth 3b3f9fe53f 12/24: Support extra arguments on authorization URL, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth 8601d89d43 18/24: Complete regexp, list and token refresh design, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth ee73bb0450 19/24: Make functions private, fix some bugs, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth 7e825a8765 21/24: Allow per-provider user-agent interaction function, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth 9b2af487e3 22/24: Use relative expiry time in seconds, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth d86dbb478d 23/24: Retest against sourcehut, fix new issues, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth 4441b79a9a 13/24: Remove explicit extra argument handling, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth 085d57961a 20/24: Make message formatting consistent, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth 0bf4a7633d 08/24: Complete confidential client support,
Thomas Fitzsimmons <=
- [elpa] externals/url-http-oauth b884e725af 05/24: Get basics working for Sourcehut, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth 38157dfb3a 09/24: Reduce overloading of "register", Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth e1408ed406 02/24: Implement provider registration, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth f5b953197b 15/24: Fix auth-source-search for path and scope, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth 40c46af10c 24/24: Bump version to 0.8.0, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth e95f685771 06/24: Begin auth-source implementation, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth 1ecb40b545 17/24: Make some adjustments based on testing, Thomas Fitzsimmons, 2023/05/08
- [elpa] externals/url-http-oauth fcb5929654 16/24: Fix auth-source lookup conflicts and config cdrs, Thomas Fitzsimmons, 2023/05/08