feat: link local library

This commit is contained in:
gabdevele
2026-06-17 20:43:56 +02:00 Unverified
parent 9a770a2af0
commit db59882f33
327 changed files with 22274 additions and 1 deletions
@@ -0,0 +1,222 @@
from enum import Enum
from typing import List, Literal, Optional
from pydantic import BaseModel, Field, HttpUrl, conlist, constr, validator
# TODO: Schema of claims is not genrated
class AcrValues(str, Enum):
l1 = "https://www.spid.gov.it/SpidL1"
l2 = "https://www.spid.gov.it/SpidL2"
l3 = "https://www.spid.gov.it/SpidL3"
class ScopeSpid(str, Enum):
openid = "openid"
offline_access = "offline_access"
class ScopeCie(str, Enum):
openid = "openid"
offline_access = "offline_access"
profile = "profile"
email = "email"
class ClaimsTypeEssential(BaseModel):
essential: Optional[bool]
class ClaimsTypeStringValue(BaseModel):
value: Optional[str]
class ClaimsTypeStringValues(BaseModel):
values: Optional[conlist(str, max_items=2, min_items=2)]
class ClaimsType(str, Enum):
essential = ClaimsTypeEssential
value = ClaimsTypeStringValue
values = ClaimsTypeStringValues
class UserInfoSpid(BaseModel):
name: Optional[dict] = Field(
alias="given_name", default=None
)
family_name: Optional[dict] = Field(
alias="family_name", default=None
)
place_of_birth: Optional[dict] = Field(
alias="place_of_birth", default=None
)
date_of_birth: Optional[dict] = Field(
alias="birthdate", default=None
)
gender: Optional[dict] = Field(
alias="gender", default=None
)
company_name: Optional[dict] = Field(
alias="https://attributes.eid.gov.it/company_name", default=None
)
registered_office: Optional[dict] = Field(
alias="https://attributes.eid.gov.it/registered_office", default=None
)
fiscal_number: Optional[dict] = Field(
alias="https://attributes.eid.gov.it/fiscal_number", default=None
)
iva_code: Optional[dict] = Field(
alias="https://attributes.eid.gov.it/vat_number", default=None
)
id_card: Optional[dict] = Field(
alias="https://attributes.eid.gov.it/document_details", default=None
)
mobile_phone: Optional[dict] = Field(
alias="https://attributes.eid.gov.it/phone_number", default=None
)
email: Optional[dict] = Field(
alias="email", default=None
)
address: Optional[dict] = Field(
alias="address", default=None
)
expiration_date: Optional[dict] = Field(
alias="https://attributes.eid.gov.it/eid_exp_date", default=None
)
digital_address: Optional[dict] = Field(
alias="https://attributes.eid.gov.it/e_delivery_service", default=None
)
class UserInfoCie(BaseModel):
given_name: Optional[dict]
family_name: Optional[dict]
email: Optional[dict]
email_verified: Optional[dict]
gender: Optional[dict]
birthdate: Optional[dict]
phone_number: Optional[dict]
phone_number_verified: Optional[dict]
address: Optional[dict]
place_of_birth: Optional[dict]
document_details: Optional[dict]
e_delivery_service: Optional[dict]
fiscal_number: Optional[dict]
physical_phone_number: Optional[dict]
class IdToken(UserInfoCie):
pass
TYPES = {
"essential": ClaimsTypeEssential,
"value": ClaimsTypeStringValue,
"values": ClaimsTypeStringValues,
}
CLAIMS_SPID = {"userinfo": UserInfoSpid}
CLAIMS_CIE = {"userinfo": UserInfoCie, "id_token": IdToken}
class AuthenticationRequest(BaseModel):
client_id: HttpUrl
response_type: Literal["code"]
scope: List[str]
code_challenge: str
code_challenge_method: Literal["S256"]
nonce: constr(min_length=32)
redirect_uri: HttpUrl
claims: Optional[dict]
state: constr(min_length=32)
# TODO: to be improved
ui_locales: Optional[List[str]]
# sub claim MUST not be used to prevent that this jwt
# could be reused as a private_key_jwt
# sub: HttpUrl
iss: HttpUrl
iat: int
exp: Optional[int]
jti: Optional[str]
aud: str | List[HttpUrl]
acr_values: Optional[List[AcrValues]]
prompt: Optional[Literal["consent", "consent login"]]
@validator("claims")
def validate_claims(cls, claims):
for k_claim, v_claim in claims.items():
cl = cls.get_claims()
claims_items = cl.get(k_claim, None)
if not claims_items:
continue
claims_items(**v_claim)
for k_item, v_item in v_claim.items():
if v_item is not None:
for k_type, v_type in TYPES.items():
v_type(**v_item)
return claims
@validator("scope")
def validate_scope(cls, scope):
if "openid" not in scope:
raise ValueError("'scope' attribute must contain 'openid'")
class AuthenticationRequestSpid(AuthenticationRequest):
scope: List[ScopeSpid]
def get_claims() -> dict:
return CLAIMS_SPID
def example(): # pragma: no cover
return AuthenticationRequestSpid( # nosec B106
client_id= "https://rp.cie.it/callback1/",
response_type= "code",
scope= ["openid", "offline_access"],
code_challenge= "codeChallenge",
code_challenge_method= "S256",
nonce= "12345678123456781234567812345678inpiu",
prompt= "consent",
redirect_uri= "https://rp.cie.it/callback1/",
acr_values= ["https://www.spid.gov.it/SpidL2", "https://www.spid.gov.it/SpidL1"],
claims= {},
state= "fyZiOL9Lf2CeKuNT2JzxiLRDink0uPcd",
ui_locales= ["codice1", "codice2", "codice3"],
sub= "https://rp.cie.it/",
iss= "https://op.spid.agid.gov.it/",
aud= ["https://rp.spid.agid.gov.it/auth"],
iat= 1648591200,
exp= 1648592200,
jti= "a72d5df0-2415-4c7c-a44f-3988b354040b",
)
class AuthenticationRequestCie(AuthenticationRequest):
scope: List[ScopeCie]
def get_claims() -> dict:
return CLAIMS_CIE
class AuthenticationRequestDoc(BaseModel):
client_id: HttpUrl
response_type: Literal["code"]
scope: List[str]
code_challenge: str
code_challenge_method: Literal["S256"]
request: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722
def example(): # pragma: no cover
return AuthenticationRequestDoc( # nosec B106
client_id= "https://rp.cie.it/callback1/",
response_type= "code",
scope= ["openid", "offline_access"],
code_challenge= "codeChallenge",
code_challenge_method= "S256",
request= "eyJhbGciOiJSUzI1NiIsImtpZCI6ImsyYmRjIn0.ew0KICJpc3MiOiAiczZCaGRSa3F0MyIsDQogImF1ZCI6ICJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsDQogInJlc3BvbnNlX3R5cGUiOiAiY29kZSBpZF90b2tlbiIsDQogImNsaWVudF9pZCI6ICJzNkJoZFJrcXQzIiwNCiAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vY2xpZW50LmV4YW1wbGUub3JnL2NiIiwNCiAic2NvcGUiOiAib3BlbmlkIiwNCiAic3RhdGUiOiAiYWYwaWZqc2xka2oiLA0KICJub25jZSI6ICJuLTBTNl9XekEyTWoiLA0KICJtYXhfYWdlIjogODY0MDAsDQogImNsYWltcyI6IA0KICB7DQogICAidXNlcmluZm8iOiANCiAgICB7DQogICAgICJnaXZlbl9uYW1lIjogeyJlc3NlbnRpYWwiOiB0cnVlfSwNCiAgICAgI.qq",
)
@@ -0,0 +1,44 @@
from typing import Literal
from pydantic import BaseModel, HttpUrl, constr
class AuthenticationResponse(BaseModel):
# TODO: is 1*VSCHAR
code: str
state: constr(min_length=32)
def example(): # pragma: no cover
return AuthenticationResponse( # nosec B106
code= "usDwMnEzJPpG5oaV8x3j&",
state= "fyZiOL9Lf2CeKuNT2JzxiLRDink0uPcd",
)
class AuthenticationResponseCie(AuthenticationResponse):
iss: HttpUrl
def example(): # pragma: no cover
return AuthenticationResponse( # nosec B106
code= "usDwMnEzJPpG5oaV8x3j&",
state= "fyZiOL9Lf2CeKuNT2JzxiLRDink0uPcd",
iss= "https://idserver.servizicie.interno.gov.it",
)
class AuthenticationErrorResponse(BaseModel):
error: Literal[
"invalid_request",
"unauthorized_client",
"access_denied",
"unsupported_response_type",
"invalid_scope",
"server_error",
"temporarily_unavailable",
]
error_description: str
state: constr(min_length=32)
class AuthenticationErrorResponseCie(AuthenticationErrorResponse):
iss: HttpUrl
@@ -0,0 +1,32 @@
from spid_cie_oidc.entity.utils import iat_now
from pydantic import BaseModel, AnyHttpUrl, validator
from typing import Optional, List
class ClientAssertion(BaseModel):
iss: AnyHttpUrl
sub: AnyHttpUrl
iat: int
exp: int
jti: Optional[str]
aud: str | List[AnyHttpUrl]
@validator("sub")
def iss_and_sub_must_match(cls, sub, values):
if values['iss'] != sub:
raise ValueError(
'Client Assertion: iss and sub must have the same value'
)
return sub
@validator("exp")
def not_expired(cls, exp, values):
_now = iat_now()
if not (values['iat'] <= _now < exp):
raise ValueError(
'Client Assertion: exp must be greater than '
'iat and less than the current time.'
f'{values["iat"]} <= {_now} < {exp}'
)
return exp
@@ -0,0 +1,11 @@
from typing import Literal
from pydantic import BaseModel, HttpUrl, constr
class IntrospectionRequest(BaseModel):
client_assertion: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722
client_assertion_type: Literal[
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
]
client_id: HttpUrl
token: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722
@@ -0,0 +1,34 @@
from typing import Literal
from pydantic import BaseModel
class IntrospectionResponse(BaseModel):
active: bool
# scope: Optional[List[str]]
# sub: Optional[str]
# exp: Optional[int]
# client_id: HttpUrl
# iss: Optional[HttpUrl]
# TODO: migliorare: array di url con almeno uno
# aud: Optional[List[HttpUrl]]
class IntrospectionErrorResponse(BaseModel):
error_description: str
class IntrospectionErrorResponseSpid(IntrospectionErrorResponse):
error: Literal[
"invalid_client", "invalid_request", "server_error", "temporarily_unavailable"
]
class IntrospectionErrorResponseCie(IntrospectionErrorResponse):
error: Literal[
"invalid_client",
"invalid_request",
"invalid_token",
"server_error",
"temporarily_unavailable",
]
@@ -0,0 +1,67 @@
import datetime
from typing import List
from django.conf import settings
from django.utils import timezone
from pydantic import BaseModel, HttpUrl, validator
from spid_cie_oidc.entity.utils import datetime_from_timestamp
try:
import spid_cie_oidc.entity.settings as entity_settings
NOW = timezone.localtime()
MAX_ACCEPTED_TIMEDIFF = getattr(
settings, "MAX_ACCEPTED_TIMEDIFF", entity_settings.MAX_ACCEPTED_TIMEDIFF
)
except ImportError: # pragma: no cover
NOW = datetime.datetime.now()
MAX_ACCEPTED_TIMEDIFF = 5
class JwtStructure(BaseModel):
iss: HttpUrl
sub: HttpUrl
iat: int
exp: int
jti: str
# TODO: migliorare: array di url con almeno uno
aud: List[HttpUrl]
@validator("exp")
def validate_exp(cls, exp):
now = int(NOW.timestamp())
if exp <= now:
raise ValueError("exp MUST be in the future")
@validator("iat")
def validate_iat(cls, iat):
dt_iat = datetime_from_timestamp(iat)
delta = datetime.timedelta(minutes=MAX_ACCEPTED_TIMEDIFF)
if abs((NOW - dt_iat).total_seconds()) > delta.total_seconds():
raise ValueError(f"iat MUST be in the last {MAX_ACCEPTED_TIMEDIFF} minutes")
class IdTokenJwt(JwtStructure):
# TODO: implementare
pass
class AccessTokenJwt(JwtStructure):
# TODO: implementare
pass
class RefreshTokenJwt(JwtStructure):
# TODO: implementare
pass
class UserInfoResponseJwtHeader(BaseModel):
# TODO: implementare
pass
class UserInfoResponseJwtPayload(JwtStructure):
# TODO: implementare
pass
@@ -0,0 +1,12 @@
from typing import Literal
from pydantic import BaseModel, HttpUrl, constr
class RevocationRequest(BaseModel):
client_assertion: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722
client_assertion_type: Literal[
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
]
client_id: HttpUrl
token: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722
@@ -0,0 +1,14 @@
from typing import Literal
from pydantic import BaseModel
class RevocationErrorResponse(BaseModel):
error: Literal[
"invalid_request",
"invalid_client",
"unauthorized_client",
"invalid_scope",
"unsupported_token_type",
]
error_description: str
@@ -0,0 +1,43 @@
from typing import Literal
from pydantic import BaseModel, HttpUrl, constr
class TokenRequest(BaseModel):
client_id: HttpUrl
client_assertion: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722
client_assertion_type: Literal[
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
]
class TokenAuthnCodeRequest(TokenRequest):
# TODO: is 1*VSCHAR
code: str
# TODO: is 43*128unreserved, where unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
code_verifier: str
grant_type: Literal["authorization_code"]
def example():
return TokenAuthnCodeRequest(
client_id = "http://example.com",
client_assertion = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjJIbm9GUzNZbkM5dGppQ2FpdmhXTFZVSjNBeHdHR3pfOTh1UkZhcU1FRXMifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvb2lkYy9ycC8iLCJzdWIiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvb2lkYy9ycC8iLCJhdWQiOlsiaHR0cDovLzEyNy4wLjAuMTo4MDAwL29pZGMvb3AvdG9rZW4vIl0sImlhdCI6MTY0ODQ5NTc1NywiZXhwIjoxNjQ4NDk3NzM3LCJqdGkiOiI0OWE2OGQyMS03NTE1LTRmNDMtODZhZC1hMDNhMWNmNDFmNTgifQ.EaWmmuDhoa8jt_go4zrY5AhTGnljIS1zNwIF-f9eBidAiWPYaKwmXRsRoXrAeybLcJ5E8-7TO1S8jZiCxcElQPdRRvuP9ZsgNhfEqDhZtabkwGBFt4gpQZnFsgGDMAi-v4sTM55VsnIJIMrHPNIckZfL-YhD-FSCtDsqrCkCnucXR58Kfp_SEx3hJvrU-2vcOeNXJBTqs3pHl9jws9GJIf6bZd7vQqTMolCC4zBzUp7yT3OXqMt9uCUhLZzLtpTRW0_-u-KW4h44eHOdEf6ePqiFhjflF9_T6cM-OBqzaK0eVvg6RcasvF3IyxdG-ME5crapAibeePqg_Hy__Q5idQ",
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
code = "bb03ad1f2e838335c1050ae280a64f4e195dbeb17ba8aa2dc6fd99e1c5181fc735be6af10b634fbf0f04f533843e199d0261bf570585ca292cb4fb8857480af5",
code_verifier = "nd6gJEXzUZApV46u4DmVUfZgBYc2SJsZGkaBftof4gcI95ByALg",
grant_type = "authorization_code"
)
class TokenRefreshRequest(TokenRequest):
grant_type: Literal["refresh_token"]
refresh_token: str
def example():
return TokenRefreshRequest( # nosec B106
client_id = "http://example.com",
client_assertion = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjJIbm9GUzNZbkM5dGppQ2FpdmhXTFZVSjNBeHdHR3pfOTh1UkZhcU1FRXMifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvb2lkYy9ycC8iLCJzdWIiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvb2lkYy9ycC8iLCJhdWQiOlsiaHR0cDovLzEyNy4wLjAuMTo4MDAwL29pZGMvb3AvdG9rZW4vIl0sImlhdCI6MTY0ODQ5NTc1NywiZXhwIjoxNjQ4NDk3NzM3LCJqdGkiOiI0OWE2OGQyMS03NTE1LTRmNDMtODZhZC1hMDNhMWNmNDFmNTgifQ.EaWmmuDhoa8jt_go4zrY5AhTGnljIS1zNwIF-f9eBidAiWPYaKwmXRsRoXrAeybLcJ5E8-7TO1S8jZiCxcElQPdRRvuP9ZsgNhfEqDhZtabkwGBFt4gpQZnFsgGDMAi-v4sTM55VsnIJIMrHPNIckZfL-YhD-FSCtDsqrCkCnucXR58Kfp_SEx3hJvrU-2vcOeNXJBTqs3pHl9jws9GJIf6bZd7vQqTMolCC4zBzUp7yT3OXqMt9uCUhLZzLtpTRW0_-u-KW4h44eHOdEf6ePqiFhjflF9_T6cM-OBqzaK0eVvg6RcasvF3IyxdG-ME5crapAibeePqg_Hy__Q5idQ",
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
refresh_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRCNjdnTDdjazNURmlJQWY3TjZfN1NIdnFrME1EWU1FUWNvR0dsa1VBQXcifQ.eyJpc3MiOiJodHRwOi8vb3AtdGVzdC9vaWRjL29wLyIsInN1YiI6Imh0dHA6Ly9ycC10ZXN0Lml0L29pZGMvcnAvIiwiZXhwIjoxNjQ4NDk3OTU2LCJpYXQiOjE2NDg0OTU5NzYsImF1ZCI6WyJodHRwOi8vcnAtdGVzdC5pdC9vaWRjL3JwLyJdLCJjbGllbnRfaWQiOiJodHRwOi8vcnAtdGVzdC5pdC9vaWRjL3JwLyIsInNjb3BlIjoib3BlbmlkIn0.yDF6BsFpT1h2JdSxyE3hE8chcF0Lq0OtGzRoZkBbN7otHTvAsTcupT-J6JTHnO817Bp1kllvLkJ5X4p3NeElOL-TbzF1xtX2azpiaxZD4xEGOYsMGkRnLeJP9LWGYbJXEBlAjHMpYW-DhZz3cE3iJvmaehi6sIRsXMPszsDIrt2cYmwFg9KrEc2Ty_qWKdIhzgxq82FEgMk2e2ehez3MTH7KAni2hRmtl32w6uf7_cfW5GlLfDjjVNm2Y9NkmrFeKmX0YUOQWXTts1MhlGfnw9ASgBstgfB5tWWmxF6Sg7ojAnPt_eKH75gcMLhUxj4iif8Aj0QNPzXuJ7VlBH9VAA",
grant_type = "refresh_token"
)
@@ -0,0 +1,19 @@
from typing import Literal
from pydantic import BaseModel, constr
class TokenResponse(BaseModel):
access_token: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722
token_type: Literal["Bearer"]
expires_in: int
id_token: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722
class TokenRefreshResponse(TokenResponse):
refresh_token: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722
class TokenErrorResponse(BaseModel):
error: Literal["invalid_request", "unauthorized_client"]
error_description: str