feat: link local library
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import logging
|
||||
from django.conf import settings
|
||||
from pydantic import ValidationError
|
||||
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from spid_cie_oidc.entity.jwtse import create_jws
|
||||
from spid_cie_oidc.entity.models import FederationEntityConfiguration
|
||||
from ..oidc import *
|
||||
from ..oauth2 import *
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from spid_cie_oidc.entity.exceptions import InvalidTrustchain
|
||||
from spid_cie_oidc.entity.models import TrustChain
|
||||
from spid_cie_oidc.entity.utils import get_key
|
||||
from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain
|
||||
from spid_cie_oidc.relying_party.exceptions import ValidationException
|
||||
from spid_cie_oidc.relying_party.settings import (
|
||||
RP_DEFAULT_PROVIDER_PROFILES,
|
||||
RP_PROVIDER_PROFILES
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TokenRequestType(str, Enum):
|
||||
refresh = "refresh"
|
||||
revocation = "revocation"
|
||||
introspection = "introspection"
|
||||
|
||||
|
||||
class SpidCieOidcRp:
|
||||
"""
|
||||
Baseclass with common methods for RPs
|
||||
"""
|
||||
|
||||
def get_oidc_op(self, request) -> TrustChain:
|
||||
"""
|
||||
get available trust to a specific OP
|
||||
"""
|
||||
if not request.GET.get("provider", None):
|
||||
logger.warning(
|
||||
"Missing provider url. Please try '?provider=https://provider-subject/'"
|
||||
)
|
||||
raise InvalidTrustchain(
|
||||
"Missing provider url. Please try '?provider=https://provider-subject/'"
|
||||
)
|
||||
|
||||
trust_anchor = request.GET.get("trust_anchor", None)
|
||||
if trust_anchor is not None and trust_anchor not in settings.OIDCFED_TRUST_ANCHORS:
|
||||
logger.warning("Unallowed Trust Anchor")
|
||||
raise InvalidTrustchain("Unallowed Trust Anchor")
|
||||
|
||||
if not trust_anchor:
|
||||
for profile, value in settings.OIDCFED_IDENTITY_PROVIDERS.items():
|
||||
if request.GET["provider"] in value:
|
||||
trust_anchor = value[request.GET["provider"]]
|
||||
|
||||
if not trust_anchor:
|
||||
trust_anchor = settings.OIDCFED_DEFAULT_TRUST_ANCHOR
|
||||
|
||||
tc = TrustChain.objects.filter(
|
||||
sub=request.GET["provider"],
|
||||
trust_anchor__sub=trust_anchor,
|
||||
).first()
|
||||
|
||||
discover_trust = False
|
||||
if not tc:
|
||||
logger.info(f'Trust Chain not found for {request.GET["provider"]}')
|
||||
discover_trust = True
|
||||
|
||||
elif not tc.is_active:
|
||||
logger.warning(f"{tc} found but DISABLED at {tc.modified}")
|
||||
raise InvalidTrustchain(f"{tc} found but DISABLED at {tc.modified}")
|
||||
|
||||
elif tc.is_expired:
|
||||
logger.warning(f"{tc} found but expired at {tc.exp}")
|
||||
logger.warning("Try to renew the trust chain")
|
||||
discover_trust = True
|
||||
|
||||
if discover_trust:
|
||||
tc = get_or_create_trust_chain(
|
||||
subject=request.GET["provider"],
|
||||
trust_anchor=trust_anchor,
|
||||
# TODO - not sure that it's required for a RP that fetches OP directly from TA
|
||||
# required_trust_marks = [],
|
||||
force=True,
|
||||
)
|
||||
return tc
|
||||
|
||||
def validate_json_schema(self, request, schema_type, error_description):
|
||||
try:
|
||||
schema = RP_PROVIDER_PROFILES[RP_DEFAULT_PROVIDER_PROFILES]
|
||||
schema[schema_type](**request)
|
||||
except ValidationError as e:
|
||||
logger.error(
|
||||
f"{error_description} "
|
||||
f"for {request.get('client_id', None)}: {e}"
|
||||
)
|
||||
raise ValidationException()
|
||||
|
||||
def get_token_request(self, auth_token, request, token_type):
|
||||
|
||||
default_logout_url = getattr(
|
||||
settings, "LOGOUT_REDIRECT_URL", None
|
||||
) or reverse("spid_cie_rp_landing")
|
||||
|
||||
global audience
|
||||
authz = auth_token.authz_request
|
||||
|
||||
rp_conf = FederationEntityConfiguration.objects.filter(
|
||||
sub=authz.client_id,
|
||||
is_active=True,
|
||||
).first()
|
||||
|
||||
token_request_data = dict(
|
||||
client_id=auth_token.authz_request.client_id,
|
||||
client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
|
||||
)
|
||||
|
||||
if token_type == TokenRequestType.refresh:
|
||||
token_request_data["grant_type"] = "refresh_token"
|
||||
token_request_data["refresh_token"] = auth_token.refresh_token
|
||||
audience = authz.provider_configuration["token_endpoint"]
|
||||
|
||||
elif token_type == TokenRequestType.revocation:
|
||||
token_request_data["token"] = auth_token.access_token
|
||||
audience = authz.provider_configuration["revocation_endpoint"]
|
||||
|
||||
elif token_type == TokenRequestType.introspection:
|
||||
token_request_data["token"] = auth_token.access_token
|
||||
audience = authz.provider_configuration["introspection_endpoint"]
|
||||
|
||||
if not audience:
|
||||
logger.warning(
|
||||
f"{authz.provider_id} doesn't expose the token ", token_type, " endpoint."
|
||||
)
|
||||
return HttpResponseRedirect(default_logout_url)
|
||||
# private_key_jwt
|
||||
client_assertion = create_jws(
|
||||
{
|
||||
"iss": authz.client_id,
|
||||
"sub": authz.client_id,
|
||||
"aud": [audience],
|
||||
"iat": iat_now(),
|
||||
"exp": exp_from_now(),
|
||||
"jti": str(uuid.uuid4())
|
||||
},
|
||||
jwk_dict = get_key(rp_conf.jwks_core)
|
||||
)
|
||||
token_request_data["client_assertion"] = client_assertion
|
||||
|
||||
try:
|
||||
token_request = requests.post(
|
||||
audience,
|
||||
data=token_request_data,
|
||||
timeout=getattr(
|
||||
settings, "HTTPC_TIMEOUT", 8
|
||||
)
|
||||
) # nosec - B113
|
||||
|
||||
if token_request.status_code != 200: # pragma: no cover
|
||||
logger.error(
|
||||
f"Something went wrong with refresh token request: {token_request.status_code}"
|
||||
)
|
||||
|
||||
return token_request
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Error in token request: {e}")
|
||||
@@ -0,0 +1,214 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from copy import deepcopy
|
||||
|
||||
from djagger.decorators import schema
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
from spid_cie_oidc.entity.exceptions import InvalidTrustchain
|
||||
from spid_cie_oidc.entity.jwtse import create_jws
|
||||
from spid_cie_oidc.entity.utils import get_jwks, get_key, KeyUsage
|
||||
from spid_cie_oidc.entity.models import FederationEntityConfiguration
|
||||
from spid_cie_oidc.relying_party.settings import OIDCFED_ACR_PROFILES, RP_PROVIDER_PROFILES, \
|
||||
RP_DEFAULT_PROVIDER_PROFILES
|
||||
|
||||
from ..models import OidcAuthentication
|
||||
from ..settings import (
|
||||
RP_PKCE_CONF,
|
||||
RP_REQUEST_CLAIM_BY_PROFILE,
|
||||
RP_REQUEST_EXP
|
||||
)
|
||||
from ..utils import (
|
||||
http_dict_to_redirect_uri_path,
|
||||
random_string
|
||||
)
|
||||
|
||||
from . import SpidCieOidcRp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
schema_profile = RP_PROVIDER_PROFILES[RP_DEFAULT_PROVIDER_PROFILES]
|
||||
|
||||
|
||||
@schema(
|
||||
summary="OIDC Relying Party Authorization begin",
|
||||
methods=['GET'],
|
||||
get_response_schema= {
|
||||
"302": schema_profile["authorization_request_doc"],
|
||||
},
|
||||
external_docs = {
|
||||
"alt_text": "AgID SPID OIDC Guidelines",
|
||||
"url": (
|
||||
"https://www.agid.gov.it/it/agenzia/stampa-e-comunicazione/"
|
||||
"notizie/2021/12/06/openid-connect-spid-adottate-linee-guida"
|
||||
),
|
||||
},
|
||||
tags = ['Relying Party']
|
||||
)
|
||||
class SpidCieOidcRpBeginView(SpidCieOidcRp, View):
|
||||
"""
|
||||
View which processes the actual Authz request and
|
||||
returns a Http Redirect
|
||||
"""
|
||||
error_template = "rp_error.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
http://localhost:8001/oidc/rp/authorization?provider=http://127.0.0.1:8002/&profile=spid
|
||||
"""
|
||||
try:
|
||||
tc = self.get_oidc_op(request)
|
||||
if not tc:
|
||||
context = {
|
||||
"error": "request rejected",
|
||||
"error_description": "Trust Chain is unavailable.",
|
||||
}
|
||||
return render(request, self.error_template, context, status = 404)
|
||||
|
||||
except InvalidTrustchain as exc:
|
||||
context = {
|
||||
"error": "request rejected",
|
||||
"error_description": str(exc.args),
|
||||
}
|
||||
return render(request, self.error_template, context, status=404)
|
||||
|
||||
except Exception as exc:
|
||||
context = {
|
||||
"error": "request rejected",
|
||||
"error_description": _(str(exc.args)),
|
||||
}
|
||||
return render(request, self.error_template, context, status=403)
|
||||
|
||||
provider_metadata = tc.metadata.get('openid_provider', None)
|
||||
if not provider_metadata:
|
||||
context = {
|
||||
"error": "request rejected",
|
||||
"error_description": _("provider metadata not found"),
|
||||
}
|
||||
return render(request, self.error_template, context, status=404)
|
||||
|
||||
entity_conf = FederationEntityConfiguration.objects.filter(
|
||||
entity_type="openid_relying_party",
|
||||
# TODO: RPs multitenancy?
|
||||
# sub = request.build_absolute_uri()
|
||||
).first()
|
||||
if not entity_conf:
|
||||
context = {
|
||||
"error": "request rejected",
|
||||
"error_description": _("Missing configuration."),
|
||||
}
|
||||
return render(request, self.error_template, context, status=404)
|
||||
|
||||
client_conf = entity_conf.metadata["openid_relying_party"]
|
||||
if not (
|
||||
provider_metadata.get("jwks_uri", None)
|
||||
or
|
||||
provider_metadata.get("jwks", None)
|
||||
):
|
||||
context = {
|
||||
"error": "request rejected",
|
||||
"error_description": _("Invalid provider Metadata."),
|
||||
}
|
||||
return render(request, self.error_template, context, status=404)
|
||||
|
||||
jwks_dict = get_jwks(provider_metadata, federation_jwks=tc.jwks)
|
||||
|
||||
# stores the resolves jwks in the provider metadata linked to this authz request
|
||||
provider_metadata['jwks'] = {'keys': jwks_dict}
|
||||
if not jwks_dict:
|
||||
_msg = f"Failed to get jwks from {tc.sub}"
|
||||
logger.error(_msg)
|
||||
context = {
|
||||
"error": "request rejected",
|
||||
"error_description": _msg
|
||||
}
|
||||
return render(request, self.error_template, context)
|
||||
|
||||
authz_endpoint = provider_metadata["authorization_endpoint"]
|
||||
|
||||
redirect_uri = request.GET.get("redirect_uri", client_conf["redirect_uris"][0])
|
||||
if redirect_uri not in client_conf["redirect_uris"]:
|
||||
logger.warning(
|
||||
f"Requested for unknown redirect uri {redirect_uri}. "
|
||||
f"Reverted to default {client_conf['redirect_uris'][0]}."
|
||||
)
|
||||
redirect_uri = client_conf["redirect_uris"][0]
|
||||
_profile = request.GET.get("profile", "spid")
|
||||
_timestamp_now = int(timezone.localtime().timestamp())
|
||||
authz_data = dict(
|
||||
iss=client_conf["client_id"],
|
||||
scope= request.GET.get("scope", None) or "openid",
|
||||
redirect_uri=redirect_uri,
|
||||
response_type=client_conf["response_types"][0],
|
||||
nonce=random_string(32),
|
||||
state=random_string(32),
|
||||
client_id=client_conf["client_id"],
|
||||
endpoint=authz_endpoint,
|
||||
acr_values= OIDCFED_ACR_PROFILES,
|
||||
iat=_timestamp_now,
|
||||
exp =_timestamp_now + RP_REQUEST_EXP,
|
||||
jti = str(uuid.uuid4()),
|
||||
aud=[tc.sub, authz_endpoint],
|
||||
claims=RP_REQUEST_CLAIM_BY_PROFILE[_profile],
|
||||
)
|
||||
|
||||
_prompt = request.GET.get("prompt", "consent login")
|
||||
|
||||
# if "offline_access" in authz_data["scope"]:
|
||||
# _prompt.extend(["consent login"])
|
||||
|
||||
authz_data["prompt"] = _prompt
|
||||
|
||||
# PKCE
|
||||
pkce_func = import_string(RP_PKCE_CONF["function"])
|
||||
pkce_values = pkce_func(**RP_PKCE_CONF["kwargs"])
|
||||
authz_data.update(pkce_values)
|
||||
#
|
||||
authz_entry = dict(
|
||||
client_id=client_conf["client_id"],
|
||||
state=authz_data["state"],
|
||||
endpoint=authz_endpoint,
|
||||
# TODO: better have here an organization name
|
||||
provider_id=tc.sub,
|
||||
data=json.dumps(authz_data),
|
||||
provider_configuration=provider_metadata,
|
||||
)
|
||||
|
||||
# TODO: Prune the old or unbounded authz ...
|
||||
OidcAuthentication.objects.create(**authz_entry)
|
||||
|
||||
authz_data.pop("code_verifier")
|
||||
# add the signed request object
|
||||
authz_data_obj = deepcopy(authz_data)
|
||||
authz_data_obj["iss"] = client_conf["client_id"]
|
||||
|
||||
# sub claim MUST not be used to prevent that this jwt
|
||||
# could be reused as a private_key_jwt
|
||||
# authz_data_obj["sub"] = client_conf["client_id"]
|
||||
|
||||
jwk_core_sig = get_key(entity_conf.jwks_core, KeyUsage.signature)
|
||||
|
||||
request_obj = create_jws(authz_data_obj, jwk_core_sig)
|
||||
authz_data["request"] = request_obj
|
||||
uri_path = http_dict_to_redirect_uri_path(
|
||||
{
|
||||
"client_id": authz_data["client_id"],
|
||||
"scope" : authz_data["scope"],
|
||||
"response_type": authz_data["response_type"],
|
||||
"code_challenge": authz_data["code_challenge"],
|
||||
"code_challenge_method": authz_data["code_challenge_method"],
|
||||
"request": authz_data["request"]
|
||||
}
|
||||
)
|
||||
if "?" in authz_endpoint:
|
||||
qstring = "&"
|
||||
else:
|
||||
qstring = "?"
|
||||
url = qstring.join((authz_endpoint, uri_path))
|
||||
logger.info(f"Starting Authz request to {url}")
|
||||
return HttpResponseRedirect(url)
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from djagger.decorators import schema
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model, login
|
||||
from django.http import HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
from spid_cie_oidc.entity.jwtse import (
|
||||
unpad_jwt_payload,
|
||||
verify_jws,
|
||||
verify_at_hash
|
||||
)
|
||||
from spid_cie_oidc.entity.models import FederationEntityConfiguration
|
||||
from spid_cie_oidc.entity.settings import HTTPC_PARAMS
|
||||
from spid_cie_oidc.entity.utils import get_jwks, get_jwk_from_jwt
|
||||
from spid_cie_oidc.relying_party.exceptions import ValidationException
|
||||
from spid_cie_oidc.relying_party.settings import RP_PROVIDER_PROFILES, RP_DEFAULT_PROVIDER_PROFILES
|
||||
|
||||
from ..models import OidcAuthentication, OidcAuthenticationToken
|
||||
from ..oauth2 import *
|
||||
from ..oidc import *
|
||||
from ..settings import (
|
||||
RP_ATTR_MAP,
|
||||
RP_USER_CREATE,
|
||||
RP_USER_LOOKUP_FIELD,
|
||||
)
|
||||
from ..utils import process_user_attributes
|
||||
|
||||
from . import SpidCieOidcRp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
schema_profile = RP_PROVIDER_PROFILES[RP_DEFAULT_PROVIDER_PROFILES]
|
||||
|
||||
|
||||
@schema(
|
||||
summary="OIDC Relying Party auth code Callback",
|
||||
methods=['GET'],
|
||||
request_schema={
|
||||
"application/x-www-form-urlencoded" : schema_profile["authn_response"],
|
||||
},
|
||||
external_docs = {
|
||||
"alt_text": "AgID SPID OIDC Guidelines",
|
||||
"url": (
|
||||
"https://www.agid.gov.it/it/agenzia/stampa-e-comunicazione/"
|
||||
"notizie/2021/12/06/openid-connect-spid-adottate-linee-guida"
|
||||
),
|
||||
},
|
||||
tags = ['Relying Party']
|
||||
)
|
||||
class SpidCieOidcRpCallbackView(View, SpidCieOidcRp, OidcUserInfo, OAuth2AuthorizationCodeGrant):
|
||||
"""
|
||||
View which processes an Authorization Response
|
||||
https://tools.ietf.org/html/rfc6749#section-4.1.2
|
||||
|
||||
eg:
|
||||
/redirect_uri?code=tYkP854StRqBVcW4Kg4sQfEN5Qz&state=R9EVqaazGsj3wg5JgxIgm8e8U4BMvf7W
|
||||
"""
|
||||
|
||||
error_template = "rp_error.html"
|
||||
|
||||
def user_reunification(self, user_attrs: dict):
|
||||
user_model = get_user_model()
|
||||
|
||||
if user_attrs.get(RP_USER_LOOKUP_FIELD, None):
|
||||
lookup = {
|
||||
f"attributes__{RP_USER_LOOKUP_FIELD}": user_attrs.get(RP_USER_LOOKUP_FIELD)
|
||||
}
|
||||
else:
|
||||
logger.warning("User attribute not found for reunification, try sub")
|
||||
lookup = {
|
||||
"username": user_attrs["username"]
|
||||
}
|
||||
user = user_model.objects.filter(**lookup).first()
|
||||
if user:
|
||||
user.attributes.update(user_attrs)
|
||||
user.save()
|
||||
logger.info(f"{RP_USER_LOOKUP_FIELD} matched on user {user}")
|
||||
return user
|
||||
elif RP_USER_CREATE:
|
||||
user = user_model.objects.create(
|
||||
username=user_attrs.get("username", user_attrs["sub"]),
|
||||
first_name=user_attrs.get("given_name", user_attrs["sub"]),
|
||||
last_name=user_attrs.get("family_name", user_attrs["sub"]),
|
||||
email=user_attrs.get("email", ""),
|
||||
attributes=user_attrs,
|
||||
)
|
||||
logger.info(f"Created new user {user}")
|
||||
return user
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
The Authorization callback, the redirect uri where the auth code lands
|
||||
"""
|
||||
request_args = {k: v for k, v in request.GET.items()}
|
||||
if "error" in request_args:
|
||||
return render(
|
||||
request,
|
||||
self.error_template,
|
||||
request_args,
|
||||
status=401
|
||||
)
|
||||
authz = OidcAuthentication.objects.filter(
|
||||
state=request_args.get("state"),
|
||||
)
|
||||
try:
|
||||
self.validate_json_schema(
|
||||
request.GET.dict(),
|
||||
"authn_response",
|
||||
"Authn response object validation failed"
|
||||
)
|
||||
except ValidationException:
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Authn response object validation failed",
|
||||
},
|
||||
status = 400
|
||||
)
|
||||
|
||||
if not authz:
|
||||
context = {
|
||||
"error": "unauthorized request",
|
||||
"error_description": _("Authentication not found"),
|
||||
}
|
||||
return render(request, self.error_template, context, status=401)
|
||||
else:
|
||||
authz = authz.last()
|
||||
|
||||
code = request.GET.get("code")
|
||||
# mixups attacks prevention
|
||||
if request.GET.get('iss', None):
|
||||
if request.GET['iss'] != authz.provider_id:
|
||||
context = {
|
||||
"error": "invalid request",
|
||||
"error_description": _(
|
||||
"authn response validation failed: mixups attack prevention."
|
||||
),
|
||||
}
|
||||
return render(request, self.error_template, context, status=400)
|
||||
|
||||
authz_token = OidcAuthenticationToken.objects.create(
|
||||
authz_request=authz, code=code
|
||||
)
|
||||
self.rp_conf = FederationEntityConfiguration.objects.filter(
|
||||
sub=authz_token.authz_request.client_id
|
||||
).first()
|
||||
if not self.rp_conf:
|
||||
context = {
|
||||
"error": "invalid request",
|
||||
"error_description": _("Relying party not found"),
|
||||
}
|
||||
return render(request, self.error_template, context, status=400)
|
||||
|
||||
authz_data = json.loads(authz.data)
|
||||
token_response = self.access_token_request(
|
||||
redirect_uri=authz_data["redirect_uri"],
|
||||
state=authz.state,
|
||||
code=code,
|
||||
issuer_id=authz.provider_id,
|
||||
client_conf=self.rp_conf,
|
||||
token_endpoint_url=authz.provider_configuration["token_endpoint"],
|
||||
audience=[authz.provider_id],
|
||||
code_verifier=authz_data.get("code_verifier"),
|
||||
)
|
||||
if not token_response:
|
||||
context = {
|
||||
"error": "invalid token response",
|
||||
"error_description": _("Token response seems not to be valid"),
|
||||
}
|
||||
return render(request, self.error_template, context, status=400)
|
||||
|
||||
else:
|
||||
try:
|
||||
self.validate_json_schema(
|
||||
token_response,
|
||||
"token_response",
|
||||
"Token response object validation failed"
|
||||
)
|
||||
except ValidationException:
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Token response object validation failed",
|
||||
},
|
||||
status = 400
|
||||
)
|
||||
jwks = get_jwks(authz.provider_configuration)
|
||||
access_token = token_response["access_token"]
|
||||
id_token = token_response["id_token"]
|
||||
|
||||
op_ac_jwk = get_jwk_from_jwt(access_token, jwks)
|
||||
op_id_jwk = get_jwk_from_jwt(id_token, jwks)
|
||||
|
||||
if not op_ac_jwk or not op_id_jwk:
|
||||
logger.warning(
|
||||
"Token signature validation error, "
|
||||
f"the tokens were signed with a different kid from: {jwks}."
|
||||
)
|
||||
context = {
|
||||
"error": "invalid_token",
|
||||
"error_description": _("Authentication token seems not to be valid."),
|
||||
}
|
||||
return render(request, self.error_template, context, status=403)
|
||||
|
||||
try:
|
||||
verify_jws(access_token, op_ac_jwk)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Access Token signature validation error: {e} "
|
||||
)
|
||||
context = {
|
||||
"error": "token verification failed",
|
||||
"error_description": _("Authentication token validation error."),
|
||||
}
|
||||
return render(request, self.error_template, context, status=403)
|
||||
|
||||
try:
|
||||
verify_jws(id_token, op_id_jwk)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"ID Token signature validation error: {e} "
|
||||
)
|
||||
context = {
|
||||
"error": "token verification failed",
|
||||
"error_description": _("ID token validation error."),
|
||||
}
|
||||
return render(request, self.error_template, context, status=403)
|
||||
|
||||
decoded_id_token = unpad_jwt_payload(id_token)
|
||||
logger.debug(decoded_id_token)
|
||||
|
||||
try:
|
||||
verify_at_hash(decoded_id_token, access_token)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"at_hash validation error: {e} "
|
||||
)
|
||||
context = {
|
||||
"error": "at_hash verification failed",
|
||||
"error_description": _("at_hash validation error."),
|
||||
}
|
||||
return render(request, self.error_template, context, status=403)
|
||||
|
||||
decoded_access_token = unpad_jwt_payload(access_token)
|
||||
logger.debug(decoded_access_token)
|
||||
|
||||
authz_token.access_token = access_token
|
||||
authz_token.id_token = id_token
|
||||
authz_token.scope = token_response.get("scope")
|
||||
authz_token.token_type = token_response["token_type"]
|
||||
authz_token.expires_in = token_response["expires_in"]
|
||||
authz_token.save()
|
||||
|
||||
userinfo = self.get_userinfo(
|
||||
authz.state,
|
||||
authz_token.access_token,
|
||||
authz.provider_configuration,
|
||||
verify=HTTPC_PARAMS.get("connection", {}).get("ssl", True)
|
||||
)
|
||||
if not userinfo:
|
||||
logger.warning(
|
||||
"Userinfo request failed for state: "
|
||||
f"{authz.state} to {authz.provider_id}"
|
||||
)
|
||||
context = {
|
||||
"error": "invalid userinfo response",
|
||||
"error_description": _("UserInfo response seems not to be valid"),
|
||||
}
|
||||
return render(request, self.error_template, context, status=400)
|
||||
|
||||
# here django user attr mapping
|
||||
user_attrs = process_user_attributes(userinfo, RP_ATTR_MAP, authz.__dict__)
|
||||
if not user_attrs:
|
||||
_msg = "No user attributes have been processed"
|
||||
logger.warning(f"{_msg}: {userinfo}")
|
||||
# TODO: verify error message and status
|
||||
context = {
|
||||
"error": "missing user attributes",
|
||||
"error_description": _(f"{_msg}: {userinfo}"),
|
||||
}
|
||||
return render(request, self.error_template, context, status=403)
|
||||
|
||||
user = self.user_reunification(user_attrs)
|
||||
if not user:
|
||||
# TODO: verify error message and status
|
||||
context = {"error": _("No user found"), "error_description": _("")}
|
||||
return render(request, self.error_template, context, status=403)
|
||||
|
||||
request.session["rt_expiration"] = 0
|
||||
|
||||
if token_response.get('refresh_token', None):
|
||||
refresh_token = token_response["refresh_token"]
|
||||
authz_token.refresh_token = refresh_token
|
||||
decoded_refresh_token = unpad_jwt_payload(refresh_token)
|
||||
request.session["rt_expiration"] = decoded_refresh_token['exp'] - iat_now()
|
||||
request.session["rt_jti"] = decoded_refresh_token['jti']
|
||||
logger.info(decoded_refresh_token)
|
||||
|
||||
# authenticate the user
|
||||
login(request, user)
|
||||
request.session["oidc_rp_user_attrs"] = user_attrs
|
||||
|
||||
request.session["at_expiration"] = decoded_access_token['exp'] - iat_now()
|
||||
request.session["at_jti"] = decoded_access_token['jti']
|
||||
|
||||
authz_token.user = user
|
||||
authz_token.save()
|
||||
return HttpResponseRedirect(
|
||||
getattr(
|
||||
settings, "LOGIN_REDIRECT_URL", None
|
||||
) or reverse("spid_cie_rp_echo_attributes")
|
||||
)
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
import logging
|
||||
|
||||
from django.shortcuts import render
|
||||
from django.views import View
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SpidCieOidcRpCallbackEchoAttributes(View):
|
||||
template = "echo_attributes.html"
|
||||
|
||||
def get(self, request):
|
||||
data = {"oidc_rp_user_attrs": request.session.get("oidc_rp_user_attrs", {}),
|
||||
"at_expiration": request.session.get("at_expiration"),
|
||||
"at_jti": request.session.get("at_jti"),
|
||||
"rt_expiration": request.session.get("rt_expiration"),
|
||||
"rt_jti": request.session.get("rt_jti")}
|
||||
return render(request, self.template, data)
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import logging
|
||||
|
||||
from djagger.decorators import schema
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
|
||||
from ..models import OidcAuthenticationToken
|
||||
from ..oauth2 import *
|
||||
from ..oidc import *
|
||||
|
||||
from . import SpidCieOidcRp, TokenRequestType
|
||||
from django.views import View
|
||||
|
||||
from spid_cie_oidc.entity.jwtse import (
|
||||
unpad_jwt_payload,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@schema(
|
||||
summary="OIDC Relying party refresh token request",
|
||||
methods=['GET'],
|
||||
external_docs={
|
||||
"alt_text": "AgID SPID OIDC Guidelines",
|
||||
"url": (
|
||||
"https://www.agid.gov.it/it/agenzia/stampa-e-comunicazione/"
|
||||
"notizie/2021/12/06/openid-connect-spid-adottate-linee-guida"
|
||||
),
|
||||
},
|
||||
tags=['Relying Party']
|
||||
)
|
||||
class SpidCieOidcRefreshToken(SpidCieOidcRp, View):
|
||||
error_template = "rp_error.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Call the token endpoint of the op
|
||||
"""
|
||||
auth_tokens = OidcAuthenticationToken.objects.filter(
|
||||
user=request.user
|
||||
).filter(revoked__isnull=True)
|
||||
|
||||
if not auth_tokens:
|
||||
logger.warning(
|
||||
"Token request failed: not found any authentication session"
|
||||
)
|
||||
|
||||
auth_token = auth_tokens.last()
|
||||
|
||||
try:
|
||||
token_response = self.get_token_request(auth_token, request, TokenRequestType.refresh) # "refresh")
|
||||
if token_response.status_code == 400:
|
||||
return HttpResponseRedirect(reverse("spid_cie_rp_landing"))
|
||||
|
||||
refresh_token_response = json.loads(token_response.content.decode())
|
||||
|
||||
auth_token.refresh_token = refresh_token_response["refresh_token"]
|
||||
auth_token.access_token = refresh_token_response["access_token"]
|
||||
auth_token.save()
|
||||
|
||||
decoded_access_token = unpad_jwt_payload(refresh_token_response["access_token"])
|
||||
decoded_refresh_token = unpad_jwt_payload(refresh_token_response["refresh_token"])
|
||||
|
||||
request.session["rt_expiration"] = decoded_refresh_token['exp'] - iat_now()
|
||||
request.session["rt_jti"] = decoded_refresh_token['jti']
|
||||
request.session["oidc_rp_user_attrs"] = request.user.attributes
|
||||
|
||||
request.session["at_expiration"] = decoded_access_token['exp'] - iat_now()
|
||||
request.session["at_jti"] = decoded_access_token['jti']
|
||||
|
||||
return HttpResponseRedirect(
|
||||
getattr(
|
||||
settings, "LOGIN_REDIRECT_URL", None
|
||||
) or reverse("spid_cie_rp_echo_attributes")
|
||||
)
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.warning(f"Refresh Token request failed: {e}")
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import logging
|
||||
|
||||
|
||||
from djagger.decorators import schema
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from ..models import OidcAuthenticationToken
|
||||
|
||||
from . import SpidCieOidcRp, TokenRequestType
|
||||
from django.views import View
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@schema(
|
||||
summary="OIDC Relying party logout",
|
||||
methods=['GET'],
|
||||
external_docs={
|
||||
"alt_text": "AgID SPID OIDC Guidelines",
|
||||
"url": (
|
||||
"https://www.agid.gov.it/it/agenzia/stampa-e-comunicazione/"
|
||||
"notizie/2021/12/06/openid-connect-spid-adottate-linee-guida"
|
||||
),
|
||||
},
|
||||
tags=['Relying Party']
|
||||
)
|
||||
class SpidCieOidcRpLogout(SpidCieOidcRp, View):
|
||||
def get(self, request):
|
||||
"""
|
||||
Call the token revocation endpoint of the op
|
||||
"""
|
||||
auth_tokens = OidcAuthenticationToken.objects.filter(
|
||||
user=request.user
|
||||
).filter(revoked__isnull=True)
|
||||
|
||||
default_logout_url = getattr(
|
||||
settings, "LOGOUT_REDIRECT_URL", None
|
||||
) or reverse("spid_cie_rp_landing")
|
||||
if not auth_tokens:
|
||||
logger.warning(
|
||||
"Token revocation failed: not found any authentication session"
|
||||
)
|
||||
return HttpResponseRedirect(default_logout_url)
|
||||
|
||||
auth_token = auth_tokens.last()
|
||||
logger.info(f"{request.user} logout")
|
||||
logout(request)
|
||||
|
||||
try:
|
||||
self.get_token_request(auth_token, request, TokenRequestType.revocation) # "revocation")
|
||||
auth_token.logged_out = timezone.localtime()
|
||||
auth_token.save()
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.warning(f"Token revocation failed: {e}")
|
||||
|
||||
auth_tokens.update(revoked=timezone.localtime())
|
||||
return HttpResponseRedirect(default_logout_url)
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import logging
|
||||
|
||||
from djagger.decorators import schema
|
||||
|
||||
from ..models import OidcAuthenticationToken
|
||||
from ..oauth2 import *
|
||||
from ..oidc import *
|
||||
|
||||
from . import SpidCieOidcRp, TokenRequestType
|
||||
from django.views import View
|
||||
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@schema(
|
||||
summary="OIDC Relying party refresh token request",
|
||||
methods=['GET'],
|
||||
external_docs={
|
||||
"alt_text": "AgID SPID OIDC Guidelines",
|
||||
"url": (
|
||||
"https://www.agid.gov.it/it/agenzia/stampa-e-comunicazione/"
|
||||
"notizie/2021/12/06/openid-connect-spid-adottate-linee-guida"
|
||||
),
|
||||
},
|
||||
tags=['Relying Party']
|
||||
)
|
||||
class SpidCieOidcRpIntrospection(SpidCieOidcRp, View):
|
||||
error_template = "rp_error.html"
|
||||
template = "rp_introspection.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Call the token endpoint of the op
|
||||
"""
|
||||
auth_tokens = OidcAuthenticationToken.objects.filter(
|
||||
user=request.user
|
||||
).filter(revoked__isnull=True)
|
||||
|
||||
if not auth_tokens:
|
||||
logger.warning(
|
||||
"Token request failed: not found any authentication session"
|
||||
)
|
||||
|
||||
auth_token = auth_tokens.last()
|
||||
|
||||
try:
|
||||
token_response = self.get_token_request(auth_token, request, TokenRequestType.introspection)
|
||||
introspection_token_response = json.loads(token_response.content.decode())
|
||||
data = {"introspection": introspection_token_response}
|
||||
return render(request, self.template, data)
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.warning(f"Refresh Token request failed: {e}")
|
||||
@@ -0,0 +1,60 @@
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
import random
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.shortcuts import render
|
||||
from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def oidc_rp_landing(request):
|
||||
spid_providers = {
|
||||
k: {"sub": v} for k, v in
|
||||
settings.OIDCFED_IDENTITY_PROVIDERS.get("spid", {}).items()
|
||||
}
|
||||
cie_providers = {
|
||||
k: {"sub": v} for k, v in
|
||||
settings.OIDCFED_IDENTITY_PROVIDERS.get("cie", {}).items()
|
||||
}
|
||||
|
||||
providers = deepcopy(spid_providers)
|
||||
providers.update(cie_providers)
|
||||
subs = list(spid_providers.keys()) + list(cie_providers.keys())
|
||||
|
||||
tcs = []
|
||||
for sub in subs:
|
||||
try:
|
||||
tc = get_or_create_trust_chain(
|
||||
subject = sub,
|
||||
trust_anchor = providers[sub]["sub"]
|
||||
)
|
||||
tcs.append(tc)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed trust chain for {sub} to {providers[sub]}: {e}"
|
||||
)
|
||||
|
||||
for i in tcs:
|
||||
if i.sub in settings.OIDCFED_IDENTITY_PROVIDERS.get("spid", {}):
|
||||
target = spid_providers
|
||||
elif i.sub in settings.OIDCFED_IDENTITY_PROVIDERS.get("cie", {}):
|
||||
target = cie_providers
|
||||
else:
|
||||
continue
|
||||
|
||||
target[i.sub] = {
|
||||
"sub": i.sub,
|
||||
"logo_uri" : i.metadata.get("openid_provider", {}).get("logo_uri", ""),
|
||||
"organization_name": i.metadata.get("openid_provider", {}).get("organization_name","")
|
||||
}
|
||||
|
||||
s_spid_providers = list(spid_providers.items())
|
||||
random.shuffle(s_spid_providers)
|
||||
content = {
|
||||
"spid_providers": dict(s_spid_providers),
|
||||
"cie_providers" : cie_providers
|
||||
}
|
||||
return render(request, "rp_landing.html", content)
|
||||
Reference in New Issue
Block a user