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.
- {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/PKG-INFO +3 -3
- {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/pyproject.toml +7 -23
- {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/constants.py +0 -2
- {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/exceptions.py +1 -28
- {iaptoolkit-0.3.7a3/src/iaptoolkit/jwt → iaptoolkit-0.3.8/src/iaptoolkit/utils}/verify.py +3 -12
- iaptoolkit-0.3.7a3/src/iaptoolkit/jwt/constants.py +0 -9
- iaptoolkit-0.3.7a3/src/iaptoolkit/jwt/flask.py +0 -142
- iaptoolkit-0.3.7a3/src/iaptoolkit/jwt/metrics.py +0 -34
- iaptoolkit-0.3.7a3/src/iaptoolkit/jwt/quart.py +0 -143
- iaptoolkit-0.3.7a3/src/iaptoolkit/jwt/verify_async.py +0 -75
- iaptoolkit-0.3.7a3/src/iaptoolkit/utils/__init__.py +0 -0
- {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/LICENSE +0 -0
- {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/README.md +0 -0
- {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/__init__.py +0 -0
- {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/headers.py +0 -0
- {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/tokens/__init__.py +0 -0
- {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/tokens/service_account.py +0 -0
- {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/tokens/structs.py +0 -0
- {iaptoolkit-0.3.7a3 → iaptoolkit-0.3.8}/src/iaptoolkit/tokens/token_datastore.py +0 -0
- {iaptoolkit-0.3.7a3/src/iaptoolkit/jwt → iaptoolkit-0.3.8/src/iaptoolkit/utils}/__init__.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
[
|
|
1
|
+
[tool.poetry]
|
|
2
2
|
name = "iaptoolkit"
|
|
3
|
-
version = "0.3.
|
|
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
|
-
|
|
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>=
|
|
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.
|
|
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 = "*"
|
|
@@ -62,31 +62,4 @@ class PublicKeyException(IAPToolkitBaseException):
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
class JWTVerificationFailure(IAPToolkitBaseException):
|
|
65
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|