comparison scripts/oauth-get-token.py @ 7084:8d9a6063cb22

Add oauth-get-token.py script .. with a short explanation in README.txt
author Ralf Schlatterbeck <rsc@runtux.com>
date Tue, 29 Nov 2022 13:15:19 +0100
parents
children 8cda8e05c9a0
comparison
equal deleted inserted replaced
7083:259f1e571470 7084:8d9a6063cb22
1 #!/usr/bin/python3
2
3 import requests
4 import time
5 import sys
6 import webbrowser
7 import ssl
8
9 from urllib.parse import urlparse, urlencode, parse_qs
10 from argparse import ArgumentParser, RawDescriptionHelpFormatter
11 from http.server import HTTPServer, BaseHTTPRequestHandler
12
13 class Request_Token:
14
15 def __init__ (self, args):
16 self.args = args
17 self.session = requests.session ()
18 self.url = '/'.join ((args.url.rstrip ('/'), args.tenant))
19 self.url = '/'.join ((self.url, 'oauth2/v2.0'))
20 self.state = None
21 # end def __init__
22
23 def check_err (self, r):
24 if not 200 <= r.status_code <= 299:
25 raise RuntimeError \
26 ( 'Invalid result: %s: %s\n %s'
27 % (r.status_code, r.reason, r.text)
28 )
29 # end def check_err
30
31 def get_url (self, path, params):
32 url = ('/'.join ((self.url, path)))
33 url = url + '?' + urlencode (params)
34 return url
35 # end def get_url
36
37 def post_or_put (self, method, path, data = None, json = None):
38 d = {}
39 if data:
40 d.update (data = data)
41 if json:
42 d.update (json = json)
43 url = ('/'.join ((self.url, path)))
44 r = method (url, **d)
45 self.check_err (r)
46 return r.json ()
47 # end def post_or_put
48
49 def post (self, path, data = None, json = None):
50 return self.post_or_put (self.session.post, path, data, json)
51 # end def post
52
53 def authcode_callback (self, handler):
54 msg = ['']
55 self.request_received = False
56 r = urlparse (handler.path)
57 if r.query:
58 q = parse_qs (r.query)
59 if 'state' in q:
60 state = q ['state'][0]
61 if state != self.state:
62 msg.append \
63 ( 'State did not match: expect "%s" got "%s"'
64 % (self.state, state)
65 )
66 elif 'code' not in q:
67 msg.append ('Got no code')
68 else:
69 with open ('oauth/authcode', 'w') as f:
70 f.write (q ['code'][0])
71 msg.append ('Wrote code to oauth/authcode')
72 self.request_received = True
73 else:
74 msg.append ('No state and no code')
75 return 200, '\n'.join (msg).encode ('utf-8')
76 # end def authcode_callback
77
78 def request_authcode (self):
79 with open ('oauth/client_id', 'r') as f:
80 client_id = f.read ()
81 self.state = 'authcode' + str (time.time ())
82 params = dict \
83 ( client_id = client_id
84 , response_type = 'code'
85 , response_mode = 'query'
86 , state = self.state
87 , redirect_uri = self.args.redirect_uri
88 , scope = ' '.join
89 (( 'https://outlook.office.com/IMAP.AccessAsUser.All'
90 , 'https://outlook.office.com/User.Read'
91 , 'offline_access'
92 ))
93 )
94 url = self.get_url ('authorize', params)
95 print (url)
96 if self.args.webbrowser:
97 browser = webbrowser.get (self.args.browser)
98 browser.open_new_tab (url)
99 if self.args.run_https_server:
100 self.https_server ()
101 if self.args.request_tokens:
102 self.request_token ()
103 # end def request_authcode
104
105 def request_token (self):
106 with open ('oauth/client_id', 'r') as f:
107 client_id = f.read ()
108 with open ('oauth/client_secret', 'r') as f:
109 client_secret = f.read ().strip ()
110 with open ('oauth/authcode', 'r') as f:
111 authcode = f.read ().strip ()
112 params = dict \
113 ( client_id = client_id
114 , code = authcode
115 , client_secret = client_secret
116 , redirect_uri = self.args.redirect_uri
117 , grant_type = 'authorization_code'
118 # Only a single scope parameter is allowed here
119 , scope = ' '.join
120 (( 'https://outlook.office.com/User.Read'
121 ,
122 ))
123 )
124 result = self.post ('token', data = params)
125 with open ('oauth/refresh_token', 'w') as f:
126 f.write (result ['refresh_token'])
127 with open ('oauth/access_token', 'w') as f:
128 f.write (result ['access_token'])
129 # end def request_token
130
131 def https_server (self):
132 self.request_received = False
133 class RQ_Handler (BaseHTTPRequestHandler):
134 token_handler = self
135
136 def do_GET (self):
137 self.close_connection = True
138 code, msg = self.token_handler.authcode_callback (self)
139 self.send_response (code)
140 self.send_header ('Content-Type', 'text/plain')
141 self.end_headers ()
142 self.wfile.write (msg)
143 self.wfile.flush ()
144
145 port = self.args.https_server_port
146 httpd = HTTPServer (('localhost', port), RQ_Handler)
147
148 httpd.socket = ssl.wrap_socket \
149 ( httpd.socket
150 , keyfile = "/etc/ssl/private/ssl-cert-snakeoil.key"
151 , certfile = "/etc/ssl/certs/ssl-cert-snakeoil.pem"
152 , server_side = True
153 )
154
155 while not self.request_received:
156 httpd.handle_request ()
157 # end def https_server
158
159 # end class Request_Token
160
161 epilog = """\
162 Retrieving the necessary refresh_token and access_token credentials
163 using this script. This asumes you have an email account (plus the
164 password) to be used for mail retrieval. And you have registered an
165 application in the cloud for this process. The registering of an
166 application will give you an application id (also called client id) and
167 a tenant in UUID format.
168
169 First define the necessary TENANT variable:
170
171 TENANT=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
172
173 You need to create a directory named 'oauth' (if not yet existing) and
174 put the client id (also called application id) into the file
175 'oauth/client_id' and the corresponding secret into the file
176 'oauth/client_secret'.
177
178 By default calling the script with no arguments, the whole process is
179 automatic, but you may want to specify the tenant explicitly using:
180
181 ./oauth-get-token.py -t $TENANT
182
183 Specifying the tenant explicitly will select the customized company
184 login form directly.
185
186 The automatic process works as follows:
187 - First the authorization URL is constructed and pushed to a local
188 browser. By default the default browser on that machine is used, you
189 can specify a different browser with the -b/--browser option.
190 This will show a login form where you should be able to select the
191 user to log in with. Log in with the username (the email address) and
192 password for that user.
193 - A web-server is started on the given port. When you fill out the
194 authentication form pushed to the browser, the last step is a redirect
195 to an URL that calls back to this webserver. The necessary
196 authentication code is transmitted in a query parameter. The code is
197 stored into the file 'oauth/authcode'. Using the authcode, the
198 refresh_token and access_token are requested and stored in the oauth
199 directory.
200
201 These steps can be broken down into individual steps by options
202 disabling one of the steps:
203 - The push to the webserver can be disabled with the option
204 -w/--dont-push-to-webbrowser -- in that case the URL is printed on
205 standard output and must be pasted into the URL input field of a
206 browser. It is typically a good idea to use a browser that is
207 currently not logged into the company network.
208 - The start of the webserver can be disabled with the option
209 -s/--dont-run-https-server -- when called with that option no
210 webserver is started. You get a redirect to a non-existing page. The
211 error-message is something like:
212
213 This site can’t be reached
214
215 Copy the URL from the browser into the file 'oauth/authcode'. The URL
216 has paramters. We're interested in the 'code' parameter, a very long
217 string. Edit the file so that only that string (without the 'code='
218 part) is in the file.
219 - Requesting the tokens can be disabled with the option
220 -n/--dont-request-tokens -- if this option is given, after receiving
221 the redirect from the webserver the authentication code is written to
222 the file 'oauth/authcode' but no token request is started.
223
224 If you have either disabled the webserver or the token request, the
225 token can be requested (using the file 'oauth/authcode' constructed by
226 hand as described above or written by the webserver) with the
227 -T/--request-token option:
228
229 ./oauth-get-token.py [-t $TENANT] -T
230
231 If successful this will create the 'oauth/access_token' and
232 'oauth/refresh_token' files. Note that the authentication code has a
233 limited lifetime.
234
235 """
236
237 def main ():
238 cmd = ArgumentParser \
239 (epilog=epilog, formatter_class=RawDescriptionHelpFormatter)
240 cmd.add_argument \
241 ( '-T', '--request-token'
242 , help = "Run only the token-request step"
243 , action = 'store_true'
244 )
245 cmd.add_argument \
246 ( '-b', '--browser'
247 , help = "Use non-default browser"
248 )
249 cmd.add_argument \
250 ( '-n', '--dont-request-tokens'
251 , dest = 'request_tokens'
252 , help = "Do not request tokens, just write authcode"
253 , action = 'store_false'
254 , default = True
255 )
256 cmd.add_argument \
257 ( '-p', '--https-server-port'
258 , type = int
259 , help = "Port for https server to listen, default=%(default)s"
260 " see also -r option, ports must (usually) match."
261 , default = 8181
262 )
263 cmd.add_argument \
264 ( '-r', '--redirect-uri'
265 , help = "Redirect URI, default=%(default)s"
266 , default = 'https://localhost:8181'
267 )
268 cmd.add_argument \
269 ( '-s', '--dont-run-https-server'
270 , dest = 'run_https_server'
271 , help = "Run https server to wait for connection of browser "
272 "to transmit auth code via GET request"
273 , action = 'store_false'
274 , default = True
275 )
276 cmd.add_argument \
277 ( '-t', '--tenant'
278 , help = "Tenant part of url, default=%(default)s"
279 , default = 'organizations'
280 )
281 cmd.add_argument \
282 ( '-u', '--url'
283 , help = "Base url for requests, default=%(default)s"
284 , default = 'https://login.microsoftonline.com'
285 )
286 cmd.add_argument \
287 ( '-w', '--dont-push-to-webbrowser'
288 , dest = 'webbrowser'
289 , help = "Do not push authcode url into the browser"
290 , action = 'store_false'
291 , default = True
292 )
293 args = cmd.parse_args ()
294 rt = Request_Token (args)
295 if args.request_token:
296 rt.request_token ()
297 else:
298 rt.request_authcode ()
299 # end def main
300
301 if __name__ == '__main__':
302 main ()

Roundup Issue Tracker: http://roundup-tracker.org/