-
Notifications
You must be signed in to change notification settings - Fork 7
/
auth-source-xoauth2.el
320 lines (277 loc) · 12.8 KB
/
auth-source-xoauth2.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
;;; auth-source-xoauth2.el --- Integrate auth-source with XOAUTH2
;; Copyright 2018 Google LLC
;;
;; Licensed under the Apache License, Version 2.0 (the "License");
;; you may not use this file except in compliance with the License.
;; You may obtain a copy of the License at
;;
;; https://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.
;; Author: Cesar Crusius <[email protected]>
;; URL: https://github.com/ccrusius/auth-source-xoauth2
;; Version: 1.0.0
;; Package-Requires: ((emacs "26.1"))
;; This file is not part of GNU Emacs.
;; This package is not an official Google product.
;;; Commentary:
;; Adds XOAuth2 authentication capabilities to auth-source.
;; Some code is basically the same as code in the external
;; `request.el' and `oauth2.el' packages.
;;; Code:
(require 'auth-source)
(require 'cl-lib)
(require 'json)
(require 'seq)
(require 'subr-x)
(require 'smtpmail)
(require 'auth-source-pass nil t)
(autoload 'nnimap-capability "nnimap")
(autoload 'nnimap-command "nnimap")
(autoload 'nnimap-login "nnimap")
(autoload 'smtpmail-command-or-throw "smtpmail")
(defvar nnimap-authenticator)
;;; Auth source interface and functions
(defvar auth-source-xoauth2-creds nil
"A property list containing values for the following XOAuth2 keys:
:token-url, :client-id, :client-secret, and :refresh-token.
If this is set to a string, it is considered the name of a file
containing one sexp that evaluates to either the property list above,
or to a hash table containing (HOST USER PORT) keys mapping to
property lists as above. Note that the hash table /must/ have its
`:test' property set to `equal'. Example:
#s(hash-table size 2 test equal
data ((\"host1.com\" \"user1\" \"port1\")
(:token-url \"token-url-1\"
:client-id \"client-id-1\"
:client-secret \"client-secret-1\"
:refresh-token \"refresh-token-1\")
(\"host2.com\" \"user2\" \"port2\")
(
:token-url \"token-url-2\"
:client-id \"client-id-2\"
:client-secret \"client-secret-2\"
:refresh-token \"refresh-token-2\")))
If this is set to a function, it will be called with HOST, USER, and
PORT values, and should return the respective property list.
This package provides a function that retrieves the values from a
password-store. See `auth-source-xoauth2-pass-creds' for details.
If you are using this to authenticate to Google, the values can be
obtained through the following procedure (note that Google changes
this procedure every now and then, so the steps may be slightly
different):
1. Go to the developer console, https://console.developers.google.com/project
2. Create a new project (if necessary), and select it once created.
3. Select \"APIs & Services\" from the navigation menu.
4. Select \"Credentials\".
5. Create new credentials of type \"OAuth Client ID\".
6. Choose application type \"Other\".
7. Choose a name for the client.
This should get you all the values but for the refresh token. For that one:
1. Install the Go development tools (from https://go.dev).
2. Clone the https://github.com/ccrusius/auth-source-xoauth2 repository.
3. Execute the following command in the cloned repository:
cd google-oauth
make
./oauth -client-id <client id from previous steps> \
-client-secret <client secret from previous steps>
4. Visit the URL the tool will print on the console. The page will ask you
for the permissions needed to access your Google acount.
5. Once you give approval, the refresh token will be printed by the tool in
the terminal. You should now have all the required values (the
:token-url value should be
\"https://accounts.google.com/o/oauth2/token\").")
(defvar auth-source-xoauth2-use-curl nil
"Whether to use cURL instead of Emacs' built-in `url-retrieve-synchronously'.
If, for whatever reason, the XOAuth2 tokens can not be retrieved using
Emacs' own `url-retrieve-synchronously', setting this variable to t
will make the package try to call cURL instead.")
(cl-defun auth-source-xoauth2-search (&rest spec
&key backend type host user port
&allow-other-keys)
"Given a property list SPEC, return search matches from the :BACKEND.
See `auth-source-search' for details on SPEC."
;; just in case, check that the type is correct (null or same as the backend)
(cl-assert (or (null type) (eq type (oref backend type)))
t "Invalid XOAuth2 search: %s %s")
(let ((hosts (if (and host (listp host)) host `(,host)))
(ports (if (and port (listp port)) port `(,port))))
(catch 'match
(dolist (host hosts)
(dolist (port ports)
(let ((match (auth-source-xoauth2--search
host user port)))
(when match
(throw 'match `(,match)))))))))
(cl-defun auth-source-xoauth2--search (host user port)
"Get the XOAuth2 authentication data for the given HOST, USER, and PORT."
(when-let ((token
(cond
((functionp auth-source-xoauth2-creds)
(funcall auth-source-xoauth2-creds host user port))
((stringp auth-source-xoauth2-creds)
(auth-source-xoauth2--file-creds
auth-source-xoauth2-creds host user port))
(t auth-source-xoauth2-creds))))
(when-let ((token-url (plist-get token :token-url))
(client-id (plist-get token :client-id))
(client-secret (plist-get token :client-secret))
(refresh-token (plist-get token :refresh-token)))
(when-let (secret (cdr (assoc 'access_token
(auth-source-xoauth2--url-post
token-url
(concat "client_id=" client-id
"&client_secret=" client-secret
"&refresh_token=" refresh-token
"&grant_type=refresh_token")))))
;; We log this secret in plain text because it is both a
;; temporary secret, and one that can not be memorized on
;; sight. Anybody that can copy this secret already has access
;; to the computer.
(auth-source-do-debug "XOAUTH2 access token (user=%s host=%s): %s"
user host secret)
(list :host host :port port :user user :secret secret)))))
(defun auth-source-xoauth2--url-post (url data)
"Post DATA to the given URL, and return the JSON-parsed reply."
(if auth-source-xoauth2-use-curl
(with-temp-buffer
(call-process "curl" nil t nil
"--silent"
"--request" "POST"
"--data" data
"--header" "Content-Type:application/x-www-form-urlencoded"
url)
(goto-char (point-min))
(json-read))
(let ((url-request-method "POST")
(url-request-data data)
(url-request-extra-headers
'(("Content-Type" . "application/x-www-form-urlencoded"))))
(with-current-buffer (url-retrieve-synchronously url)
(goto-char (point-min))
(when (search-forward-regexp "^$" nil t)
(let ((data (json-read)))
(kill-buffer (current-buffer))
data))))))
;;;###autoload
(defun auth-source-xoauth2-enable ()
"Enable auth-source-xoauth2.
This function installs hooks that allow the use of a `xoauth2' authenticator
with `nnimap' and `smtpmail'. To use this with other services, similar hooks
may have to be written to add the necessary protocol handling code to those
services. If you write such a hook, please consider sending it for inclusion
in this package."
(add-to-list 'auth-sources 'xoauth2)
;; Add functionality to nnimap-login
(advice-add #'nnimap-login :around
(lambda (fn user password)
(if (and (eq nnimap-authenticator 'xoauth2)
(nnimap-capability "AUTH=XOAUTH2")
(nnimap-capability "SASL-IR"))
(nnimap-command
(concat "AUTHENTICATE XOAUTH2 "
(base64-encode-string
(concat "user=" user "\1auth=Bearer " password "\1\1")
t)))
(funcall fn user password))))
;; Add the functionality to smtpmail-try-auth-method
(add-to-list 'smtpmail-auth-supported 'xoauth2)
(cond
((>= emacs-major-version 27)
(cl-defmethod smtpmail-try-auth-method
(process (_mech (eql xoauth2)) user password)
(auth-source-xoauth2--smtpmail-auth-method process user password)))
(t
(advice-add #'smtpmail-try-auth-method :around
(lambda (fn process mech user password)
(if (eq mech 'xoauth2)
(auth-source-xoauth2--smtpmail-auth-method process user password)
(funcall fn process mech user password)))))))
(defvar auth-source-xoauth2-backend
(auth-source-backend
(format "xoauth2")
:source "." ;; not used
:type 'xoauth2
:search-function #'auth-source-xoauth2-search)
"XOAuth2 backend for password-store.")
(defun auth-source-xoauth2-backend-parse (entry)
"Create a XOAuth2 auth-source backend from ENTRY."
(when (eq entry 'xoauth2)
(auth-source-backend-parse-parameters entry auth-source-xoauth2-backend)))
(advice-add 'auth-source-backend-parse :before-until #'auth-source-xoauth2-backend-parse)
;;; File sub-backend
(defun auth-source-xoauth2--file-creds (file host user port)
"Load FILE and evaluate it, matching entries to HOST, USER, and PORT."
(unless (string= "gpg" (file-name-extension file))
(error "The auth-source-xoauth2-creds file must be GPG encrypted"))
(when-let
((creds (condition-case err
(eval (with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(read (current-buffer)))
t)
(error
"Error parsing contents of %s: %s"
file (error-message-string err)))))
(cond
((hash-table-p creds)
(auth-source-do-debug
"Searching hash table for (%S %S %S)" host user port)
(gethash `(,host ,user ,port) creds))
(creds))))
;;; Password-store sub-backend
(cl-defun auth-source-xoauth2-pass--find-match (host user port)
"Find password for given HOST, USER, and PORT.
This is a wrapper around `auth-pass--find-match`, which is needed
because the MELPA and Emacs 26 versions of the function accept
a different number of arguments."
(condition-case nil
(auth-source-pass--find-match host user port)
(wrong-number-of-arguments
(auth-source-pass--find-match host user))))
(cl-defun auth-source-xoauth2--smtpmail-auth-method (process user password)
"Authenticate to SMTP PROCESS with USER and PASSWORD via XOAuth2."
(smtpmail-command-or-throw
process
(concat "AUTH XOAUTH2 "
(base64-encode-string
(concat "user=" user "\1auth=Bearer " password "\1\1")
t))
235))
(defun auth-source-xoauth2--pass-get (key entry)
"Retrieve KEY from password-store ENTRY.
ENTRY can be either a string specifying the password store entry name
or an association list containing the pre-parsed the entry data."
(let ((ret (cond
((stringp entry) (auth-source-pass-get key entry))
((listp entry) (cdr (assoc key entry))))))
(or ret (message "Missing XOAuth2 entry value for '%s'" key))
ret))
(defun auth-source-xoauth2-pass-creds (host user port)
"Retrieve a XOAUTH2 access token using `auth-source-pass'.
This function retrieve a password-store entry matching HOST, USER, and
PORT. This entry should contain the following key-value pairs:
xoauth2_token_url: <value>
xoauth2_client_id: <value>
xoauth2_client_secret: <value>
xoauth2_refresh_token: <value>
which are used to build and return the property list required by
`auth-source-xoauth2-creds'."
(when-let ((entry (auth-source-xoauth2-pass--find-match host user port)))
(when-let
((url (auth-source-xoauth2--pass-get "xoauth2_token_url" entry))
(id (auth-source-xoauth2--pass-get "xoauth2_client_id" entry))
(secret (auth-source-xoauth2--pass-get "xoauth2_client_secret" entry))
(token (auth-source-xoauth2--pass-get "xoauth2_refresh_token" entry)))
(list
:token-url url
:client-id id
:client-secret secret
:refresh-token token))))
(provide 'auth-source-xoauth2)
;;; auth-source-xoauth2.el ends here