Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: status endpoint #92

Merged
merged 15 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 87 additions & 32 deletions pyeudiw/satosa/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import uuid
from datetime import datetime, timedelta
from typing import Union
from urllib.parse import quote_plus, urlencode
from urllib.parse import quote_plus, urlencode, urlparse, parse_qs

import satosa.logging_util as lu
from satosa.backends.base import BackendModule
Expand Down Expand Up @@ -175,17 +175,32 @@ def entity_configuration_endpoint(self, context, *args):

def pre_request_endpoint(self, context, internal_request, **kwargs):

session_id = str(context.state["SESSION_ID"])
state = str(uuid.uuid4())

# Init session
try:
self.db_engine.init_session(
state=state,
session_id=session_id
)
except Exception as e:
_msg = (f"Error while initializing session with state {state} and {session_id}.\n"
f"{e.__class__.__name__}: {e}")
return self.handle_error(context, message=_msg, err_code="500")

# PAR
payload = {
'client_id': self.client_id,
'request_uri': self.absolute_request_url
'request_uri': self.absolute_request_url + f'?id={state}',
}

url_params = urlencode(payload, quote_via=quote_plus)

if is_smartphone(context.http_headers.get('HTTP_USER_AGENT')):
# Same Device flow
res_url = f'{self.config["authorization"]["url_scheme"]}://authorize?{url_params}'
res_url = \
f'{self.config["authorization"]["url_scheme"]}://authorize?{url_params}'
return Redirect(res_url)

# Cross Device flow
Expand Down Expand Up @@ -233,7 +248,7 @@ def _translate_response(self, response, issuer):

def _handle_vp(self, vp_token: str, context: Context) -> dict:
valid, value = None, None

valid, value = check_vp_token(
vp_token, None, self.metadata_jwks_by_kids)

Expand Down Expand Up @@ -303,21 +318,23 @@ def redirect_endpoint(self, context, *args):
try:
result = self._handle_vp(vp, context)
except InvalidVPToken as e:
self.handle_error(context=context, message=f"Cannot validate SD_JWT", err_code="400")
return self.handle_error(context=context, message=f"Cannot validate SD_JWT", err_code="400")
except NoNonceInVPToken as e:
self.handle_error(context=context, message=f"Nonce is missing in vp", err_code="400")
return self.handle_error(context=context, message=f"Nonce is missing in vp", err_code="400")
except ValidationError as e:
self.handle_error(context=context, message=f"Error validating schemas: {e}", err_code="400")
return self.handle_error(context=context, message=f"Error validating schemas: {e}", err_code="400")
except KIDNotFound as e:
self.handle_error(context=context, message=f"Kid error: {e}", err_code="400")
return self.handle_error(context=context, message=f"Kid error: {e}", err_code="400")
except Exception as e:
self.handle_error(context=context, message=f"VP parsing error: {e}", err_code="400")
return self.handle_error(context=context, message=f"VP parsing error: {e}", err_code="400")

# TODO: this is not clear ... since the nonce must be taken from the originatin authz request, taken from the storage (mongodb)
if not nonce:
nonce = result["nonce"]
elif nonce != result["nonce"]:
self.handle_error(context=self, message=f"Presentation has divergent nonces: {e}", err_code="401")
return self.handle_error(context=self,
message=f"Presentation has divergent nonces:\n{nonce}!={result['nonce']}",
err_code="401")
else:
claims.append(result["claims"])

Expand Down Expand Up @@ -346,7 +363,7 @@ def redirect_endpoint(self, context, *args):
try:
self.db_engine.update_response_object(nonce, state, internal_resp)
except Exception as e:
self.handle_error(context=context, message=f"Cannot update response object: {e}", err_code="500")
return self.handle_error(context=context, message=f"Cannot update response object: {e}", err_code="500")

return self.auth_callback_func(context, internal_resp)

Expand Down Expand Up @@ -404,50 +421,57 @@ def _request_endpoint_dpop(self, context, *args) -> Union[JsonResponse, None]:
self._log(context, level='warning', message=_msg)

def request_endpoint(self, context, *args):
jwk = self.metadata_jwk

# check DPOP for WIA if any
dpop_validation_error = self._request_endpoint_dpop(context)
if dpop_validation_error:
return dpop_validation_error

# TODO: do customization if the WIA is available

# TODO: take the response and extract from jwt the public key of holder

try:
entity_id = self.db_engine.init_session(
dpop_proof=context.http_headers['HTTP_DPOP'],
attestation=context.http_headers['HTTP_AUTHORIZATION']
)
state = context.qs_params["id"]
except Exception as e:
self.handle_error(context=context, message=f"Cannot init session: {e}", err_code="500")

nonce = str(uuid.uuid4())
state = str(uuid.uuid4())
_msg = "Error while retrieving id from qs_params: "\
f"{e.__class__.__name__}: {e}"
return self.handle_error(context, message=_msg, err_code="403")

try:
dpop_proof = context.http_headers['HTTP_DPOP']
attestation = context.http_headers['HTTP_AUTHORIZATION']
except KeyError as e:
_msg = f"Error while accessing http headers: {e}"
return self.handle_error(context, message=_msg, err_code="403")

# verify the jwt
helper = JWSHelper(jwk)
data = {
"scope": ' '.join(self.config['authorization']['scopes']),
"client_id_scheme": "entity_id", # that's federation.
"client_id": self.client_id,
"response_mode": "direct_post.jwt", # only HTTP POST is allowed.
"response_type": "vp_token",
"response_uri": self.config["metadata"]["redirect_uris"][0],
"nonce": nonce,
"nonce": str(uuid.uuid4()),
"state": state,
"iss": self.client_id,
"iat": iat_now(),
"exp": iat_now() + (self.default_exp * 60) # in seconds
}
jwt = helper.sign(data)
response = {"response": jwt}


try:
self.db_engine.update_request_object(entity_id, nonce, state, data)
document = self.db_engine.get_by_state(state)
document_id = document["document_id"]
self.db_engine.add_dpop_proof_and_attestation(document_id, dpop_proof, attestation)
self.db_engine.update_request_object(document_id, data)
self.db_engine.set_finalized(document_id)
except ValueError as e:
_msg = "Error while retrieving request object from database: "\
f"{e.__class__.__name__}: {e}"
return self.handle_error(context, message=_msg, err_code="403")
except Exception as e:
self.handle_error(context=context, message=f"Cannot update request object: {e}", err_code="500")
_msg = f"Error while updating request object: {e}"
return self.handle_error(context, message=_msg, err_code="500")

helper = JWSHelper(self.metadata_jwk)
jwt = helper.sign(data)
response = {"response": jwt}

return JsonResponse(
response,
Expand Down Expand Up @@ -477,3 +501,34 @@ def handle_error(
},
status=err_code
)

def state_endpoint(self, context):
session_id = context.state["SESSION_ID"]
salvatorelaiso marked this conversation as resolved.
Show resolved Hide resolved
try:
state = context.qs_params["id"]
except TypeError as e:
_msg = f"No query params found! {e}"
return self.handle_error(context, message=_msg, err_code="403")
except KeyError as e:
_msg = f"No id found in qs_params! {e}"
return self.handle_error(context, message=_msg, err_code="403")

try:
session = self.db_engine.get_by_state_and_session_id(state=state, session_id=session_id)
except ValueError as e:
_msg = f"Error while retrieving session by state {state} and session_id {session_id}.\n{e}"
return self.handle_error(context, message=_msg, err_code="403")

if session["finalized"]:
return JsonResponse({
"response": "Authentication successful"
},
status="302"
)
else:
return JsonResponse(
{
"response": "Request object issued"
},
status="204"
)
salvatorelaiso marked this conversation as resolved.
Show resolved Hide resolved
16 changes: 14 additions & 2 deletions pyeudiw/storage/base_storage.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
class BaseStorage(object):
def init_session(self, document_id: str, dpop_proof: dict, attestation: dict):
def init_session(self, document_id: str, session_id: str, state: str):
NotImplementedError()

def update_request_object(self, document_id: str, nonce: str, state: str | None, request_object: dict):
def add_dpop_proof_and_attestation(self, document_id: str, *, dpop_proof: dict, attestation: dict) -> str:
NotImplementedError()

def set_finalized(self, document_id: str):
NotImplementedError()

def update_request_object(self, document_id: str, request_object: dict):
NotImplementedError()

def update_response_object(self, nonce: str, state: str | None, response_object: dict):
NotImplementedError()

def exists_by_state_and_session_id(self, *, state: str, session_id: str | None = None) -> bool:
NotImplementedError()

def get_by_state_and_session_id(self, *, state: str, session_id: str | None = None):
NotImplementedError()
72 changes: 65 additions & 7 deletions pyeudiw/storage/db_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,60 @@ def _handle_instance(self, instance: dict) -> dict[BaseStorage | None, BaseCache

return storage_instance, cache_instance

def init_session(self, dpop_proof: dict, attestation: dict) -> str:
def init_session(self, session_id: str, state: str) -> str:
document_id = str(uuid.uuid4())
for db_name, storage in self.storages:
try:
storage.init_session(document_id, dpop_proof, attestation)
storage.init_session(document_id, session_id=session_id, state=state)
except Exception as e:
logger.critical(f"Error {str(e)}")
logger.critical(
f"Cannot write document with id {document_id} on {db_name}")
f"Error while initializing session with document_id {document_id}."
f"Cannot write document with id {document_id} on {db_name}.\n"
f"{e.__class__.__name__}: {e}")

return document_id

def update_request_object(self, document_id: str, nonce: str, state: str | None, request_object: dict) -> tuple[str, str, int]:
def add_dpop_proof_and_attestation(self, document_id, dpop_proof: dict, attestation: dict):
replica_count = 0
for db_name, storage in self.storages:
try:
storage.add_dpop_proof_and_attestation(
document_id, dpop_proof=dpop_proof, attestation=attestation)
replica_count += 1
except Exception as e:
logger.critical(f"Error {str(e)}")
logger.critical(
f"Cannot update document with id {document_id} on {db_name}")

if replica_count == 0:
raise Exception(
f"Cannot update document {document_id} on any instance")

return replica_count

def set_finalized(self, document_id: str):
replica_count = 0
for db_name, storage in self.storages:
try:
storage.set_finalized(document_id)
replica_count += 1
except Exception as e:
logger.critical(f"Error {str(e)}")
logger.critical(
f"Cannot update document with id {document_id} on {db_name}")

if replica_count == 0:
raise Exception(
f"Cannot update document {document_id} on any instance")

return replica_count

def update_request_object(self, document_id: str, request_object: dict) -> int:
replica_count = 0
for db_name, storage in self.storages:
try:
storage.update_request_object(
document_id, nonce, state, request_object)
document_id, request_object)
replica_count += 1
except Exception as e:
logger.critical(f"Error {str(e)}")
Expand All @@ -69,7 +105,7 @@ def update_request_object(self, document_id: str, nonce: str, state: str | None,
raise Exception(
f"Cannot update document {document_id} on any instance")

return nonce, state, replica_count
return replica_count

def update_response_object(self, nonce: str, state: str, response_object: dict) -> int:
replica_count = 0
Expand Down Expand Up @@ -137,3 +173,25 @@ def overwrite(self, object_name: str, value_gen_fn: Callable[[], str]) -> dict:
"Cannot overwrite cache object with identifier {object_name} on cache {cache_name}")

return cache_object

def exists_by_state_and_session_id(self, state: str, session_id: str | None = None) -> bool:
for db_name, storage in self.storages:
found = storage.exists_by_state_and_session_id(state=state, session_id=session_id)
if found:
return True
return False

def get_by_state(self, state: str):
return self.get_by_state_and_session_id(state=state)

def get_by_state_and_session_id(self, state: str, session_id: str | None = None):
for db_name, storage in self.storages:
try:
document = storage.get_by_state_and_session_id(state, session_id)
return document
except ValueError:
logger.debug(
f"Document object with state {state} and session_id {session_id} not found in db {db_name}")

logger.error(f"Document object with state {state} and session_id {session_id} not found!")
raise ValueError(f"Document object with state {state} and session_id {session_id} not found!")
Loading
Loading