ory hydra update to version 2
This commit is contained in:
		
							parent
							
								
									4a31250bca
								
							
						
					
					
						commit
						65ceb2abbd
					
				
					 6 changed files with 52 additions and 44 deletions
				
			
		
							
								
								
									
										3
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
{
 | 
			
		||||
    "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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/<client_id>', 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():
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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/<client_id>', 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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue