From ee5a76a808b06f758cdd7dfb91dca2842fb31182 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 30 Apr 2025 16:42:40 +0200 Subject: [PATCH] auth improvements (JWT stuff, esp tests); new: storage.message (event store) --- scopes/storage/message.py | 20 ++++++++++++++++++++ scopes/tests/data_auth.py | 15 ++++++++------- scopes/tests/tlib_web.py | 3 +++ scopes/web/auth/oidc.py | 25 +++++++------------------ 4 files changed, 38 insertions(+), 25 deletions(-) create mode 100644 scopes/storage/message.py diff --git a/scopes/storage/message.py b/scopes/storage/message.py new file mode 100644 index 0000000..309231e --- /dev/null +++ b/scopes/storage/message.py @@ -0,0 +1,20 @@ +# scopes.storage.message + +"""Generic messages (or events) to be stored in SQL database.""" + +from scopes.storage.common import registerContainerClass +from scopes.storage.tracking import Container, Track + +class Message(Track): + + headFields = ['domain', 'action', 'class', 'item'] + prefix = 'msg' + + +@registerContainerClass +class Messages(Container): + + itemFactory = Message + indexes = [('domain', 'action', 'class', 'item'), ('domain', 'class', 'item')] + tableName = 'messages' + insertOnChange = True diff --git a/scopes/tests/data_auth.py b/scopes/tests/data_auth.py index f629e5e..a3b9425 100644 --- a/scopes/tests/data_auth.py +++ b/scopes/tests/data_auth.py @@ -2,6 +2,13 @@ """provide response data for testing (via dummy_requests)""" +from cryptography.hazmat.primitives.asymmetric import rsa +from scopes import util + +private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) +public_key = private_key.public_key() +public_key_n = util.b64e(public_key.public_numbers().n.to_bytes(256)).decode('ASCII') + oidc_data = { 'test://oidc/.well-known/openid-configuration': { "issuer": "test://oidc", @@ -14,17 +21,11 @@ oidc_data = { "device_authorization_endpoint": "test://oidc/oauth/v2/device_authorization", "jwks_uri": "test://oidc/oauth/v2/keys"}, 'test://oidc/oauth/v2/keys': { "keys": [ - {"use": "sig", - "kty": "RSA", - "kid": "316638486247563085", - "alg": "RS256", - "n": "167qFCfRa0tRR0MZv-PQVwdiVFf0NtfN-zFAogRASm6437sbXfsfxkpbh1F77TwQdl4qlR5Na_Ecs8VTxOuyHmuhIJ4FyZV4M0h71KRw7LCTVuNw7mWLpbjKPBzidyhctbkJrkcKtJymnHELsct0CdT16Lb27phd_0cBJexGbwhVNQBs10VbkvUJHHOJe6A_JVS9Q3_3MEWyCyFoHPeMchlk_Gd6yMiH4aJ1ql3GZD6c2JB9crloTH_oPWWFQObGoXTKcFonEBdkrwuCQfRVOfGh8UIhIcTM0JNgqtQOCcIkf0emfI30SoWSc6Qz8lU70Vpmb3qQgsqATFICgzgABw", - "e": "AQAB"}, {"use": "sig", "kty": "RSA", "kid": "316766976250797901", "alg": "RS256", - "n": "yZKIsrUWT2fEj4OtUUFYQbEe_Clodz464tn5vMAQ0q8zV07bqFaA7WKuBflowYctDNxoxdbiFNISpKEOx6yFnx7_g6Zd46DWsj5ggGZvNkgOa9SqTIsA7ho9nk7LDLQRpV0k5N1HkiG66GUqUCV2llJhstpTDQQLDvhI3qussG2HyylpTQSu-9b6gry0rb397yjAnXQu6tFOubEDteTN0fLNMblcdd2AvZKpGA2o_-M5U6AckezfmBCBdHWmrwxpjGGf7KWqGg8j6bJkV3sMg4XfD2x0KNog_3D-0pSx6k8dSWZGkNlDxB5AdWvNDYg1stkvjeNEbIJAhv0-awLs9Q", + "n": public_key_n, "e": "AQAB"}]} } diff --git a/scopes/tests/tlib_web.py b/scopes/tests/tlib_web.py index 80f3b77..b556346 100644 --- a/scopes/tests/tlib_web.py +++ b/scopes/tests/tlib_web.py @@ -37,3 +37,6 @@ def test_auth(self, config): headers = dict(response.getHeaders()) logger.info('test_auth: response %s %s', response.getStatus(), headers) self.assertEqual(response.getStatus(), 302) + uri = config.oidc_params['op_uris']['jwks_uri'] + keys = oidc.loadOidcKeys(uri) + logger.info('test_auth keys: %s', keys) diff --git a/scopes/web/auth/oidc.py b/scopes/web/auth/oidc.py index feaaa52..03fee41 100644 --- a/scopes/web/auth/oidc.py +++ b/scopes/web/auth/oidc.py @@ -104,8 +104,6 @@ class Authenticator(DummyFolder): return None def login(self): - req = self.request - #print('***', dir(req)) state = util.rndstr() nonce = util.rndstr() codeVerifier = util.rndstr2() @@ -123,7 +121,7 @@ class Authenticator(DummyFolder): authUrl = self.params['op_uris']['authorization_endpoint'] loginUrl = '?'.join((authUrl, urlencode(args))) logger.debug('login: URL %s', loginUrl) - req.response.redirect(loginUrl, trusted=True) + self.request.response.redirect(loginUrl, trusted=True) def callback(self): req = self.request @@ -142,24 +140,18 @@ class Authenticator(DummyFolder): tokenUrl = self.params['op_uris']['token_endpoint'] tokenResponse = requests.post(tokenUrl, data=args) tdata = tokenResponse.json() - #print('*** token response', tdata) userData = self.getIdTokenData(tdata['id_token']) - #print('*** token id claims', userData) - #headers = dict(Authorization='Bearer ' + tdata['access_token']) - #userInfoUrl = self.params['op_uris']['userinfo_endpoint'] - #userData = requests.get(userInfoUrl, headers=headers).json() - #print('*** user data', userData) groupInfo = userData.get('urn:zitadel:iam:org:project:roles', {}) - #print('*** group info', groupInfo) - groupInfo = userData.get('urn:zitadel:iam:org:project:roles') ndata = dict( userid=userData['preferred_username'], name=userData['name'], email=userData['email'], groups=list(groupInfo.keys()), access_token=tdata['access_token'], + session_id=userData['sid'], ) self.storeSession(ndata) + logger.debug('callback: session data: %s', ndata) req.response.redirect(self.reqUrl, trusted=True) def logout(self): @@ -173,7 +165,7 @@ class Authenticator(DummyFolder): options = dict( path='/', expires=formatdate(time() + lifetime, localtime=False, usegmt=True), - #httponly=True, + httponly=True, ) options['max-age'] = lifetime domain = self.params['cookie_domain'] @@ -181,7 +173,6 @@ class Authenticator(DummyFolder): options['domain'] = domain name = self.params['cookie_name'] value = json.dumps(data) - #print('*** storeSession', name, value, options) if self.cookieCrypt: value = self.cookieCrypt.encrypt(value.encode('UTF-8')).decode('ASCII') self.request.response.setCookie(name, value, **options) @@ -192,7 +183,6 @@ class Authenticator(DummyFolder): return {} if self.cookieCrypt: cookie = self.cookieCrypt.decrypt(cookie) - #print('*** loadSession', self.params['cookie_name'], cookie) # !error check: return None - or raise error? data = json.loads(cookie) return data @@ -205,10 +195,6 @@ class Authenticator(DummyFolder): return jwt.decode(token, key, audience=self.params['client_id']) -def loadOidcKeys(uri): - return dict((item['kid'], item) for item in requests.get(uri).json()['keys']) - - @register('auth') def authView(context, request): return Authenticator(request) @@ -248,3 +234,6 @@ def loadOidcProviderData(force=False): uris[key] = opData[key] #if force or params.get('op_keys') is None: #params['op_keys'] = requests.get(uris['jwks_uri']).json()['keys'] + +def loadOidcKeys(uri): + return dict((item['kid'], item) for item in requests.get(uri).json()['keys'])