iaptoolkit 0.3.7a3__tar.gz → 0.3.8__tar.gz

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.
Files changed (21) hide show
  1. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/PKG-INFO +3 -3
  2. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/pyproject.toml +7 -23
  3. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/constants.py +0 -2
  4. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/exceptions.py +1 -28
  5. {iaptoolkit-0.3.7a3/src/iaptoolkit/jwt → iaptoolkit-0.3.8/src/iaptoolkit/utils}/verify.py +3 -12
  6. iaptoolkit-0.3.7a3/src/iaptoolkit/jwt/constants.py +0 -9
  7. iaptoolkit-0.3.7a3/src/iaptoolkit/jwt/flask.py +0 -142
  8. iaptoolkit-0.3.7a3/src/iaptoolkit/jwt/metrics.py +0 -34
  9. iaptoolkit-0.3.7a3/src/iaptoolkit/jwt/quart.py +0 -143
  10. iaptoolkit-0.3.7a3/src/iaptoolkit/jwt/verify_async.py +0 -75
  11. iaptoolkit-0.3.7a3/src/iaptoolkit/utils/__init__.py +0 -0
  12. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/LICENSE +0 -0
  13. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/README.md +0 -0
  14. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/__init__.py +0 -0
  15. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/headers.py +0 -0
  16. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/tokens/__init__.py +0 -0
  17. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/tokens/service_account.py +0 -0
  18. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/tokens/structs.py +0 -0
  19. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/tokens/token_datastore.py +0 -0
  20. {iaptoolkit-0.3.7a3/src/iaptoolkit/jwt → iaptoolkit-0.3.8/src/iaptoolkit/utils}/__init__.py +0 -0
  21. {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/utils/urls.py +0 -0
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: iaptoolkit
3
- Version: 0.3.7a3
3
+ Version: 0.3.8
4
4
  Summary: Library of common utils for interacting with Identity-Aware Proxies
5
5
  Author: Rob Voigt
6
6
  Author-email: code@ravoigt.com
7
- Requires-Python: >=3.11
7
+ Requires-Python: >=3.11,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: Programming Language :: Python :: 3.13
12
12
  Requires-Dist: google-auth (>=2.29.0,<3.0.0)
13
- Requires-Dist: kvcommon[k8s] (>=0.2.12,<0.3.0)
13
+ Requires-Dist: kvcommon[k8s] (>=0.4.0,<0.5.0)
14
14
  Requires-Dist: requests (>=2.31.0,<3.0.0)
15
15
  Requires-Dist: toml (>=0.10.2,<0.11.0)
16
16
  Project-URL: Homepage, https://github.com/RAVoigt/iaptoolkit
@@ -1,24 +1,16 @@
1
- [project]
1
+ [tool.poetry]
2
2
  name = "iaptoolkit"
3
- version = "0.3.7-a3"
3
+ version = "0.3.8"
4
4
  description = "Library of common utils for interacting with Identity-Aware Proxies"
5
- authors = [
6
- {name = "Rob Voigt", email = "code@ravoigt.com"}
7
- ]
5
+ authors = ["Rob Voigt <code@ravoigt.com>"]
8
6
  readme = "README.md"
9
- requires-python = ">=3.11"
10
-
11
- # [tool.poetry]
12
- # packages = [{include = "iaptoolkit"}]
7
+ homepage = "https://github.com/RAVoigt/iaptoolkit"
8
+ repository = "https://github.com/RAVoigt/iaptoolkit"
13
9
 
14
10
  [build-system]
15
- requires = ["poetry-core>=2.0"]
11
+ requires = ["poetry-core>=1.0.0"] # Poetry 1.x style; not PEP 621
16
12
  build-backend = "poetry.core.masonry.api"
17
13
 
18
- [project.urls]
19
- Homepage = "https://github.com/RAVoigt/iaptoolkit"
20
- Repository = "https://github.com/RAVoigt/iaptoolkit"
21
-
22
14
  # ================================
23
15
  # Tools etc.
24
16
  [tool.black]
@@ -33,11 +25,7 @@ python = "^3.11"
33
25
  google-auth = "^2.29.0"
34
26
  requests = "^2.31.0"
35
27
  toml = "^0.10.2"
36
- kvcommon = {extras = ["k8s"], version = "^0.2.12"}
37
-
38
- # flask = { version = "^0.20.0", optional = true }
39
- # quart = { version = "^0.20.0", optional = true }
40
- # prometheus-client = { version = "^0.20.0", optional = true }
28
+ kvcommon = {extras = ["k8s"], version = "^0.4.0"}
41
29
 
42
30
  [tool.poetry.group.dev.dependencies]
43
31
  black = "*"
@@ -49,7 +37,3 @@ pytest = "*"
49
37
  pytest-cov = "*"
50
38
  pytest-socket = "*"
51
39
  pyfakefs = "^5.3.2"
52
-
53
- flask = "*"
54
- quart = "*"
55
- prometheus_client = "*"
@@ -10,5 +10,3 @@ GOOGLE_IAP_AUTH_HEADER = "Authorization"
10
10
  GOOGLE_IAP_AUTH_HEADER_PROXY = "Proxy-Authorization"
11
11
 
12
12
  GOOGLE_IAP_PUBLIC_KEY_URL = "https://www.gstatic.com/iap/verify/public_key-jwk"
13
-
14
- GOOGLE_IAP_JWT_HEADER_KEY = "x-goog-iap-jwt-assertion"
@@ -62,31 +62,4 @@ class PublicKeyException(IAPToolkitBaseException):
62
62
 
63
63
 
64
64
  class JWTVerificationFailure(IAPToolkitBaseException):
65
- def __init__(self, message: str, google_exception: DefaultCredentialsError | None):
66
- self.google_exception = google_exception
67
- self.message = (f"{message}, google_exception='{str(google_exception)}'")
68
- super().__init__(self.message)
69
-
70
-
71
- class JWTInvalidData(JWTVerificationFailure):
72
- def __init__(self, message: str | None = None, google_exception: DefaultCredentialsError | None = None):
73
- message = message or "Invalid JWT values"
74
- super().__init__(message=message, google_exception=google_exception)
75
-
76
-
77
- class JWTMalformed(JWTVerificationFailure):
78
- def __init__(self, message: str | None = None, google_exception: DefaultCredentialsError | None = None):
79
- message = message or "Malformed JWT schema"
80
- super().__init__(message=message, google_exception=google_exception)
81
-
82
-
83
- class JWTInvalidAudience(JWTVerificationFailure):
84
- def __init__(self, message: str | None = None):
85
- message = message or "JWT audience mismatch"
86
- super().__init__(message=message, google_exception=None)
87
-
88
-
89
- class JWTDisallowedUser(JWTVerificationFailure):
90
- def __init__(self, message: str | None = None):
91
- message = message or "User from JWT not allowed"
92
- super().__init__(message=message, google_exception=None)
65
+ pass
@@ -3,14 +3,10 @@ import json
3
3
  import requests
4
4
 
5
5
  from google.auth import jwt
6
- from google.auth.exceptions import InvalidValue
7
- from google.auth.exceptions import MalformedError
8
6
 
9
7
  from iaptoolkit.constants import GOOGLE_IAP_PUBLIC_KEY_URL
10
8
  from iaptoolkit.exceptions import PublicKeyException
11
- from iaptoolkit.exceptions import JWTInvalidData
12
- from iaptoolkit.exceptions import JWTInvalidAudience
13
- from iaptoolkit.exceptions import JWTMalformed
9
+ from iaptoolkit.exceptions import JWTVerificationFailure
14
10
 
15
11
 
16
12
  class GooglePublicKeyException(PublicKeyException):
@@ -73,18 +69,13 @@ def verify_iap_jwt(iap_jwt: str, expected_audience: str|None) -> str:
73
69
  if not google_public_keys:
74
70
  google_public_keys = GoogleIAPKeys()
75
71
 
76
- try:
77
- decoded_jwt = jwt.decode(iap_jwt, certs=google_public_keys.certs, verify=True)
78
- except InvalidValue as ex:
79
- raise JWTInvalidData(google_exception=ex)
80
- except MalformedError as ex:
81
- raise JWTMalformed(google_exception=ex)
72
+ decoded_jwt = jwt.decode(iap_jwt, certs=google_public_keys.certs, verify=True)
82
73
 
83
74
  # Extract claims
84
75
  email = decoded_jwt.get("email")
85
76
  audience = decoded_jwt.get("aud")
86
77
 
87
78
  if expected_audience and audience != expected_audience:
88
- raise JWTInvalidAudience()
79
+ raise JWTVerificationFailure("Audience mismatch when verifying IAP JWT")
89
80
 
90
81
  return email
@@ -1,9 +0,0 @@
1
- from enum import StrEnum
2
-
3
- class JWT_Event(StrEnum):
4
- SUCCESS = "success"
5
- FAIL_NO_HEADER = "fail_no_header"
6
- FAIL_INVALID_JWT = "fail_invalid"
7
- FAIL_NO_EMAIL = "fail_no_email"
8
- FAIL_WRONG_USER = "fail_wrong_user"
9
- FAIL_WRONG_AUDIENCE = "fail_wrong_audience"
@@ -1,142 +0,0 @@
1
- import typing as t
2
- from functools import wraps
3
-
4
- from flask import request
5
- from flask.wrappers import Request
6
- from flask.wrappers import Response
7
-
8
- from iaptoolkit.constants import GOOGLE_IAP_JWT_HEADER_KEY
9
- from iaptoolkit.exceptions import JWTDisallowedUser
10
- from iaptoolkit.exceptions import JWTInvalidAudience
11
- from iaptoolkit.exceptions import JWTInvalidData
12
- from iaptoolkit.exceptions import JWTMalformed
13
-
14
- from .constants import JWT_Event
15
- from .metrics import Counter
16
- from .metrics import default_metric
17
- from .metrics import inc_metric
18
- from .verify import verify_iap_jwt
19
-
20
-
21
- def _verify_jwt(
22
- request: Request,
23
- jwt_header_key: str,
24
- jwt_audience: str,
25
- allowed_users: set[str] | None = None,
26
- response_cls: t.Type[Response] = Response,
27
- metric: Counter | None = default_metric
28
- ) -> Response | None:
29
- jwt_header: str = request.headers.get(jwt_header_key.lower(), "")
30
- if not jwt_header:
31
- inc_metric(metric, event=JWT_Event.FAIL_NO_HEADER)
32
- return response_cls(f"No Google IAP JWT header in request at key: '{jwt_header_key}'", status=401)
33
-
34
- try:
35
- user_email = verify_iap_jwt(iap_jwt=jwt_header, expected_audience=jwt_audience)
36
- if not user_email:
37
- raise JWTInvalidData("No user_email in decoded JWT")
38
-
39
- if allowed_users and user_email not in allowed_users:
40
- raise JWTDisallowedUser(message=f"User '{user_email}' from JWT not allowed for route")
41
-
42
- except (JWTInvalidData, JWTMalformed) as ex:
43
- inc_metric(metric, event=JWT_Event.FAIL_INVALID_JWT)
44
- return response_cls(f"Forbidden: '{ex.message}'", status=401)
45
-
46
- except JWTInvalidAudience as ex:
47
- inc_metric(metric, event=JWT_Event.FAIL_WRONG_AUDIENCE)
48
- return response_cls(f"Forbidden: '{ex.message}'", status=403)
49
-
50
- except JWTDisallowedUser as ex:
51
- inc_metric(metric, event=JWT_Event.FAIL_WRONG_USER)
52
- return response_cls(f"Forbidden: '{ex.message}'", status=403)
53
-
54
- return None
55
-
56
-
57
- def requires_iap_jwt(
58
- jwt_audience: str,
59
- response_cls: t.Type[Response] = Response,
60
- jwt_header_key: str = GOOGLE_IAP_JWT_HEADER_KEY,
61
- metric: Counter | None = default_metric
62
- ):
63
- """
64
- A decorator that ensures the incoming request has a valid IAP JWT for a Flask route,
65
- and that the user in the JWT has permission for the route.
66
-
67
- Params:
68
- jwt_audience: JWT Audience string (or IAP Client ID) to verify JWT against
69
- response_cls: Flask response class or subclass thereof to return from decorator
70
- jwt_header_key: request header key from which to retrieve the JWT (Default: 'x-goog-iap-jwt-assertion')
71
- metric:
72
- prometheus_client.Counter object (or None) to inc() for different outcomes.
73
- Must have a single label: 'event'.
74
- Set metric param to 'None' to disable metrics.
75
-
76
- Returns:
77
- Flask response of type determined by response_cls param on JWT Failure, else result of decorated view function
78
- """
79
- def decorator(f: t.Callable) -> t.Callable:
80
-
81
- @wraps(f)
82
- def decorated_function(*args, **kwargs) -> Response:
83
- resp: Response | None = _verify_jwt(
84
- request,
85
- jwt_header_key=jwt_header_key,
86
- jwt_audience=jwt_audience,
87
- allowed_users=None,
88
- response_cls=response_cls,
89
- metric=metric
90
- )
91
- if resp is not None:
92
- return resp
93
- return f(*args, **kwargs)
94
-
95
- return decorated_function
96
-
97
- return decorator
98
-
99
-
100
- def requires_iap_jwt_valid_user(
101
- jwt_audience: str,
102
- allowed_users: set[str],
103
- response_cls: t.Type[Response] = Response,
104
- jwt_header_key: str = GOOGLE_IAP_JWT_HEADER_KEY,
105
- metric: Counter | None = default_metric
106
- ):
107
- """
108
- A decorator that ensures the incoming request has a valid IAP JWT for a Flask route,
109
- and that the user in the JWT has permission for the route
110
-
111
- Params:
112
- jwt_audience: JWT Audience string (or IAP Client ID) to verify JWT against
113
- allowed_users: set of email strings to check against user_email in JWT for permission to access decorated view func
114
- response_cls: Flask response class or subclass thereof to return from decorator
115
- jwt_header_key: request header key from which to retrieve the JWT (Default: 'x-goog-iap-jwt-assertion')
116
- metric:
117
- prometheus_client.Counter object (or None) to inc() for different outcomes.
118
- Must have a single label: 'event'.
119
- Set metric param to 'None' to disable metrics.
120
-
121
- Returns:
122
- Flask response of type determined by response_cls param on JWT Failure, else result of decorated view function
123
- """
124
- def decorator(f: t.Callable) -> t.Callable:
125
-
126
- @wraps(f)
127
- def decorated_function(*args, **kwargs) -> Response:
128
- resp: Response | None = _verify_jwt(
129
- request,
130
- jwt_header_key=jwt_header_key,
131
- jwt_audience=jwt_audience,
132
- allowed_users=allowed_users,
133
- response_cls=response_cls,
134
- metric=metric
135
- )
136
- if resp is not None:
137
- return resp
138
- return f(*args, **kwargs)
139
-
140
- return decorated_function
141
-
142
- return decorator
@@ -1,34 +0,0 @@
1
- import asyncio
2
- import typing as t
3
-
4
- try:
5
- from prometheus_client import Counter
6
- # TODO move to constants
7
- default_metric = Counter(
8
- "iaptoolkit_jwt_event_total",
9
- "Count of JWT verification events",
10
- labelnames=["event"]
11
- )
12
- except ImportError:
13
- Counter = t.TypeVar("Counter")
14
- default_metric = None
15
-
16
-
17
- def inc_metric(metric: Counter | None, event: str):
18
- if not metric:
19
- return
20
- metric.labels(event=event).inc()
21
-
22
-
23
- async def inc_metric_async(metric: Counter | None, event: str):
24
- if not metric:
25
- return
26
- # TODO: Does prometheus_client have built-in async support?
27
- await asyncio.to_thread(metric.labels(event=event).inc)
28
-
29
- __all__ = [
30
- "default_metric",
31
- "Counter",
32
- "inc_metric",
33
- "inc_metric_async"
34
- ]
@@ -1,143 +0,0 @@
1
- import typing as t
2
- from functools import wraps
3
-
4
- from quart import request
5
- from quart.wrappers import Request
6
- from quart.wrappers import Response
7
-
8
- from iaptoolkit.constants import GOOGLE_IAP_JWT_HEADER_KEY
9
- from iaptoolkit.exceptions import JWTDisallowedUser
10
- from iaptoolkit.exceptions import JWTInvalidAudience
11
- from iaptoolkit.exceptions import JWTInvalidData
12
- from iaptoolkit.exceptions import JWTMalformed
13
- from iaptoolkit.jwt.constants import JWT_Event
14
- from .constants import JWT_Event
15
- from .metrics import Counter
16
- from .metrics import default_metric
17
- from .metrics import inc_metric_async
18
- from .verify_async import verify_iap_jwt_async
19
-
20
-
21
- async def _verify_jwt_async(
22
- request: Request,
23
- jwt_header_key: str,
24
- jwt_audience: str,
25
- allowed_users: set[str] | None = None,
26
- response_cls: t.Type[Response] = Response,
27
- metric: Counter | None = default_metric
28
- ) -> Response | None:
29
-
30
- jwt_header: str = request.headers.get(jwt_header_key.lower(), "")
31
- if not jwt_header:
32
- await inc_metric_async(metric, event=JWT_Event.FAIL_NO_HEADER)
33
- return response_cls(f"No Google IAP JWT header in request at key: '{jwt_header_key}'", status=401)
34
-
35
- try:
36
- user_email = await verify_iap_jwt_async(iap_jwt=jwt_header, expected_audience=jwt_audience)
37
- if not user_email:
38
- raise JWTInvalidData("No user_email in decoded JWT")
39
-
40
- if allowed_users and user_email not in allowed_users:
41
- raise JWTDisallowedUser(message=f"User '{user_email}' from JWT not allowed for route")
42
-
43
- except (JWTInvalidData, JWTMalformed) as ex:
44
- await inc_metric_async(metric, event=JWT_Event.FAIL_INVALID_JWT)
45
- return response_cls(f"Forbidden: '{ex.message}'", status=401)
46
-
47
- except JWTInvalidAudience as ex:
48
- await inc_metric_async(metric, event=JWT_Event.FAIL_WRONG_AUDIENCE)
49
- return response_cls(f"Forbidden: '{ex.message}'", status=403)
50
-
51
- except JWTDisallowedUser as ex:
52
- await inc_metric_async(metric, event=JWT_Event.FAIL_WRONG_USER)
53
- return response_cls(f"Forbidden: '{ex.message}'", status=403)
54
-
55
- return None
56
-
57
-
58
- def requires_iap_jwt(
59
- jwt_audience: str,
60
- response_cls: t.Type[Response] = Response,
61
- jwt_header_key: str = GOOGLE_IAP_JWT_HEADER_KEY,
62
- metric: Counter | None = default_metric
63
- ):
64
- """
65
- A decorator that ensures the incoming request has a valid IAP JWT for a Quart route,
66
- and that the user in the JWT has permission for the route.
67
-
68
- Params:
69
- jwt_audience: JWT Audience string (or IAP Client ID) to verify JWT against
70
- response_cls: Quart response class or subclass thereof to return from decorator
71
- jwt_header_key: request header key from which to retrieve the JWT (Default: 'x-goog-iap-jwt-assertion')
72
- metric:
73
- prometheus_client.Counter object (or None) to inc() for different outcomes.
74
- Must have a single label: 'event'.
75
- Set metric param to 'None' to disable metrics.
76
-
77
- Returns:
78
- Quart response of type determined by response_cls param on JWT Failure, else result of decorated view function
79
- """
80
- def decorator(f: t.Callable) -> t.Callable:
81
-
82
- @wraps(f)
83
- async def decorated_function(*args, **kwargs) -> Response:
84
- resp: Response | None = await _verify_jwt_async(
85
- request,
86
- jwt_header_key=jwt_header_key,
87
- jwt_audience=jwt_audience,
88
- allowed_users=None,
89
- response_cls=response_cls,
90
- metric=metric
91
- )
92
- if resp is not None:
93
- return resp
94
- return await f(*args, **kwargs)
95
-
96
- return decorated_function
97
-
98
- return decorator
99
-
100
-
101
- def requires_iap_jwt_valid_user_async(
102
- jwt_audience: str,
103
- allowed_users: set[str],
104
- response_cls: t.Type[Response] = Response,
105
- jwt_header_key: str = GOOGLE_IAP_JWT_HEADER_KEY,
106
- metric: Counter | None = default_metric
107
- ):
108
- """
109
- A decorator that ensures the incoming request has a valid IAP JWT for a Quart route,
110
- and that the user in the JWT has permission for the route.
111
-
112
- Params:
113
- jwt_audience: JWT Audience string (or IAP Client ID) to verify JWT against
114
- allowed_users: set of email strings to check against user_email in JWT for permission to access decorated view func
115
- response_cls: Quart response class or subclass thereof to return from decorator
116
- jwt_header_key: request header key from which to retrieve the JWT (Default: 'x-goog-iap-jwt-assertion')
117
- metric:
118
- prometheus_client.Counter object (or None) to inc() for different outcomes.
119
- Must have a single label: 'event'.
120
- Set metric param to 'None' to disable metrics.
121
-
122
- Returns:
123
- Quart response of type determined by response_cls param on JWT Failure, else result of decorated view function
124
- """
125
- def decorator(f: t.Callable) -> t.Callable:
126
-
127
- @wraps(f)
128
- async def decorated_function(*args, **kwargs) -> Response:
129
- resp: Response | None = await _verify_jwt_async(
130
- request,
131
- jwt_header_key=jwt_header_key,
132
- jwt_audience=jwt_audience,
133
- allowed_users=allowed_users,
134
- response_cls=response_cls,
135
- metric=metric
136
- )
137
- if resp is not None:
138
- return resp
139
- return await f(*args, **kwargs)
140
-
141
- return decorated_function
142
-
143
- return decorator
@@ -1,75 +0,0 @@
1
- import asyncio
2
- import datetime
3
-
4
- from google.auth import jwt
5
- from google.auth.exceptions import InvalidValue
6
- from google.auth.exceptions import MalformedError
7
-
8
- from iaptoolkit.exceptions import JWTInvalidData
9
- from iaptoolkit.exceptions import JWTInvalidAudience
10
- from iaptoolkit.exceptions import JWTMalformed
11
-
12
- from .verify import GoogleIAPKeys
13
-
14
-
15
- class GoogleIAPKeys_Async(GoogleIAPKeys):
16
- """
17
- Rudimentary async wrapper class for GoogleIAPKeys using asyncio threads
18
-
19
- Retrieve Google's public keys for JWT verification and record the timestamp at retrieval.
20
- If the retrieval was >5m ago (default), refresh the keys in case of rotation or expiry
21
- """
22
-
23
- _retrieved_timestamp: datetime.datetime
24
- _key_ttl_seconds: int = 300 # 5 mins
25
- _certs: dict
26
-
27
- def __init__(self, key_ttl_seconds: int = 300) -> None:
28
- self._key_ttl_seconds = key_ttl_seconds
29
-
30
- async def refresh_async(self):
31
- await asyncio.to_thread(self.refresh)
32
-
33
- @property
34
- async def certs(self) -> dict:
35
- if self.should_refresh:
36
- await self.refresh_async()
37
- return self._certs.copy()
38
-
39
-
40
- google_public_keys_async: GoogleIAPKeys_Async | None = None
41
-
42
- async def get_google_public_keys() -> GoogleIAPKeys_Async:
43
- global google_public_keys_async # Use as singleton
44
- if not google_public_keys_async:
45
- google_public_keys_async = GoogleIAPKeys_Async()
46
- await google_public_keys_async.refresh_async()
47
- return google_public_keys_async
48
-
49
-
50
-
51
- async def verify_iap_jwt_async(iap_jwt: str, expected_audience: str|None) -> str:
52
- # Rudimentary async wrapper func for verify_iap_jwt using asyncio threads until google.auth.jwt_async is public
53
-
54
- google_public_keys_async = await get_google_public_keys()
55
-
56
- try:
57
- decoded_jwt = await asyncio.to_thread(
58
- jwt.decode,
59
- iap_jwt,
60
- certs=await google_public_keys_async.certs,
61
- verify=True
62
- )
63
- except InvalidValue as ex:
64
- raise JWTInvalidData(google_exception=ex)
65
- except MalformedError as ex:
66
- raise JWTMalformed(google_exception=ex)
67
-
68
- # Extract claims
69
- email = decoded_jwt.get("email")
70
- audience = decoded_jwt.get("aud")
71
-
72
- if expected_audience and audience != expected_audience:
73
- raise JWTInvalidAudience()
74
-
75
- return email
File without changes
File without changes
File without changes