Skip to content

Commit ea3ff2d

Browse files
authored
feat(client): Add multithreading support for refresh token, JWKS and revocation list on background. (#14)
* Add task module for background thread job. * Add bg task to refresh token, jwks & revoke list * Implement health check method. * Update docs for multithreading support. * Add exception handling on background task. * Add background threads healthcheck. * Add logger for background task exception.
1 parent c913218 commit ea3ff2d

11 files changed

Lines changed: 159 additions & 11 deletions

File tree

‎CHANGELOG.rst‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
Changelog
33
=========
44

5+
0.10.0 (2021-11-05)
6+
-------------------
7+
8+
* Add backgroud refresh token, jwks and revocation list.
9+
* Add Flask framework support.
10+
511
0.8.0 (2021-10-22)
612
------------------
713

‎README.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ Features
3636
* Get role and validate permission
3737
* Validate scope, role and audience
3838
* Verify ban, phone and email user status
39+
* Backgroud refresh token, jwks and revocation list.
40+
* Flask framework support.

‎docs/frameworks.rst‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,11 @@ You can customize these default configurations according to your service/apps ne
6464
app.config["IAM_TOKEN_HEADER_TYPE"] = "Bearer"
6565
app.config["IAM_TOKEN_COOKIE_NAME"] = "access_token"
6666
app.config["IAM_TOKEN_COOKIE_PATH"] = "/"
67+
68+
.. note::
69+
This module has been tested with Flask default WSGI server for development.
70+
For production use, this module has been tested with *Gunicorn* and *uWSGI*.
71+
You can use Gunicorn with sync and gthread worker. Since this SDK use multithreading, please
72+
make sure you enable the thread support with **--enable-threads** options when you use uWSGI.
73+
74+
For more information about Flask deployment, please read more information `here <https://flask.palletsprojects.com/en/latest/deploying/>`_

‎docs/iam_python_sdk.rst‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ iam\_python\_sdk\.models module
7676
:undoc-members:
7777
:show-inheritance:
7878

79+
iam\_python\_sdk\.task module
80+
-----------------------------
81+
82+
.. automodule:: iam_python_sdk.task
83+
:members:
84+
:undoc-members:
85+
:show-inheritance:
86+
7987
iam\_python\_sdk\.utils module
8088
------------------------------
8189

‎iam_python_sdk/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
__author__ = """Analytics AccelByte"""
1919
__email__ = 'justice-analytics-team@accelbyte.net'
20-
__version__ = '0.8.0'
20+
__version__ = '0.10.0'
2121
__all__ = [
2222
'NewDefaultClient',
2323
'Config'

‎iam_python_sdk/client.py‎

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@
2121

2222
from .bloom import BloomFilter
2323
from .cache import Cache
24-
from .config import Config
25-
from .config import CLIENT_INFO_EXPIRATION, CLIENT_INFORMATION_PATH, GET_ROLE_PATH, GRANT_PATH, JWKS_PATH, \
26-
MAX_BACKOFF_TIME, REVOCATION_LIST_PATH, SCOPE_SEPARATOR, VERIFY_PATH
24+
from .config import DEFAULT_JWKS_REFRESH_INTERVAL, DEFAULT_REVOCATION_LIST_REFRESH_INTERVAL, Config
25+
from .config import CLIENT_INFO_EXPIRATION, CLIENT_INFORMATION_PATH, DEFAULT_TOKEN_REFRESH_RATE, GET_ROLE_PATH, \
26+
GRANT_PATH, JWKS_PATH, MAX_BACKOFF_TIME, REVOCATION_LIST_PATH, SCOPE_SEPARATOR, VERIFY_PATH
2727
from .errors import ClientTokenGrantError, GetClientInformationError, GetJWKSError, GetRevocationListError, \
2828
GetRolePermissionError, HTTPClientError, InvalidTokenSignatureKeyError, NilClaimError, NoLocalValidationError, \
2929
RefreshAccessTokenError, StartLocalValidationError, TokenRevokedError, UserRevokedError, ValidateAccessTokenError, \
3030
ValidateAndParseClaimsError, ValidateAudienceError, ValidateJWTError, ValidatePermissionError, ValidateScopeError
3131
from .models import ClientInformation, JWTClaims, Permission, RevocationList, Role, TokenResponse
3232
from .log import logger
33+
from .task import Task
3334
from .utils import parse_nanotimestamp
3435

3536

@@ -81,7 +82,9 @@ def __init__(self, config: Config,
8182
httpClient: HttpClient
8283
) -> None:
8384
self._lock = RLock()
85+
self._threads = {}
8486
self._clientAccessToken = ""
87+
self._tokenRefreshActive = False
8588
self._localValidationActive = False
8689
self._jwks = {}
8790
self._revokedUsers = {}
@@ -390,7 +393,12 @@ def ClientTokenGrant(self) -> None:
390393
self._clientAccessToken = token_response.AccessToken
391394
logger.info("token grant success")
392395

393-
# TODO: Background refresh token
396+
if not self._tokenRefreshActive:
397+
self._tokenRefreshActive = True
398+
self._threads["refresh_token"] = Task(
399+
token_response.ExpiresIn * DEFAULT_TOKEN_REFRESH_RATE,
400+
self._refresh_access_token
401+
)
394402

395403
except (json.JSONDecodeError, ValueError) as e:
396404
raise ClientTokenGrantError("unable to unmarshal response body") from e
@@ -410,9 +418,17 @@ def StartLocalValidation(self) -> None:
410418
try:
411419
self._get_jwks()
412420
self._get_revocation_list()
413-
self._localValidationActive = True
414421

415-
# TODO: Background refresh JWKS and Revocation list
422+
if not self._localValidationActive:
423+
self._localValidationActive = True
424+
self._threads["refresh_jwks"] = Task(
425+
DEFAULT_JWKS_REFRESH_INTERVAL,
426+
self._get_jwks
427+
)
428+
self._threads["refresh_revocation"] = Task(
429+
DEFAULT_REVOCATION_LIST_REFRESH_INTERVAL,
430+
self._get_revocation_list
431+
)
416432

417433
except GetJWKSError as e:
418434
raise StartLocalValidationError("unable to get JWKS") from e
@@ -641,7 +657,33 @@ def HealthCheck(self) -> bool:
641657
Returns:
642658
bool: health status
643659
"""
644-
return False
660+
with self._lock:
661+
refresh_token = self._threads.get("refresh_token")
662+
refresh_jwks = self._threads.get("refresh_jwks")
663+
refresh_revocation = self._threads.get("refresh_revocation")
664+
665+
if not refresh_token or not refresh_jwks or not refresh_revocation:
666+
logger.warning("refresh token, jwks or revocation list background thread not started")
667+
return False
668+
669+
if refresh_token.error is not None:
670+
logger.warning("error refresh token")
671+
logger.error(refresh_token.error)
672+
return False
673+
674+
if refresh_jwks.error is not None:
675+
logger.warning("error refresh jwks")
676+
logger.error(refresh_jwks.error)
677+
return False
678+
679+
if refresh_revocation.error is not None:
680+
logger.warning("error refresh revocation list")
681+
logger.error(refresh_revocation.error)
682+
return False
683+
684+
logger.info("all OK")
685+
686+
return True
645687

646688
def ValidateAudience(self, claims: Union[JWTClaims, None]) -> None:
647689
"""Validate audience of user access token

‎iam_python_sdk/models.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class TokenResponse(Model):
115115
"""Token response class on successful token request."""
116116
AccessToken: str = ""
117117
RefreshToken: str = ""
118-
ExpiresIn: str = ""
118+
ExpiresIn: int = -1
119119
TokenType: str = ""
120120
Roles: List[str] = [""]
121121
AcceptedPolicyVersion: List[str] = [""]

‎iam_python_sdk/task.py‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright 2021 AccelByte Inc
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Task module."""
16+
17+
18+
from threading import RLock, Timer
19+
from typing import Any, Callable, Union
20+
21+
from .log import logger
22+
23+
24+
class Task:
25+
"""Task module for background task.
26+
"""
27+
def __init__(self, interval: Union[int, float], function: Callable[..., Any], *args, **kwargs) -> None:
28+
self._lock = RLock()
29+
self._timer = None
30+
self.interval = interval
31+
self.function = function
32+
self.args = args
33+
self.kwargs = kwargs
34+
self.status = 'STOP'
35+
self.error = None
36+
37+
if kwargs.pop('autostart', True):
38+
self.start()
39+
40+
def _run(self) -> None:
41+
with self._lock:
42+
self.status = 'RUNNING'
43+
44+
try:
45+
self.function(*self.args, **self.kwargs)
46+
except Exception as e:
47+
# We catch all exceptions here because we dont know what error will occur on background task
48+
logger.error(e)
49+
self.error = e
50+
51+
self.start(repeat=True)
52+
53+
def start(self, repeat: bool = False) -> None:
54+
"""Start the thread in background(daemon).
55+
56+
Args:
57+
repeat (bool, optional): Status if the task is repetitive. Defaults to False.
58+
"""
59+
with self._lock:
60+
if repeat or self.status == 'STOP':
61+
self.status = 'WAITING'
62+
self._timer = Timer(self.interval, self._run)
63+
self._timer.daemon = True
64+
self._timer.start()
65+
66+
def stop(self) -> None:
67+
"""Stop the background task.
68+
"""
69+
with self._lock:
70+
self.status = 'STOP'
71+
if self._timer:
72+
self._timer.cancel()

‎setup.cfg‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.8.0
2+
current_version = 0.10.0
33
commit = True
44
tag = True
55
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(rc(?P<rc>\d+))?

‎setup.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,6 @@
5050
test_suite='tests',
5151
tests_require=test_requirements,
5252
url='https://accelbyte.github.io/iam-python-sdk',
53-
version='0.8.0',
53+
version='0.10.0',
5454
zip_safe=False,
5555
)

0 commit comments

Comments
 (0)