summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2020-12-30 00:14:25 -0800
committerMike Crute <mike@crute.us>2020-12-30 00:14:25 -0800
commita7eb5ac2e19a46e9b70d3af43b0482d061b31623 (patch)
treeea36c7130acbc1c08c174499f173511fd5ca11b7 /bin
parentd50ff95aa7f5397d3dbac133955294441131dd91 (diff)
downloaddotfiles-a7eb5ac2e19a46e9b70d3af43b0482d061b31623.tar.bz2
dotfiles-a7eb5ac2e19a46e9b70d3af43b0482d061b31623.tar.xz
dotfiles-a7eb5ac2e19a46e9b70d3af43b0482d061b31623.zip
Add gmail mutt token hook
Diffstat (limited to 'bin')
-rwxr-xr-xbin/oauth2.py348
1 files changed, 348 insertions, 0 deletions
diff --git a/bin/oauth2.py b/bin/oauth2.py
new file mode 100755
index 0000000..db4ea79
--- /dev/null
+++ b/bin/oauth2.py
@@ -0,0 +1,348 @@
1#!/usr/bin/python3
2#
3# Copyright 2012 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9 # http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Performs client tasks for testing IMAP OAuth2 authentication.
18
19To use this script, you'll need to have registered with Google as an OAuth
20application and obtained an OAuth client ID and client secret.
21See https://developers.google.com/identity/protocols/OAuth2 for instructions on
22registering and for documentation of the APIs invoked by this code.
23
24This script has 3 modes of operation.
25
261. The first mode is used to generate and authorize an OAuth2 token, the
27first step in logging in via OAuth2.
28
29 oauth2 --user=xxx@gmail.com \
30 --client_id=1038[...].apps.googleusercontent.com \
31 --client_secret=VWFn8LIKAMC-MsjBMhJeOplZ \
32 --generate_oauth2_token
33
34The script will converse with Google and generate an oauth request
35token, then present you with a URL you should visit in your browser to
36authorize the token. Once you get the verification code from the Google
37website, enter it into the script to get your OAuth access token. The output
38from this command will contain the access token, a refresh token, and some
39metadata about the tokens. The access token can be used until it expires, and
40the refresh token lasts indefinitely, so you should record these values for
41reuse.
42
432. The script will generate new access tokens using a refresh token.
44
45 oauth2 --user=xxx@gmail.com \
46 --client_id=1038[...].apps.googleusercontent.com \
47 --client_secret=VWFn8LIKAMC-MsjBMhJeOplZ \
48 --refresh_token=1/Yzm6MRy4q1xi7Dx2DuWXNgT6s37OrP_DW_IoyTum4YA
49
503. The script will generate an OAuth2 string that can be fed
51directly to IMAP or SMTP. This is triggered with the --generate_oauth2_string
52option.
53
54 oauth2 --generate_oauth2_string --user=xxx@gmail.com \
55 --access_token=ya29.AGy[...]ezLg
56
57The output of this mode will be a base64-encoded string. To use it, connect to a
58IMAPFE and pass it as the second argument to the AUTHENTICATE command.
59
60 a AUTHENTICATE XOAUTH2 a9sha9sfs[...]9dfja929dk==
61"""
62
63import base64
64import imaplib
65import json
66from optparse import OptionParser
67import smtplib
68import sys
69import urllib.request
70import urllib.parse
71
72
73def SetupOptionParser():
74 # Usage message is the module's docstring.
75 parser = OptionParser(usage=__doc__)
76 parser.add_option('--generate_oauth2_token',
77 action='store_true',
78 dest='generate_oauth2_token',
79 help='generates an OAuth2 token for testing')
80 parser.add_option('--generate_oauth2_string',
81 action='store_true',
82 dest='generate_oauth2_string',
83 help='generates an initial client response string for '
84 'OAuth2')
85 parser.add_option('--client_id',
86 default=None,
87 help='Client ID of the application that is authenticating. '
88 'See OAuth2 documentation for details.')
89 parser.add_option('--client_secret',
90 default=None,
91 help='Client secret of the application that is '
92 'authenticating. See OAuth2 documentation for '
93 'details.')
94 parser.add_option('--access_token',
95 default=None,
96 help='OAuth2 access token')
97 parser.add_option('--refresh_token',
98 default=None,
99 help='OAuth2 refresh token')
100 parser.add_option('--scope',
101 default='https://mail.google.com/',
102 help='scope for the access token. Multiple scopes can be '
103 'listed separated by spaces with the whole argument '
104 'quoted.')
105 parser.add_option('--test_imap_authentication',
106 action='store_true',
107 dest='test_imap_authentication',
108 help='attempts to authenticate to IMAP')
109 parser.add_option('--test_smtp_authentication',
110 action='store_true',
111 dest='test_smtp_authentication',
112 help='attempts to authenticate to SMTP')
113 parser.add_option('--user',
114 default=None,
115 help='email address of user whose account is being '
116 'accessed')
117 parser.add_option('--quiet',
118 action='store_true',
119 default=False,
120 dest='quiet',
121 help='Omit verbose descriptions and only print '
122 'machine-readable outputs.')
123 return parser
124
125
126# The URL root for accessing Google Accounts.
127GOOGLE_ACCOUNTS_BASE_URL = 'https://accounts.google.com'
128
129
130# Hardcoded dummy redirect URI for non-web apps.
131REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
132
133
134def AccountsUrl(command):
135 """Generates the Google Accounts URL.
136
137 Args:
138 command: The command to execute.
139
140 Returns:
141 A URL for the given command.
142 """
143 return '%s/%s' % (GOOGLE_ACCOUNTS_BASE_URL, command)
144
145
146def UrlEscape(text):
147 # See OAUTH 5.1 for a definition of which characters need to be escaped.
148 return urllib.parse.quote(text, safe='~-._')
149
150
151def UrlUnescape(text):
152 # See OAUTH 5.1 for a definition of which characters need to be escaped.
153 return urllib.parse.unquote(text)
154
155
156def FormatUrlParams(params):
157 """Formats parameters into a URL query string.
158
159 Args:
160 params: A key-value map.
161
162 Returns:
163 A URL query string version of the given parameters.
164 """
165 param_fragments = []
166 for param in sorted(params.items(), key=lambda x: x[0]):
167 param_fragments.append('%s=%s' % (param[0], UrlEscape(param[1])))
168 return '&'.join(param_fragments)
169
170
171def GeneratePermissionUrl(client_id, scope='https://mail.google.com/'):
172 """Generates the URL for authorizing access.
173
174 This uses the "OAuth2 for Installed Applications" flow described at
175 https://developers.google.com/accounts/docs/OAuth2InstalledApp
176
177 Args:
178 client_id: Client ID obtained by registering your app.
179 scope: scope for access token, e.g. 'https://mail.google.com'
180 Returns:
181 A URL that the user should visit in their browser.
182 """
183 params = {}
184 params['client_id'] = client_id
185 params['redirect_uri'] = REDIRECT_URI
186 params['scope'] = scope
187 params['response_type'] = 'code'
188 return '%s?%s' % (AccountsUrl('o/oauth2/auth'),
189 FormatUrlParams(params))
190
191
192def AuthorizeTokens(client_id, client_secret, authorization_code):
193 """Obtains OAuth access token and refresh token.
194
195 This uses the application portion of the "OAuth2 for Installed Applications"
196 flow at https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse
197
198 Args:
199 client_id: Client ID obtained by registering your app.
200 client_secret: Client secret obtained by registering your app.
201 authorization_code: code generated by Google Accounts after user grants
202 permission.
203 Returns:
204 The decoded response from the Google Accounts server, as a dict. Expected
205 fields include 'access_token', 'expires_in', and 'refresh_token'.
206 """
207 params = {}
208 params['client_id'] = client_id
209 params['client_secret'] = client_secret
210 params['code'] = authorization_code
211 params['redirect_uri'] = REDIRECT_URI
212 params['grant_type'] = 'authorization_code'
213 request_url = AccountsUrl('o/oauth2/token')
214
215 response = urllib.request.urlopen(request_url, urllib.parse.urlencode(params).encode("utf-8")).read()
216 return json.loads(response)
217
218
219def RefreshToken(client_id, client_secret, refresh_token):
220 """Obtains a new token given a refresh token.
221
222 See https://developers.google.com/accounts/docs/OAuth2InstalledApp#refresh
223
224 Args:
225 client_id: Client ID obtained by registering your app.
226 client_secret: Client secret obtained by registering your app.
227 refresh_token: A previously-obtained refresh token.
228 Returns:
229 The decoded response from the Google Accounts server, as a dict. Expected
230 fields include 'access_token', 'expires_in', and 'refresh_token'.
231 """
232 params = {}
233 params['client_id'] = client_id
234 params['client_secret'] = client_secret
235 params['refresh_token'] = refresh_token
236 params['grant_type'] = 'refresh_token'
237 request_url = AccountsUrl('o/oauth2/token')
238
239 response = urllib.request.urlopen(request_url, urllib.parse.urlencode(params).encode("utf-8")).read()
240 return json.loads(response)
241
242
243def GenerateOAuth2String(username, access_token, base64_encode=True):
244 """Generates an IMAP OAuth2 authentication string.
245
246 See https://developers.google.com/google-apps/gmail/oauth2_overview
247
248 Args:
249 username: the username (email address) of the account to authenticate
250 access_token: An OAuth2 access token.
251 base64_encode: Whether to base64-encode the output.
252
253 Returns:
254 The SASL argument for the OAuth2 mechanism.
255 """
256 auth_string = 'user=%s\1auth=Bearer %s\1\1' % (username, access_token)
257 if base64_encode:
258 auth_string = base64.b64encode(auth_string)
259 return auth_string
260
261
262def TestImapAuthentication(user, auth_string):
263 """Authenticates to IMAP with the given auth_string.
264
265 Prints a debug trace of the attempted IMAP connection.
266
267 Args:
268 user: The Gmail username (full email address)
269 auth_string: A valid OAuth2 string, as returned by GenerateOAuth2String.
270 Must not be base64-encoded, since imaplib does its own base64-encoding.
271 """
272 print()
273 imap_conn = imaplib.IMAP4_SSL('imap.gmail.com')
274 imap_conn.debug = 4
275 imap_conn.authenticate('XOAUTH2', lambda x: auth_string)
276 imap_conn.select('INBOX')
277
278
279def TestSmtpAuthentication(user, auth_string):
280 """Authenticates to SMTP with the given auth_string.
281
282 Args:
283 user: The Gmail username (full email address)
284 auth_string: A valid OAuth2 string, not base64-encoded, as returned by
285 GenerateOAuth2String.
286 """
287 print()
288 smtp_conn = smtplib.SMTP('smtp.gmail.com', 587)
289 smtp_conn.set_debuglevel(True)
290 smtp_conn.ehlo('test')
291 smtp_conn.starttls()
292 smtp_conn.docmd('AUTH', 'XOAUTH2 ' + base64.b64encode(auth_string))
293
294
295def RequireOptions(options, *args):
296 missing = [arg for arg in args if getattr(options, arg) is None]
297 if missing:
298 print('Missing options: %s' % ' '.join(missing))
299 sys.exit(-1)
300
301
302def main(argv):
303 options_parser = SetupOptionParser()
304 (options, args) = options_parser.parse_args()
305 if options.refresh_token:
306 RequireOptions(options, 'client_id', 'client_secret')
307 response = RefreshToken(options.client_id, options.client_secret,
308 options.refresh_token)
309 if options.quiet:
310 print(response['access_token'])
311 else:
312 print('Access Token: %s' % response['access_token'])
313 print('Access Token Expiration Seconds: %s' % response['expires_in'])
314 elif options.generate_oauth2_string:
315 RequireOptions(options, 'user', 'access_token')
316 oauth2_string = GenerateOAuth2String(options.user, options.access_token)
317 if options.quiet:
318 print(oauth2_string)
319 else:
320 print('OAuth2 argument:\n' + oauth2_string)
321 elif options.generate_oauth2_token:
322 RequireOptions(options, 'client_id', 'client_secret')
323 print('To authorize token, visit this url and follow the directions:')
324 print(' %s' % GeneratePermissionUrl(options.client_id, options.scope))
325 authorization_code = input('Enter verification code: ')
326 response = AuthorizeTokens(options.client_id, options.client_secret,
327 authorization_code)
328 print('Refresh Token: %s' % response['refresh_token'])
329 print('Access Token: %s' % response['access_token'])
330 print('Access Token Expiration Seconds: %s' % response['expires_in'])
331 elif options.test_imap_authentication:
332 RequireOptions(options, 'user', 'access_token')
333 TestImapAuthentication(options.user,
334 GenerateOAuth2String(options.user, options.access_token,
335 base64_encode=False))
336 elif options.test_smtp_authentication:
337 RequireOptions(options, 'user', 'access_token')
338 TestSmtpAuthentication(options.user,
339 GenerateOAuth2String(options.user, options.access_token,
340 base64_encode=False))
341 else:
342 options_parser.print_help()
343 print('Nothing to do, exiting.')
344 return
345
346
347if __name__ == '__main__':
348 main(sys.argv)