ory hydra update to version 2

This commit is contained in:
TuxCoder 2023-03-17 08:52:33 +01:00
parent 4a31250bca
commit 65ceb2abbd
6 changed files with 52 additions and 44 deletions

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix"
}

View file

@ -3,7 +3,7 @@ from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization 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.oid import NameOID, ExtendedKeyUsageOID
from cryptography.x509 import ObjectIdentifier from cryptography.x509 import ObjectIdentifier
from pathlib import Path from pathlib import Path
@ -109,6 +109,9 @@ class Pki(object):
_public_key = serialization.load_pem_public_key( _public_key = serialization.load_pem_public_key(
publickey.encode(), backend=default_backend()) 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_private_key, ca_cert = self._init_ca(service)
ca_name = service.name ca_name = service.name
username = str(user.username) username = str(user.username)

View file

@ -6,8 +6,8 @@ from flask.typing import ResponseReturnValue
from flask_login import current_user, logout_user from flask_login import current_user, logout_user
from oauthlib.oauth2.rfc6749.errors import TokenExpiredError from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
from authlib.integrations.base_client.errors import InvalidTokenError 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.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 OAuth2Client, GenericError from ory_hydra_client.models import OAuth20Client, GenericError
from typing import Optional from typing import Optional
from collections.abc import Iterable from collections.abc import Iterable
import logging import logging
@ -77,7 +77,7 @@ def registration_accept(registration_id) -> ResponseReturnValue:
@admin_views.route('/clients') @admin_views.route('/clients')
async def clients() -> ResponseReturnValue: 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) return render_template('admin/clients.html.j2', clients=clients)
@admin_views.route('/client/<client_id>', methods=['GET', 'POST']) @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(): if form.validate_on_submit():
form.populate_obj(client) 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): if client is None or isinstance(client, GenericError):
logger.error(f"oauth2 client update failed: '{client_id}'") logger.error(f"oauth2 client update failed: '{client_id}'")
return 'client update failed', 500 return 'client update failed', 500
@ -105,7 +105,7 @@ async def client(client_id: str) -> ResponseReturnValue:
@admin_views.route('/client_new', methods=['GET','POST']) @admin_views.route('/client_new', methods=['GET','POST'])
async def client_new() -> ResponseReturnValue: async def client_new() -> ResponseReturnValue:
client = OAuth2Client() client = OAuth20Client()
form = OAuth2ClientForm() form = OAuth2ClientForm()
if form.validate_on_submit(): if form.validate_on_submit():

View file

@ -16,7 +16,7 @@ import secrets
from ..model import db, User from ..model import db, User
from ..hydra import hydra_service from ..hydra import hydra_service
from ..lenticular_services import lenticular_services 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 from ory_hydra_client.models import GenericError

View file

@ -17,8 +17,9 @@ import crypt
from datetime import datetime from datetime import datetime
import logging import logging
import json 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.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.models import AcceptLoginRequest, AcceptConsentRequest, ConsentRequestSession, GenericError, ConsentRequestSessionAccessToken, ConsentRequestSessionIdToken from ory_hydra_client import models as ory_hydra_m
from ory_hydra_client.models import TheRequestPayloadUsedToAcceptALoginOrConsentRequest, TheRequestPayloadUsedToAcceptAConsentRequest, GenericError
from typing import Optional from typing import Optional
from ..model import db, User, SecurityUser from ..model import db, User, SecurityUser
@ -43,19 +44,11 @@ async def consent() -> ResponseReturnValue:
remember_for = 60*60*24*30 # remember for 30 days remember_for = 60*60*24*30 # remember for 30 days
#try: #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')) 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_scope = consent_request.requested_scope
requested_audiences = consent_request.requested_access_token_audience 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] user = User.query.get(consent_request.subject) # type: Optional[User]
if user is None: if user is None:
return 'internal error', 500 return 'internal error', 500
token_data = { access_token = {
'name': str(user.username), 'name': str(user.username),
'preferred_username': str(user.username), 'preferred_username': str(user.username),
'username': str(user.username), 'username': str(user.username),
@ -73,22 +66,20 @@ async def consent() -> ResponseReturnValue:
#'family_name': '-', #'family_name': '-',
'groups': [group.name for group in user.groups] 'groups': [group.name for group in user.groups]
} }
id_token_data = {} id_token = {}
if isinstance(requested_scope, list) and 'openid' in requested_scope: if isinstance(requested_scope, list) and 'openid' in requested_scope:
id_token_data = token_data id_token = access_token
access_token=ConsentRequestSessionAccessToken.from_dict(token_data) body = TheRequestPayloadUsedToAcceptAConsentRequest(
id_token=ConsentRequestSessionIdToken.from_dict(id_token_data)
body = AcceptConsentRequest(
grant_scope= requested_scope, grant_scope= requested_scope,
grant_access_token_audience= requested_audiences, grant_access_token_audience= requested_audiences,
remember= form.data['remember'], remember= form.data['remember'],
remember_for= remember_for, remember_for= remember_for,
session= ConsentRequestSession( session= ory_hydra_m.PassSessionDataToAConsentRequest(
access_token= access_token, access_token= access_token,
id_token= id_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, json_body=body,
consent_challenge=consent_request.challenge) consent_challenge=consent_request.challenge)
if resp is None or isinstance( resp, GenericError): if resp is None or isinstance( resp, GenericError):
@ -107,15 +98,15 @@ async def login() -> ResponseReturnValue:
login_challenge = request.args.get('login_challenge') login_challenge = request.args.get('login_challenge')
if login_challenge is None: if login_challenge is None:
return 'login_challenge missing', 400 return 'login_challenge missing', 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 or isinstance( login_request, GenericError): if login_request is None or isinstance( login_request, ory_hydra_m.OAuth20RedirectBrowserTo):
logger.exception("could not fetch login request") logger.exception("could not fetch login request")
return redirect(url_for('frontend.index')) return redirect(url_for('frontend.index'))
if login_request.skip: 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, 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): if resp is None or isinstance( resp, GenericError):
return 'internal error, could not forward request', 503 return 'internal error, could not forward request', 503
@ -138,7 +129,7 @@ async def login_auth() -> ResponseReturnValue:
login_challenge = request.args.get('login_challenge') login_challenge = request.args.get('login_challenge')
if login_challenge is None: if login_challenge is None:
return 'missing login_challenge, bad request', 400 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: if login_request is None:
return redirect(url_for('frontend.index')) return redirect(url_for('frontend.index'))
@ -166,8 +157,8 @@ async def login_auth() -> ResponseReturnValue:
subject = user.id subject = user.id
user.last_login = datetime.now() user.last_login = datetime.now()
db.session.commit() db.session.commit()
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( login_challenge=login_challenge, json_body=ory_hydra_m.HandledLoginRequestIsTheRequestPayloadUsedToAcceptALoginRequest(
subject=subject, subject=subject,
remember=remember_me, remember=remember_me,
)) ))
@ -198,7 +189,7 @@ async def logout() -> ResponseReturnValue:
if logout_challenge is None: if logout_challenge is None:
return 'invalid request, logout_challenge not set', 400 return 'invalid request, logout_challenge not set', 400
# TODO confirm # 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): if resp is None or isinstance( resp, GenericError):
return 'internal error, could not forward request', 503 return 'internal error, could not forward request', 503
return redirect(resp.redirect_to) return redirect(resp.redirect_to)

View file

@ -2,7 +2,7 @@
from authlib.integrations.base_client.errors import MissingTokenError, InvalidTokenError from authlib.integrations.base_client.errors import MissingTokenError, InvalidTokenError
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from fido2 import cbor 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 Blueprint, Response, redirect, request
from flask import current_app from flask import current_app
from flask import jsonify, session, flash from flask import jsonify, session, flash
@ -15,13 +15,13 @@ from datetime import timedelta
from base64 import b64decode from base64 import b64decode
from flask.typing import ResponseReturnValue from flask.typing import ResponseReturnValue
from oauthlib.oauth2.rfc6749.errors import TokenExpiredError 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 ory_hydra_client.models import GenericError
from urllib.parse import urlencode, parse_qs from urllib.parse import urlencode, parse_qs
from random import SystemRandom from random import SystemRandom
import string import string
from collections.abc import Iterable 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 ..model import db, User, SecurityUser, Totp, AppToken, WebauthnCredential
from ..form.frontend import ClientCertForm, TOTPForm, \ 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]: def webauthn_credentials(user: User) -> list[AttestedCredentialData]:
"""get and decode all credentials for given user""" """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: def random_string(length=32) -> str:
@ -267,8 +275,9 @@ def webauthn_pkcco_route() -> ResponseReturnValue:
user_handle = random_string() user_handle = random_string()
exclude_credentials = webauthn_credentials(user) exclude_credentials = webauthn_credentials(user)
pkcco, state = webauthn.register_begin( pkcco, state = webauthn.register_begin(
{'id': user_handle.encode('utf-8'), 'name': user.username, 'displayName': user.username}, user=PublicKeyCredentialUserEntity(id=user_handle.encode('utf-8'), name=user.username, display_name=user.username),
exclude_credentials) credentials=exclude_credentials
)
session['webauthn_register_user_handle'] = user_handle session['webauthn_register_user_handle'] = user_handle
session['webauthn_register_state'] = state session['webauthn_register_state'] = state
return Response(b64encode(cbor.encode(pkcco)).decode('utf-8'), mimetype='text/plain') 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(): if form.validate_on_submit():
try: try:
attestation = cbor.decode(b64decode(form.attestation.data)) 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( auth_data = webauthn.register_complete(
session.pop('webauthn_register_state'), session.pop('webauthn_register_state'),
ClientData(attestation['clientDataJSON']), CollectedClientData(attestation['clientDataJSON']),
AttestationObject(attestation['attestationObject'])) AttestationObject(attestation['attestationObject']))
db.session.add(WebauthnCredential( db.session.add(WebauthnCredential(
@ -331,7 +342,7 @@ def password_change_post() -> ResponseReturnValue:
async def oauth2_tokens() -> ResponseReturnValue: async def oauth2_tokens() -> ResponseReturnValue:
subject = oauth2.custom.get('/userinfo').json()['sub'] 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): if consent_sessions is None or isinstance( consent_sessions, GenericError):
return 'internal error, could not fetch sessions', 500 return 'internal error, could not fetch sessions', 500
return render_template( return render_template(
@ -342,7 +353,7 @@ async def oauth2_tokens() -> ResponseReturnValue:
@frontend_views.route('/oauth2_token/<client_id>', methods=['DELETE']) @frontend_views.route('/oauth2_token/<client_id>', methods=['DELETE'])
async def oauth2_token_revoke(client_id: str) -> ResponseReturnValue: async def oauth2_token_revoke(client_id: str) -> ResponseReturnValue:
subject = oauth2.session.get('/userinfo').json()['sub'] 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, subject=subject,
client=client_id) client=client_id)