spotifywebapiPython 1.0.242__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spotifywebapipython/__init__.py +35 -0
- spotifywebapipython/const.py +114 -0
- spotifywebapipython/docs/__init__.html +2156 -0
- spotifywebapipython/docs/codecopy.js +70 -0
- spotifywebapipython/docs/index.html +514 -0
- spotifywebapipython/docs/search.js +46 -0
- spotifywebapipython/docs/spotifywebapipython/const.html +636 -0
- spotifywebapipython/docs/spotifywebapipython/saappmessages.html +540 -0
- spotifywebapipython/docs/spotifywebapipython/sautils.html +620 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +341 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +598 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +612 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +16557 -0
- spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +518 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +421 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +400 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +635 -0
- spotifywebapipython/docs/spotifywebapipython.ico +0 -0
- spotifywebapipython/docs/spotifywebapipython_OLD.ico +0 -0
- spotifywebapipython/models/__init__.py +121 -0
- spotifywebapipython/models/album.py +170 -0
- spotifywebapipython/models/albumpagesaved.py +85 -0
- spotifywebapipython/models/albumpagesimplified.py +88 -0
- spotifywebapipython/models/albumsaved.py +102 -0
- spotifywebapipython/models/albumsimplified.py +308 -0
- spotifywebapipython/models/artist.py +146 -0
- spotifywebapipython/models/artistinfo.py +282 -0
- spotifywebapipython/models/artistinfotourevent.py +127 -0
- spotifywebapipython/models/artistpage.py +70 -0
- spotifywebapipython/models/artistsimplified.py +163 -0
- spotifywebapipython/models/audiobook.py +87 -0
- spotifywebapipython/models/audiobookpagesimplified.py +88 -0
- spotifywebapipython/models/audiobooksimplified.py +362 -0
- spotifywebapipython/models/audiofeatures.py +333 -0
- spotifywebapipython/models/author.py +67 -0
- spotifywebapipython/models/category.py +168 -0
- spotifywebapipython/models/categorypage.py +70 -0
- spotifywebapipython/models/chapter.py +84 -0
- spotifywebapipython/models/chapterpagesimplified.py +88 -0
- spotifywebapipython/models/chaptersimplified.py +387 -0
- spotifywebapipython/models/context.py +113 -0
- spotifywebapipython/models/copyright.py +79 -0
- spotifywebapipython/models/device.py +301 -0
- spotifywebapipython/models/episode.py +84 -0
- spotifywebapipython/models/episodepagesaved.py +85 -0
- spotifywebapipython/models/episodepagesimplified.py +88 -0
- spotifywebapipython/models/episodesaved.py +99 -0
- spotifywebapipython/models/episodesimplified.py +370 -0
- spotifywebapipython/models/explicitcontent.py +80 -0
- spotifywebapipython/models/externalids.py +91 -0
- spotifywebapipython/models/externalurls.py +104 -0
- spotifywebapipython/models/followers.py +81 -0
- spotifywebapipython/models/imageobject.py +193 -0
- spotifywebapipython/models/imagevibrantcolors.py +182 -0
- spotifywebapipython/models/linkedfrom.py +165 -0
- spotifywebapipython/models/narrator.py +67 -0
- spotifywebapipython/models/owner.py +162 -0
- spotifywebapipython/models/pageobject.py +343 -0
- spotifywebapipython/models/playeractions.py +196 -0
- spotifywebapipython/models/playerlastplayedinfo.py +334 -0
- spotifywebapipython/models/playerplaystate.py +467 -0
- spotifywebapipython/models/playerqueueinfo.py +207 -0
- spotifywebapipython/models/playhistory.py +136 -0
- spotifywebapipython/models/playhistorypage.py +85 -0
- spotifywebapipython/models/playlist.py +125 -0
- spotifywebapipython/models/playlistpage.py +85 -0
- spotifywebapipython/models/playlistpagesimplified.py +107 -0
- spotifywebapipython/models/playlistsimplified.py +334 -0
- spotifywebapipython/models/playlisttrack.py +143 -0
- spotifywebapipython/models/playlisttracksummary.py +80 -0
- spotifywebapipython/models/recommendationseed.py +135 -0
- spotifywebapipython/models/restrictions.py +86 -0
- spotifywebapipython/models/resumepoint.py +79 -0
- spotifywebapipython/models/searchresponse.py +391 -0
- spotifywebapipython/models/searchresultbase.py +299 -0
- spotifywebapipython/models/show.py +87 -0
- spotifywebapipython/models/showpagesaved.py +85 -0
- spotifywebapipython/models/showpagesimplified.py +88 -0
- spotifywebapipython/models/showsaved.py +99 -0
- spotifywebapipython/models/showsimplified.py +320 -0
- spotifywebapipython/models/spotifyconnectdevice.py +321 -0
- spotifywebapipython/models/spotifyconnectdevices.py +441 -0
- spotifywebapipython/models/track.py +142 -0
- spotifywebapipython/models/trackpage.py +110 -0
- spotifywebapipython/models/trackpagesaved.py +85 -0
- spotifywebapipython/models/trackpagesimplified.py +88 -0
- spotifywebapipython/models/trackrecommendations.py +88 -0
- spotifywebapipython/models/tracksaved.py +99 -0
- spotifywebapipython/models/tracksimplified.py +398 -0
- spotifywebapipython/models/userprofile.py +184 -0
- spotifywebapipython/models/userprofilesimplified.py +206 -0
- spotifywebapipython/models/zeroconfdiscoveryresult.py +592 -0
- spotifywebapipython/models/zeroconfproperty.py +93 -0
- spotifywebapipython/oauthcli/__init__.py +7 -0
- spotifywebapipython/oauthcli/authclient.py +1229 -0
- spotifywebapipython/saappmessages.py +86 -0
- spotifywebapipython/sautils.py +285 -0
- spotifywebapipython/spotifyapierror.py +66 -0
- spotifywebapipython/spotifyapimessage.py +269 -0
- spotifywebapipython/spotifyauthtoken.py +264 -0
- spotifywebapipython/spotifyclient.py +17885 -0
- spotifywebapipython/spotifyconnect/__init__.py +19 -0
- spotifywebapipython/spotifyconnect/spotifyconnectdeviceeventargs.py +67 -0
- spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +2564 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +467 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +701 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +359 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastmultizonelistener.py +212 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +20 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
- spotifywebapipython/spotifydiscovery.py +473 -0
- spotifywebapipython/spotifymediatypes.py +53 -0
- spotifywebapipython/spotifywebapiauthenticationerror.py +122 -0
- spotifywebapipython/spotifywebapierror.py +106 -0
- spotifywebapipython/spotifywebplayertoken.py +743 -0
- spotifywebapipython/vibrant/__init__.py +17 -0
- spotifywebapipython/vibrant/generator.py +261 -0
- spotifywebapipython/vibrant/image.py +100 -0
- spotifywebapipython/vibrant/main.py +34 -0
- spotifywebapipython/vibrant/models.py +93 -0
- spotifywebapipython/vibrant/utils.py +56 -0
- spotifywebapipython/zeroconfapi/__init__.py +23 -0
- spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
- spotifywebapipython/zeroconfapi/blobbuilder.py +370 -0
- spotifywebapipython/zeroconfapi/credentials.py +37 -0
- spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +89 -0
- spotifywebapipython/zeroconfapi/helpers.py +64 -0
- spotifywebapipython/zeroconfapi/spotifyzeroconfapierror.py +108 -0
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +1327 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +572 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +109 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfodrmmediaformat.py +90 -0
- spotifywebapipython/zeroconfapi/zeroconfresponse.py +195 -0
- spotifywebapipython-1.0.242.dist-info/LICENSE +22 -0
- spotifywebapipython-1.0.242.dist-info/METADATA +219 -0
- spotifywebapipython-1.0.242.dist-info/RECORD +138 -0
- spotifywebapipython-1.0.242.dist-info/WHEEL +5 -0
- spotifywebapipython-1.0.242.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1229 @@
|
|
|
1
|
+
# external package imports.
|
|
2
|
+
import base64
|
|
3
|
+
import contextlib
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os.path
|
|
8
|
+
import platformdirs
|
|
9
|
+
import secrets
|
|
10
|
+
import socket
|
|
11
|
+
import threading
|
|
12
|
+
import webbrowser
|
|
13
|
+
|
|
14
|
+
from oauthlib.oauth2 import InvalidGrantError, Client, WebApplicationClient, TokenExpiredError, AccessDeniedError
|
|
15
|
+
from requests_oauthlib import OAuth2Session
|
|
16
|
+
from typing import Optional, Union, Callable, Iterable
|
|
17
|
+
from wsgiref.simple_server import WSGIServer, WSGIRequestHandler, make_server as WSGIMakeServer
|
|
18
|
+
from wsgiref.util import request_uri as WSGIRequestUri
|
|
19
|
+
|
|
20
|
+
# get smartinspect logger reference; create a new session for this module name.
|
|
21
|
+
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SIColors
|
|
22
|
+
import logging
|
|
23
|
+
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
|
24
|
+
if (_logsi == None):
|
|
25
|
+
_logsi = SIAuto.Si.AddSession(__name__, True)
|
|
26
|
+
_logsi.SystemLogger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AuthClient:
|
|
30
|
+
"""
|
|
31
|
+
OAuth2 Authorization client class.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
_DEFAULT_AUTH_CODE_MESSAGE = "Enter the authorization code: "
|
|
35
|
+
"""
|
|
36
|
+
Enter the authorization code:
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
_DEFAULT_AUTH_PROMPT_MESSAGE = "Please visit this URL to authorize this application: {url}"
|
|
40
|
+
"""
|
|
41
|
+
Please visit this URL to authorize this application: {url}
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
_DEFAULT_WEB_SUCCESS_MESSAGE = "The authentication flow has completed; you may close this window (or tab)." #</br>Auth Token:</br>%s"
|
|
45
|
+
"""
|
|
46
|
+
The authentication flow has completed; you may close this window (or tab).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self,
|
|
50
|
+
authorizationType:str,
|
|
51
|
+
authorizationUrl:str=None,
|
|
52
|
+
tokenUrl:str=None,
|
|
53
|
+
scope:str = None,
|
|
54
|
+
clientId:str=None,
|
|
55
|
+
clientSecret:str=None,
|
|
56
|
+
oauth2Client:Client=None,
|
|
57
|
+
oauth2Session:OAuth2Session=None,
|
|
58
|
+
tokenProviderId:str=None,
|
|
59
|
+
tokenProfileId:str=None,
|
|
60
|
+
tokenStorageDir:str=None,
|
|
61
|
+
tokenStorageFile:str=None,
|
|
62
|
+
tokenUpdater:Callable=None,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Initializes a new instance of the class.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
authorizationType (str):
|
|
69
|
+
OAuth2 authorization type title (e.g. "Client Credentials", etc).
|
|
70
|
+
authorizationUrl (str):
|
|
71
|
+
URL used to authorize the requested access.
|
|
72
|
+
tokenUrl (str):
|
|
73
|
+
URL used to request an access token.
|
|
74
|
+
scope (str | list[str]):
|
|
75
|
+
A space-delimited list of scopes you wish to request access to.
|
|
76
|
+
If no scopes are specified, authorization will be granted only to access publicly
|
|
77
|
+
available information.
|
|
78
|
+
clientId (str):
|
|
79
|
+
The unique identifier of the application.
|
|
80
|
+
clientSecret (str):
|
|
81
|
+
The application's secret key, used to authorize your Web API or SDK calls.
|
|
82
|
+
oauth2Client (Client):
|
|
83
|
+
OAuth2 Client instance to use for this request.
|
|
84
|
+
If null, a new WebApplicationClient will be created with the specified clientId and scope argument values.
|
|
85
|
+
oauth2Session (requests_oauthlib.oauth2_session.OAuth2Session):
|
|
86
|
+
A `OAuth2Session` instance to use for this request.
|
|
87
|
+
If null, a new 'requests_oauthlib.oauth2_session.OAuth2Session' will be created with the specified
|
|
88
|
+
clientId and scope argument values.
|
|
89
|
+
tokenProviderId (str):
|
|
90
|
+
Provider identifier used when storing the token to disk.
|
|
91
|
+
A null value will default to `Shared`.
|
|
92
|
+
Default: `Shared`
|
|
93
|
+
tokenProfileId (str):
|
|
94
|
+
Profile identifier used when storing the token to disk.
|
|
95
|
+
A null value will default to `Shared`.
|
|
96
|
+
Default: `Shared`
|
|
97
|
+
tokenStorageDir (str):
|
|
98
|
+
The directory path that will contain the Token Cache file.
|
|
99
|
+
A null value will default to the platform specific storage location:
|
|
100
|
+
Example for Windows OS = `C:\\ProgramData\\SpotifyWebApiPython`
|
|
101
|
+
tokenStorageFile (str):
|
|
102
|
+
The filename and extension of the Token Cache file.
|
|
103
|
+
Default is `tokens.json`.
|
|
104
|
+
tokenUpdater (Callable):
|
|
105
|
+
A method to call when a token needs to be refreshed by an external provider.
|
|
106
|
+
The defined method is called with no parameters, and should return a token dictionary.
|
|
107
|
+
Default is null.
|
|
108
|
+
"""
|
|
109
|
+
# if scope is a list then convert it to space-delimited string.
|
|
110
|
+
if isinstance(scope, list):
|
|
111
|
+
scope = ' '.join(scope)
|
|
112
|
+
|
|
113
|
+
# verify token storage directory exists.
|
|
114
|
+
if tokenStorageDir is None:
|
|
115
|
+
tokenStorageDir = platformdirs.site_config_dir('SpotifyWebApiPython', ensure_exists=True, appauthor=False)
|
|
116
|
+
os.makedirs(tokenStorageDir, exist_ok=True) # succeeds even if directory exists.
|
|
117
|
+
|
|
118
|
+
# verify token storage filename was specified.
|
|
119
|
+
if tokenStorageFile is None:
|
|
120
|
+
tokenStorageFile = 'tokens.json'
|
|
121
|
+
|
|
122
|
+
# if token providerId not set, then default to shared.
|
|
123
|
+
if tokenProviderId is None:
|
|
124
|
+
tokenProviderId = 'Shared'
|
|
125
|
+
|
|
126
|
+
# if token profileId not set, then default to shared.
|
|
127
|
+
if tokenProfileId is None:
|
|
128
|
+
tokenProfileId = 'Shared'
|
|
129
|
+
|
|
130
|
+
# initialize storage.
|
|
131
|
+
self._AuthorizationType:str = authorizationType
|
|
132
|
+
self._AuthorizationUrl:str = authorizationUrl
|
|
133
|
+
self._ClientId:str = clientId
|
|
134
|
+
self._ClientSecret:str = clientSecret
|
|
135
|
+
self._CodeVerifier:str = None
|
|
136
|
+
self._DefaultLocalHost:str = 'localhost'
|
|
137
|
+
self._Session:OAuth2Session = oauth2Session
|
|
138
|
+
self._TokenProviderId:str = tokenProviderId
|
|
139
|
+
self._TokenStorageDir:str = tokenStorageDir
|
|
140
|
+
self._TokenStorageFile:str = tokenStorageFile
|
|
141
|
+
self._TokenStoragePath:str = os.path.join(tokenStorageDir, tokenStorageFile)
|
|
142
|
+
self._TokenUpdater:Callable = tokenUpdater
|
|
143
|
+
self._TokenUpdater_Lock = threading.Lock() # non re-entrant lock to sync access to token updates.
|
|
144
|
+
self._TokenUrl:str = tokenUrl
|
|
145
|
+
self._TokenProfileId:str = tokenProfileId
|
|
146
|
+
|
|
147
|
+
# create OAuth2 Session instance if necessary.
|
|
148
|
+
if self._Session is None:
|
|
149
|
+
|
|
150
|
+
if oauth2Client is None:
|
|
151
|
+
oauth2Client = WebApplicationClient(client_id=clientId)
|
|
152
|
+
|
|
153
|
+
# create the OAuth2 session with the specified clientId and scope.
|
|
154
|
+
# note that this will not load the 'token' property!
|
|
155
|
+
self._Session:OAuth2Session = OAuth2Session(clientId, scope=scope, client=oauth2Client)
|
|
156
|
+
|
|
157
|
+
# load the token from storage, if session does not currently have a token assigned.
|
|
158
|
+
token:dict = self._Session.token
|
|
159
|
+
if self._Session.token is None or len(self._Session.token) == 0:
|
|
160
|
+
token:dict = self._LoadToken()
|
|
161
|
+
|
|
162
|
+
# was a token loaded?
|
|
163
|
+
if token is not None:
|
|
164
|
+
|
|
165
|
+
# check for scope change between the existing token and the newly requested session.
|
|
166
|
+
_logsi.LogVerbose('Verifying OAuth2 authorization access scope has not changed')
|
|
167
|
+
hasScopeChanged:bool = self.HasScopeChanged(token, scope)
|
|
168
|
+
if hasScopeChanged == True:
|
|
169
|
+
# if scope change detected, then destroy the token as we need to force an auth refresh.
|
|
170
|
+
_logsi.LogVerbose('OAuth2 authorization access scope has changed; token will be destroyed', colorValue=SIColors.Gold)
|
|
171
|
+
self._SaveToken(None)
|
|
172
|
+
else:
|
|
173
|
+
# set session token reference to the loaded token.
|
|
174
|
+
self._Session.token = token
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def AuthorizationType(self) -> str:
|
|
179
|
+
"""
|
|
180
|
+
OAuth2 authorization type title (e.g. "Client Credentials", etc).
|
|
181
|
+
"""
|
|
182
|
+
return self._AuthorizationType
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def AuthorizationUrl(self) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Url used to request user authorization permission for an authorization token.
|
|
189
|
+
"""
|
|
190
|
+
return self._AuthorizationUrl
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def ClientId(self) -> str:
|
|
195
|
+
"""
|
|
196
|
+
The unique identifier of the application.
|
|
197
|
+
"""
|
|
198
|
+
return self._ClientId
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def CodeVerifier(self) -> str:
|
|
203
|
+
"""
|
|
204
|
+
The code verifier string used as part of the token request authorization process.
|
|
205
|
+
|
|
206
|
+
According to the PKCE standard, a code verifier is a high-entropy cryptographic
|
|
207
|
+
random string with a length between 43 and 128 characters (the longer the better).
|
|
208
|
+
It can contain letters, digits, underscores, periods, hyphens, or tildes.
|
|
209
|
+
"""
|
|
210
|
+
return self._CodeVerifier
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def IsAuthorized(self) -> bool:
|
|
215
|
+
"""
|
|
216
|
+
Indicates whether this session has an OAuth token 'access_token' value or not.
|
|
217
|
+
|
|
218
|
+
If True, you can reasonably expect OAuth-protected requests to the resource to succeed.
|
|
219
|
+
|
|
220
|
+
If False, you need the user to go through the OAuth authentication dance before
|
|
221
|
+
OAuth-protected requests to the resource will succeed.
|
|
222
|
+
"""
|
|
223
|
+
# mimic behavior of self._Session.authorized, in case we are using a Home Assistant OAuth2Session
|
|
224
|
+
# instance, which does not have an 'authorized' method.
|
|
225
|
+
result:bool = False
|
|
226
|
+
if self._Session is None:
|
|
227
|
+
return result
|
|
228
|
+
if self._Session.token is None:
|
|
229
|
+
return result
|
|
230
|
+
if self._Session.token.get('access_token') is None:
|
|
231
|
+
return result
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def Session(self) -> OAuth2Session:
|
|
237
|
+
"""
|
|
238
|
+
OAuth 2 extension to the `requests.Session` class.
|
|
239
|
+
|
|
240
|
+
Supports any grant type adhering to :class:`oauthlib.oauth2.Client` spec
|
|
241
|
+
including the four core OAuth 2 grants.
|
|
242
|
+
|
|
243
|
+
Can be used to create authorization urls, fetch tokens and access protected
|
|
244
|
+
resources using the `requests.Session` class interface you are used to.
|
|
245
|
+
"""
|
|
246
|
+
return self._Session
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def TokenProfileId(self) -> str:
|
|
251
|
+
"""
|
|
252
|
+
Profile identifier used when loading / storing the token from / to disk.
|
|
253
|
+
"""
|
|
254
|
+
return self._TokenProfileId
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def TokenUrl(self) -> str:
|
|
259
|
+
"""
|
|
260
|
+
Url used to request or renew an authorization token.
|
|
261
|
+
"""
|
|
262
|
+
return self._TokenUrl
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def Logout(self):
|
|
266
|
+
"""
|
|
267
|
+
Removes a stored token, but does not clear the current session.
|
|
268
|
+
|
|
269
|
+
Warning: a request with the current session can refresh and save
|
|
270
|
+
the token, making this call ineffective.
|
|
271
|
+
"""
|
|
272
|
+
_logsi.LogVerbose('OAuth2 logout called; token will be destroyed', colorValue=SIColors.Gold)
|
|
273
|
+
self._SaveToken(None)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def process_url(self, api:str) -> str:
|
|
277
|
+
return api
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def request(self, method:str, api:str, **kwargs) -> object:
|
|
281
|
+
return self._Session.request(method, self.process_url(api), **kwargs)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def get(self, api:str, **kwargs):
|
|
285
|
+
return self._Session.get(self.process_url(api), **kwargs)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def post(self, api:str, **kwargs):
|
|
289
|
+
return self._Session.post(self.process_url(api), **kwargs)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def put(self, api:str, **kwargs):
|
|
293
|
+
return self._Session.put(self.process_url(api), **kwargs)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def patch(self, api:str, **kwargs):
|
|
297
|
+
return self._Session.patch(self.process_url(api), **kwargs)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def delete(self, api:str, **kwargs):
|
|
301
|
+
return self._Session.delete(self.process_url(api), **kwargs)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def head(self, api:str, **kwargs):
|
|
305
|
+
return self._Session.head(self.process_url(api), **kwargs)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def options(self, api:str, **kwargs):
|
|
309
|
+
return self._Session.options(self.process_url(api), **kwargs)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _CheckAuthorization(self, force:bool, token_test:Optional[Callable]=None) -> bool:
|
|
313
|
+
"""
|
|
314
|
+
Checks if the access token is authorized or not.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
force (bool):
|
|
318
|
+
True to force the access token to be refreshed regardless of
|
|
319
|
+
authorized status.
|
|
320
|
+
tokenTest (Callable):
|
|
321
|
+
Function that receives this object for a param, makes a call, and
|
|
322
|
+
returns the response.
|
|
323
|
+
Default is null.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
True if the access token is authorized; otherwise, False if it is not
|
|
327
|
+
authorized or the force argument is true.
|
|
328
|
+
"""
|
|
329
|
+
# force the user to authorize the application access if we do not have an authorized
|
|
330
|
+
# access token, if the scope has changed, or if the caller requested us to (by force).
|
|
331
|
+
if (not self.IsAuthorized) or (force == True):
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
result:bool = True
|
|
335
|
+
if token_test is not None:
|
|
336
|
+
try:
|
|
337
|
+
resp = token_test(self)
|
|
338
|
+
if resp.status_code % 100 == 4:
|
|
339
|
+
result = False
|
|
340
|
+
except: # noqa: E722
|
|
341
|
+
result = False
|
|
342
|
+
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _FindOpenPortOnLocalhost(self, portRange:list[int]):
|
|
347
|
+
"""
|
|
348
|
+
Find a port to open for the redirect web server.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
portRange (list[int]):
|
|
352
|
+
A list that contains a starting port number (index 0) and the ending
|
|
353
|
+
port number (index 1) to check.
|
|
354
|
+
Default port range is 8080 - 8180.
|
|
355
|
+
|
|
356
|
+
If portRange argument is null, then the default range will be used.
|
|
357
|
+
If only one port number is passed, then only that port will be checked.
|
|
358
|
+
"""
|
|
359
|
+
_logsi.LogVerbose('Finding an available local port for the temporary server; port range to check: "%s"' % (str(portRange)))
|
|
360
|
+
|
|
361
|
+
# default starting and ending ports.
|
|
362
|
+
portStart:int = 8080
|
|
363
|
+
portStop = portStart + 100
|
|
364
|
+
localhostIp:str = '127.0.0.1'
|
|
365
|
+
|
|
366
|
+
# validations.
|
|
367
|
+
if portRange is None:
|
|
368
|
+
pass
|
|
369
|
+
elif len(portRange) == 1:
|
|
370
|
+
portStart = int(portRange[0])
|
|
371
|
+
portStop = portStart
|
|
372
|
+
elif len(portRange) == 2:
|
|
373
|
+
portStart = int(portRange[0])
|
|
374
|
+
portStop = int(portRange[1])
|
|
375
|
+
|
|
376
|
+
# find an available port within the specified range of ports.
|
|
377
|
+
for port in range(portStart, portStop):
|
|
378
|
+
|
|
379
|
+
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
|
380
|
+
try:
|
|
381
|
+
sock.bind((localhostIp, port))
|
|
382
|
+
sock.listen(1)
|
|
383
|
+
except socket.error:
|
|
384
|
+
is_open = False
|
|
385
|
+
else:
|
|
386
|
+
is_open = True
|
|
387
|
+
|
|
388
|
+
# if port is open for listening, then use it.
|
|
389
|
+
if is_open:
|
|
390
|
+
return port
|
|
391
|
+
|
|
392
|
+
# if we could not find a port, then it's a problem!
|
|
393
|
+
raise ConnectionError('Could not find an open port on localhost address "%s", in the range of "%s" to "%s"' % (localhostIp, portStart, portStop))
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@staticmethod
|
|
397
|
+
def HasTokenForKey(
|
|
398
|
+
clientId:str=None,
|
|
399
|
+
tokenProviderId:str=None,
|
|
400
|
+
tokenProfileId:str=None,
|
|
401
|
+
tokenStorageDir:str=None,
|
|
402
|
+
tokenStorageFile:str=None,
|
|
403
|
+
) -> bool:
|
|
404
|
+
"""
|
|
405
|
+
Checks if a token exists in the token storage file for the ProviderId / ClientId key.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
clientId (str):
|
|
409
|
+
The unique identifier of the application.
|
|
410
|
+
tokenProviderId (str):
|
|
411
|
+
Provider identifier used when storing the token to disk.
|
|
412
|
+
tokenProfileId (str):
|
|
413
|
+
Profile identifier used when storing the token to disk.
|
|
414
|
+
tokenStorageDir (str):
|
|
415
|
+
The directory path that will contain the `tokens.json` file.
|
|
416
|
+
A null value will default to the platform specific storage location:
|
|
417
|
+
Example for Windows OS = `C:\\ProgramData\\SpotifyWebApiPython`
|
|
418
|
+
tokenStorageFile (str):
|
|
419
|
+
The filename and extension of the Token Cache file.
|
|
420
|
+
Default is `tokens.json`.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
True if the token was found in the token storage file for the
|
|
424
|
+
specified ProviderId and ClientId key; otherwise, null.
|
|
425
|
+
"""
|
|
426
|
+
apiMethodName:str = 'HasTokenForKey'
|
|
427
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
|
|
431
|
+
# trace.
|
|
432
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
433
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Checking if token exists in token storage file", apiMethodParms)
|
|
434
|
+
|
|
435
|
+
# verify token storage directory exists.
|
|
436
|
+
if tokenStorageDir is None:
|
|
437
|
+
tokenStorageDir = platformdirs.site_config_dir('SpotifyWebApiPython', ensure_exists=True, appauthor=False)
|
|
438
|
+
|
|
439
|
+
# verify token storage filename was specified.
|
|
440
|
+
if tokenStorageFile is None:
|
|
441
|
+
tokenStorageFile = 'tokens.json'
|
|
442
|
+
|
|
443
|
+
# if token clientId not set, then default to shared.
|
|
444
|
+
if clientId is None:
|
|
445
|
+
clientId = 'Shared'
|
|
446
|
+
|
|
447
|
+
# if token providerId not set, then default to shared.
|
|
448
|
+
if tokenProviderId is None:
|
|
449
|
+
tokenProviderId = 'Shared'
|
|
450
|
+
|
|
451
|
+
# if token profileId not set, then default to shared.
|
|
452
|
+
if tokenProfileId is None:
|
|
453
|
+
tokenProfileId = 'Shared'
|
|
454
|
+
|
|
455
|
+
# formulate token storage path.
|
|
456
|
+
tokenStoragePath:str = os.path.join(tokenStorageDir, tokenStorageFile)
|
|
457
|
+
|
|
458
|
+
# formulate token storage key.
|
|
459
|
+
tokenKey:str = f'{tokenProviderId}/{clientId}/{tokenProfileId}'
|
|
460
|
+
_logsi.LogVerbose('Token storage key: "%s"' % (tokenKey))
|
|
461
|
+
_logsi.LogVerbose('Token storage file path: "%s"' % (tokenStoragePath))
|
|
462
|
+
|
|
463
|
+
# does the token storage file exist?
|
|
464
|
+
if os.path.exists(tokenStoragePath):
|
|
465
|
+
|
|
466
|
+
# open the token storage file, and load it's contents.
|
|
467
|
+
_logsi.LogVerbose('Opening token storage file')
|
|
468
|
+
with open(tokenStoragePath, 'r') as f:
|
|
469
|
+
|
|
470
|
+
tokens = json.load(f)
|
|
471
|
+
|
|
472
|
+
# return if token key exists or not.
|
|
473
|
+
if tokenKey in tokens:
|
|
474
|
+
_logsi.LogDictionary(SILevel.Verbose, 'Token was found in token storage file for key: "%s"' % (tokenKey), tokens[tokenKey], prettyPrint=True)
|
|
475
|
+
return True
|
|
476
|
+
|
|
477
|
+
# indicate token was not found.
|
|
478
|
+
_logsi.LogVerbose('Token was not found in token storage file for key: "%s"' % (tokenKey))
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
except Exception as ex:
|
|
482
|
+
|
|
483
|
+
# trace.
|
|
484
|
+
_logsi.LogException('Could not verify OAuth2 Token from token storage file: "%s"' % (tokenStoragePath), ex)
|
|
485
|
+
raise
|
|
486
|
+
|
|
487
|
+
finally:
|
|
488
|
+
|
|
489
|
+
# trace.
|
|
490
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _LoadToken(self) -> dict:
|
|
494
|
+
"""
|
|
495
|
+
Loads a token from the token storage file for the ProviderId / ClientId key.
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
A token dictionary, if one was found in the token storage file for the
|
|
499
|
+
specified ProviderId and ClientId key; otherwise, null.
|
|
500
|
+
"""
|
|
501
|
+
apiMethodName:str = '_LoadToken'
|
|
502
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
|
|
506
|
+
# trace.
|
|
507
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
508
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Loading Token from token storage", apiMethodParms)
|
|
509
|
+
|
|
510
|
+
# if we don't have a clientId then don't bother.
|
|
511
|
+
if not self._Session.client_id:
|
|
512
|
+
_logsi.LogVerbose('Client ID is not set - nothing to do')
|
|
513
|
+
return
|
|
514
|
+
|
|
515
|
+
# formulate token storage key.
|
|
516
|
+
tokenKey:str = f'{self._TokenProviderId}/{self._Session.client_id}/{self._TokenProfileId}'
|
|
517
|
+
_logsi.LogVerbose('Token storage key: "%s"' % (tokenKey))
|
|
518
|
+
_logsi.LogVerbose('Token storage file path: "%s"' % (self._TokenStoragePath))
|
|
519
|
+
|
|
520
|
+
# does the token storage file exist?
|
|
521
|
+
if os.path.exists(self._TokenStoragePath):
|
|
522
|
+
|
|
523
|
+
# open the token storage file, and load it's contents.
|
|
524
|
+
_logsi.LogVerbose('Opening token storage file')
|
|
525
|
+
with open(self._TokenStoragePath, 'r') as f:
|
|
526
|
+
|
|
527
|
+
tokens = json.load(f)
|
|
528
|
+
|
|
529
|
+
# if token key exists then load the token.
|
|
530
|
+
if tokenKey in tokens:
|
|
531
|
+
_logsi.LogDictionary(SILevel.Verbose, 'Token was loaded from token storage file for key: "%s"' % (tokenKey), tokens[tokenKey], prettyPrint=True)
|
|
532
|
+
return tokens[tokenKey]
|
|
533
|
+
else:
|
|
534
|
+
_logsi.LogVerbose('Token was not found in token storage file for key: "%s"' % (tokenKey))
|
|
535
|
+
|
|
536
|
+
except Exception as ex:
|
|
537
|
+
|
|
538
|
+
# trace.
|
|
539
|
+
_logsi.LogException('Could not load OAuth2 Token from token storage file: "%s"' % (self._TokenStoragePath), ex)
|
|
540
|
+
raise
|
|
541
|
+
|
|
542
|
+
finally:
|
|
543
|
+
|
|
544
|
+
# trace.
|
|
545
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _SaveToken(self, token:dict=None) -> None:
|
|
549
|
+
"""
|
|
550
|
+
Saves a token to the token storage file (e.g. disk) for the ProviderId / ClientId key.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
token (dict):
|
|
554
|
+
The token dictionary object to save.
|
|
555
|
+
Specify null to remove the token for the specified ProviderId / ClientId.
|
|
556
|
+
|
|
557
|
+
Raises:
|
|
558
|
+
IOError:
|
|
559
|
+
If an error occurs saving the tokens file.
|
|
560
|
+
"""
|
|
561
|
+
apiMethodName:str = '_SaveToken'
|
|
562
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
563
|
+
|
|
564
|
+
try:
|
|
565
|
+
|
|
566
|
+
# trace.
|
|
567
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
568
|
+
apiMethodParms.AppendKeyValue("token", token)
|
|
569
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Saving token to token storage", apiMethodParms)
|
|
570
|
+
_logsi.LogDictionary(SILevel.Verbose, "Token to save (pretty print)", token, prettyPrint=True)
|
|
571
|
+
|
|
572
|
+
# formulate token storage key.
|
|
573
|
+
tokenKey:str = f'{self._TokenProviderId}/{self._Session.client_id}/{self._TokenProfileId}'
|
|
574
|
+
_logsi.LogVerbose('Token storage key: "%s"' % (tokenKey))
|
|
575
|
+
_logsi.LogVerbose('Token storage file path: "%s"' % (self._TokenStoragePath))
|
|
576
|
+
|
|
577
|
+
tokens:dict = {}
|
|
578
|
+
|
|
579
|
+
# does the token storage file exist?
|
|
580
|
+
if os.path.exists(self._TokenStoragePath):
|
|
581
|
+
|
|
582
|
+
# open the token storage file, and load it's contents.
|
|
583
|
+
_logsi.LogVerbose('Loading token storage file contents')
|
|
584
|
+
with open(self._TokenStoragePath, 'r') as f:
|
|
585
|
+
tokens = json.load(f)
|
|
586
|
+
|
|
587
|
+
if token is None:
|
|
588
|
+
|
|
589
|
+
# if token not specified, then remove the existing token for the providerId / clientId.
|
|
590
|
+
_logsi.LogVerbose('Removing token from token storage file')
|
|
591
|
+
if tokenKey in tokens:
|
|
592
|
+
del tokens[tokenKey]
|
|
593
|
+
|
|
594
|
+
else:
|
|
595
|
+
|
|
596
|
+
# store the token for the providerId / clientId.
|
|
597
|
+
_logsi.LogVerbose('Storing token in token storage file')
|
|
598
|
+
tokens[tokenKey] = token
|
|
599
|
+
|
|
600
|
+
# save the token storage file changes.
|
|
601
|
+
_logsi.LogVerbose('Saving token storage file updates')
|
|
602
|
+
with open(self._TokenStoragePath, 'w') as f:
|
|
603
|
+
json.dump(tokens, f, indent=4, sort_keys=True)
|
|
604
|
+
|
|
605
|
+
except Exception as ex:
|
|
606
|
+
|
|
607
|
+
# trace.
|
|
608
|
+
_logsi.LogException('Could not store Token in the token storage file', ex)
|
|
609
|
+
raise
|
|
610
|
+
|
|
611
|
+
finally:
|
|
612
|
+
|
|
613
|
+
# trace.
|
|
614
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def authorization_url(self, **kwargs):
|
|
618
|
+
"""
|
|
619
|
+
Constructs the authorization url, complete with all querystring parameters.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
**kwargs
|
|
623
|
+
Keyword arguments for the authorization type.
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
The authorization url querystring and a generated state value.
|
|
627
|
+
"""
|
|
628
|
+
_logsi.LogVerbose('Creating OAuth2 authorization url')
|
|
629
|
+
|
|
630
|
+
# according to the PKCE standard, a code verifier is a high-entropy cryptographic
|
|
631
|
+
# random string with a length between 43 and 128 characters (the longer the better).
|
|
632
|
+
# it can contain letters, digits, underscores, periods, hyphens, or tildes.
|
|
633
|
+
codeVerifierLength:int = 128
|
|
634
|
+
codeVerifier:str = secrets.token_urlsafe(codeVerifierLength)[0:codeVerifierLength]
|
|
635
|
+
_logsi.LogText(SILevel.Verbose, 'OAuth2 code verifier (urlsafe, %d bytes): %s' % (codeVerifierLength, codeVerifier), codeVerifier)
|
|
636
|
+
self._CodeVerifier = codeVerifier
|
|
637
|
+
|
|
638
|
+
# once the code verifier has been generated, we must transform (hash) it using the SHA256 algorithm.
|
|
639
|
+
codeHash = hashlib.sha256(codeVerifier.encode('utf-8'))
|
|
640
|
+
_logsi.LogText(SILevel.Verbose, 'OAuth2 code verifier SHA256 hashed bytes: %s' % (codeHash.hexdigest()), codeHash.hexdigest())
|
|
641
|
+
|
|
642
|
+
# we will then convert the hash value to a base64 encoded string.
|
|
643
|
+
# this is the value that will be sent within the user authorization request.
|
|
644
|
+
codeChallengeBytes = base64.urlsafe_b64encode(codeHash.digest())
|
|
645
|
+
codeChallenge = codeChallengeBytes.decode('utf-8').rstrip('=') # drop '=' padding characters
|
|
646
|
+
_logsi.LogText(SILevel.Verbose, 'OAuth2 code challenge url-safe BASE64 encoded string: %s' % (codeChallenge), codeChallenge)
|
|
647
|
+
kwargs.setdefault("code_challenge_method", "S256")
|
|
648
|
+
kwargs.setdefault("code_challenge", codeChallenge)
|
|
649
|
+
|
|
650
|
+
# state is an opaque value that is used to prevent cross-site request forgery.
|
|
651
|
+
stateLength:int = 16
|
|
652
|
+
state:str = secrets.token_urlsafe(stateLength)[0:stateLength]
|
|
653
|
+
_logsi.LogText(SILevel.Verbose, 'OAuth2 state value (urlsafe, %d bytes): %s' % (stateLength, state), state)
|
|
654
|
+
|
|
655
|
+
# create the authorization URL.
|
|
656
|
+
url, state = self._Session.authorization_url(self._AuthorizationUrl, state, **kwargs)
|
|
657
|
+
|
|
658
|
+
# trace.
|
|
659
|
+
_logsi.LogText(SILevel.Verbose, 'OAuth2 authorization url: "%s"' % (url), url)
|
|
660
|
+
return url, state
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def FetchToken(self, **kwargs) -> dict:
|
|
664
|
+
"""
|
|
665
|
+
Fetch an access token from the token endpoint.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
**kwargs:
|
|
669
|
+
Additional keyword arguments to add to the
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
A token dictionary.
|
|
673
|
+
"""
|
|
674
|
+
apiMethodName:str = 'FetchToken'
|
|
675
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
676
|
+
|
|
677
|
+
try:
|
|
678
|
+
|
|
679
|
+
# trace.
|
|
680
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
681
|
+
apiMethodParms.AppendKeyValue("self._TokenUrl", self._TokenUrl)
|
|
682
|
+
apiMethodParms.AppendKeyValue("self._ClientId", self._ClientId)
|
|
683
|
+
apiMethodParms.AppendKeyValue("self._ClientSecret", self._ClientSecret)
|
|
684
|
+
apiMethodParms.AppendKeyValue("self._CodeVerifier", self._CodeVerifier)
|
|
685
|
+
apiMethodParms.AppendKeyValue("**kwargs", kwargs)
|
|
686
|
+
_logsi.LogMethodParmList(SILevel.Verbose, 'Fetching access token for authorization type "%s"' % self._AuthorizationType, apiMethodParms)
|
|
687
|
+
|
|
688
|
+
# if client secret specified then include it.
|
|
689
|
+
includeClientId:bool = False
|
|
690
|
+
if self._ClientId is not None:
|
|
691
|
+
includeClientId = True
|
|
692
|
+
|
|
693
|
+
# if code verifier specified then include it so that the code challenge is verified.
|
|
694
|
+
if self._CodeVerifier is not None:
|
|
695
|
+
kwargs.setdefault("code_verifier", self._CodeVerifier)
|
|
696
|
+
|
|
697
|
+
# fetch the authorization token.
|
|
698
|
+
# this will also automatically set the 'self._Session.token' instance.
|
|
699
|
+
token = self._Session.fetch_token(self._TokenUrl,
|
|
700
|
+
include_client_id=includeClientId,
|
|
701
|
+
client_secret=self._ClientSecret,
|
|
702
|
+
**kwargs)
|
|
703
|
+
|
|
704
|
+
# trace.
|
|
705
|
+
_logsi.LogDictionary(SILevel.Verbose, 'Access token was successfully fetched', token, prettyPrint=True)
|
|
706
|
+
|
|
707
|
+
# save the new token to disk.
|
|
708
|
+
self._SaveToken(token)
|
|
709
|
+
|
|
710
|
+
# return new token to the caller.
|
|
711
|
+
return token
|
|
712
|
+
|
|
713
|
+
except AccessDeniedError as ex:
|
|
714
|
+
|
|
715
|
+
_logsi.LogException('AccessDenied Error: normally, this indicates the user cancelled the login request', ex)
|
|
716
|
+
raise
|
|
717
|
+
|
|
718
|
+
except Exception as ex:
|
|
719
|
+
|
|
720
|
+
# trace.
|
|
721
|
+
_logsi.LogException('Could not fetch Token', ex)
|
|
722
|
+
raise
|
|
723
|
+
|
|
724
|
+
finally:
|
|
725
|
+
|
|
726
|
+
# trace.
|
|
727
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def HasScopeChanged(self, token:dict, scope:str=None) -> bool:
|
|
731
|
+
"""
|
|
732
|
+
Indicates whether the scope has changed between this session and the authorization
|
|
733
|
+
access token scope values.
|
|
734
|
+
|
|
735
|
+
Args:
|
|
736
|
+
token (dict):
|
|
737
|
+
The authorization access token dictionary to check the scope of.
|
|
738
|
+
scope (str | list[str]):
|
|
739
|
+
A space-delimited list of scope identifiers you wish to request access to.
|
|
740
|
+
|
|
741
|
+
If True, you need the user to go through the OAuth authentication dance before
|
|
742
|
+
OAuth-protected requests to the resource will succeed.
|
|
743
|
+
|
|
744
|
+
If False, then you can reasonably expect OAuth-protected requests to the resource to succeed.
|
|
745
|
+
"""
|
|
746
|
+
# if token not set then don't bother - indicate scope has changed.
|
|
747
|
+
if token is None:
|
|
748
|
+
return True
|
|
749
|
+
|
|
750
|
+
arrScopeSession:list[str] = []
|
|
751
|
+
arrScopeToken:list[str] = []
|
|
752
|
+
|
|
753
|
+
# get scope values for session and token.
|
|
754
|
+
# we will convert them to array and sort them before comparing for differences.
|
|
755
|
+
if scope is not None:
|
|
756
|
+
arrScopeSession = scope
|
|
757
|
+
if isinstance(arrScopeSession, str):
|
|
758
|
+
arrScopeSession = arrScopeSession.split(' ')
|
|
759
|
+
arrScopeSession.sort()
|
|
760
|
+
|
|
761
|
+
if token is not None and isinstance(token, dict):
|
|
762
|
+
arrScopeToken = token.get('scope', None) or []
|
|
763
|
+
if isinstance(arrScopeToken, str):
|
|
764
|
+
arrScopeToken = arrScopeSession.split(' ')
|
|
765
|
+
arrScopeToken.sort()
|
|
766
|
+
|
|
767
|
+
# get string values for both scope's for comparison.
|
|
768
|
+
strScopeToken = ' '.join(arrScopeToken)
|
|
769
|
+
strScopeSession = ' '.join(arrScopeSession)
|
|
770
|
+
|
|
771
|
+
# does the token scope match the requested session scope?
|
|
772
|
+
if strScopeToken != strScopeSession:
|
|
773
|
+
|
|
774
|
+
_logsi.LogVerbose('Token scope change detected, forcing authorization access; Token scope="%s", Session scope="%s"' % (strScopeToken, strScopeSession))
|
|
775
|
+
return True
|
|
776
|
+
|
|
777
|
+
# indicate scope has not changed.
|
|
778
|
+
return False
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def AuthorizeWithConsole(
|
|
782
|
+
self,
|
|
783
|
+
authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE,
|
|
784
|
+
open_browser=True,
|
|
785
|
+
code_message=_DEFAULT_AUTH_CODE_MESSAGE,
|
|
786
|
+
token_audience=None,
|
|
787
|
+
force: bool = False,
|
|
788
|
+
token_test: Optional[Callable] = None,
|
|
789
|
+
**kwargs
|
|
790
|
+
):
|
|
791
|
+
"""
|
|
792
|
+
Executes the authorization flow without starting a web server.
|
|
793
|
+
|
|
794
|
+
Note that you must have 'urn:ietf:wg:oauth:2.0:oob' as a redirect URI value
|
|
795
|
+
in the provider app settings for this to work.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
authorization_prompt_message (str | None):
|
|
799
|
+
The message to display that will inform the user to navigate to the
|
|
800
|
+
authorization URL. If null, then no message is displayed.
|
|
801
|
+
open_browser (bool):
|
|
802
|
+
True to open the authorization URL in the user's browser; otherwise, False
|
|
803
|
+
to not open a browser.
|
|
804
|
+
code_message (str):
|
|
805
|
+
The message to display in the console prompting the user to enter the authorization code.
|
|
806
|
+
token_audience (str):
|
|
807
|
+
Passed along with the request for an access token.
|
|
808
|
+
It determines the endpoints with which the token can be used.
|
|
809
|
+
Default is null.
|
|
810
|
+
force (bool):
|
|
811
|
+
True to authorize, even if we already have a token; otherwise, False
|
|
812
|
+
to only authorize if the token is not authorized, has expired, or the
|
|
813
|
+
scope has changed.
|
|
814
|
+
token_test (Callable):
|
|
815
|
+
Function that receives this object for a param, makes a call, and returns
|
|
816
|
+
the response.
|
|
817
|
+
kwargs:
|
|
818
|
+
Additional keyword arguments passed through to `authorization_url`.
|
|
819
|
+
|
|
820
|
+
Returns:
|
|
821
|
+
The OAuth 2.0 credentials for the user.
|
|
822
|
+
"""
|
|
823
|
+
apiMethodName:str = 'AuthorizeWithServer'
|
|
824
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
825
|
+
|
|
826
|
+
wsgiServer:WSGIServer = None
|
|
827
|
+
|
|
828
|
+
try:
|
|
829
|
+
|
|
830
|
+
# trace.
|
|
831
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
832
|
+
apiMethodParms.AppendKeyValue("authorization_prompt_message", authorization_prompt_message)
|
|
833
|
+
apiMethodParms.AppendKeyValue("open_browser", open_browser)
|
|
834
|
+
apiMethodParms.AppendKeyValue("code_message", code_message)
|
|
835
|
+
apiMethodParms.AppendKeyValue("token_audience", token_audience)
|
|
836
|
+
apiMethodParms.AppendKeyValue("force", force)
|
|
837
|
+
apiMethodParms.AppendKeyValue("token_test", token_test)
|
|
838
|
+
apiMethodParms.AppendKeyValue("**kwargs", kwargs)
|
|
839
|
+
_logsi.LogMethodParmList(SILevel.Verbose, 'Executing authorization flow using local console for authorization type "%s"' % self._AuthorizationType, apiMethodParms)
|
|
840
|
+
|
|
841
|
+
# is the access token authorized? if so, then we are done.
|
|
842
|
+
if self._CheckAuthorization(force, token_test):
|
|
843
|
+
_logsi.LogVerbose('Token is authorized; nothing else to do')
|
|
844
|
+
return self
|
|
845
|
+
|
|
846
|
+
self._Session.redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
|
|
847
|
+
auth_url, _ = self.authorization_url(**kwargs)
|
|
848
|
+
|
|
849
|
+
if open_browser:
|
|
850
|
+
_logsi.LogVerbose('Opening a browser window (or tab) to the authorization request url')
|
|
851
|
+
webbrowser.open(auth_url, new=2, autoraise=True)
|
|
852
|
+
|
|
853
|
+
if authorization_prompt_message:
|
|
854
|
+
message:str = authorization_prompt_message.format(url=auth_url)
|
|
855
|
+
_logsi.LogWarning(message, logToSystemLogger=True)
|
|
856
|
+
|
|
857
|
+
# prompt the user (in the console) to enter the authorization code response.
|
|
858
|
+
_logsi.LogVerbose('Waiting for a console response from the user for the authorization request response code')
|
|
859
|
+
while True:
|
|
860
|
+
auth_code = input(code_message).strip()
|
|
861
|
+
if auth_code:
|
|
862
|
+
break
|
|
863
|
+
|
|
864
|
+
# fetch the newly issued authorization token.
|
|
865
|
+
self.FetchToken(code=auth_code, audience=token_audience)
|
|
866
|
+
|
|
867
|
+
# return to caller.
|
|
868
|
+
return self
|
|
869
|
+
|
|
870
|
+
except Exception as ex:
|
|
871
|
+
|
|
872
|
+
# trace.
|
|
873
|
+
_logsi.LogException('Authorize with console exception', ex)
|
|
874
|
+
raise
|
|
875
|
+
|
|
876
|
+
finally:
|
|
877
|
+
|
|
878
|
+
# trace.
|
|
879
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def AuthorizeWithServer(
|
|
883
|
+
self,
|
|
884
|
+
host: Optional[str] = None,
|
|
885
|
+
bind_addr: Optional[int] = None,
|
|
886
|
+
port: Union[int, list[int]] = 8080,
|
|
887
|
+
authorization_prompt_message: Optional[str] = _DEFAULT_AUTH_PROMPT_MESSAGE,
|
|
888
|
+
success_message: str = _DEFAULT_WEB_SUCCESS_MESSAGE,
|
|
889
|
+
open_browser: bool = True,
|
|
890
|
+
redirect_uri_path: str = '/',
|
|
891
|
+
timeout_seconds: Optional[int] = 120,
|
|
892
|
+
token_audience: Optional[str] = None,
|
|
893
|
+
force: bool = False,
|
|
894
|
+
token_test: Optional[Callable] = None,
|
|
895
|
+
**kwargs
|
|
896
|
+
):
|
|
897
|
+
"""
|
|
898
|
+
Executes the authorization flow by starting a temporary local web server
|
|
899
|
+
to listen for the authorization response.
|
|
900
|
+
|
|
901
|
+
Note that you must have 'http://localhost:8080/' as a redirect URI value
|
|
902
|
+
in the provider app settings for this to work.
|
|
903
|
+
|
|
904
|
+
The server strategy instructs the user to open the authorization URL in
|
|
905
|
+
their browser and will attempt to automatically open the URL for them.
|
|
906
|
+
It will start a local web server to listen for the authorization
|
|
907
|
+
response. Once authorization is complete the authorization server will
|
|
908
|
+
redirect the user's browser to the local web server. The web server
|
|
909
|
+
will get the authorization code from the response and shutdown. The
|
|
910
|
+
code is then exchanged for a token.
|
|
911
|
+
|
|
912
|
+
Args:
|
|
913
|
+
host (str):
|
|
914
|
+
The hostname for the local redirect server. This will be served
|
|
915
|
+
over http, not https.
|
|
916
|
+
bind_addr (str):
|
|
917
|
+
Optionally provide an ip address for the redirect server to listen
|
|
918
|
+
on when it is not the same as host (e.g. in a container).
|
|
919
|
+
Default value is None, which means that the redirect server will listen
|
|
920
|
+
on the ip address specified in the host parameter.
|
|
921
|
+
port (int / list[int]):
|
|
922
|
+
The port for the local redirect server.
|
|
923
|
+
If a list, it would find the first open port in the range.
|
|
924
|
+
authorization_prompt_message (str | None):
|
|
925
|
+
The message to display that will inform the user to navigate to the
|
|
926
|
+
authorization URL. If null, then no message is displayed.
|
|
927
|
+
success_message (str):
|
|
928
|
+
The message to display in the web browser that the authorization flow
|
|
929
|
+
is complete.
|
|
930
|
+
open_browser (bool):
|
|
931
|
+
True to open the authorization URL in the user's browser; otherwise, False
|
|
932
|
+
to not open a browser.
|
|
933
|
+
redirect_uri_path (str):
|
|
934
|
+
Path value to add when constructing the redirect_uri; otherwise,
|
|
935
|
+
None to not add a path value.
|
|
936
|
+
Default value is '/'.
|
|
937
|
+
timeout_seconds (int):
|
|
938
|
+
If set, an error will be raised after the timeout value if the user did not
|
|
939
|
+
respond to the authorization request. The value is in seconds.
|
|
940
|
+
When set to None there is no timeout.
|
|
941
|
+
Default value is 120 (2 minutes).
|
|
942
|
+
token_audience (str):
|
|
943
|
+
Passed along with the request for an access token.
|
|
944
|
+
It determines the endpoints with which the token can be used.
|
|
945
|
+
Default is null.
|
|
946
|
+
force (bool):
|
|
947
|
+
True to authorize, even if we already have a token; otherwise, False
|
|
948
|
+
to only authorize if the token is not authorized, has expired, or the
|
|
949
|
+
scope has changed.
|
|
950
|
+
token_test (Callable):
|
|
951
|
+
Function that receives this object for a param, makes a call, and returns
|
|
952
|
+
the response.
|
|
953
|
+
kwargs:
|
|
954
|
+
Additional keyword arguments passed through to `authorization_url`.
|
|
955
|
+
|
|
956
|
+
Returns:
|
|
957
|
+
The OAuth 2.0 credentials for the user.
|
|
958
|
+
"""
|
|
959
|
+
apiMethodName:str = 'AuthorizeWithServer'
|
|
960
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
961
|
+
|
|
962
|
+
wsgiServer:WSGIServer = None
|
|
963
|
+
|
|
964
|
+
try:
|
|
965
|
+
|
|
966
|
+
# trace.
|
|
967
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
968
|
+
apiMethodParms.AppendKeyValue("host", host)
|
|
969
|
+
apiMethodParms.AppendKeyValue("bind_addr", bind_addr)
|
|
970
|
+
apiMethodParms.AppendKeyValue("port", port)
|
|
971
|
+
apiMethodParms.AppendKeyValue("authorization_prompt_message", authorization_prompt_message)
|
|
972
|
+
apiMethodParms.AppendKeyValue("success_message", success_message)
|
|
973
|
+
apiMethodParms.AppendKeyValue("open_browser", open_browser)
|
|
974
|
+
apiMethodParms.AppendKeyValue("redirect_uri_path", redirect_uri_path)
|
|
975
|
+
apiMethodParms.AppendKeyValue("timeout_seconds", timeout_seconds)
|
|
976
|
+
apiMethodParms.AppendKeyValue("token_audience", token_audience)
|
|
977
|
+
apiMethodParms.AppendKeyValue("force", force)
|
|
978
|
+
apiMethodParms.AppendKeyValue("token_test", token_test)
|
|
979
|
+
apiMethodParms.AppendKeyValue("**kwargs", kwargs)
|
|
980
|
+
_logsi.LogMethodParmList(SILevel.Verbose, 'Executing authorization flow using local web server for authorization type "%s"' % self._AuthorizationType, apiMethodParms)
|
|
981
|
+
|
|
982
|
+
# is the access token authorized? if so, then we are done.
|
|
983
|
+
if self._CheckAuthorization(force, token_test):
|
|
984
|
+
_logsi.LogVerbose('Token is authorized; nothing else to do')
|
|
985
|
+
return self
|
|
986
|
+
|
|
987
|
+
# if a port range was passed, then find an available port in that range.
|
|
988
|
+
if isinstance(port, list):
|
|
989
|
+
port = self._FindOpenPortOnLocalhost(port)
|
|
990
|
+
|
|
991
|
+
# default the local redirect server host if one was not specified.
|
|
992
|
+
if not host:
|
|
993
|
+
host = self._DefaultLocalHost
|
|
994
|
+
|
|
995
|
+
# initialize and create a local Web Server Gateway Interface (WSGI) server.
|
|
996
|
+
_logsi.LogVerbose('Creating temporary WSGI local web server to handle the OAuth2 authorization request response')
|
|
997
|
+
wsgiApp:_WSGIAppRedirectHandler = _WSGIAppRedirectHandler(success_message)
|
|
998
|
+
WSGIServer.allow_reuse_address = False # Fail fast if the address is occupied
|
|
999
|
+
wsgiServer:WSGIServer = WSGIMakeServer(bind_addr or host,
|
|
1000
|
+
port,
|
|
1001
|
+
wsgiApp,
|
|
1002
|
+
handler_class=_WSGIRequestHandler
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
# construct the redirect uri, which is where the user is redirected after
|
|
1006
|
+
# authentication success or failure.
|
|
1007
|
+
redirectUri:str = 'http://{}:{}'.format(host, wsgiServer.server_port)
|
|
1008
|
+
if redirect_uri_path is not None:
|
|
1009
|
+
redirectUri = redirectUri + redirect_uri_path
|
|
1010
|
+
self._Session.redirect_uri = redirectUri
|
|
1011
|
+
|
|
1012
|
+
# construct the authorization url and state values.
|
|
1013
|
+
auth_url, _ = self.authorization_url(**kwargs)
|
|
1014
|
+
|
|
1015
|
+
# open a new tab page using the local default browser.
|
|
1016
|
+
if open_browser:
|
|
1017
|
+
_logsi.LogVerbose('Opening a browser window (or tab) to the authorization request url')
|
|
1018
|
+
webbrowser.open(auth_url, new=2, autoraise=True)
|
|
1019
|
+
|
|
1020
|
+
if authorization_prompt_message:
|
|
1021
|
+
message:str = authorization_prompt_message.format(url=auth_url)
|
|
1022
|
+
_logsi.LogWarning(message, logToSystemLogger=True)
|
|
1023
|
+
|
|
1024
|
+
# set the timeout value, and handle the user authorization request response.
|
|
1025
|
+
# the 'handle_request' call will block until a response is received, or a timeout occurs.
|
|
1026
|
+
_logsi.LogVerbose('WSGI server will now wait for a response from the user for the authorization request url (timeout=%s seconds)' % timeout_seconds)
|
|
1027
|
+
wsgiServer.timeout = timeout_seconds
|
|
1028
|
+
wsgiServer.handle_request()
|
|
1029
|
+
|
|
1030
|
+
# get the user authorization request response value.
|
|
1031
|
+
authResponse:str = wsgiApp.LastRequestUri
|
|
1032
|
+
if authResponse is None:
|
|
1033
|
+
raise AccessDeniedError('An authorization response was not returned, which usually indicates the server timed out waiting for a response from the user to authorize the request')
|
|
1034
|
+
|
|
1035
|
+
# force https in case it needs it, as OAuth 2.0 can only occur over https.
|
|
1036
|
+
authResponse = authResponse.replace("http", "https")
|
|
1037
|
+
_logsi.LogText(SILevel.Verbose, 'WSGI server intercepted the user authorization response: "%s"' % (authResponse), authResponse)
|
|
1038
|
+
|
|
1039
|
+
# fetch the newly issued authorization token.
|
|
1040
|
+
self.FetchToken(authorization_response=authResponse,
|
|
1041
|
+
audience=token_audience
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
# return to caller.
|
|
1045
|
+
return self
|
|
1046
|
+
|
|
1047
|
+
except Exception as ex:
|
|
1048
|
+
|
|
1049
|
+
# trace.
|
|
1050
|
+
_logsi.LogException('Authorize with server exception', ex)
|
|
1051
|
+
raise
|
|
1052
|
+
|
|
1053
|
+
finally:
|
|
1054
|
+
|
|
1055
|
+
# ensure wsgi server is shutdown.
|
|
1056
|
+
if wsgiServer is not None:
|
|
1057
|
+
_logsi.LogVerbose('WSGI server is shutting down and freeing resources.')
|
|
1058
|
+
wsgiServer.server_close()
|
|
1059
|
+
wsgiServer = None
|
|
1060
|
+
|
|
1061
|
+
# trace.
|
|
1062
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def RefreshToken(self, **kwargs) -> dict:
|
|
1066
|
+
"""
|
|
1067
|
+
Refreshes the current session token.
|
|
1068
|
+
|
|
1069
|
+
If a TokenUpdater callable was specified, then the token is refreshed externally by the
|
|
1070
|
+
callable function.
|
|
1071
|
+
|
|
1072
|
+
Otherwise, the session `refresh_token` method is invoked using the refresh token value.
|
|
1073
|
+
|
|
1074
|
+
Args:
|
|
1075
|
+
**kwargs:
|
|
1076
|
+
Additional keyword arguments to include in the token request.
|
|
1077
|
+
|
|
1078
|
+
Returns:
|
|
1079
|
+
A token dictionary.
|
|
1080
|
+
"""
|
|
1081
|
+
token:dict = None
|
|
1082
|
+
|
|
1083
|
+
try:
|
|
1084
|
+
|
|
1085
|
+
# trace.
|
|
1086
|
+
_logsi.LogVerbose("Refreshing OAuth2 authorization token for the \"%s\" authorization type" % self._AuthorizationType, colorValue=SIColors.Gold)
|
|
1087
|
+
|
|
1088
|
+
# was a token updater supplied?
|
|
1089
|
+
if self._TokenUpdater is not None:
|
|
1090
|
+
|
|
1091
|
+
# log trace message if we are waiting on another token update to complete!
|
|
1092
|
+
_logsi.LogVerbose("Preparing to set the lock for external token refresh", colorValue=SIColors.Gold)
|
|
1093
|
+
if self._TokenUpdater_Lock.locked():
|
|
1094
|
+
_logsi.LogVerbose("Waiting on a previous external token refresh to complete", colorValue=SIColors.Red)
|
|
1095
|
+
|
|
1096
|
+
# only allow one thread to update authorization token at a time.
|
|
1097
|
+
with self._TokenUpdater_Lock:
|
|
1098
|
+
|
|
1099
|
+
_logsi.LogVerbose('Calling External Token Updater to refresh the token', colorValue=SIColors.Gold)
|
|
1100
|
+
token = self._TokenUpdater()
|
|
1101
|
+
|
|
1102
|
+
_logsi.LogDictionary(SILevel.Verbose, 'External Token Updater has refreshed the token', token, colorValue=SIColors.Gold)
|
|
1103
|
+
|
|
1104
|
+
# if nothing returned then it's an error.
|
|
1105
|
+
if token is None:
|
|
1106
|
+
raise TokenExpiredError('External Token Updater did not return a token.')
|
|
1107
|
+
|
|
1108
|
+
else:
|
|
1109
|
+
|
|
1110
|
+
# get refresh token from session token.
|
|
1111
|
+
refreshToken:str = None
|
|
1112
|
+
if self._Session is not None and self._Session.token is not None:
|
|
1113
|
+
_logsi.LogDictionary(SILevel.Verbose, 'OAuth2 Session token', self._Session.token)
|
|
1114
|
+
refreshToken = self._Session.token.get('refresh_token')
|
|
1115
|
+
|
|
1116
|
+
# if refresh token is not present, then it's an error.
|
|
1117
|
+
if refreshToken is None:
|
|
1118
|
+
raise TokenExpiredError('Token cannot be refreshed as there is no "refresh_token" key in the session token.')
|
|
1119
|
+
|
|
1120
|
+
# add the clientId if the session has been established.
|
|
1121
|
+
if self._Session.client_id is not None:
|
|
1122
|
+
kwargs.setdefault("client_id", self._Session.client_id)
|
|
1123
|
+
|
|
1124
|
+
_logsi.LogDictionary(SILevel.Verbose, 'Refreshing the OAuth2 session token via url "%s" - kwargs' % self._TokenUrl, kwargs)
|
|
1125
|
+
|
|
1126
|
+
# refresh the authorization token, using it's refresh token value.
|
|
1127
|
+
token = self._Session.refresh_token(self._TokenUrl, refreshToken, **kwargs)
|
|
1128
|
+
|
|
1129
|
+
# store the refresh token to the token storage file.
|
|
1130
|
+
self._SaveToken(token)
|
|
1131
|
+
|
|
1132
|
+
# trace.
|
|
1133
|
+
_logsi.LogVerbose('OAuth2 authorization token was successfully refreshed for the "%s" authorization type' % self._AuthorizationType)
|
|
1134
|
+
|
|
1135
|
+
# return the refreshed authorization token to the caller.
|
|
1136
|
+
return token
|
|
1137
|
+
|
|
1138
|
+
except Exception as ex:
|
|
1139
|
+
|
|
1140
|
+
# trace.
|
|
1141
|
+
#_logsi.LogException('OAuth2 token refresh error: %s' % str(ex), ex)
|
|
1142
|
+
|
|
1143
|
+
# if refresh token was revoked, then remove the token from the token storage file
|
|
1144
|
+
# so that it will force an authorization refresh next time around.
|
|
1145
|
+
if isinstance(ex, InvalidGrantError):
|
|
1146
|
+
if ex.description == 'Refresh token revoked':
|
|
1147
|
+
if self.AuthorizationType in ['OAuth2Token']:
|
|
1148
|
+
_logsi.LogWarning('Refresh token was revoked for authorization type "%s". Ensure that you are saving the refreshed token in the host provider session to avoid this problem; see the "tokenUpdater" argument for more details' % self.AuthorizationType)
|
|
1149
|
+
|
|
1150
|
+
# if internal storage provider, then remove the token from the storage file.
|
|
1151
|
+
if self._TokenUpdater is None:
|
|
1152
|
+
_logsi.LogVerbose('Refresh token error detected for internal storage provider; token will be destroyed', colorValue=SIColors.Gold)
|
|
1153
|
+
self._SaveToken(None)
|
|
1154
|
+
|
|
1155
|
+
# pass exception on thru.
|
|
1156
|
+
raise
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
class _WSGIRequestHandler(WSGIRequestHandler):
|
|
1160
|
+
"""
|
|
1161
|
+
WSGI Request Handler.
|
|
1162
|
+
|
|
1163
|
+
Uses a named logger instead of printing to stderr.
|
|
1164
|
+
"""
|
|
1165
|
+
|
|
1166
|
+
def log_message(self, format, *args) -> None:
|
|
1167
|
+
# pylint: disable=redefined-builtin
|
|
1168
|
+
# (format is the argument name defined in the superclass.)
|
|
1169
|
+
|
|
1170
|
+
# trace.
|
|
1171
|
+
_logsi.LogVerbose(format, *args, logToSystemLogger=False)
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
class _WSGIAppRedirectHandler(object):
|
|
1175
|
+
"""
|
|
1176
|
+
WSGI App that will handle the authorization redirect.
|
|
1177
|
+
|
|
1178
|
+
Stores the request URI and displays the given success message.
|
|
1179
|
+
"""
|
|
1180
|
+
|
|
1181
|
+
def __init__(self, successMessage) -> None:
|
|
1182
|
+
"""
|
|
1183
|
+
Args:
|
|
1184
|
+
successMessage (str):
|
|
1185
|
+
The message to display in the web browser that the authorization flow is complete.
|
|
1186
|
+
"""
|
|
1187
|
+
self._LastRequestUri:str = None
|
|
1188
|
+
self._SuccessMessage = successMessage
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def __call__(self, environ, start_response) -> Iterable[bytes]:
|
|
1192
|
+
"""
|
|
1193
|
+
WSGI Callable.
|
|
1194
|
+
|
|
1195
|
+
Args:
|
|
1196
|
+
environ (Mapping[str, Any]):
|
|
1197
|
+
The WSGI environment.
|
|
1198
|
+
start_response (Callable[str, list]):
|
|
1199
|
+
The WSGI start_response callable.
|
|
1200
|
+
|
|
1201
|
+
Returns:
|
|
1202
|
+
The response body.
|
|
1203
|
+
"""
|
|
1204
|
+
# respond to the authorization server with response code 200 (ok) message,
|
|
1205
|
+
# which indicates that we received their response.
|
|
1206
|
+
start_response("200 OK", [("Content-type", "text/plain; charset=utf-8")])
|
|
1207
|
+
|
|
1208
|
+
# store the full request URI (including the query string) so that it can
|
|
1209
|
+
# be accessed by the caller.
|
|
1210
|
+
self._LastRequestUri = WSGIRequestUri(environ)
|
|
1211
|
+
|
|
1212
|
+
# return authorization flow was completed successfully message.
|
|
1213
|
+
return [self._SuccessMessage.encode("utf-8")]
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
@property
|
|
1217
|
+
def LastRequestUri(self) -> str:
|
|
1218
|
+
"""
|
|
1219
|
+
The full request URI, optionally including the query string.
|
|
1220
|
+
"""
|
|
1221
|
+
return self._LastRequestUri
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
@property
|
|
1225
|
+
def SuccessMessage(self) -> str:
|
|
1226
|
+
"""
|
|
1227
|
+
The message to display in the web browser that the authorization flow is complete.
|
|
1228
|
+
"""
|
|
1229
|
+
return self._SuccessMessage
|