ory hydra update to version 2
This commit is contained in:
parent
4a31250bca
commit
65ceb2abbd
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.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)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue