* Fix some compiler warnings for bad casting in some functions in the file
[alpine.git] / scripts / ooauth2.py
blobd9d66b7306d4d9844b1e86ad24728de2621c2238
1 #!/usr/bin/python3
3 # Copyright 2020,-2022 Eduardo Chappa <eduardo.chappa@gmx.com>
4 # Based on the oauth2.py script Copyright 2012 Google Inc.
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
10 # http://www.apache.org/licenses/LICENSE-2.0
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
18 """
19 This script can be used to obtain the initial refresh token and access token
20 for an app, or to renew an access token, and in both cases obtain the encoded
21 base64 encoded string that is used to add to an authorization command in an
22 IMAP or SMTP server.
24 * In order to get the initial refresh token and access token, determine the tenant
25 you will use. The default is 'common'. You also need to supply the client-id of
26 your app.
28 ooauth2 [--tenant=common] --client_id=f21d... --generate_refresh_and_access_token
30 The script will give you a url and a code. Open the url with a browser and enter
31 the code where requested. You will be redirected to login with your username
32 and password. After a successful login, you will be asked to authorize
33 the app. Once you have authorized the app, close that window and return to
34 this script. Press "ENTER" and you will see your refresh-token, access-token
35 and total amount of time (in seconds) that your token is valid. This is typically
36 3600 seconds (one hour). Please note that the refresh token and access token are
37 very long strings, each one them should be saved in a file one line long each.
39 * You can also use this script to generate a new access_token. In order to do this
40 you need the tenant, the client-id, and a refresh-token. Then you would run this
41 script as
43 ooauth2 [--tenant=common] --client_id=f21d... --refresh_token=MCRagxlHaZfUvV9kG0lnBk...
45 as an advice copy and paste the refresh token that you were given into a file,
46 and replace the command line option
47 --refresh_token=MCRagxlHaZfUvV9kG0lnBk...
49 --refresh_token=`cat filename`
51 * The last way to use this script is to use the previous commands, but add
52 --encoded to any of the previous commands. This will produce a base64 string that
53 can be added to an IMAP "AUTHENTICATE XOAUTH2" command, or an "AUTH XOAUTH2" SMTP
54 command, to login to that server. The access token will not be displayed, only
55 the encoded base64 string. If you use this option, you must also provide
56 the --user option. For example:
58 ooauth2 [--tenant=common] --client_id=f21d... --generate_refresh_and_access_token \
59 --encoded --user=YourID@outlook.com
63 ooauth2 [--tenant=common] --client_id=f21d... --refresh_token=MCRagxlHaZfUvV9kG0lnBk...
64 --encoded --user=YourID@outlook.com
65 """
67 import base64
68 import json
69 from optparse import OptionParser
70 import sys
71 import urllib.request, urllib.parse, urllib.error
73 # The URL root for authorizations (device code, refresh token and access token)
74 MICROSOFT_BASE_URL = 'https://login.microsoftonline.com'
76 # Default grant type
77 GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code'
79 # Default scope to access IMAP and SMTP.
80 SCOPE = 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send'
82 def SetupOptionParser():
83 # Usage message is the module's docstring.
84 parser = OptionParser(usage=__doc__)
85 parser.add_option('--generate_refresh_and_access_token',
86 action='store_true',
87 dest='generate_refresh_and_access_token',
88 help='generates an OAuth2 token for testing')
89 parser.add_option('--generate_access_token',
90 action='store_true',
91 dest='generate_access_token',
92 help='generates an initial client response string for '
93 'OAuth2')
94 parser.add_option('--user',
95 default=None,
96 help='your username. Only needed if --encoded is needed')
97 parser.add_option('--encoded',
98 action='store_true',
99 default=False,
100 dest='encoded',
101 help='returns a base64 encoded string, ready to add to your authentication request')
102 parser.add_option('--client_id',
103 default=None,
104 help='Client ID of the application that is authenticating. '
105 'See OAuth2 documentation for details.')
106 parser.add_option('--tenant',
107 default='common',
108 help='Use a specific tenant. Default: common')
109 parser.add_option('--scope',
110 default=SCOPE,
111 help='scope for the access token. Multiple scopes can be '
112 'listed separated by spaces with the whole argument '
113 'quoted.')
114 parser.add_option('--access_token',
115 default=None,
116 help='OAuth2 access token')
117 parser.add_option('--refresh_token',
118 default=None,
119 help='OAuth2 refresh token')
120 return parser
122 def AccountsUrl(tenant, command):
123 return '%s/%s/%s' % (MICROSOFT_BASE_URL, tenant, command)
125 def UrlEscape(text):
126 return urllib.parse.quote(text, safe='~-._')
128 def UrlUnescape(text):
129 return urllib.parse.unquote(text)
131 def FormatUrlParams(params):
132 param_fragments = []
133 for param in sorted(iter(params.items()), key=lambda x: x[0]):
134 param_fragments.append('%s=%s' % (param[0], UrlEscape(param[1])))
135 return '&'.join(param_fragments)
137 def GeneratePermissionUrl(tenant, client_id, scope=SCOPE):
138 params = {}
139 params['client_id'] = client_id
140 params['scope'] = scope
141 request_url = AccountsUrl(tenant, 'oauth2/v2.0/devicecode')
142 response = urllib.request.urlopen(request_url, urllib.parse.urlencode(params).encode()).read()
143 return json.loads(response)
145 def AuthorizeTokens(tenant, client_id, DeviceCode):
146 params = {}
147 params['client_id'] = client_id
148 params['device_code'] = DeviceCode
149 params['grant_type'] = GRANT_TYPE
150 request_url = AccountsUrl(tenant, 'oauth2/v2.0/token')
151 response = urllib.request.urlopen(request_url, urllib.parse.urlencode(params).encode()).read()
152 return json.loads(response)
154 def GenerateAccessToken(tenant, client_id, refresh_token, scope=SCOPE):
155 params = {}
156 params['client_id'] = client_id
157 params['refresh_token'] = refresh_token
158 params['grant_type'] = scope
159 params['grant_type'] = 'refresh_token'
160 request_url = AccountsUrl(tenant, 'oauth2/v2.0/token')
161 response = urllib.request.urlopen(request_url, urllib.parse.urlencode(params)).read()
162 return json.loads(response)['access_token']
164 def Oauth2EncodedString(user, access_token):
165 rawstring = 'user=%s\1auth=Bearer %s\1\1' % (user, access_token)
166 return base64.b64encode(str.encode(rawstring)).decode()
168 def RequireOptions(options, *args):
169 missing = [arg for arg in args if getattr(options, arg) is None]
170 if missing:
171 print('Missing options: %s' % ' '.join(missing))
172 sys.exit(-1)
174 def main(argv):
175 options_parser = SetupOptionParser()
176 (options, args) = options_parser.parse_args()
177 if options.generate_access_token:
178 RequireOptions(options, 'tenant', 'refresh_token')
179 access_token = GenerateAccessToken(options.tenant, options.client_id, options.refresh_token, options.scope)
180 if options.encoded:
181 RequireOptions(options, 'user')
182 print('%s' % Oauth2EncodedString(options.user, access_token))
183 else:
184 print('%s' % access_token)
185 elif options.generate_refresh_and_access_token:
186 RequireOptions(options, 'tenant', 'client_id')
187 response = GeneratePermissionUrl(options.tenant, options.client_id, options.scope)
188 print('%s' % response['message'])
189 input('Go to the URL above, complete the authorizaion process, and press ENTER when you are done')
190 response = AuthorizeTokens(options.tenant, options.client_id, response['device_code'])
191 print('Refresh Token: %s' % response['refresh_token'])
192 if options.encoded:
193 RequireOptions(options, 'user')
194 print('%s' % Oauth2EncodedString(options.user, response['access_token']))
195 else:
196 print('Access Token: %s' % response['access_token'])
197 print('Access Token Expiration Seconds: %s' % response['expires_in'])
198 else:
199 options_parser.print_help()
200 print('Nothing to do, exiting.')
201 return
203 if __name__ == '__main__':
204 main(sys.argv)