contentgrid-extension-helpers 0.0.1__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.
File without changes
@@ -0,0 +1,2 @@
1
+ from .oidc import create_current_user_dependency, decode_and_verify_token, create_oauth2_scheme, get_jwks_client
2
+ from .user import ContentGridUser
@@ -0,0 +1,238 @@
1
+ import logging
2
+ import os
3
+ import hyperlink
4
+ from typing import Annotated, Optional
5
+
6
+ from contentgrid_extension_helpers.authentication.user import ContentGridUser
7
+ import jwt
8
+ import requests
9
+ from dotenv import load_dotenv
10
+ from fastapi import Depends, HTTPException, status
11
+ from fastapi.security import OAuth2PasswordBearer
12
+ from pydantic import BaseModel
13
+
14
+ # Load environment variables
15
+ load_dotenv()
16
+ load_dotenv(".env.secret")
17
+
18
+ # Default constants
19
+ DEFAULT_ALGORITHM = "RS256"
20
+
21
+
22
+ def _get_jwks_client_from_config_uri(config_uri: str) -> tuple[dict, jwt.PyJWKClient]:
23
+ """
24
+ Common helper function to retrieve JWKS client from a configuration URI.
25
+
26
+ Args:
27
+ config_uri: The URI to find the OAuth/OIDC configuration.
28
+
29
+ Returns:
30
+ Tuple of OAuth/OIDC configuration and JWKS client.
31
+ """
32
+ try:
33
+ config = requests.get(config_uri).json()
34
+ return config, jwt.PyJWKClient(config["jwks_uri"])
35
+ except (requests.RequestException, KeyError) as e:
36
+ logging.exception(f"Error retrieving configuration: {e}")
37
+ raise
38
+
39
+ def get_oidc_jwks_client(
40
+ oidc_issuer: str,
41
+ ) -> tuple[dict, jwt.PyJWKClient]:
42
+ """
43
+ Retrieves the JWKS (JSON Web Key Set) client from the OIDC (OpenID Connect) issuer configuration URI.
44
+
45
+ Args:
46
+ oidc_issuer (str): The OIDC issuer URL. The issuer URL is used to retrieve the JWKS URI.
47
+ The url is adjusted to point to the .well-known/openid-configuration path, based on the OpenID Connect specification.
48
+ If not on the default endpoint, use function _get_jwks_client_from_config_uri instead.
49
+
50
+ Returns:
51
+ Tuple[dict, jwt.PyJWKClient]: A tuple containing the OIDC configuration and the JWKS client.
52
+ """
53
+ oidc_issuer_url: hyperlink.URL = hyperlink.parse(oidc_issuer)
54
+ assert oidc_issuer_url.absolute
55
+
56
+ oidc_config_uri = oidc_issuer_url.replace(
57
+ path=oidc_issuer_url.path + (".well-known", "openid-configuration")
58
+ ).to_iri().to_text()
59
+
60
+ return _get_jwks_client_from_config_uri(oidc_config_uri)
61
+
62
+ def get_oauth_jwks_client(
63
+ oauth_issuer: str,
64
+ oauth_config_uri: Optional[str] = None,
65
+ ) -> tuple[dict, jwt.PyJWKClient]:
66
+ """
67
+ Retrieve JWKS client from a generic OAuth 2.0 issuer. Constructs the configuration URI if not provided.
68
+
69
+ Args:
70
+ oauth_issuer: OAuth issuer used to construct the configuration URI.
71
+ oauth_config_uri: Optional URI to find the OAuth config. If not provided, will be derived from oauth_issuer.
72
+
73
+ Returns:
74
+ Tuple of OAuth configuration and JWKS client.
75
+ """
76
+ if not oauth_issuer:
77
+ oauth_issuer = os.environ.get("OAUTH_ISSUER", None)
78
+ assert oauth_issuer
79
+ oauth_issuer_url = hyperlink.parse(oauth_issuer)
80
+ assert oauth_issuer_url.absolute
81
+
82
+ if oauth_config_uri is None:
83
+ oauth_config_uri = oauth_issuer_url.replace(
84
+ path=(".well-known", "oauth-authorization-server") + oauth_issuer_url.path
85
+ ).to_iri().to_text()
86
+
87
+ return _get_jwks_client_from_config_uri(oauth_config_uri)
88
+
89
+ def decode_and_verify_token(
90
+ access_token: str,
91
+ jwks_client: jwt.PyJWKClient,
92
+ issuer: str,
93
+ algorithms: Optional[list[str]] = None,
94
+ audience: Optional[str] = None,
95
+ verify_exp: bool = True,
96
+ verify_aud: bool = True,
97
+ verify_iss: bool = True,
98
+ verify_nbf: bool = False,
99
+ verify_iat: bool = False,
100
+ ) -> dict:
101
+ """
102
+ Decode and verify the access token with flexible configuration.
103
+ """
104
+ if not issuer:
105
+ issuer = os.environ.get("OIDC_ISSUER", None)
106
+
107
+ if not audience:
108
+ audience = os.environ.get("AUDIENCE", None)
109
+
110
+ if verify_iss and issuer is None:
111
+ raise Exception("Issuer not provided while expecting to verify the issuer.")
112
+
113
+ if verify_aud and audience is None:
114
+ raise Exception("Audience not provided while expecting to verify the audience.")
115
+
116
+ if algorithms is None:
117
+ algorithms = [os.environ.get("ALGORITHM", DEFAULT_ALGORITHM)]
118
+
119
+ try:
120
+ signing_key = jwks_client.get_signing_key_from_jwt(access_token)
121
+ data = jwt.decode(
122
+ access_token,
123
+ signing_key.key,
124
+ algorithms=algorithms,
125
+ issuer=issuer,
126
+ audience=audience,
127
+ options={
128
+ "verify_exp": verify_exp,
129
+ "verify_aud": verify_aud,
130
+ "verify_iss": verify_iss,
131
+ "verify_iat": verify_iat,
132
+ "verify_nbf": verify_nbf
133
+ },
134
+ )
135
+ data["access_token"] = access_token
136
+ return data
137
+ except jwt.PyJWTError as e:
138
+ logging.exception(f"Token verification failed: {e}")
139
+ raise
140
+
141
+ def create_oauth2_scheme(token_url: str = "token") -> OAuth2PasswordBearer:
142
+ """
143
+ Create OAuth2 password bearer scheme with configurable token URL.
144
+ """
145
+ return OAuth2PasswordBearer(tokenUrl=token_url)
146
+
147
+ def create_current_user_dependency(
148
+ jwks_client: jwt.PyJWKClient,
149
+ oidc_issuer: Optional[str] = None,
150
+ audience: Optional[str] = None,
151
+ user_model: type[BaseModel] = ContentGridUser,
152
+ token_url: str = "token",
153
+ algorithms: Optional[list[str]] = None,
154
+ verify_exp: bool = True,
155
+ verify_aud: bool = True,
156
+ verify_iss: bool = True,
157
+ verify_nbf: bool = False,
158
+ verify_iat: bool = False,
159
+ ):
160
+ """
161
+ Creates a FastAPI dependency for retrieving and verifying the current user from a JWT.
162
+
163
+ This function sets up an OAuth2PasswordBearer scheme and a dependency that can be used
164
+ in FastAPI endpoints to authenticate users based on JWTs (JSON Web Tokens). It handles
165
+ token decoding, verification, and user data extraction.
166
+
167
+ Args:
168
+ jwks_client: The PyJWKClient instance used to verify the JWT signature. This client
169
+ should be initialized with the correct JWKS (JSON Web Key Set) for your
170
+ authentication provider.
171
+ oidc_issuer: The expected issuer of the JWT (e.g., "https://extensions.eu-west-1.contentgrid.cloud/authentication/external"). This value
172
+ is used to verify the `iss` (issuer) claim in the JWT. If provided, the
173
+ `verify_iss` parameter must be True.
174
+ audience: The expected audience of the JWT (e.g., "contentgrid:extension:extract"). This value is used to
175
+ verify the `aud` (audience) claim in the JWT. If provided, the `verify_aud`
176
+ parameter must be True. This is a crucial security setting.
177
+ user_model: The Pydantic model that represents the user data. Defaults to ContentGridUser.
178
+ This model will be used to deserialize the JWT payload after successful
179
+ verification. It should match the structure of the claims in your JWT.
180
+ token_url: The URL of the token endpoint. This is used by the OAuth2PasswordBearer
181
+ scheme for its OpenAPI documentation and is typically "/token".
182
+ algorithms: A list of allowed algorithms for JWT verification (e.g., ["RS256", "ES256"]).
183
+ Defaults to None, which might use a default algorithm or an algorithm from the environment.
184
+ It's highly recommended to explicitly specify the allowed algorithms for security reasons.
185
+ verify_exp: Whether to verify the 'exp' (expiration time) claim of the JWT. Defaults to True.
186
+ verify_aud: Whether to verify the 'aud' (audience) claim of the JWT. Defaults to True.
187
+ verify_iss: Whether to verify the 'iss' (issuer) claim of the JWT. Defaults to True.
188
+ verify_nbf: Whether to verify the 'nbf' (not before) claim of the JWT. Defaults to False.
189
+ verify_iat: Whether to verify the 'iat' (issued at) claim of the JWT. Defaults to False.
190
+
191
+ Returns:
192
+ A callable (dependency) that can be used with FastAPI's `Depends()` to inject
193
+ the authenticated user into an endpoint. The dependency raises an HTTPException
194
+ with a 401 status code if authentication fails.
195
+
196
+ Raises:
197
+ HTTPException: If the token is invalid, expired, has an invalid signature, incorrect issuer or audience,
198
+ or if the user data cannot be extracted.
199
+
200
+ Example:
201
+ # Assuming you have a jwks_client and oidc_issuer configured:
202
+ get_current_user = create_current_user_dependency(
203
+ jwks_client=my_jwks_client,
204
+ oidc_issuer="https://extensions.eu-west-1.contentgrid.cloud/authentication/external",
205
+ audience="contentgrid:extension:extract" # Important: Set the audience!
206
+ )
207
+
208
+ @app.get("/items/")
209
+ async def read_items(current_user: ContentGridUser = Depends(get_current_user)):
210
+ # current_user is now an instance of ContentGridUser, populated from the JWT
211
+ return {"user": current_user}
212
+ """
213
+ oauth2_scheme = create_oauth2_scheme(token_url)
214
+
215
+ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> BaseModel:
216
+ try:
217
+ token_data = decode_and_verify_token(
218
+ access_token=token,
219
+ jwks_client=jwks_client,
220
+ audience=audience,
221
+ issuer=oidc_issuer,
222
+ algorithms=algorithms,
223
+ verify_aud=verify_aud,
224
+ verify_exp=verify_exp,
225
+ verify_iss=verify_iss,
226
+ verify_nbf=verify_nbf,
227
+ verify_iat=verify_iat
228
+ )
229
+ return user_model(**token_data)
230
+ except Exception:
231
+ logging.exception("Exception thrown in user verification.")
232
+ raise HTTPException(
233
+ status_code=status.HTTP_401_UNAUTHORIZED,
234
+ detail="Invalid authentication credentials",
235
+ headers={"WWW-Authenticate": "Bearer"},
236
+ )
237
+
238
+ return get_current_user
@@ -0,0 +1,9 @@
1
+ from pydantic import BaseModel
2
+
3
+ class ContentGridUser(BaseModel):
4
+ sub: str
5
+ iss: str
6
+ exp: float
7
+ name: str | None = None
8
+ email: str | None = None
9
+ access_token: str
@@ -0,0 +1 @@
1
+ from .json_logging import setup_json_logging, XenitJsonFormatter
@@ -0,0 +1,93 @@
1
+ import datetime
2
+ import json
3
+ import logging
4
+ import time
5
+ from typing import List, Optional, Any, Dict
6
+
7
+ class XenitJsonFormatter(logging.Formatter):
8
+ def __init__(
9
+ self,
10
+ component: str,
11
+ additional_keys: dict[str,str] = {},
12
+ fmt: Optional[str] = None,
13
+ datefmt: Optional[str] = None,
14
+ style: str = "%",
15
+ validate: bool = True,
16
+ defaults: Optional[Dict[str, Any]] = None
17
+ ):
18
+ super().__init__(fmt, datefmt, style, validate, defaults=defaults)
19
+ self.component = component
20
+ self.additional_keys = additional_keys
21
+
22
+ def format(self, record: logging.LogRecord) -> str:
23
+ log_entry = {
24
+ "timestamp": time.time(),
25
+ "type": "application",
26
+ "component": self.component,
27
+ "time": datetime.datetime.now(datetime.UTC).isoformat(
28
+ sep=" ", timespec="milliseconds"
29
+ ),
30
+ "shortMessage": record.getMessage() if record.getMessage() else "",
31
+ "loggerName": record.name,
32
+ "severity": record.levelname,
33
+ "thread": record.threadName,
34
+ "fullMessage": self._get_full_message(record),
35
+ "level": record.levelno,
36
+ "sourceClassName": record.pathname,
37
+ "sourceLineNumber": record.lineno,
38
+ "sourceMethodName": record.funcName,
39
+ }
40
+
41
+ # Add additional keys dynamically
42
+ for additional_key, additional_value in self.additional_keys.items():
43
+ log_entry[additional_key] = getattr(record, additional_value, None)
44
+
45
+ return json.dumps(log_entry, ensure_ascii=False)
46
+
47
+ def _get_full_message(self, record: logging.LogRecord) -> str:
48
+ """
49
+ Safely extract full error message
50
+ """
51
+ if record.exc_info:
52
+ return self.formatException(record.exc_info)
53
+ return record.exc_text or ""
54
+
55
+ def setup_json_logging(
56
+ component: str = "cg-extension",
57
+ additional_keys: dict[str, str] = [],
58
+ log_level: int = logging.DEBUG
59
+ ) -> logging.Logger:
60
+ """
61
+ Setup JSON logging with a single logger
62
+
63
+ Args:
64
+ component (str): Name of the component for logging
65
+ additional_keys (dict[str, str]): Additional keys to include in log. The key is the key in the json logline, the value is the value of in the extra field for the logging lib.
66
+ log_level (int): Logging level (default: DEBUG)
67
+
68
+ Returns:
69
+ logging.Logger: Configured logger
70
+ """
71
+ # Get the root logger
72
+ root_logger = logging.getLogger()
73
+ root_logger.setLevel(log_level)
74
+
75
+ # Remove existing handlers to prevent duplicate logs
76
+ for handler in root_logger.handlers[:]:
77
+ root_logger.removeHandler(handler)
78
+
79
+ # Create console handler
80
+ ch = logging.StreamHandler()
81
+ ch.setLevel(log_level)
82
+
83
+ # Create JSON formatter
84
+ formatter = XenitJsonFormatter(
85
+ component=component,
86
+ additional_keys=additional_keys
87
+ )
88
+
89
+ # Add formatter to handler
90
+ ch.setFormatter(formatter)
91
+
92
+ # Add handler to logger
93
+ root_logger.addHandler(ch)
@@ -0,0 +1,13 @@
1
+ Copyright 2024 Xenit Solutions
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.
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.2
2
+ Name: contentgrid-extension-helpers
3
+ Version: 0.0.1
4
+ Summary: Helper functions for contentgrid extensions.
5
+ Author-email: Ranec Belpaire <ranec.belpaire@xenit.eu>
6
+ License: Copyright 2024 Xenit Solutions
7
+
8
+ Licensed under the Apache License, Version 2.0 (the "License");
9
+ you may not use this file except in compliance with the License.
10
+ You may obtain a copy of the License at
11
+
12
+ http://www.apache.org/licenses/LICENSE-2.0
13
+
14
+ Unless required by applicable law or agreed to in writing, software
15
+ distributed under the License is distributed on an "AS IS" BASIS,
16
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ See the License for the specific language governing permissions and
18
+ limitations under the License.
19
+ Classifier: Development Status :: 3 - Alpha
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Programming Language :: Python :: 3
22
+ Requires-Python: >=3.5
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: requests<3,>=2.20.0
26
+ Requires-Dist: uri-template<2
27
+ Requires-Dist: fastapi>=0.111
28
+ Requires-Dist: PyJWT>2
29
+
30
+ ### ContentGrid-Extension-Helpers
31
+
32
+ ContentGrid extensions helpers is a python library that is used for defining reusable helper functions for python extensions.
@@ -0,0 +1,11 @@
1
+ contentgrid_extension_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ contentgrid_extension_helpers/authentication/__init__.py,sha256=b6Bp8jCg2-T7bcTGid7pCk5PfNYwH-52UFUX2Op8ArQ,146
3
+ contentgrid_extension_helpers/authentication/oidc.py,sha256=14XEmp_WWDVygb3oBKB9S29UlgJV8wnm_1lw36U4xxc,9820
4
+ contentgrid_extension_helpers/authentication/user.py,sha256=pr3DVZKchLxpJXU6k2uUNLquwr9LjWfr_sArdAjUjZU,185
5
+ contentgrid_extension_helpers/logging/__init__.py,sha256=u6edUMrsULUinBNN94yJywZyINz4azhIAXO2LMbmtaY,64
6
+ contentgrid_extension_helpers/logging/json_logging.py,sha256=QRoDHZjtMXM-JGPR6JWqJnCljwSQ912wjsZ38XMA7Tw,3119
7
+ contentgrid_extension_helpers-0.0.1.dist-info/LICENSE,sha256=tk6n-p8lEmzLJg-O4052CkMgfUtt1q2Zoh1QLAyL7S8,555
8
+ contentgrid_extension_helpers-0.0.1.dist-info/METADATA,sha256=eP5YG_IsyyRtfonDnTiwnLlKPuQHZ7JWkB2s4aFaO2k,1347
9
+ contentgrid_extension_helpers-0.0.1.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
10
+ contentgrid_extension_helpers-0.0.1.dist-info/top_level.txt,sha256=yJGGofrNVsl5psVGO0vLFHO1610ob88GtB9zpvS8iIk,30
11
+ contentgrid_extension_helpers-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ contentgrid_extension_helpers