diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..12b229e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix" +} \ No newline at end of file diff --git a/lenticular_cloud/pki.py b/lenticular_cloud/pki.py index b95deb0..2062782 100644 --- a/lenticular_cloud/pki.py +++ b/lenticular_cloud/pki.py @@ -3,7 +3,7 @@ from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric import rsa, dh from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID from cryptography.x509 import ObjectIdentifier from pathlib import Path @@ -109,6 +109,9 @@ class Pki(object): _public_key = serialization.load_pem_public_key( publickey.encode(), backend=default_backend()) + if isinstance(_public_key, dh.DHPublicKey): + raise AssertionError('key can not be a dsa key') + ca_private_key, ca_cert = self._init_ca(service) ca_name = service.name username = str(user.username) diff --git a/lenticular_cloud/views/admin.py b/lenticular_cloud/views/admin.py index 39556c8..9678dec 100644 --- a/lenticular_cloud/views/admin.py +++ b/lenticular_cloud/views/admin.py @@ -6,8 +6,8 @@ from flask.typing import ResponseReturnValue from flask_login import current_user, logout_user from oauthlib.oauth2.rfc6749.errors import TokenExpiredError from authlib.integrations.base_client.errors import InvalidTokenError -from ory_hydra_client.api.admin import list_o_auth_2_clients, get_o_auth_2_client, update_o_auth_2_client, create_o_auth_2_client -from ory_hydra_client.models import OAuth2Client, GenericError +from ory_hydra_client.api.o_auth_2 import list_o_auth_2_clients, get_o_auth_2_client, set_o_auth_2_client, create_o_auth_2_client +from ory_hydra_client.models import OAuth20Client, GenericError from typing import Optional from collections.abc import Iterable import logging @@ -77,7 +77,7 @@ def registration_accept(registration_id) -> ResponseReturnValue: @admin_views.route('/clients') async def clients() -> ResponseReturnValue: - clients = await list_o_auth_2_clients.asyncio(_client=hydra_service.hydra_client) + clients = await list_o_auth_2_clients.asyncio_detailed(_client=hydra_service.hydra_client) return render_template('admin/clients.html.j2', clients=clients) @admin_views.route('/client/', methods=['GET', 'POST']) @@ -92,7 +92,7 @@ async def client(client_id: str) -> ResponseReturnValue: if form.validate_on_submit(): form.populate_obj(client) - client = await update_o_auth_2_client.asyncio(id=client_id ,json_body=client, _client=hydra_service.hydra_client) + client = await set_o_auth_2_client.asyncio(id=client_id ,json_body=client, _client=hydra_service.hydra_client) if client is None or isinstance(client, GenericError): logger.error(f"oauth2 client update failed: '{client_id}'") return 'client update failed', 500 @@ -105,7 +105,7 @@ async def client(client_id: str) -> ResponseReturnValue: @admin_views.route('/client_new', methods=['GET','POST']) async def client_new() -> ResponseReturnValue: - client = OAuth2Client() + client = OAuth20Client() form = OAuth2ClientForm() if form.validate_on_submit(): diff --git a/lenticular_cloud/views/api.py b/lenticular_cloud/views/api.py index 64f419d..2de32c0 100644 --- a/lenticular_cloud/views/api.py +++ b/lenticular_cloud/views/api.py @@ -16,7 +16,7 @@ import secrets from ..model import db, User from ..hydra import hydra_service from ..lenticular_services import lenticular_services -from ory_hydra_client.api.admin import introspect_o_auth_2_token +from ory_hydra_client.api.o_auth_2 import introspect_o_auth_2_token from ory_hydra_client.models import GenericError diff --git a/lenticular_cloud/views/auth.py b/lenticular_cloud/views/auth.py index 940f249..a153edb 100644 --- a/lenticular_cloud/views/auth.py +++ b/lenticular_cloud/views/auth.py @@ -17,8 +17,9 @@ import crypt from datetime import datetime import logging import json -from ory_hydra_client.api.admin import get_consent_request, accept_consent_request, accept_login_request, get_login_request, accept_login_request, accept_logout_request, get_login_request -from ory_hydra_client.models import AcceptLoginRequest, AcceptConsentRequest, ConsentRequestSession, GenericError, ConsentRequestSessionAccessToken, ConsentRequestSessionIdToken +from ory_hydra_client.api.o_auth_2 import get_o_auth_2_consent_request, accept_o_auth_2_consent_request, accept_o_auth_2_login_request, get_o_auth_2_login_request, accept_o_auth_2_login_request, accept_o_auth_2_logout_request, get_o_auth_2_login_request +from ory_hydra_client import models as ory_hydra_m +from ory_hydra_client.models import TheRequestPayloadUsedToAcceptALoginOrConsentRequest, TheRequestPayloadUsedToAcceptAConsentRequest, GenericError from typing import Optional from ..model import db, User, SecurityUser @@ -43,19 +44,11 @@ async def consent() -> ResponseReturnValue: remember_for = 60*60*24*30 # remember for 30 days #try: - consent_request = await get_consent_request.asyncio(consent_challenge=request.args['consent_challenge'],_client=hydra_service.hydra_client) + consent_request = await get_o_auth_2_consent_request.asyncio(consent_challenge=request.args['consent_challenge'],_client=hydra_service.hydra_client) - if consent_request is None or isinstance( consent_request, GenericError): + if consent_request is None or isinstance( consent_request, ory_hydra_m.OAuth20RedirectBrowserTo): return redirect(url_for('frontend.index')) - -# except ory_hydra_client.exceptions.ApiValueError: -# logger.info('ory exception - could not fetch user data ApiValueError') -# return redirect(url_for('frontend.index')) -# except ory_hydra_client.exceptions.ApiException: -# logger.exception('ory exception - could not fetch user data') -# return redirect(url_for('frontend.index')) - requested_scope = consent_request.requested_scope requested_audiences = consent_request.requested_access_token_audience @@ -63,7 +56,7 @@ async def consent() -> ResponseReturnValue: user = User.query.get(consent_request.subject) # type: Optional[User] if user is None: return 'internal error', 500 - token_data = { + access_token = { 'name': str(user.username), 'preferred_username': str(user.username), 'username': str(user.username), @@ -73,22 +66,20 @@ async def consent() -> ResponseReturnValue: #'family_name': '-', 'groups': [group.name for group in user.groups] } - id_token_data = {} + id_token = {} if isinstance(requested_scope, list) and 'openid' in requested_scope: - id_token_data = token_data - access_token=ConsentRequestSessionAccessToken.from_dict(token_data) - id_token=ConsentRequestSessionIdToken.from_dict(id_token_data) - body = AcceptConsentRequest( + id_token = access_token + body = TheRequestPayloadUsedToAcceptAConsentRequest( grant_scope= requested_scope, grant_access_token_audience= requested_audiences, remember= form.data['remember'], remember_for= remember_for, - session= ConsentRequestSession( + session= ory_hydra_m.PassSessionDataToAConsentRequest( access_token= access_token, id_token= id_token ) ) - resp = await accept_consent_request.asyncio(_client=hydra_service.hydra_client, + resp = await accept_o_auth_2_consent_request.asyncio(_client=hydra_service.hydra_client, json_body=body, consent_challenge=consent_request.challenge) if resp is None or isinstance( resp, GenericError): @@ -107,15 +98,15 @@ async def login() -> ResponseReturnValue: login_challenge = request.args.get('login_challenge') if login_challenge is None: return 'login_challenge missing', 400 - login_request = await get_login_request.asyncio(_client=hydra_service.hydra_client, login_challenge=login_challenge) - if login_request is None or isinstance( login_request, GenericError): + login_request = await get_o_auth_2_login_request.asyncio(_client=hydra_service.hydra_client, login_challenge=login_challenge) + if login_request is None or isinstance( login_request, ory_hydra_m.OAuth20RedirectBrowserTo): logger.exception("could not fetch login request") return redirect(url_for('frontend.index')) if login_request.skip: - resp = await accept_login_request.asyncio(_client=hydra_service.hydra_client, + resp = await accept_o_auth_2_login_request.asyncio(_client=hydra_service.hydra_client, login_challenge=login_challenge, - json_body=AcceptLoginRequest(subject=login_request.subject)) + json_body=ory_hydra_m.HandledLoginRequestIsTheRequestPayloadUsedToAcceptALoginRequest(subject=login_request.subject)) if resp is None or isinstance( resp, GenericError): return 'internal error, could not forward request', 503 @@ -138,7 +129,7 @@ async def login_auth() -> ResponseReturnValue: login_challenge = request.args.get('login_challenge') if login_challenge is None: return 'missing login_challenge, bad request', 400 - login_request = await get_login_request.asyncio(_client=hydra_service.hydra_client, login_challenge=login_challenge) + login_request = await get_o_auth_2_login_request.asyncio(_client=hydra_service.hydra_client, login_challenge=login_challenge) if login_request is None: return redirect(url_for('frontend.index')) @@ -166,8 +157,8 @@ async def login_auth() -> ResponseReturnValue: subject = user.id user.last_login = datetime.now() db.session.commit() - resp = await accept_login_request.asyncio(_client=hydra_service.hydra_client, - login_challenge=login_challenge, json_body=AcceptLoginRequest( + resp = await accept_o_auth_2_login_request.asyncio(_client=hydra_service.hydra_client, + login_challenge=login_challenge, json_body=ory_hydra_m.HandledLoginRequestIsTheRequestPayloadUsedToAcceptALoginRequest( subject=subject, remember=remember_me, )) @@ -198,7 +189,7 @@ async def logout() -> ResponseReturnValue: if logout_challenge is None: return 'invalid request, logout_challenge not set', 400 # TODO confirm - resp = await accept_logout_request.asyncio(_client=hydra_service.hydra_client, logout_challenge=logout_challenge) + resp = await accept_o_auth_2_logout_request.asyncio(_client=hydra_service.hydra_client, logout_challenge=logout_challenge) if resp is None or isinstance( resp, GenericError): return 'internal error, could not forward request', 503 return redirect(resp.redirect_to) diff --git a/lenticular_cloud/views/frontend.py b/lenticular_cloud/views/frontend.py index 8bb16b1..9fc53e5 100644 --- a/lenticular_cloud/views/frontend.py +++ b/lenticular_cloud/views/frontend.py @@ -2,7 +2,7 @@ from authlib.integrations.base_client.errors import MissingTokenError, InvalidTokenError from base64 import b64encode, b64decode from fido2 import cbor -from fido2.webauthn import AttestationObject, AttestedCredentialData, AuthenticatorData +from fido2.webauthn import CollectedClientData, AttestationObject, AttestedCredentialData, AuthenticatorData, PublicKeyCredentialUserEntity from flask import Blueprint, Response, redirect, request from flask import current_app from flask import jsonify, session, flash @@ -15,13 +15,13 @@ from datetime import timedelta from base64 import b64decode from flask.typing import ResponseReturnValue from oauthlib.oauth2.rfc6749.errors import TokenExpiredError -from ory_hydra_client.api.admin import list_subject_consent_sessions, revoke_consent_sessions +from ory_hydra_client.api.o_auth_2 import list_o_auth_2_consent_sessions, revoke_o_auth_2_consent_sessions from ory_hydra_client.models import GenericError from urllib.parse import urlencode, parse_qs from random import SystemRandom import string from collections.abc import Iterable -from typing import Optional +from typing import Optional, Mapping, Iterator, List from ..model import db, User, SecurityUser, Totp, AppToken, WebauthnCredential from ..form.frontend import ClientCertForm, TOTPForm, \ @@ -247,9 +247,17 @@ def webauthn_delete_route(webauthn_id: str) -> ResponseReturnValue: + def webauthn_credentials(user: User) -> list[AttestedCredentialData]: """get and decode all credentials for given user""" - return [AttestedCredentialData.create(**cbor.decode(cred.credential_data)) for cred in user.webauthn_credentials] + + def decode(creds: List[WebauthnCredential]) -> Iterator[AttestedCredentialData]: + for cred in creds: + data = cbor.decode(cred.credential_data) + if isinstance(data, Mapping): + yield AttestedCredentialData.create(**data) + + return list(decode(user.webauthn_credentials)) def random_string(length=32) -> str: @@ -267,8 +275,9 @@ def webauthn_pkcco_route() -> ResponseReturnValue: user_handle = random_string() exclude_credentials = webauthn_credentials(user) pkcco, state = webauthn.register_begin( - {'id': user_handle.encode('utf-8'), 'name': user.username, 'displayName': user.username}, - exclude_credentials) + user=PublicKeyCredentialUserEntity(id=user_handle.encode('utf-8'), name=user.username, display_name=user.username), + credentials=exclude_credentials + ) session['webauthn_register_user_handle'] = user_handle session['webauthn_register_state'] = state return Response(b64encode(cbor.encode(pkcco)).decode('utf-8'), mimetype='text/plain') @@ -283,9 +292,11 @@ def webauthn_register_route() -> ResponseReturnValue: if form.validate_on_submit(): try: attestation = cbor.decode(b64decode(form.attestation.data)) + if not isinstance(attestation, Mapping) or 'clientDataJSON' not in attestation or 'attestationObject' not in attestation: + return 'invalid attestion data', 400 auth_data = webauthn.register_complete( session.pop('webauthn_register_state'), - ClientData(attestation['clientDataJSON']), + CollectedClientData(attestation['clientDataJSON']), AttestationObject(attestation['attestationObject'])) db.session.add(WebauthnCredential( @@ -331,7 +342,7 @@ def password_change_post() -> ResponseReturnValue: async def oauth2_tokens() -> ResponseReturnValue: subject = oauth2.custom.get('/userinfo').json()['sub'] - consent_sessions = await list_subject_consent_sessions.asyncio(subject=subject, _client=hydra_service.hydra_client) + consent_sessions = await list_o_auth_2_consent_sessions.asyncio(subject=subject, _client=hydra_service.hydra_client) if consent_sessions is None or isinstance( consent_sessions, GenericError): return 'internal error, could not fetch sessions', 500 return render_template( @@ -342,7 +353,7 @@ async def oauth2_tokens() -> ResponseReturnValue: @frontend_views.route('/oauth2_token/', methods=['DELETE']) async def oauth2_token_revoke(client_id: str) -> ResponseReturnValue: subject = oauth2.session.get('/userinfo').json()['sub'] - await revoke_consent_sessions.asyncio( _client=hydra_service.hydra_client, + await revoke_o_auth_2_consent_sessions.asyncio_detailed( _client=hydra_service.hydra_client, subject=subject, client=client_id)