feat: link local library
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
import logging
|
||||
import uuid
|
||||
from cryptojwt.jws.utils import left_hash
|
||||
from django.conf import settings
|
||||
from pydantic import ValidationError
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
import urllib
|
||||
from spid_cie_oidc.entity.jwtse import create_jws, unpad_jwt_head, unpad_jwt_payload, verify_jws
|
||||
from spid_cie_oidc.entity.models import FederationEntityConfiguration, TrustChain
|
||||
from spid_cie_oidc.entity.settings import HTTPC_PARAMS
|
||||
from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain
|
||||
from spid_cie_oidc.entity.utils import datetime_from_timestamp, exp_from_now, iat_now
|
||||
from spid_cie_oidc.entity.utils import get_jwks
|
||||
from spid_cie_oidc.entity.exceptions import TrustchainMissingMetadata
|
||||
from spid_cie_oidc.provider.exceptions import (
|
||||
AuthzRequestReplay,
|
||||
ExpiredAuthCode,
|
||||
InvalidSession,
|
||||
RevokedSession,
|
||||
ValidationException
|
||||
)
|
||||
from spid_cie_oidc.provider.models import OidcSession
|
||||
from spid_cie_oidc.provider.schemas.client_assertion import ClientAssertion
|
||||
|
||||
from spid_cie_oidc.provider.settings import (
|
||||
OIDCFED_ATTRNAME_I18N,
|
||||
OIDCFED_DEFAULT_PROVIDER_PROFILE,
|
||||
OIDCFED_PROVIDER_AUTH_CODE_MAX_AGE,
|
||||
OIDCFED_PROVIDER_PROFILES,
|
||||
OIDCFED_PROVIDER_PROFILES_ACR_4_REFRESH,
|
||||
OIDCFED_PROVIDER_PROFILES_ID_TOKEN_CLAIMS
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpBase:
|
||||
"""
|
||||
Baseclass with common methods for OPs
|
||||
"""
|
||||
|
||||
def redirect_response_data(self, redirect_uri: str, **kwargs) -> HttpResponseRedirect:
|
||||
if "?" in redirect_uri:
|
||||
qstring = "&"
|
||||
else:
|
||||
qstring = "?"
|
||||
url = f'{redirect_uri}{qstring}{urllib.parse.urlencode(kwargs)}'
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
def find_jwk(self, header: dict, jwks: list) -> dict:
|
||||
for jwk in jwks:
|
||||
valid_jwk = jwk.get("kid", None)
|
||||
if valid_jwk and header["kid"] == valid_jwk:
|
||||
return jwk
|
||||
|
||||
def validate_authz_request_object(self, req) -> TrustChain:
|
||||
state = getattr(self, 'payload', {}).get("state", "")
|
||||
try:
|
||||
self.payload = unpad_jwt_payload(req)
|
||||
header = unpad_jwt_head(req)
|
||||
except Exception as e:
|
||||
_msg = (
|
||||
f"Error in Authz request object {dict(req.GET)}: {e}."
|
||||
f" error=invalid_request"
|
||||
f"state={state}"
|
||||
)
|
||||
logger.error(_msg)
|
||||
raise Exception(_msg)
|
||||
|
||||
self.is_a_replay_authz()
|
||||
rp_trust_chain = TrustChain.objects.filter(
|
||||
metadata__openid_relying_party__isnull=False,
|
||||
sub=self.payload["iss"],
|
||||
trust_anchor__sub__in=settings.OIDCFED_TRUST_ANCHORS
|
||||
).first()
|
||||
if rp_trust_chain and not rp_trust_chain.is_active:
|
||||
_msg = (
|
||||
f"Disabled client {rp_trust_chain.sub} requests an authorization. "
|
||||
"error = access_denied, "
|
||||
f"state={state}"
|
||||
)
|
||||
logger.warning(_msg)
|
||||
raise Exception(_msg)
|
||||
|
||||
elif not rp_trust_chain or rp_trust_chain.is_expired:
|
||||
rp_trust_chain = None
|
||||
# TODO: get async here
|
||||
for ta in settings.OIDCFED_TRUST_ANCHORS:
|
||||
try:
|
||||
rp_trust_chain = get_or_create_trust_chain(
|
||||
subject=self.payload["iss"],
|
||||
trust_anchor=ta,
|
||||
httpc_params=HTTPC_PARAMS,
|
||||
required_trust_marks=getattr(
|
||||
settings, "OIDCFED_REQUIRED_TRUST_MARKS", []
|
||||
),
|
||||
)
|
||||
if rp_trust_chain and rp_trust_chain.metadata:
|
||||
break
|
||||
except TrustchainMissingMetadata as e:
|
||||
logger.debug(f"TrustchainMissingMetadata: {e}")
|
||||
# unless we find the good TA
|
||||
continue
|
||||
|
||||
if not rp_trust_chain or not rp_trust_chain.is_valid:
|
||||
_msg = (
|
||||
f"Failed trust chain validation for {self.payload['iss']}. "
|
||||
"error=unauthorized_client, "
|
||||
f"state={state}"
|
||||
)
|
||||
logger.warning(_msg)
|
||||
raise Exception(_msg)
|
||||
|
||||
jwks = get_jwks(
|
||||
rp_trust_chain.metadata['openid_relying_party'],
|
||||
federation_jwks=rp_trust_chain.jwks
|
||||
)
|
||||
jwk = self.find_jwk(header, jwks)
|
||||
if not jwk:
|
||||
_msg = (
|
||||
f"Invalid jwk for {self.payload['iss']}. "
|
||||
f"{header['kid']} not found in {jwks}. "
|
||||
"error=unauthorized_client, "
|
||||
f"state={state}"
|
||||
)
|
||||
logger.error(_msg)
|
||||
raise Exception(_msg)
|
||||
|
||||
try:
|
||||
verify_jws(req, jwk)
|
||||
except Exception as e:
|
||||
_msg = (
|
||||
"Authz request object signature validation failed "
|
||||
f"for {self.payload['iss']}: {e}. "
|
||||
f"error=access_denied, state={state}"
|
||||
)
|
||||
logger.error(_msg)
|
||||
raise Exception(_msg)
|
||||
|
||||
return rp_trust_chain
|
||||
|
||||
def is_a_replay_authz(self):
|
||||
preexistent_authz = OidcSession.objects.filter(
|
||||
client_id=self.payload["client_id"],
|
||||
nonce=self.payload["nonce"]
|
||||
).first()
|
||||
if preexistent_authz:
|
||||
raise AuthzRequestReplay(
|
||||
f"{preexistent_authz.client_id} with {preexistent_authz.nonce}"
|
||||
)
|
||||
|
||||
def check_session(self, request) -> OidcSession:
|
||||
if not request.user.is_authenticated:
|
||||
raise InvalidSession()
|
||||
|
||||
auth_code = request.session.get("oidc", {}).get("auth_code", None)
|
||||
if not auth_code:
|
||||
# FIXME: to do test
|
||||
raise InvalidSession()
|
||||
|
||||
session = OidcSession.objects.filter(
|
||||
auth_code=request.session["oidc"]["auth_code"]
|
||||
).first()
|
||||
|
||||
if not session:
|
||||
raise InvalidSession("Unknown Session")
|
||||
|
||||
if session.user != request.user:
|
||||
raise InvalidSession(
|
||||
f"Different user, session is of {session.user} "
|
||||
f"but request user is {request.user}"
|
||||
)
|
||||
|
||||
if session.revoked:
|
||||
raise RevokedSession(
|
||||
"Session was revoked"
|
||||
)
|
||||
|
||||
session_not_after = session.created + timezone.timedelta(
|
||||
minutes=OIDCFED_PROVIDER_AUTH_CODE_MAX_AGE
|
||||
)
|
||||
if session_not_after < timezone.localtime():
|
||||
raise ExpiredAuthCode(
|
||||
f"The Session is expired at {session_not_after}"
|
||||
)
|
||||
return session
|
||||
|
||||
def get_issuer(self):
|
||||
return FederationEntityConfiguration.objects.filter(
|
||||
entity_type="openid_provider"
|
||||
).first()
|
||||
|
||||
def check_client_assertion(self, client_id: str, client_assertion: str) -> bool:
|
||||
head = unpad_jwt_head(client_assertion)
|
||||
payload = unpad_jwt_payload(client_assertion)
|
||||
_sub = payload.get('sub', None)
|
||||
_aud = payload.get('aud', [])
|
||||
_op = self.get_issuer()
|
||||
_op_eid = _op.sub
|
||||
_op_eid_authz_endpoint = [_op.metadata['openid_provider']['authorization_endpoint']]
|
||||
|
||||
try:
|
||||
ClientAssertion(**payload)
|
||||
except Exception as e:
|
||||
raise Exception(f"Client Assertion: json schema validation error: {e}")
|
||||
|
||||
if isinstance(_aud, str):
|
||||
_aud = [_aud]
|
||||
_allowed_auds = _aud + _op_eid_authz_endpoint
|
||||
|
||||
if _sub != client_id:
|
||||
logger.warning(
|
||||
f"Client assertion failed: {_sub} != {client_id}"
|
||||
)
|
||||
# TODO Specialize exceptions
|
||||
raise Exception("Client Assertion: sub != client_id")
|
||||
|
||||
if _op_eid:
|
||||
_allowed_auds.append(_op_eid)
|
||||
|
||||
if not _op_eid or self.request.build_absolute_uri() not in _allowed_auds:
|
||||
logger.warning(
|
||||
"Client assertion failed, fake audience: "
|
||||
f"{self.request.build_absolute_uri()} not in {_allowed_auds}"
|
||||
)
|
||||
# TODO Specialize exceptions
|
||||
raise Exception("Client Assertion: fake audience")
|
||||
|
||||
tc = TrustChain.objects.get(sub=client_id, is_active=True)
|
||||
jwk = self.find_jwk(head, tc.metadata['openid_relying_party']['jwks']['keys'])
|
||||
verify_jws(client_assertion, jwk)
|
||||
return True
|
||||
|
||||
def validate_json_schema(self, payload, schema_type, error_description):
|
||||
try:
|
||||
schema = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE]
|
||||
schema[schema_type](**payload)
|
||||
except (ValidationError, Exception) as e:
|
||||
logger.error(
|
||||
f"{error_description} "
|
||||
f"for {payload.get('client_id', None)}: {e}"
|
||||
)
|
||||
raise ValidationException()
|
||||
|
||||
def get_jwt_common_data(self):
|
||||
return {
|
||||
"exp": exp_from_now(),
|
||||
"iat": iat_now()
|
||||
}
|
||||
|
||||
def get_access_token(
|
||||
self, iss_sub: str, sub: str, authz: OidcSession, commons: dict
|
||||
) -> dict:
|
||||
|
||||
access_token = {
|
||||
"iss": iss_sub,
|
||||
"sub": sub,
|
||||
"aud": [iss_sub, reverse("oidc_provider_userinfo_endpoint")],
|
||||
"client_id": authz.client_id,
|
||||
"scope": authz.authz_request["scope"],
|
||||
"jti": str(uuid.uuid4())
|
||||
}
|
||||
access_token.update(commons)
|
||||
|
||||
return access_token
|
||||
|
||||
def get_id_token_claims(
|
||||
self,
|
||||
authz: OidcSession
|
||||
) -> dict:
|
||||
_provider_profile = getattr(settings, 'OIDCFED_DEFAULT_PROVIDER_PROFILE', OIDCFED_DEFAULT_PROVIDER_PROFILE)
|
||||
claims = {}
|
||||
allowed_id_token_claims = OIDCFED_PROVIDER_PROFILES_ID_TOKEN_CLAIMS[_provider_profile]
|
||||
if not allowed_id_token_claims:
|
||||
return claims
|
||||
|
||||
for claim in (
|
||||
authz.authz_request.get(
|
||||
"claims", {}
|
||||
).get("id_token", {}).keys()
|
||||
):
|
||||
if claim in allowed_id_token_claims and authz.user.attributes.get(claim, None):
|
||||
claims[claim] = authz.user.attributes[claim]
|
||||
return claims
|
||||
|
||||
def get_id_token(
|
||||
self,
|
||||
iss_sub: str,
|
||||
sub: str,
|
||||
authz: OidcSession,
|
||||
jwt_at: str,
|
||||
commons: dict
|
||||
) -> dict:
|
||||
|
||||
id_token = {
|
||||
"sub": sub,
|
||||
"nonce": authz.authz_request["nonce"],
|
||||
"at_hash": left_hash(jwt_at, "HS256"),
|
||||
"c_hash": left_hash(authz.auth_code, "HS256"),
|
||||
"aud": [authz.client_id],
|
||||
"iss": iss_sub,
|
||||
"acr": authz.acr,
|
||||
"jti": str(uuid.uuid4())
|
||||
}
|
||||
claims = self.get_id_token_claims(authz)
|
||||
if claims:
|
||||
id_token.update(claims)
|
||||
|
||||
id_token.update(commons)
|
||||
return id_token
|
||||
|
||||
def get_refresh_token(
|
||||
self,
|
||||
iss_sub: str,
|
||||
sub: str,
|
||||
authz: OidcSession,
|
||||
jwt_at: str,
|
||||
commons: dict
|
||||
) -> dict:
|
||||
# refresh token is scope offline_access and prompt == consent
|
||||
refresh_acrs = OIDCFED_PROVIDER_PROFILES_ACR_4_REFRESH[OIDCFED_DEFAULT_PROVIDER_PROFILE]
|
||||
acrs = authz.authz_request.get('acr_values', [])
|
||||
if (
|
||||
"offline_access" in authz.authz_request['scope'] and
|
||||
'consent' in authz.authz_request['prompt'] and
|
||||
set(refresh_acrs).intersection(set(acrs))
|
||||
):
|
||||
refresh_token = {
|
||||
"sub": sub,
|
||||
"at_hash": left_hash(jwt_at, "HS256"),
|
||||
"c_hash": left_hash(authz.auth_code, "HS256"),
|
||||
"aud": authz.client_id,
|
||||
"iss": iss_sub,
|
||||
"jti": str(uuid.uuid4())
|
||||
}
|
||||
refresh_token.update(commons)
|
||||
return refresh_token
|
||||
|
||||
def get_iss_token_data(self, session: OidcSession, issuer: FederationEntityConfiguration):
|
||||
_sub = session.pairwised_sub(provider_id=issuer.sub)
|
||||
iss_sub = issuer.sub
|
||||
commons = self.get_jwt_common_data()
|
||||
jwk = issuer.jwks_core[0]
|
||||
|
||||
access_token = self.get_access_token(iss_sub, _sub, session, commons)
|
||||
jwt_at = create_jws(access_token, jwk, typ="at+jwt")
|
||||
id_token = self.get_id_token(iss_sub, _sub, session, jwt_at, commons)
|
||||
jwt_id = create_jws(id_token, jwk)
|
||||
|
||||
iss_token_data = dict(
|
||||
session=session,
|
||||
access_token=jwt_at,
|
||||
id_token=jwt_id,
|
||||
expires=datetime_from_timestamp(commons["exp"])
|
||||
)
|
||||
|
||||
_refresh_token = self.get_refresh_token(iss_sub, _sub, session, jwt_at, commons)
|
||||
if _refresh_token:
|
||||
jwt_rt = create_jws(_refresh_token, jwk)
|
||||
iss_token_data["refresh_token"] = jwt_rt
|
||||
return iss_token_data
|
||||
|
||||
def get_expires_in(self, iat: int, exp: int):
|
||||
return timezone.timedelta(
|
||||
seconds=exp - iat
|
||||
).seconds
|
||||
|
||||
def attributes_names_to_release(self, request, session: OidcSession) -> dict:
|
||||
# get up fresh claims
|
||||
user_claims = request.user.attributes
|
||||
user_claims["email"] = user_claims.get("email", request.user.email)
|
||||
user_claims["username"] = request.user.username
|
||||
|
||||
# filter on requested claims
|
||||
filtered_user_claims = {}
|
||||
for target, claims in session.authz_request.get("claims", {}).items():
|
||||
for claim in claims:
|
||||
if claim in user_claims:
|
||||
filtered_user_claims[claim] = user_claims[claim]
|
||||
# IDA support/overload of the claims, the verified has priorities and overwrite the unverified
|
||||
if "verified_claims" in claims:
|
||||
claims = claims["verified_claims"].get('claims', {})
|
||||
for claim in claims:
|
||||
if claim in user_claims:
|
||||
filtered_user_claims[claim] = user_claims[claim]
|
||||
|
||||
# mapping with human names
|
||||
i18n_user_claims = [
|
||||
OIDCFED_ATTRNAME_I18N.get(i, i)
|
||||
for i in filtered_user_claims.keys()
|
||||
]
|
||||
return dict(
|
||||
i18n_user_claims=i18n_user_claims,
|
||||
filtered_user_claims=filtered_user_claims
|
||||
)
|
||||
|
||||
def get_client_organization_name(self, tc):
|
||||
rp_metadata = (
|
||||
tc.metadata.get(
|
||||
"federation_entity", {}
|
||||
) or
|
||||
tc.metadata.get(
|
||||
"openid_relying_party", {}
|
||||
)
|
||||
)
|
||||
if rp_metadata:
|
||||
name = (
|
||||
rp_metadata.get("organization_name", "") or
|
||||
rp_metadata.get("client_name", "") or
|
||||
rp_metadata.get("client_id", "")
|
||||
)
|
||||
return name
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import urllib.parse
|
||||
import uuid
|
||||
import json
|
||||
|
||||
from djagger.decorators import schema
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.forms.utils import ErrorList
|
||||
from django.http import (
|
||||
HttpResponseBadRequest,
|
||||
HttpResponseForbidden,
|
||||
HttpResponseRedirect
|
||||
)
|
||||
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.exceptions import InvalidEntityConfiguration
|
||||
from spid_cie_oidc.provider.schemas.authn_requests import AcrValues
|
||||
from spid_cie_oidc.provider.forms import AuthLoginForm, AuthzHiddenForm
|
||||
from spid_cie_oidc.provider.models import OidcSession
|
||||
from spid_cie_oidc.provider.exceptions import AuthzRequestReplay, InvalidRefreshRequestException, ValidationException
|
||||
from spid_cie_oidc.provider.settings import (
|
||||
OIDCFED_DEFAULT_PROVIDER_PROFILE,
|
||||
OIDCFED_PROVIDER_PROFILES,
|
||||
OIDCFED_PROVIDER_PROFILES_DEFAULT_ACR
|
||||
)
|
||||
from . import OpBase
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
schema_profile = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE]
|
||||
|
||||
|
||||
@schema(
|
||||
summary="OIDC Provider Authorization endpoint",
|
||||
methods=['GET', 'POST'],
|
||||
get_request_schema = {
|
||||
"application/x-www-form-urlencoded": schema_profile["authorization_request_doc"],
|
||||
"request object - jwt payload": schema_profile["authorization_request"]
|
||||
},
|
||||
post_response_schema= {
|
||||
"302":schema_profile["authorization_response"],
|
||||
"403": schema_profile["authorization_error_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 = ['Provider']
|
||||
)
|
||||
class AuthzRequestView(OpBase, View):
|
||||
"""
|
||||
View which processes the actual Authz request and
|
||||
returns a Http Redirect
|
||||
"""
|
||||
|
||||
template = "op_user_login.html"
|
||||
|
||||
def string_to_list(self, payload, must_list):
|
||||
for i in must_list:
|
||||
if isinstance(payload.get(i, None), str):
|
||||
if ' ' in payload[i]:
|
||||
payload[i] = payload[i].split(' ')
|
||||
else:
|
||||
payload[i] = [payload[i]]
|
||||
return payload
|
||||
|
||||
def validate_authz(self, payload: dict):
|
||||
|
||||
must_list = ("scope", "acr_values")
|
||||
payload = self.string_to_list(payload, must_list)
|
||||
if (
|
||||
'offline_access' in payload['scope'] and
|
||||
'consent' not in payload['prompt']
|
||||
):
|
||||
raise InvalidRefreshRequestException(
|
||||
"scope with offline_access without prompt = consent"
|
||||
)
|
||||
redirect_uri = payload.get("redirect_uri", "")
|
||||
p = urllib.parse.urlparse(redirect_uri)
|
||||
scheme_fqdn = f"{p.scheme}://{p.hostname}"
|
||||
client_id = payload.get("client_id") or ""
|
||||
if scheme_fqdn not in client_id:
|
||||
raise ValidationException("client_id not in redirect_uri")
|
||||
|
||||
self.validate_json_schema(
|
||||
payload,
|
||||
"authorization_request",
|
||||
"Authen request object validation failed "
|
||||
)
|
||||
|
||||
def get_url_consent(self, user):
|
||||
url = reverse("oidc_provider_consent")
|
||||
if (
|
||||
user.is_staff and
|
||||
'spid_cie_oidc.relying_party_test' in settings.INSTALLED_APPS
|
||||
):
|
||||
try:
|
||||
url = reverse("oidc_provider_staff_testing")
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"testigng page url reverse failed: {e}")
|
||||
return url
|
||||
|
||||
def get_login_form(self):
|
||||
return AuthLoginForm
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
The Authorization request of a RPs is validated and a login prompt is rendered to the user
|
||||
"""
|
||||
req = request.GET.get("request", None)
|
||||
if not req:
|
||||
logger.error(
|
||||
f"Missing Authz request object in {dict(request.GET)} "
|
||||
f"error=invalid_request"
|
||||
)
|
||||
return HttpResponseBadRequest()
|
||||
# yes, again. We MUST.
|
||||
tc = None
|
||||
try:
|
||||
tc = self.validate_authz_request_object(req)
|
||||
except InvalidEntityConfiguration as e:
|
||||
# FIXME: to do test
|
||||
logger.error(f"Invalid Entity Configuration: {e}")
|
||||
return self.redirect_response_data(
|
||||
self.payload["redirect_uri"],
|
||||
error = "invalid_request",
|
||||
error_description =_("Failed to establish the Trust"),
|
||||
state = self.payload.get("state", "")
|
||||
)
|
||||
except AuthzRequestReplay as e:
|
||||
logger.error(
|
||||
"Replay on authz request detected for "
|
||||
f"{request.GET.get('client_id', 'unknow')}: {e}"
|
||||
)
|
||||
return self.redirect_response_data(
|
||||
self.payload["redirect_uri"],
|
||||
error = "invalid_request",
|
||||
error_description =_(
|
||||
"An Unknown error raised during validation of "
|
||||
f" authz request object: {e}"
|
||||
),
|
||||
state = self.payload.get("state", "")
|
||||
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error during authz request validation for "
|
||||
f"{request.GET.get('client_id', 'unknown')}: {e}"
|
||||
)
|
||||
return self.redirect_response_data(
|
||||
self.payload["redirect_uri"],
|
||||
error="invalid_request",
|
||||
error_description=_("Authorization request not valid"),
|
||||
state = self.payload.get("state", "")
|
||||
|
||||
)
|
||||
try:
|
||||
self.validate_authz(self.payload)
|
||||
except ValidationException:
|
||||
return self.redirect_response_data(
|
||||
self.payload["redirect_uri"],
|
||||
error="invalid_request",
|
||||
error_description=_("Authorization request validation error"),
|
||||
state = self.payload.get("state", "")
|
||||
)
|
||||
except InvalidRefreshRequestException as e:
|
||||
logger.warning(f"Invalid session: {e}")
|
||||
return HttpResponseForbidden()
|
||||
if self.payload.get("acr_values", None):
|
||||
acr_value = AcrValues(self.payload["acr_values"][0])
|
||||
else:
|
||||
# set this as default
|
||||
acr_value = AcrValues.l2
|
||||
prompt = self.payload.get("prompt", "login")
|
||||
if request.user:
|
||||
if (
|
||||
request.user.is_authenticated and
|
||||
acr_value == AcrValues.l1 and
|
||||
"login" not in prompt
|
||||
):
|
||||
try:
|
||||
session = self.check_session(request)
|
||||
if session.acr != AcrValues.l1.value:
|
||||
logout(request)
|
||||
return self.get(request)
|
||||
else:
|
||||
url = self.get_url_consent(request.user)
|
||||
return HttpResponseRedirect(url)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
f"Failed SSO check session for {request.user}"
|
||||
)
|
||||
logout(request)
|
||||
return self.get(request)
|
||||
|
||||
# stores the authz request in a hidden field in the form
|
||||
form = self.get_login_form()()
|
||||
context = {
|
||||
"client_organization_name": self.get_client_organization_name(tc),
|
||||
"hidden_form": AuthzHiddenForm(dict(authz_request_object=req)),
|
||||
"form": form,
|
||||
"redirect_uri": self.payload["redirect_uri"],
|
||||
"obj_request": json.dumps(self.payload, indent=2),
|
||||
"acr_value": acr_value.name,
|
||||
"state": self.payload["state"]
|
||||
}
|
||||
return render(request, self.template, context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
When the User prompts his credentials
|
||||
TODO: REFACTOR this method doesn't support PAR!
|
||||
"""
|
||||
form = self.get_login_form()(request.POST)
|
||||
if not form.is_valid():
|
||||
return render(
|
||||
request,
|
||||
self.template,
|
||||
{
|
||||
"form": form,
|
||||
"hidden_form": AuthzHiddenForm(request.POST),
|
||||
}
|
||||
)
|
||||
|
||||
authz_form = AuthzHiddenForm(request.POST)
|
||||
authz_form.is_valid()
|
||||
authz_request = authz_form.cleaned_data.get("authz_request_object")
|
||||
try:
|
||||
self.validate_authz_request_object(authz_request)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Authz request object validation failed "
|
||||
f"for {authz_request}: {e} "
|
||||
)
|
||||
# we don't have a redirect_uri here
|
||||
return HttpResponseForbidden()
|
||||
|
||||
# autenticate the user
|
||||
username = form.cleaned_data.get("username")
|
||||
password = form.cleaned_data.get("password")
|
||||
user = authenticate(username=username, password=password)
|
||||
|
||||
if not user:
|
||||
errors = form._errors.setdefault("username", ErrorList())
|
||||
errors.append(_("invalid username or password"))
|
||||
return render(
|
||||
request,
|
||||
self.template,
|
||||
{
|
||||
"form": form,
|
||||
"hidden_form": AuthzHiddenForm(request.POST),
|
||||
"redirect_uri": self.payload["redirect_uri"],
|
||||
"state": self.payload["state"]
|
||||
}
|
||||
)
|
||||
else:
|
||||
login(request, user)
|
||||
|
||||
# create auth_code
|
||||
auth_code = hashlib.sha512(
|
||||
'-'.join(
|
||||
(
|
||||
f'{uuid.uuid4()}',
|
||||
f'{self.payload["client_id"]}',
|
||||
f'{self.payload["nonce"]}'
|
||||
)
|
||||
).encode()
|
||||
).hexdigest()
|
||||
# put the auth_code in the user web session
|
||||
request.session["oidc"] = {"auth_code": auth_code}
|
||||
|
||||
# store the User session
|
||||
_provider_profile = getattr(
|
||||
settings,
|
||||
'OIDCFED_DEFAULT_PROVIDER_PROFILE',
|
||||
OIDCFED_DEFAULT_PROVIDER_PROFILE
|
||||
)
|
||||
default_acr = OIDCFED_PROVIDER_PROFILES_DEFAULT_ACR[_provider_profile]
|
||||
self.payload = self.string_to_list(self.payload, ["acr_values"])
|
||||
len_acr = len(self.payload.get("acr_values",[]))
|
||||
session = OidcSession.objects.create(
|
||||
user=user,
|
||||
user_uid=user.username,
|
||||
nonce=self.payload["nonce"],
|
||||
authz_request=self.payload,
|
||||
client_id=self.payload["client_id"],
|
||||
auth_code=auth_code,
|
||||
acr=(
|
||||
self.payload["acr_values"][len_acr - 1]
|
||||
if len_acr > 0
|
||||
else default_acr
|
||||
)
|
||||
)
|
||||
session.set_sid(request)
|
||||
url = self.get_url_consent(user)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,34 @@
|
||||
import logging
|
||||
|
||||
from djagger.decorators import schema
|
||||
from django.http import Http404
|
||||
from django.http import JsonResponse
|
||||
|
||||
from spid_cie_oidc.entity.models import FederationEntityConfiguration
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@schema(
|
||||
summary="OIDC Provider openid-configuration",
|
||||
methods=['GET'],
|
||||
tags = ['Provider']
|
||||
)
|
||||
def openid_configuration(request):
|
||||
"""
|
||||
OIDC Discovery configuration at
|
||||
.well-known/openid-configuration
|
||||
"""
|
||||
_sub = request.build_absolute_uri().split("/.well-known/openid-configuration")[0]
|
||||
conf = FederationEntityConfiguration.objects.filter(
|
||||
# TODO: check for reverse proxy and forwarders ...
|
||||
sub=_sub,
|
||||
is_active=True,
|
||||
).first()
|
||||
if not conf: # pragma: no cover
|
||||
raise Http404()
|
||||
|
||||
content = conf.entity_configuration_as_dict['metadata'].get('openid_provider', None)
|
||||
if content:
|
||||
return JsonResponse(content, safe=False)
|
||||
else:
|
||||
raise Http404()
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import logging
|
||||
from django.core.paginator import Paginator
|
||||
import urllib.parse
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from djagger.decorators import schema
|
||||
from django.contrib.auth import logout
|
||||
from django.http import (
|
||||
HttpResponseForbidden,
|
||||
HttpResponseRedirect,
|
||||
)
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
from spid_cie_oidc.entity.models import (
|
||||
TrustChain
|
||||
)
|
||||
from spid_cie_oidc.provider.forms import ConsentPageForm
|
||||
from spid_cie_oidc.provider.models import IssuedToken, OidcSession
|
||||
from spid_cie_oidc.provider.settings import OIDCFED_PROVIDER_HISTORY_PER_PAGE
|
||||
|
||||
from . import OpBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsentPageView(OpBase, View):
|
||||
|
||||
template = "op_user_consent.html"
|
||||
|
||||
def get_consent_form(self):
|
||||
return ConsentPageForm
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
session = self.check_session(request)
|
||||
except Exception:
|
||||
logger.warning("Invalid session on Consent page")
|
||||
return HttpResponseForbidden()
|
||||
|
||||
tc = TrustChain.objects.filter(
|
||||
sub=session.client_id,
|
||||
metadata__openid_relying_party__isnull=False,
|
||||
is_active = True
|
||||
).first()
|
||||
|
||||
# if this auth code was already been used ... forbidden
|
||||
if IssuedToken.objects.filter(session=session):
|
||||
logger.warning(f"Auth code Replay {session}")
|
||||
return HttpResponseForbidden()
|
||||
|
||||
i18n_user_claims = self.attributes_names_to_release(
|
||||
request, session
|
||||
)['i18n_user_claims']
|
||||
|
||||
context = {
|
||||
"form": self.get_consent_form()(),
|
||||
"session": session,
|
||||
"client_organization_name": self.get_client_organization_name(tc),
|
||||
"user_claims": sorted(set(i18n_user_claims),),
|
||||
"redirect_uri": session.authz_request["redirect_uri"],
|
||||
"state": session.authz_request["state"]
|
||||
}
|
||||
return render(request, self.template, context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
try:
|
||||
session = self.check_session(request)
|
||||
except Exception as e:
|
||||
logger.warning(f"Invalid session: {e}")
|
||||
return HttpResponseForbidden()
|
||||
|
||||
self.payload = session.authz_request
|
||||
form = self.get_consent_form()(request.POST)
|
||||
if not form.is_valid():
|
||||
return self.redirect_response_data(
|
||||
self.payload["redirect_uri"],
|
||||
# TODO: this is not normative -> check AgID/IPZS
|
||||
error="rejected_by_user",
|
||||
error_description=_("User rejected the release of attributes"),
|
||||
state = self.payload.get("state", "")
|
||||
)
|
||||
issuer = self.get_issuer()
|
||||
|
||||
iss_token_data = self.get_iss_token_data(session, issuer)
|
||||
IssuedToken.objects.create(**iss_token_data)
|
||||
|
||||
return self.redirect_response_data(
|
||||
self.payload["redirect_uri"],
|
||||
code=session.auth_code,
|
||||
state=session.authz_request["state"],
|
||||
iss=issuer.sub if issuer else "",
|
||||
)
|
||||
|
||||
|
||||
def oidc_provider_not_consent(request):
|
||||
redirect_uri = urlparse(request.GET.get("redirect_uri"))
|
||||
state = request.GET.get("state", "")
|
||||
logout(request)
|
||||
kwargs = dict(
|
||||
error = "invalid_request",
|
||||
error_description = _(
|
||||
"Authentication request rejected by user"
|
||||
),
|
||||
state = state
|
||||
)
|
||||
path = redirect_uri.path if redirect_uri.path else "/"
|
||||
base = f'{redirect_uri.scheme}://{redirect_uri.netloc}{path}'
|
||||
url = f'{base}?{urllib.parse.urlencode(kwargs)}'
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class UserAccessHistoryView(OpBase, View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
session = self.check_session(request)
|
||||
except Exception: # pragma: no cover
|
||||
logger.warning("Invalid session on Access History page")
|
||||
return HttpResponseForbidden()
|
||||
user_access_history = OidcSession.objects.filter(
|
||||
user_uid=session.user_uid,
|
||||
).exclude(auth_code=session.auth_code)
|
||||
paginator = Paginator(
|
||||
user_access_history,
|
||||
OIDCFED_PROVIDER_HISTORY_PER_PAGE
|
||||
)
|
||||
page = request.GET.get("page", 1)
|
||||
history = paginator.get_page(page)
|
||||
context = {
|
||||
"history": history,
|
||||
"user": request.user
|
||||
}
|
||||
return render(request, "op_user_history.html", context)
|
||||
|
||||
|
||||
@schema(
|
||||
methods = [],
|
||||
djagger_exclude=True
|
||||
)
|
||||
class RevokeSessionView(OpBase, View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.check_session(request)
|
||||
except Exception: # pragma: no cover
|
||||
logger.warning("Invalid session on revoke page")
|
||||
return HttpResponseForbidden()
|
||||
|
||||
if request.GET.get("auth_code"):
|
||||
auth_code = request.GET["auth_code"]
|
||||
session_to_revoke = OidcSession.objects.filter(
|
||||
auth_code=auth_code,
|
||||
user=request.user
|
||||
).first()
|
||||
session_to_revoke.revoke(destroy_session=False)
|
||||
return redirect("oidc_provider_access_history")
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import logging
|
||||
|
||||
from djagger.decorators import schema
|
||||
from django.http import (
|
||||
HttpResponseBadRequest,
|
||||
HttpResponseForbidden,
|
||||
JsonResponse
|
||||
)
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from pydantic import BaseModel
|
||||
from spid_cie_oidc.provider.exceptions import ValidationException
|
||||
from spid_cie_oidc.provider.models import IssuedToken
|
||||
from spid_cie_oidc.provider.settings import (
|
||||
OIDCFED_DEFAULT_PROVIDER_PROFILE,
|
||||
OIDCFED_PROVIDER_PROFILES
|
||||
)
|
||||
|
||||
from . import OpBase
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
schema_profile = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE]
|
||||
|
||||
|
||||
@schema(
|
||||
methods=['GET', 'POST'],
|
||||
post_request_schema = {
|
||||
"application/x-www-form-urlencoded": schema_profile["introspection_request"]
|
||||
},
|
||||
post_response_schema= {
|
||||
"200":schema_profile["introspection_response"],
|
||||
"400":schema_profile["introspection_error_response"],
|
||||
},
|
||||
get_response_schema= {
|
||||
"400": BaseModel
|
||||
},
|
||||
tags = ['Provider']
|
||||
)
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class IntrospectionEndpoint(OpBase, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.validate_json_schema(
|
||||
request.POST.dict(),
|
||||
"introspection_request",
|
||||
"Introspection request object validation failed"
|
||||
)
|
||||
client_id = request.POST['client_id']
|
||||
self.check_client_assertion(
|
||||
client_id,
|
||||
request.POST['client_assertion']
|
||||
)
|
||||
except ValidationException:
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Introspection request object validation failed",
|
||||
},
|
||||
status = 400
|
||||
)
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(e)
|
||||
return HttpResponseForbidden()
|
||||
|
||||
required_token = request.POST['token']
|
||||
# query con client_id, access token
|
||||
token = IssuedToken.objects.filter(
|
||||
access_token=required_token
|
||||
).first()
|
||||
session = token.session
|
||||
if session.client_id != client_id:
|
||||
return JsonResponse( # pragma: no cover
|
||||
error = "invalid_client",
|
||||
error_description = "Client not recognized"
|
||||
)
|
||||
active = token and not token.is_revoked and not token.expired
|
||||
exp = token.expires
|
||||
sub = token.session.client_id
|
||||
issuer = self.get_issuer()
|
||||
iss = issuer.sub
|
||||
authz_request = session.authz_request
|
||||
scope = authz_request["scope"]
|
||||
response = {
|
||||
"active": active,
|
||||
"exp": exp,
|
||||
"sub" : sub,
|
||||
"iss": iss,
|
||||
"client_id": client_id,
|
||||
"aud": [client_id],
|
||||
"scope": scope
|
||||
}
|
||||
return JsonResponse(response)
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import logging
|
||||
|
||||
from djagger.decorators import schema
|
||||
from django.http import (
|
||||
HttpResponse,
|
||||
JsonResponse
|
||||
)
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from spid_cie_oidc.provider.exceptions import ValidationException
|
||||
from spid_cie_oidc.provider.models import IssuedToken
|
||||
from spid_cie_oidc.provider.settings import (
|
||||
OIDCFED_DEFAULT_PROVIDER_PROFILE,
|
||||
OIDCFED_PROVIDER_PROFILES
|
||||
)
|
||||
|
||||
from . import OpBase
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
schema_profile = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE]
|
||||
|
||||
|
||||
@schema(
|
||||
methods=['POST'],
|
||||
post_request_schema = {
|
||||
"application/x-www-form-urlencoded": schema_profile["revocation_request"],
|
||||
"application/json": schema_profile["revocation_request"],
|
||||
},
|
||||
post_response_schema = {
|
||||
"200": {},
|
||||
"400": schema_profile["revocation_response"]
|
||||
},
|
||||
tags = ['Provider']
|
||||
)
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class RevocationEndpoint(OpBase, View):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.validate_json_schema(
|
||||
request.POST.dict(),
|
||||
"revocation_request",
|
||||
"Revocation request object validation failed "
|
||||
)
|
||||
self.check_client_assertion(
|
||||
request.POST['client_id'],
|
||||
request.POST['client_assertion']
|
||||
)
|
||||
except ValidationException: # pragma: no cover
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Revocation request object validation failed",
|
||||
},
|
||||
status = 400
|
||||
)
|
||||
except Exception:
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Validation of client assertion failed",
|
||||
|
||||
},
|
||||
status = 400
|
||||
)
|
||||
|
||||
access_token = request.POST.get('token', None)
|
||||
if not access_token: # pragma: no cover
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "invalid_request",
|
||||
"error_description": "The request does not include Access Token",
|
||||
|
||||
},
|
||||
status = 400
|
||||
)
|
||||
|
||||
token = IssuedToken.objects.filter(
|
||||
access_token = access_token,
|
||||
revoked = False
|
||||
).first()
|
||||
|
||||
if not token or token.expired:
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Access Token not found or expired",
|
||||
|
||||
},
|
||||
status = 400
|
||||
)
|
||||
|
||||
if token.is_revoked: # pragma: no cover
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Access Token revoked",
|
||||
},
|
||||
status = 400
|
||||
)
|
||||
|
||||
token.session.revoke()
|
||||
return HttpResponse()
|
||||
@@ -0,0 +1,228 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from djagger.decorators import schema
|
||||
from djagger.utils import schema_set_examples
|
||||
from django.conf import settings
|
||||
from django.http import (
|
||||
HttpResponseBadRequest,
|
||||
HttpResponseForbidden,
|
||||
JsonResponse
|
||||
)
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from pydantic import BaseModel
|
||||
from spid_cie_oidc.entity.jwtse import unpad_jwt_payload
|
||||
from spid_cie_oidc.provider.exceptions import ValidationException
|
||||
from spid_cie_oidc.provider.models import IssuedToken, OidcSession
|
||||
from spid_cie_oidc.provider.settings import (
|
||||
OIDCFED_DEFAULT_PROVIDER_PROFILE,
|
||||
OIDCFED_PROVIDER_PROFILES,
|
||||
OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME
|
||||
)
|
||||
|
||||
from spid_cie_oidc.entity.utils import iat_now
|
||||
from . import OpBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
schema_profile = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE]
|
||||
|
||||
|
||||
@schema(
|
||||
methods=['GET', 'POST'],
|
||||
post_request_schema={
|
||||
"authn_request": schema_profile["authorization_code"],
|
||||
"refresh_request": schema_profile["refresh_token"],
|
||||
|
||||
},
|
||||
post_response_schema={
|
||||
"200": schema_profile["authorization_code_response"],
|
||||
# TODO
|
||||
# "200": schema_profile["refresh_token_response"],
|
||||
"400": schema_profile["token_error_response"],
|
||||
},
|
||||
get_response_schema={
|
||||
"400": BaseModel
|
||||
},
|
||||
tags=['Provider']
|
||||
)
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TokenEndpoint(OpBase, View):
|
||||
"""
|
||||
Request content type is 'application/x-www-form-urlencoded'
|
||||
"""
|
||||
schema = {}
|
||||
schema["authn_request"] = schema_set_examples({}, schema_profile["authorization_code"])
|
||||
schema["refresh_request"] = schema_set_examples({}, schema_profile["refresh_token"])
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
def grant_auth_code(self, request, *args, **kwargs):
|
||||
"""
|
||||
Token request for an authorization code grant
|
||||
"""
|
||||
# PKCE check - based on authz.authz_request["code_challenge_method"] == S256
|
||||
code_challenge = hashlib.sha256(request.POST["code_verifier"].encode()).digest()
|
||||
code_challenge_b64 = base64.urlsafe_b64encode(code_challenge).decode("utf-8")
|
||||
code_challenge_unpad = code_challenge_b64.replace("=", "")
|
||||
if code_challenge_unpad != self.authz.authz_request["code_challenge"]:
|
||||
logger.warning(
|
||||
f"PCKE validation failed. Produced code challenge [{code_challenge_unpad}]"
|
||||
f" if different from which sent by RP [{self.authz.authz_request['code_challenge']}]"
|
||||
)
|
||||
return HttpResponseForbidden()
|
||||
#
|
||||
|
||||
issued_token = IssuedToken.objects.filter(
|
||||
session=self.authz,
|
||||
revoked=False
|
||||
).first()
|
||||
|
||||
jwk_at = unpad_jwt_payload(issued_token.access_token)
|
||||
expires_in = self.get_expires_in(jwk_at['iat'], jwk_at['exp'])
|
||||
|
||||
iss_token_data = dict( # nosec B106
|
||||
access_token=issued_token.access_token,
|
||||
id_token=issued_token.id_token,
|
||||
token_type="Bearer", # nosec B106
|
||||
expires_in=expires_in,
|
||||
# TODO: remove unsupported scope
|
||||
scope=self.authz.authz_request["scope"],
|
||||
)
|
||||
if issued_token.refresh_token:
|
||||
iss_token_data['refresh_token'] = issued_token.refresh_token
|
||||
return JsonResponse(iss_token_data)
|
||||
|
||||
def is_token_renewable(self, session) -> bool:
|
||||
provider = getattr(settings, "OIDCFED_PROVIDER_PROFILE")
|
||||
if provider == "cie":
|
||||
issuedToken = IssuedToken.objects.filter(
|
||||
session=session
|
||||
).first()
|
||||
|
||||
id_token = unpad_jwt_payload(issuedToken.id_token)
|
||||
consent_expiration = id_token['iat'] + OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME
|
||||
delta = consent_expiration - iat_now()
|
||||
if delta > 0:
|
||||
return True
|
||||
return False
|
||||
elif provider == "spid":
|
||||
# TODO: check also ACR
|
||||
issuedToken = IssuedToken.objects.filter(
|
||||
session=session
|
||||
)
|
||||
return (
|
||||
(issuedToken.count() - 1) < getattr(
|
||||
settings, "OIDCFED_PROVIDER_MAX_REFRESH", 1
|
||||
)
|
||||
)
|
||||
|
||||
def grant_refresh_token(self, request, *args, **kwargs):
|
||||
"""
|
||||
client_id=https://rp.cie.it&
|
||||
client_assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlNQSUQiLCJhZG1pbiI6dHJ1ZX0.LVyRDPVJm0S9q7oiXcYVIIqGWY0wWQlqxvFGYswL...&
|
||||
client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&
|
||||
refresh_token=8xLOxBtZp8 &
|
||||
grant_type=refresh_token
|
||||
"""
|
||||
# 1. get the IssuedToken refresh one, revoked = None
|
||||
# 2. create a new instance of issuedtoken linked to the same sessions and revoke the older
|
||||
# 3. response with a new refresh, access and id_token
|
||||
issued_token = IssuedToken.objects.filter(
|
||||
refresh_token=request.POST['refresh_token'],
|
||||
revoked=False
|
||||
).first()
|
||||
|
||||
if not issued_token:
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Refresh token not found",
|
||||
|
||||
},
|
||||
status=400
|
||||
)
|
||||
session = issued_token.session
|
||||
if not self.is_token_renewable(session): # pragma: no cover
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Refresh Token can no longer be updated",
|
||||
|
||||
}, status=400
|
||||
)
|
||||
iss_token_data = self.get_iss_token_data(session, self.get_issuer())
|
||||
IssuedToken.objects.create(**iss_token_data)
|
||||
issued_token.revoked = True
|
||||
issued_token.save()
|
||||
|
||||
jwk_at = unpad_jwt_payload(iss_token_data['access_token'])
|
||||
expires_in = self.get_expires_in(jwk_at['iat'], jwk_at['exp'])
|
||||
|
||||
data = dict( # nosec B106
|
||||
access_token=iss_token_data['access_token'],
|
||||
id_token=iss_token_data['id_token'],
|
||||
refresh_token=iss_token_data['refresh_token'],
|
||||
token_type="Bearer", # nosec B106
|
||||
expires_in=expires_in
|
||||
)
|
||||
|
||||
return JsonResponse(data)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
logger.debug(f"{request.headers}: {request.POST}")
|
||||
try:
|
||||
self.validate_json_schema(
|
||||
request.POST.dict(),
|
||||
request.POST["grant_type"],
|
||||
"Token request object validation failed "
|
||||
)
|
||||
except ValidationException:
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Token request object validation failed",
|
||||
},
|
||||
status=400
|
||||
)
|
||||
self.commons = self.get_jwt_common_data()
|
||||
self.issuer = self.get_issuer()
|
||||
# check client_assertion and client ownership
|
||||
try:
|
||||
self.check_client_assertion(
|
||||
request.POST['client_id'],
|
||||
request.POST['client_assertion']
|
||||
)
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.warning(
|
||||
"Client authentication failed for "
|
||||
f"{request.POST.get('client_id', 'unknown')}: {e}"
|
||||
)
|
||||
return JsonResponse(
|
||||
# TODO: error message here
|
||||
{
|
||||
'error': "unauthorized_client",
|
||||
'error_description': ""
|
||||
|
||||
}, status=403
|
||||
)
|
||||
|
||||
if request.POST.get("grant_type") == 'authorization_code':
|
||||
self.authz = OidcSession.objects.filter(
|
||||
auth_code=request.POST["code"],
|
||||
revoked=False
|
||||
).first()
|
||||
|
||||
if not self.authz:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
return self.grant_auth_code(request)
|
||||
elif request.POST.get("grant_type") == 'refresh_token':
|
||||
return self.grant_refresh_token(request)
|
||||
else:
|
||||
# Token exchange? :-)
|
||||
raise NotImplementedError()
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import logging
|
||||
|
||||
from django.http import (
|
||||
HttpResponse,
|
||||
HttpResponseForbidden,
|
||||
)
|
||||
from djagger.decorators import schema
|
||||
from django.utils import timezone
|
||||
from django.views import View
|
||||
from spid_cie_oidc.entity.jwtse import (
|
||||
create_jws,
|
||||
create_jwe,
|
||||
unpad_jwt_payload
|
||||
)
|
||||
|
||||
from spid_cie_oidc.entity.models import (
|
||||
TrustChain
|
||||
)
|
||||
from spid_cie_oidc.entity.utils import get_jwks, get_key, KeyUsage
|
||||
from spid_cie_oidc.provider.models import IssuedToken
|
||||
|
||||
from . import OpBase
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@schema(
|
||||
summary="OIDC Provider UserInfo endpoint",
|
||||
methods=['GET'],
|
||||
# TODO
|
||||
# security=[
|
||||
# dict(
|
||||
# description = "",
|
||||
# name = "",
|
||||
# bearerFormat = "JWT",
|
||||
# )
|
||||
# ],
|
||||
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 = ['Provider']
|
||||
)
|
||||
class UserInfoEndpoint(OpBase, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Userinfo endpoint just needs a HTTP GET with the Access Token
|
||||
in a Authorization Http Header like this:
|
||||
|
||||
Authorization: Bearer $JWT
|
||||
"""
|
||||
ah = request.headers.get("Authorization", None)
|
||||
if not ah or "Bearer " not in ah:
|
||||
return HttpResponseForbidden()
|
||||
bearer = ah.split("Bearer ")[1]
|
||||
|
||||
token = IssuedToken.objects.filter(
|
||||
access_token=bearer,
|
||||
revoked=False,
|
||||
session__revoked=False,
|
||||
expires__gte=timezone.localtime(),
|
||||
).first()
|
||||
|
||||
if not token:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
rp_tc = TrustChain.objects.filter(
|
||||
sub=token.session.client_id,
|
||||
metadata__openid_relying_party__isnull=False,
|
||||
is_active=True
|
||||
).first()
|
||||
if not rp_tc:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
issuer = self.get_issuer()
|
||||
access_token_data = unpad_jwt_payload(token.access_token)
|
||||
|
||||
# TODO: user claims
|
||||
jwt = {"sub": access_token_data["sub"]}
|
||||
for claim in (
|
||||
token.session.authz_request.get(
|
||||
"claims", {}
|
||||
).get("userinfo", {}).keys()
|
||||
):
|
||||
if claim in token.session.user.attributes:
|
||||
jwt[claim] = token.session.user.attributes[claim]
|
||||
|
||||
# sign the data
|
||||
key = get_key(issuer.jwks_core, KeyUsage.signature)
|
||||
jws = create_jws(jwt, key)
|
||||
|
||||
# encrypt the data
|
||||
client_jwks = get_jwks(
|
||||
rp_tc.metadata['openid_relying_party'],
|
||||
federation_jwks = rp_tc.jwks
|
||||
)
|
||||
client_jwk = get_key(client_jwks, KeyUsage.encryption)
|
||||
|
||||
jwe = create_jwe(
|
||||
jws,
|
||||
client_jwk,
|
||||
cty="JWT"
|
||||
)
|
||||
return HttpResponse(jwe, content_type="application/jwt")
|
||||
Reference in New Issue
Block a user