italia/spid-cie-oidc-django

View on GitHub
spid_cie_oidc/provider/schemas/authn_requests.py

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
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",
        )