22
33from __future__ import annotations
44
5+ import json
6+
57from diracx .core .properties import SecurityProperty
68from diracx .core .s3 import s3_bucket_exists
79
1719from typing import TYPE_CHECKING , Annotated , Any , Self , TypeVar
1820
1921from aiobotocore .session import get_session
20- from authlib .jose import JsonWebKey
2122from botocore .config import Config
2223from botocore .errorfactory import ClientError
2324from cryptography .fernet import Fernet
25+ from joserfc .jwk import KeySet , RSAKey
2426from pydantic import (
27+ AliasChoices ,
2528 AnyUrl ,
2629 BeforeValidator ,
2730 Field ,
@@ -51,28 +54,52 @@ class SqlalchemyDsn(AnyUrl):
5154 )
5255
5356
54- class _TokenSigningKey (SecretStr ):
55- jwk : JsonWebKey
57+ class _TokenSigningKeyStore (SecretStr ):
58+ jwks : KeySet
5659
5760 def __init__ (self , data : str ):
5861 super ().__init__ (data )
59- self .jwk = JsonWebKey .import_key (self .get_secret_value ())
60-
61-
62- def _maybe_load_key_from_file (value : Any ) -> Any :
63- """Load private keys from files if needed."""
64- if isinstance (value , str ) and not value .strip ().startswith ("-----BEGIN" ):
65- url = TypeAdapter (LocalFileUrl ).validate_python (value )
66- if not url .scheme == "file" :
67- raise ValueError ("Only file:// URLs are supported" )
68- if url .path is None :
69- raise ValueError ("No path specified" )
70- value = Path (url .path ).read_text ()
62+
63+ # Load the keys from the JSON string
64+ try :
65+ keys = json .loads (self .get_secret_value ())
66+ except json .JSONDecodeError as e :
67+ raise ValueError ("Invalid JSON string" ) from e
68+ if not isinstance (keys , dict ):
69+ raise ValueError ("Invalid JSON string" )
70+ self .jwks = KeySet .import_key_set (keys ) # type: ignore
71+
72+
73+ def _maybe_load_keys_from_file (value : Any ) -> Any :
74+ """Load jwks from files if needed."""
75+ if isinstance (value , str ):
76+ # If the value is a string, we need to check if it is a JSON string or a file URL
77+ if not (value .strip ().startswith ("{" ) or value .startswith ("[" )):
78+ # If it is not a JSON string, we assume it is a file URL
79+ url = TypeAdapter (LocalFileUrl ).validate_python (value )
80+ if not url .scheme == "file" :
81+ raise ValueError ("Only file:// URLs are supported" )
82+ if url .path is None :
83+ raise ValueError ("No path specified" )
84+ value = Path (url .path ).read_text ()
85+
86+ if isinstance (value , str ) and value .strip ().startswith ("-----BEGIN" ):
87+ return json .dumps (
88+ KeySet (
89+ keys = [
90+ RSAKey .import_key (
91+ value , # type: ignore
92+ parameters = {"key_ops" : ["sign" , "verify" ], "alg" : "RS256" }, # type: ignore
93+ )
94+ ]
95+ ).as_dict (private = True )
96+ )
7197 return value
7298
7399
74- TokenSigningKey = Annotated [
75- _TokenSigningKey , BeforeValidator (_maybe_load_key_from_file )
100+ TokenSigningKeyStore = Annotated [
101+ _TokenSigningKeyStore ,
102+ BeforeValidator (_maybe_load_keys_from_file ),
76103]
77104
78105
@@ -124,7 +151,9 @@ def create(cls) -> Self:
124151class AuthSettings (ServiceSettingsBase ):
125152 """Settings for the authentication service."""
126153
127- model_config = SettingsConfigDict (env_prefix = "DIRACX_SERVICE_AUTH_" )
154+ model_config = SettingsConfigDict (
155+ env_prefix = "DIRACX_SERVICE_AUTH_" , validate_by_name = True
156+ )
128157
129158 dirac_client_id : str = "myDIRACClientID"
130159 # TODO: This should be taken dynamically
@@ -137,8 +166,14 @@ class AuthSettings(ServiceSettingsBase):
137166 state_key : FernetKey
138167
139168 token_issuer : str
140- token_key : TokenSigningKey
141- token_algorithm : str = "RS256" # noqa: S105
169+ token_keystore : TokenSigningKeyStore = Field (
170+ validation_alias = AliasChoices (
171+ "token_keystore" ,
172+ "DIRACX_SERVICE_AUTH_TOKEN_KEYSTORE" ,
173+ "DIRACX_SERVICE_AUTH_TOKEN_KEY" ,
174+ )
175+ )
176+ token_allowed_algorithms : list [str ] = ["RS256" , "EdDSA" ] # noqa: S105
142177 access_token_expire_minutes : int = 20
143178 refresh_token_expire_minutes : int = 60
144179
0 commit comments