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,36 @@
from django.contrib import admin
from .models import IssuedToken, OidcSession
@admin.register(OidcSession)
class OidcSessionAdmin(admin.ModelAdmin):
list_display = ("user_uid", "client_id", "created", "revoked")
list_filter = ("created", "revoked")
search_fields = ("client_id", "user_uid")
readonly_fields = (
"nonce",
"auth_code",
"user_uid",
"user",
"sid",
"client_id",
"created",
"revoked",
"user_uid",
"authz_request",
)
@admin.register(IssuedToken)
class IssuedTokenAdmin(admin.ModelAdmin):
list_display = ("client_id", "user_uid", "created")
list_filter = ("created",)
search_fields = ("session__user_uid", "session__client_id")
readonly_fields = (
"access_token",
"id_token",
"refresh_token",
"created",
"session",
)
@@ -0,0 +1,8 @@
from django.apps import AppConfig
class SpidCieOidcOpConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "spid_cie_oidc.provider"
verbose_name = "SPID/CIE OIDC Federation Provider"
label = "spid_cie_oidc_provider"
@@ -0,0 +1,22 @@
class AuthzRequestReplay(Exception):
pass
class InvalidSession(Exception):
pass
class RevokedSession(Exception):
pass
class ValidationException(Exception):
pass
class ExpiredAuthCode(Exception):
pass
class InvalidRefreshRequestException(Exception):
pass
@@ -0,0 +1,38 @@
from django import forms
from django.utils.translation import gettext_lazy as _
class AuthLoginForm(forms.Form):
"""
The User Authentication Form for an Authorized request
"""
username = forms.CharField(
initial="",
label=_("Username"),
error_messages={"required": _("Enter your username")},
)
password = forms.CharField(
initial="",
label=_("Password"),
error_messages={"required": _("Enter your password")},
widget=forms.PasswordInput(),
)
# when a prue SSO will be enabled.
# forget_agreement = forms.BooleanField(label=_("Delete previous agreement"),
# required=False,
# localize=True)
# forget_login = forms.BooleanField(label=_("Forget access"),
# required=False,
# localize=True)
class AuthzHiddenForm(forms.Form):
"""
A hidden form to carry the original authz request
"""
authz_request_object = forms.CharField(widget=forms.HiddenInput())
class ConsentPageForm(forms.Form):
agree = forms.BooleanField(initial=True, widget=forms.HiddenInput())
@@ -0,0 +1,106 @@
import json
import logging
from django.conf import settings
from django.core.management.base import BaseCommand
from django.utils.translation import gettext as _
from spid_cie_oidc.entity.statements import (
get_entity_configurations,
get_http_url,
EntityConfiguration,
)
from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Attribute release query"
def add_arguments(self, parser):
parser.epilog = "Example: ./manage.py fetch_openid_providers"
parser.add_argument(
"--from",
nargs="*",
default=[],
help=_(
"A list of url of authorities, separated by space where "
"download the list of OPs"
),
)
parser.add_argument(
"-f",
"--force",
action="store_true",
required=False,
help=_("Don't use already cached statements and chains"),
)
parser.add_argument(
"-debug", required=False, action="store_true", help="see debug message"
)
def handle(self, *args, **options):
if not options["from"]:
return # pragma: no cover
res = []
rp_subs = []
jwts = get_entity_configurations(options["from"])
auth_ecs = []
for i in jwts:
ec = EntityConfiguration(i)
if ec.validate_by_itself():
auth_ecs.append(ec)
if not auth_ecs:
logger.warning(
f"Any of {options['from']} has a valid EntityConfiguration "
"to get a List endpoint"
)
return
logger.info(
f"Found {auth_ecs} federation entities where to query a list of RPs"
)
list_urls = []
for i in auth_ecs:
endpoint = (
i.payload.get("metadata", {})
.get("federation_entity", {})
.get("federation_list_endpoint", "")
)
if endpoint:
list_urls.append(f"{endpoint}?type=openid_relying_party")
rp_subs = []
for rp_sub in get_http_url(list_urls, httpc_params=settings.HTTPC_PARAMS):
try:
urls = json.loads(rp_sub)
rp_subs.extend(urls)
except Exception as e:
logger.error(f"Failed {e}")
continue
for rp_sub in rp_subs:
try:
tc = get_or_create_trust_chain(
subject=rp_sub,
trust_anchor=settings.OIDCFED_DEFAULT_TRUST_ANCHOR,
metadata__relying_party__is_null=False,
httpc_params=settings.HTTPC_PARAMS,
required_trust_marks=getattr(
settings, "OIDCFED_REQUIRED_TRUST_MARKS", []
),
force=options["force"],
)
if tc.is_valid:
res.append(tc)
logger.info(f"Final Metadata for {tc.sub}:\n\n{tc.metadata}")
except Exception as e:
logger.exception(f"Failed to download {rp_sub} due to: {e}")
@@ -0,0 +1,84 @@
# Generated by Django 4.0.2 on 2022-03-02 22:40
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="OidcSession",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("modified", models.DateTimeField(auto_now=True)),
("user_uid", models.CharField(max_length=120)),
("client_id", models.URLField(blank=True, null=True)),
("nonce", models.CharField(max_length=2048)),
("authz_request", models.JSONField(max_length=2048)),
("sub", models.CharField(blank=True, max_length=254, null=True)),
("user_claims", models.JSONField(blank=True, default=dict)),
("revoked", models.BooleanField(default=False)),
("auth_code", models.CharField(max_length=2048)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "User Session",
"verbose_name_plural": "User Sessions",
"unique_together": {("client_id", "nonce")},
},
),
migrations.CreateModel(
name="IssuedToken",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("modified", models.DateTimeField(auto_now=True)),
("access_token", models.TextField(blank=True, null=True)),
("id_token", models.TextField(blank=True, null=True)),
("refresh_token", models.TextField(blank=True, null=True)),
(
"session",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="spid_cie_oidc_provider.oidcsession",
),
),
],
options={
'ordering': ['-created'],
"verbose_name": "Issued Token",
"verbose_name_plural": "Issued Tokens",
},
),
]
@@ -0,0 +1,17 @@
# Generated by Django 4.0.2 on 2022-03-02 23:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_provider", "0001_initial"),
]
operations = [
migrations.RemoveField(
model_name="oidcsession",
name="user_claims",
),
]
@@ -0,0 +1,29 @@
# Generated by Django 4.0.2 on 2022-03-05 01:18
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_provider", "0002_remove_oidcsession_user_claims"),
]
operations = [
migrations.RemoveField(
model_name="oidcsession",
name="sub",
),
migrations.AddField(
model_name="issuedtoken",
name="expires",
field=models.DateTimeField(default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name="issuedtoken",
name="revoked",
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,21 @@
# Generated by Django 4.0.2 on 2022-03-06 23:11
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('spid_cie_oidc_provider', '0003_remove_oidcsession_sub_issuedtoken_expires_and_more'),
]
operations = [
migrations.AlterField(
model_name='oidcsession',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.0.2 on 2022-03-07 00:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spid_cie_oidc_provider', '0004_alter_oidcsession_user'),
]
operations = [
migrations.AddField(
model_name='oidcsession',
name='sid',
field=models.CharField(blank=True, help_text='django session key', max_length=1024, null=True),
),
]
@@ -0,0 +1,19 @@
# Generated by Django 4.0.2 on 2022-03-16 16:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spid_cie_oidc_provider', '0005_oidcsession_sid'),
]
operations = [
migrations.AddField(
model_name='oidcsession',
name='acr',
field=models.CharField(default='https://www.spid.gov.it/SpidL2', max_length=1024),
preserve_default=False,
),
]
@@ -0,0 +1,21 @@
# Generated by Django 4.0.3 on 2022-03-19 17:20
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('spid_cie_oidc_provider', '0006_oidcsession_acr'),
]
operations = [
migrations.AlterModelOptions(
name='issuedtoken',
options={'verbose_name': 'Issued Token', 'verbose_name_plural': 'Issued Tokens'},
),
migrations.AlterModelOptions(
name='oidcsession',
options={'ordering': ['-created'], 'verbose_name': 'User Session', 'verbose_name_plural': 'User Sessions'},
),
]
@@ -0,0 +1,20 @@
# Generated by Django 4.2.3 on 2023-12-13 14:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"spid_cie_oidc_provider",
"0007_alter_issuedtoken_options_alter_oidcsession_options",
),
]
operations = [
migrations.AlterField(
model_name="oidcsession",
name="authz_request",
field=models.JSONField(max_length=65536),
),
]
@@ -0,0 +1,107 @@
import hashlib
import logging
from django.contrib.auth import get_user_model
from django.contrib.sessions.models import Session
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext as _
from spid_cie_oidc.entity.abstract_models import TimeStampedModel
from spid_cie_oidc.provider.settings import OIDCFED_PROVIDER_SALT
logger = logging.getLogger(__name__)
class OidcSession(TimeStampedModel):
"""
Store UserSessionInfo, ClientSessionInfo and Grant
"""
user_uid = models.CharField(max_length=120)
user = models.ForeignKey(
get_user_model(), on_delete=models.SET_NULL, blank=True, null=True
)
client_id = models.URLField(blank=True, null=True)
sid = models.CharField(
max_length=1024, blank=True, null=True,
help_text=_("django session key")
)
nonce = models.CharField(max_length=2048, blank=False, null=False)
authz_request = models.JSONField(max_length=65536, blank=False, null=False)
revoked = models.BooleanField(default=False)
auth_code = models.CharField(max_length=2048, blank=False, null=False)
acr = models.CharField(max_length=1024, blank=False, null=False)
def set_sid(self, request):
try:
Session.objects.get(session_key=request.session.session_key)
self.sid = request.session.session_key
except Exception:
logger.warning(
f"Error setting SID for OidcSession {self} "
"due to multiple authentication with different users with "
"the same browser."
)
self.sid = self.auth_code
self.save()
def revoke(self, destroy_session=True):
session = Session.objects.filter(session_key=self.sid)
if session and destroy_session:
session.delete()
self.revoked = True
iss_tokens = IssuedToken.objects.filter(session=self)
iss_tokens.update(revoked=True)
self.save()
def pairwised_sub(self, provider_id:str):
return hashlib.sha256(
f"{self.user_uid}{self.client_id}{provider_id}{OIDCFED_PROVIDER_SALT}".encode()
).hexdigest()
def public_sub(self):
return hashlib.sha256(
f"{self.user_uid}{OIDCFED_PROVIDER_SALT}".encode()
).hexdigest()
def __str__(self):
return "{} {}".format(self.user_uid, self.auth_code)
class Meta:
verbose_name = "User Session"
verbose_name_plural = "User Sessions"
unique_together = ("client_id", "nonce")
ordering = ["-created"]
class IssuedToken(TimeStampedModel):
session = models.ForeignKey(OidcSession, on_delete=models.CASCADE)
access_token = models.TextField(blank=True, null=True)
id_token = models.TextField(blank=True, null=True)
refresh_token = models.TextField(blank=True, null=True)
expires = models.DateTimeField()
revoked = models.BooleanField(default=False)
class Meta:
verbose_name = "Issued Token"
verbose_name_plural = "Issued Tokens"
@property
def client_id(self):
return self.session.client_id
@property
def user_uid(self):
return self.session.user_uid
@property
def expired(self):
return timezone.localtime() >= self.expires
@property
def is_revoked(self):
return self.session.revoked or self.revoked
def __str__(self):
return "{} @ {}".format(self.session.user_uid, self.session.client_id)
@@ -0,0 +1,8 @@
import hashlib
def spidCode(user_info: dict, client_conf: dict, data: dict = None, kwargs: dict = {}):
return hashlib.sha512(
f"{user_info['username']}{kwargs.get('salt', '')}".encode()
).hexdigest()
@@ -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
@@ -0,0 +1,207 @@
from copy import deepcopy
from django.conf import settings
from django.utils.translation import gettext as _
from spid_cie_oidc.entity.schemas.op_metadata import (
OPMetadataCie,
OPMetadataSpid
)
from spid_cie_oidc.provider.schemas.authn_requests import (
AcrValues,
AuthenticationRequestCie,
AuthenticationRequestDoc,
AuthenticationRequestSpid
)
from spid_cie_oidc.provider.schemas.authn_response import AuthenticationErrorResponse, AuthenticationErrorResponseCie, \
AuthenticationResponse, AuthenticationResponseCie
from spid_cie_oidc.provider.schemas.introspection_request import IntrospectionRequest
from spid_cie_oidc.provider.schemas.introspection_response import IntrospectionErrorResponseCie, \
IntrospectionErrorResponseSpid, IntrospectionResponse
from spid_cie_oidc.provider.schemas.revocation_request import RevocationRequest
from spid_cie_oidc.provider.schemas.revocation_response import RevocationErrorResponse
from spid_cie_oidc.provider.schemas.token_requests import TokenAuthnCodeRequest, TokenRefreshRequest
from spid_cie_oidc.provider.schemas.token_response import TokenErrorResponse, TokenRefreshResponse, TokenResponse
OIDCFED_PROVIDER_PROFILES_MEDIA = getattr(
settings,
"OIDCFED_PROVIDER_PROFILES_MEDIA",
{
"spid": {
"logo": "svg/spid-logo-c-lb.svg",
"arc_position": ""
},
"cie": {
"logo": "svg/logo-cie.svg",
"arc_position": "mt-5"
},
},
)
OIDCFED_PROVIDER_ATTRIBUTES_SPID_MAP = {
"https://attributes.eid.gov.it/spid_code": (
{
"func": "spid_cie_oidc.provider.processors.spidCode",
"kwargs": {"salt": "signiicusenz"},
},
),
"given_name": ("given_name",),
"family_name": ("family_name",),
"place_of_birth": ("place_of_birth",),
"birthdate": ("birthdate",),
"gender": ("gender",),
"https://attributes.eid.gov.it/company_name": ("company_name",),
"https://attributes.eid.gov.it/registered_office": ("registered_office",),
"https://attributes.eid.gov.it/fiscal_number": ("fiscal_number", ),
"https://attributes.eid.gov.it/company_fiscal_number": ("company_fiscal_number",),
"https://attributes.eid.gov.it/vat_number": ("vat_number",),
"document_details": ("document_details",),
"phone_number": (
"phone_number",
"mobile_phone",
"phone",
"telephone",
),
"email": ("email",),
"https://attributes.eid.gov.it/e_delivery_service": ("e_delivery_service",),
"https://attributes.eid.gov.it/eid_exp_date": ("eid_exp_date",),
"address": ("address",),
}
OIDCFED_PROVIDER_ATTRIBUTES_CIE_MAP = {
"given_name": ("name", "given_name"),
"family_name": ("family_name", "surname"),
"email": ("email",),
"fiscal_number": ("fiscal_number", "tin"),
"email_verified": ("email",),
"gender": ("gender",),
"birthdate": ("date_of_birth",),
"phone_number": ("mobile_phone", "phone", "telephone"),
"phone_number_verified": ("mobile_phone", "phone", "telephone"),
"address": ("address",),
"place_of_birth": ("place_of_birth",),
# "document_details": ,
# "e_delivery_service": ,
"physical_phone_number": ("mobile_phone", "phone", "telephone"),
}
OIDCFED_PROVIDER_ATTRIBUTES_MAP = deepcopy(OIDCFED_PROVIDER_ATTRIBUTES_SPID_MAP)
OIDCFED_PROVIDER_ATTRIBUTES_MAP.update(OIDCFED_PROVIDER_ATTRIBUTES_CIE_MAP)
OIDCFED_PROVIDER_PROFILES_ID_TOKEN_CLAIMS = dict(
spid = dict(),
cie = OIDCFED_PROVIDER_ATTRIBUTES_CIE_MAP
)
OIDCFED_PROVIDER_PROFILES = getattr(
settings,
"OIDCFED_PROVIDER_PROFILES",
{
"spid": {
"authorization_request": AuthenticationRequestSpid,
"authorization_request_doc": AuthenticationRequestDoc,
"authorization_response": AuthenticationResponse,
"authorization_error_response": AuthenticationErrorResponse,
"op_metadata": OPMetadataSpid,
"authorization_code": TokenAuthnCodeRequest,
"authorization_code_response": TokenResponse,
"refresh_token": TokenRefreshRequest,
"refresh_token_response": TokenRefreshResponse,
"token_error_response": TokenErrorResponse,
"revocation_request": RevocationRequest,
"revocation_response": RevocationErrorResponse,
"introspection_request" : IntrospectionRequest,
"introspection_response" : IntrospectionResponse,
"introspection_error_response" : IntrospectionErrorResponseSpid,
},
"cie": {
"authorization_request": AuthenticationRequestCie,
"authorization_request_doc": AuthenticationRequestDoc,
"authorization_response": AuthenticationResponseCie,
"authorization_error_response": AuthenticationErrorResponseCie,
"op_metadata": OPMetadataCie,
"authorization_code": TokenAuthnCodeRequest,
"authorization_code_response": TokenResponse,
"refresh_token": TokenRefreshRequest,
"refresh_token_response": TokenRefreshResponse,
"token_error_response": TokenErrorResponse,
"revocation_request": RevocationRequest,
"revocation_response": RevocationErrorResponse,
"introspection_request" : IntrospectionRequest,
"introspection_response" : IntrospectionResponse,
"introspection_error_response" : IntrospectionErrorResponseCie,
},
},
)
OIDCFED_PROVIDER_SALT = getattr(settings, "OIDCFED_PROVIDER_SALT", "CHANGEME")
OIDCFED_DEFAULT_PROVIDER_PROFILE = getattr(settings, "OIDCFED_PROVIDER_PROFILE", "spid")
OIDCFED_PROVIDER_MAX_REFRESH = getattr(settings, "OIDCFED_PROVIDER_MAX_REFRESH", 1)
OIDCFED_PROVIDER_HISTORY_PER_PAGE = getattr(settings, "OIDCFED_PROVIDER_HISTORY_PER_PAGE", 50)
# lifetime of validity of an auth code
OIDCFED_PROVIDER_AUTH_CODE_MAX_AGE = getattr(
settings,
"OIDCFED_PROVIDER_AUTH_CODE_MAX_AGE",
10
)
OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME = getattr(
settings,
"OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME",
2
)
OIDCFED_PROVIDER_PROFILES_DEFAULT_ACR = getattr(
settings,
"OIDCFED_PROVIDER_PROFILES_DEFAULT_ACR",
dict(
spid = AcrValues.l2.value,
cie = AcrValues.l2.value
)
)
OIDCFED_PROVIDER_PROFILES_ACR_4_REFRESH = getattr(
settings,
"OIDCFED_PROVIDER_PROFILES_ACR_4_REFRESH",
dict(
spid = [AcrValues.l1.value],
cie = [AcrValues.l1.value]
)
)
# SPID and CIE attribute labels (CIE entries override SPID for shared keys)
_OIDCFED_ATTRNAME_I18N_SPID = {
"given_name": _("Name"),
"family_name": _("Family name"),
"place_of_birth": _("Place of birth"),
"birthdate": _("Date of birth"),
"gender": _("Gender"),
"https://attributes.eid.gov.it/company_name": _("Company Name"),
"https://attributes.eid.gov.it/registered_office": _("Registered Office"),
"https://attributes.eid.gov.it/fiscal_number": _("Tax payer id"),
"https://attributes.eid.gov.it/vat_number": _("Vat number"),
"document_details": _("Id card"),
"phone_number": _("Mobile phone"),
"email": _("Email"),
"address": _("Address"),
"https://attributes.eid.gov.it/eid_exp_date": _("Expiration date"),
"https://attributes.eid.gov.it/e_delivery_service": _("Digital address"),
}
_OIDCFED_ATTRNAME_I18N_CIE = {
"given_name": _("Name"),
"family_name": _("Family name"),
"email": _("Email"),
"fiscal_number": _("Tax payer id"),
"email_verified": _("Email verified"),
"gender": _("Gender"),
"birthdate": _("Date of birth"),
"phone_number": _("Phone number"),
"phone_number_verified": _("Verified phone"),
"address": _("Address"),
"place_of_birth": _("Place of birth"),
# "document_details": ,
# "e_delivery_service": ,
"physical_phone_number": _("Phone number"),
}
OIDCFED_ATTRNAME_I18N = {**_OIDCFED_ATTRNAME_I18N_SPID, **_OIDCFED_ATTRNAME_I18N_CIE}
@@ -0,0 +1,352 @@
@import url(https://fonts.googleapis.com/css?family=Titillium+Web:400,600);
.italia-it-button {
display: inline-block;
position: relative;
padding: 0;
color: #FFF;
font-family: "Titillium Web", HelveticaNeue, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif;
font-weight: 600;
line-height: 1em;
text-decoration: none;
border: 0;
text-align: center;
cursor: pointer;
border-radius: 15px;
}
.italia-it-button-icon,
.italia-it-button-text {
display: block;
float: left
}
.italia-it-button-icon {
margin: 0 -.4em 0 0;
padding: 0.6em .8em .5em;
border-right: rgba(255, 255, 255, 0.1) 0.1em solid
}
.italia-it-button-text {
padding: .95em 1em .85em 1em;
font-size: 1.15em;
text-align: center
}
svg {
width: 1.8em;
height: 1.8em;
fill: #fff
}
.italia-it-block {
display: block
}
.italia-it-button-size-s {
font-size: 10px;
width: 150px;
border-radius: 5px;
}
.italia-it-button-size-s > span img {
width: 19px;
height: 19px;
border: 0
}
.italia-it-button-size-m {
font-size: 15px;
width: 220px;
border-radius: 0px;
}
.italia-it-button-size-m > span img {
width: 29px;
height: 29px;
border: 0
}
.italia-it-button-size-l {
font-size: 20px;
width: 280px
}
.italia-it-button-size-l > span img {
width: 38px;
height: 38px;
border: 0
}
.italia-it-button-size-xl {
font-size: 24px;
width: 340px;
}
.italia-it-button-size-xl > span img {
width: 47px;
height: 47px;
border: 0
}
.button-spid {
background-color: #06C;
color: #FFF
}
.button-spid svg {
fill: #FFF
}
.button-spid:hover {
background-color: #036;
color: #FFF
}
.button-spid:active {
background-color: #83BEED;
color: #036
}
.button-cie {
background-color: #4c5c6c;
color: #FFF
}
.button-cie svg {
fill: #FFF
}
.button-cie:hover {
background-color: #414c58;
color: #FFF
}
.button-cie:active {
background-color: #394046;
color: #036
}
.spid-idp-button {
position: absolute;
z-index: 1039;
display: none
}
.spid-idp-button .spid-idp-button-menu,
.spid-idp-button .spid-idp-button-panel {
list-style: none;
background: white;
border: solid 1px #ddd;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
overflow: visible;
padding: 0;
margin: 0
}
#spid-idp-button-small-get,
#spid-idp-button-medium-get,
#spid-idp-button-small-post,
#spid-idp-button-medium-post {
width: 230px
}
#spid-idp-button-large-get,
#spid-idp-button-large-post {
width: 270px
}
#spid-idp-button-xlarge-get,
#spid-idp-button-xlarge-post {
width: 330px
}
.spid-idp-button .spid-idp-button-panel {
padding: 10px
}
.spid-idp-button.spid-idp-button-tip {
margin-top: 8px
}
.spid-idp-button.spid-idp-button-tip:before {
position: absolute;
top: -6px;
left: 9px;
content: "";
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #ddd;
display: inline-block
}
.spid-idp-button.spid-idp-button-tip:after {
position: absolute;
top: -5px;
left: 10px;
content: "";
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid white;
display: inline-block
}
.spid-idp-button.spid-idp-button-tip.spid-idp-button-anchor-right:before {
left: auto;
right: 9px
}
.spid-idp-button.spid-idp-button-tip.spid-idp-button-anchor-right:after {
left: auto;
right: 10px
}
.spid-idp-button.spid-idp-button-scroll .spid-idp-button-menu,
.spid-idp-button.spid-idp-button-scroll .spid-idp-button-panel {
max-height: 180px;
overflow: auto
}
.spid-idp-button .spid-idp-button-menu li {
list-style: none;
padding: 0 0;
margin: 0;
line-height: 18px
}
.spid-idp-button .spid-idp-button-menu li > a,
.spid-idp-button .spid-idp-button-menu label {
display: block;
font-family: "Titillium Web", HelveticaNeue, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif;
font-weight: 600;
font-size: .9em;
color: #3b61b6;
text-decoration: underline;
line-height: 18px;
padding-top: 5px;
white-space: nowrap;
border-bottom: 1px solid #ddd;
}
.spid-idp-button .spid-idp-button-menu li > a:hover,
.spid-idp-button .spid-idp-button-menu label:hover {
color: #036;
cursor: pointer;
background-color: #F0F0F0
}
.spid-idp-button .spid-idp-button-menu li > a img {
height: 35px;
padding-bottom: 5px;
border: 0;
width: 100%;
}
.idp-button-idp-logo {
font-size: 100%;
height: 10%;
width: 100%;
border: 0;
border-bottom: 1px solid #CCC;
background-color: #FFF;
padding: 15px;
text-align: left;
cursor: pointer
}
.idp-button-idp-logo:hover {
background-color: #F0F0F0
}
.idp-button-idp-logo img {
height: 25px;
vertical-align: middle;
cursor: pointer;
}
.spid-top-menu-space-10 > a img {
margin-top: 10px
}
.spid-top-menu-space-20 > a img {
margin-top: 20px
}
.spid-top-menu-line {
border-top: 5px solid #000
}
.spid-idp-support-link > a {
padding: 5px 0 10px 10px
}
.spid-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0
}
.button-login {
border-radius: 30px !important;
background-color: #06C;
color: #FFF
}
.button-login svg {
fill: #FFF
}
.button-login:hover {
background-color: #036;
color: #FFF
}
.button-login:active {
background-color: #83BEED;
color: #036
}
.italia-it-button-cancel-m {
font-size: 15px;
width: 220px;
border-radius: 0px;
}
.italia-it-button-cancel-s {
font-size: 15px;
width: 150px;
border-radius: 0px;
}
.button-cancel {
border-radius: 30px !important;
border: 1px solid #0066cc !important;
background-color: #FFF;
color: #3d4955;
}
.button-cancel:hover {
background-color: #0066cc;
color: #FFF
}
.button-cancel:active {
background-color: #83BEED;
color: #036
}
.italia-it-button-icon,
.italia-it-buttons-text {
display: block;
}
.italia-it-buttons-text {
padding: .95em 1em .85em 1em;
font-size: 1.15em;
text-align: center
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

@@ -0,0 +1 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 587.6 587.6"><style>.st0{fill:#FFF}.st1{fill:#4c5c6c}</style><path id="XMLID_3_" class="st0" d="M587.6 293.8c0 162.3-131.5 293.8-293.8 293.8C131.6 587.6 0 456.1 0 293.8S131.6 0 293.8 0c162.3 0 293.8 131.5 293.8 293.8"/><path id="XMLID_2_" class="st1" d="M294.6 319c-24.4 0-44.5-8.2-60.3-24.8-15.8-16.5-23.7-37-23.7-61.4 0-24.5 7.9-44.8 23.6-61 15.7-16.2 35.7-24.3 60.2-24.3 24.4 0 44.3 8.2 59.6 24.9 15.3 16.6 23 37 23 61.5 0 24.3-7.7 44.6-23 60.8-15.3 16.1-35 24.3-59.4 24.3"/><path id="XMLID_1_" class="st1" d="M210.6 439.1c0-24.5 7.9-44.8 23.5-61 15.7-16.2 35.7-24.3 60.4-24.3 24.4 0 44.3 8.2 59.5 24.9 15.3 16.7 23 37.1 23 61.5"/></svg>

After

Width:  |  Height:  |  Size: 706 B

@@ -0,0 +1,39 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#006ac6" stroke="none">
<path d="M3800 4641 c-88 -30 -1828 -633 -1840 -638 -8 -3 18 -10 58 -15 108
-13 284 -60 404 -108 361 -144 672 -425 852 -770 409 -784 123 -1733 -659
-2186 l-49 -28 29 -7 c1004 -221 1300 -281 1373 -281 113 1 205 74 239 191 20
70 13 200 -17 291 -39 116 -19 182 87 293 44 45 63 72 59 83 -3 9 -6 22 -6 29
0 7 -15 36 -34 63 l-34 50 24 6 c38 10 90 62 102 104 14 45 7 74 -38 160 -26
51 -31 71 -27 104 7 54 59 106 151 154 141 72 232 158 262 246 15 46 15 52 -1
96 -36 97 -116 212 -315 454 -258 313 -255 309 -279 373 -43 115 -48 258 -14
444 8 47 15 137 15 200 0 137 -11 173 -130 465 -81 198 -96 221 -157 230 -16
3 -41 1 -55 -3z"/>
<path d="M1660 3675 l0 -105 100 0 100 0 0 105 0 105 -100 0 -100 0 0 -105z"/>
<path d="M1013 3760 c-130 -24 -217 -79 -266 -168 -110 -202 -112 -735 -4
-949 34 -65 101 -125 173 -154 54 -22 73 -24 239 -24 107 0 214 6 263 14 l83
14 -2 84 -2 85 -41 -7 c-88 -14 -300 -23 -357 -15 -173 25 -222 127 -223 465
-1 432 74 512 446 476 64 -6 131 -14 147 -17 l31 -6 0 81 c0 56 -4 83 -12 86
-82 29 -383 51 -475 35z"/>
<path d="M2274 3400 c-70 -23 -141 -80 -179 -145 -14 -23 -35 -80 -46 -125
-30 -117 -24 -350 11 -453 29 -86 89 -155 162 -188 49 -22 66 -24 218 -24 149
0 312 16 336 33 5 4 8 37 6 73 l-4 67 -188 -6 c-210 -6 -261 2 -304 47 -33 35
-56 91 -56 141 l0 40 289 0 288 0 6 88 c17 240 -65 401 -230 452 -87 27 -224
26 -309 0z m256 -162 c55 -29 81 -76 88 -163 l5 -65 -201 0 -200 0 5 43 c13
110 51 174 115 194 59 19 140 15 188 -9z"/>
<path d="M1660 2940 l0 -460 100 0 100 0 0 460 0 460 -100 0 -100 0 0 -460z"/>
<path d="M1460 1685 l0 -615 100 0 100 0 0 615 0 615 -100 0 -100 0 0 -615z"/>
<path d="M1860 1685 l0 -617 278 5 c208 3 290 8 330 20 223 65 305 229 306
607 0 157 -3 198 -22 272 -42 172 -105 251 -238 300 -55 20 -81 22 -356 26
l-298 4 0 -617z m551 430 c59 -21 105 -68 129 -132 29 -79 43 -255 31 -394
-11 -134 -38 -214 -88 -270 -55 -60 -103 -74 -273 -74 l-145 0 -3 443 -2 442
154 0 c113 0 165 -4 197 -15z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg"
height="300px" width="300px"
version="1.1"
viewBox="-300 -300 600 600"
font-family="Bitstream Vera Sans,Liberation Sans, Arial, sans-serif"
font-size="72"
text-anchor="middle" >
<circle stroke="#AAA" stroke-width="10" r="280" fill="#FFF"/>
<text style="fill:#444;">
<tspan x="0" y="-8">NO IMAGE</tspan><tspan x="0" y="80">AVAILABLE</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 459 B

@@ -0,0 +1 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 587.6 587.6"><style>.st0{fill:#FFF}.st1{fill:#06C}</style><path id="XMLID_3_" class="st0" d="M587.6 293.8c0 162.3-131.5 293.8-293.8 293.8C131.6 587.6 0 456.1 0 293.8S131.6 0 293.8 0c162.3 0 293.8 131.5 293.8 293.8"/><path id="XMLID_2_" class="st1" d="M294.6 319c-24.4 0-44.5-8.2-60.3-24.8-15.8-16.5-23.7-37-23.7-61.4 0-24.5 7.9-44.8 23.6-61 15.7-16.2 35.7-24.3 60.2-24.3 24.4 0 44.3 8.2 59.6 24.9 15.3 16.6 23 37 23 61.5 0 24.3-7.7 44.6-23 60.8-15.3 16.1-35 24.3-59.4 24.3"/><path id="XMLID_1_" class="st1" d="M210.6 439.1c0-24.5 7.9-44.8 23.5-61 15.7-16.2 35.7-24.3 60.4-24.3 24.4 0 44.3 8.2 59.5 24.9 15.3 16.7 23 37.1 23 61.5"/></svg>

After

Width:  |  Height:  |  Size: 703 B

@@ -0,0 +1 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 143 69.9"><style>.st0{fill:#06C}</style><path id="XMLID_85_" class="st0" d="M29.8 25c-6.3-.8-10.8-1.2-13.3-1.2s-4.2.2-4.9.7c-.7.5-1.1 1.2-1.1 2.2 0 1 .5 1.7 1.5 2.1 1 .4 3.6.9 7.7 1.7 4.1.7 7.1 2 8.8 3.6 1.7 1.7 2.6 4.4 2.6 8.2 0 8.3-5.1 12.4-15.4 12.4-3.4 0-7.5-.5-12.3-1.4l-2.5-.5.3-8.6c6.3.8 10.8 1.2 13.2 1.2 2.5 0 4.2-.2 5.1-.7.9-.5 1.3-1.2 1.3-2.2 0-1-.5-1.7-1.4-2.1-.9-.5-3.4-1-7.4-1.7s-6.9-1.8-8.9-3.4C1.2 33.7.2 30.9.2 26.8s1.4-7.1 4.1-9.2c2.8-2.1 6.3-3.1 10.7-3.1 3 0 7.1.5 12.4 1.5l2.5.5-.1 8.5z"/><path id="XMLID_82_" class="st0" d="M55.4 23.7c-1.9 0-3.8.4-5.8 1.2l-.9.4v20.1c2.3.3 4.2.5 5.6.5 3 0 5-.9 6.1-2.6 1.1-1.7 1.7-4.7 1.7-8.9 0-7.2-2.2-10.7-6.7-10.7M38.5 69.9V15.4h10.2v2.1c3.3-2 6.2-3 8.8-3 5.2 0 9 1.6 11.4 4.6 2.5 3.1 3.7 8.4 3.7 15.8s-1.4 12.6-4.1 15.5c-2.7 2.9-7.2 4.3-13.4 4.3-1.7 0-3.5-.2-5.5-.5l-.9-.2v15.8H38.5z"/><path id="XMLID_79_" class="st0" d="M131.6 44.5l1.1-.2V24.1c-2.8-.5-5.3-.8-7.5-.8-4.1 0-6.2 3.7-6.2 11.1 0 4 .5 6.8 1.4 8.5.9 1.7 2.4 2.5 4.6 2.5s4.3-.2 6.6-.9M143 0v53.8h-10.2v-1.6c-3.6 1.7-6.7 2.5-9.3 2.5-5.6 0-9.5-1.6-11.7-4.8-2.2-3.2-3.3-8.3-3.3-15.3s1.3-12.1 4-15.3c2.6-3.2 6.6-4.8 11.9-4.8 1.6 0 3.9.3 6.8.8l1.5.3V0H143z"/><path id="XMLID_78_" class="st0" d="M90.5 37.7c-3.2 0-5.8-1.1-7.8-3.2s-3.1-4.8-3.1-8 1-5.8 3.1-7.9c2-2.1 4.6-3.1 7.8-3.1s5.8 1.1 7.7 3.2c2 2.2 3 4.8 3 8s-1 5.8-3 7.9c-2 2-4.5 3.1-7.7 3.1"/><path id="XMLID_77_" class="st0" d="M79.6 53.2c0-3.2 1-5.8 3.1-7.9 2-2.1 4.6-3.1 7.8-3.1s5.8 1.1 7.7 3.2c2 2.2 3 4.8 3 8"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="Layer_1"
viewBox="0 0 143 69.9"
version="1.1"
sodipodi:docname="spid-logo-c-lb.svg-LTS.svg"
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs12" />
<sodipodi:namedview
id="namedview10"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="3.3762517"
inkscape:cx="65.753392"
inkscape:cy="39.244704"
inkscape:window-width="1440"
inkscape:window-height="847"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" />
<style
id="style2">.st0{fill:#06C}</style>
<path
id="XMLID_85_"
class="st0"
d="M29.8 25c-6.3-.8-10.8-1.2-13.3-1.2s-4.2.2-4.9.7c-.7.5-1.1 1.2-1.1 2.2 0 1 .5 1.7 1.5 2.1 1 .4 3.6.9 7.7 1.7 4.1.7 7.1 2 8.8 3.6 1.7 1.7 2.6 4.4 2.6 8.2 0 8.3-5.1 12.4-15.4 12.4-3.4 0-7.5-.5-12.3-1.4l-2.5-.5.3-8.6c6.3.8 10.8 1.2 13.2 1.2 2.5 0 4.2-.2 5.1-.7.9-.5 1.3-1.2 1.3-2.2 0-1-.5-1.7-1.4-2.1-.9-.5-3.4-1-7.4-1.7s-6.9-1.8-8.9-3.4C1.2 33.7.2 30.9.2 26.8s1.4-7.1 4.1-9.2c2.8-2.1 6.3-3.1 10.7-3.1 3 0 7.1.5 12.4 1.5l2.5.5-.1 8.5z" />
<path
id="XMLID_82_"
class="st0"
d="M55.4 23.7c-1.9 0-3.8.4-5.8 1.2l-.9.4v20.1c2.3.3 4.2.5 5.6.5 3 0 5-.9 6.1-2.6 1.1-1.7 1.7-4.7 1.7-8.9 0-7.2-2.2-10.7-6.7-10.7M38.5 69.9V15.4h10.2v2.1c3.3-2 6.2-3 8.8-3 5.2 0 9 1.6 11.4 4.6 2.5 3.1 3.7 8.4 3.7 15.8s-1.4 12.6-4.1 15.5c-2.7 2.9-7.2 4.3-13.4 4.3-1.7 0-3.5-.2-5.5-.5l-.9-.2v15.8H38.5z" />
<path
id="XMLID_79_"
class="st0"
d="M131.6 44.5l1.1-.2V24.1c-2.8-.5-5.3-.8-7.5-.8-4.1 0-6.2 3.7-6.2 11.1 0 4 .5 6.8 1.4 8.5.9 1.7 2.4 2.5 4.6 2.5s4.3-.2 6.6-.9M143 0v53.8h-10.2v-1.6c-3.6 1.7-6.7 2.5-9.3 2.5-5.6 0-9.5-1.6-11.7-4.8-2.2-3.2-3.3-8.3-3.3-15.3s1.3-12.1 4-15.3c2.6-3.2 6.6-4.8 11.9-4.8 1.6 0 3.9.3 6.8.8l1.5.3V0H143z" />
<path
id="XMLID_78_"
class="st0"
d="M90.5 37.7c-3.2 0-5.8-1.1-7.8-3.2s-3.1-4.8-3.1-8 1-5.8 3.1-7.9c2-2.1 4.6-3.1 7.8-3.1s5.8 1.1 7.7 3.2c2 2.2 3 4.8 3 8s-1 5.8-3 7.9c-2 2-4.5 3.1-7.7 3.1" />
<path
id="XMLID_77_"
class="st0"
d="M79.6 53.2c0-3.2 1-5.8 3.1-7.9 2-2.1 4.6-3.1 7.8-3.1s5.8 1.1 7.7 3.2c2 2.2 3 4.8 3 8" />
<text
xml:space="preserve"
style="fill:#ff0000"
x="5.6275425"
y="66.938133"
id="text172"><tspan
sodipodi:role="line"
id="tspan170"
x="5.6275425"
y="66.938133" /></text>
<text
xml:space="preserve"
style="fill:#ff0000"
x="109.88517"
y="97.149155"
id="text280"><tspan
sodipodi:role="line"
id="tspan278"
x="109.88517"
y="97.149155" /></text>
<text
xml:space="preserve"
style="fill:#ff0000"
x="36.727119"
y="89.152122"
id="text284"><tspan
sodipodi:role="line"
id="tspan282"
x="36.727119"
y="89.152122" /></text>
<text
xml:space="preserve"
style="fill:#ff0000"
x="33.46907"
y="85.597885"
id="text288"><tspan
sodipodi:role="line"
id="tspan290"
x="33.46907"
y="85.597885" /></text>
<text
xml:space="preserve"
style="font-size:26.8362px;fill:#ff0000;stroke-width:2.23637"
x="-0.47511318"
y="21.411407"
id="text285"
transform="scale(1.2833473,0.77921228)"><tspan
sodipodi:role="line"
id="tspan291"
style="font-weight:bold;stroke-width:2.23637"
x="-0.47511318"
y="21.411407"> LONG</tspan><tspan
sodipodi:role="line"
id="tspan293"
style="font-weight:bold;stroke-width:2.23637"
x="-0.47511318"
y="54.956661"> TERM</tspan><tspan
sodipodi:role="line"
id="tspan295"
style="font-weight:bold;stroke-width:2.23637"
x="-0.47511318"
y="88.501915">SESSION</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,91 @@
<!-- Extends default Bootstrap Unical Italia template -->
{% extends 'bootstrap-italia-base.html' %}
{% load static %}
{% load i18n %}
{% block extra_head %}
{{ block.super }}
<link type="text/css" rel="stylesheet" href="/static/css/provider.css">
{% endblock extra_head %}
{% block page_title %}
{% trans "OIDC Provider" %}
{% endblock page_title %}
<!-- URL link top left -->
{% block header_slim_org_url %}
https://www.spid.gov.it/
{% endblock header_slim_org_url %}
<!-- Name top left -->
{% block header_slim_org_name %}
{% trans "OIDC Provider" %}
{% endblock header_slim_org_name %}
<!-- Mobile slim_org_name -->
{% block header_slim_mobile_org_name %}
{% trans "OIDC Provider" %}
{% endblock header_slim_mobile_org_name %}
<!-- Make empty areas -->
{% block header_mobile_arrow %}{% endblock header_mobile_arrow %}
{% block header_mobile_slim_menu %}{% endblock header_mobile_slim_menu %}
<!-- Logo in Header -->
{% block header_center_logo %}
{% endblock header_center_logo %}
<!-- Organization name in Header -->
{% block header_center_org_name %}
<a href="#">{% trans "OIDC Provider" %}</a>
{% endblock header_center_org_name %}
<!-- Organization description in Header -->
{% block header_center_org_subname %}
{% endblock header_center_org_subname %}
{% block container %}
<div>
<div class="container pt-2 p-4">
<div class="row d-lg-flex">
<div class="col-12">
<div class="card-wrapper card-space">
<div class="card card-bg no-after">
<div class="card-body">
<div class="row h-100 px-4 pb-4">
{% block centered_container %}{% endblock %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock container %}
<!-- Logo in Footer -->
{% block footer_logo %}
{% endblock footer_logo %}
<!-- Organization name in Footer -->
{% block footer_org_name %}
{% trans "OIDC Provider" %}
{% endblock footer_org_name %}
<!-- Organization name in Footer -->
{% block footer_org_subname %}
{% trans "OIDC Provider" %}
{% endblock footer_org_subname %}
{% block extra_scripts %}
{% block bootstrap_italia_validation %}
{% endblock bootstrap_italia_validation %}
{% endblock extra_scripts %}
{% block footer_menu_section %}
{% endblock footer_menu_section %}
@@ -0,0 +1,109 @@
{% extends 'op_base.html' %}
{% load i18n %}
{% load static %}
{% load spid_cie_op %}
{% block menu_links %}
<li class="nav-item ">
<a class="nav-link {% if request.resolver_match.view_name == 'oidc_provider_consent' %}active{% endif %}"
href="{% url 'oidc_provider_consent' %}">
<span>{% trans 'Home' %}</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.view_name == 'oidc_provider_access_history' %}active{% endif %}"
href="{% url 'oidc_provider_access_history' %}">
<span>{% trans 'Access History' %}</span>
</a>
</li>
{% endblock menu_links %}
{% block centered_container %}
<div class="col-12">
<div class="row row-header border-bottom no-border-bottom-xs">
<div class="wrap-container">
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-6">
<a href="/" title="torna alla home">
<img src="{% oidc_provider_logo %}" alt="Home" width="120">
</a>
</div>
<div class="clear pt-2"></div>
</div>
</div>
<div class="row mt-5 ml-1">
<div>
<div class="">
<h4 class="text-muted">{% trans 'SPID level access request from' %}</h4>
<h3>{{ client_organization_name }}</h3>
</div>
</div>
</div>
<div class="row mt-5 ml-1">
<div>
<p class="font-weight-semibold">
{% trans 'For the proper functioning of the requestor service you must give your consent to the release of the following data '%}
</p>
</div>
</div>
<form method="POST" action="{% url 'oidc_provider_consent' %}">
{% csrf_token %}
<fieldset>
<div class="row">
{% for form_field in form.visible_fields %}
<div class="form-row">
<div class="col-12 form-group my-3">
<label for="id_{{ form_field.html_name }}">
<span class="form-label-content">
{{ form_field.label }}
</span>
</label>
{% if form_field.field.widget.input_type%}
<input type="{{ form_field.field.widget.input_type }}"
class="form-control" id="id_{{ form_field.html_name }}"
name="{{ form_field.name }}" value="{{form_field.value|default_if_none:""}}">
{% else%}
<textarea id="id_{{ form_field.html_name }}"
name="{{ form_field.name }}">{{ form.public_jwks.value}}</textarea>
{% endif %}
</div>
<span for="id_{{ form_field.html_name }}" class="text-danger">{{ form_field.errors }}</span>
</div>
{% endfor %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
<div>
<ul>
{% for claim in user_claims %}
<li>{{ claim }}</li>
{% endfor %}
</ul>
<div class="col-sm-12 pt-4">
<a href="{% url 'oidc_provider_not_consent' %}?redirect_uri={{ redirect_uri }}&state={{state}}"
class="italia-it-button italia-it-button-cancel-m button-cancel mb-4 mr-4" id="do_not_agree">
<span class="italia-it-buttons-text">{% trans "DO NOT AGREE" %}</span>
</a>
<button id="agree" class="italia-it-button italia-it-button-size-m button-login" type="submit">
<span class="italia-it-buttons-text">{% trans "AGREE" %}</span></button>
</div>
</div>
</div>
</div>
</fieldset>
</form>
</div>
<script src="{% static 'js/spid-sp-access-button.js' %}"></script>
<script src="{% static 'js/spid_button.js' %}"></script>
{% endblock %}
@@ -0,0 +1,137 @@
{% extends 'op_base.html' %}
{% load i18n %}
{% load static %}
{% load spid_cie_op %}
{% block menu_links %}
<li class="nav-item ">
{% if user.is_staff %}
<a class="nav-link {% if request.resolver_match.view_name == 'oidc_provider_staff_testing' %}active{% endif %}"
href="{% url 'oidc_provider_staff_testing' %}">
<span>{% trans 'Home' %}</span>
</a>
{% else %}
<a class="nav-link {% if request.resolver_match.view_name == 'oidc_provider_consent' %}active{% endif %}"
href="{% url 'oidc_provider_consent' %}">
<span>{% trans 'Home' %}</span>
</a>
{% endif %}
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.view_name == 'oidc_provider_access_history' %}active{% endif %}"
href="{% url 'oidc_provider_access_history' %}">
<span>{% trans 'Access History' %}</span>
</a>
</li>
{% endblock menu_links %}
{% block centered_container %}
<div class="col-12">
<div class="row row-header border-bottom no-border-bottom-xs">
<div class="wrap-container">
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-6">
<a href="/" title="torna alla home">
<img src="{% oidc_provider_logo %}" alt="Home" width="120">
</a>
</div>
<div class="clear pt-2"></div>
</div>
</div>
<div class="row mt-4 ml-1">
<h4 class="text-left">
{% blocktrans %}
Access history of {{user}}
{% endblocktrans%}
</h4>
</div>
<div class="row mt-2 ml-1">
<div>
<p class="">
{% blocktrans%}
Accesses logs and status are listed below,
you can revoke an access to a Relying Party by clicking on "revoke" button
{% endblocktrans %}
</p>
</div>
</div>
{% csrf_token %}
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">{% trans "Relying party" %}</th>
<th scope="col">{% trans "Sid" %}</th>
<th scope="col">{% trans "Created" %}</th>
<th scope="col">{% trans "Revoked" %}</th>
</tr>
</thead>
<tbody>
{% for session in history %}
<tr>
<th scope="row">{{ forloop.counter }}</th>
<td>{{ session.client_id }}</td>
<td>{{ session.sid|truncatechars:33 }}</td>
<td>{{ session.created | date:'Y-m-d H:i'}}</td>
{% if session.revoked %}
<td class="text-success">
{{ session.revoked }}
</td>
{% else %}
<td>
<a href="{% url 'oidc_provider_revoke_session' %}?auth_code={{session.auth_code}}"
class="btn btn-secondary" role="button">{% trans "Revoke" %}</a>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<nav class="pagination-wrapper justify-content-center">
<ul class="pagination">
{% if history.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{history.previous_page_number}}" tabindex="-1" aria-hidden="true">
<svg class="icon icon-primary">
<use href="{% static 'svg/sprite.svg' %}#it-chevron-left"></use>
</svg>
<span class="sr-only">{% trans "previous page" %}</span>
</a>
</li>
{% endif %}
{% for i in history.paginator.page_range%}
{% if i == history.number %}
<li class="page-item active">
<a class="page-link" href="?page={{ i }}" aria-current="page">
<span class="d-inline-block d-sm-none">{% trans "page" %}</span>{{ i }}
</a>
</li>
{% else %}
<li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if history.has_next%}
<li class="page-item">
<a class="page-link" href="?page={{history.next_page_number}}">
<span class="sr-only">{% trans "next page" %}</span>
<svg class="icon icon-primary">
<use href="{% static 'svg/sprite.svg' %}#it-chevron-right"></use>
</svg>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endblock %}
@@ -0,0 +1,118 @@
{% extends 'op_base.html' %}
{% load i18n %}
{% load static %}
{% load spid_cie_op %}
{% block menu_links %}{% endblock menu_links %}
{% block centered_container %}
<div class="col-12">
<div class="row row-header border-bottom no-border-bottom-xs">
<div class="wrap-container">
<div class="row">
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-6">
<a href="/" title="torna alla home">
<img src="{% oidc_provider_logo %}" alt="Home" width="120">
</a>
</div>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-6 {% oidc_provider_arc_value_position %}">
<div class="mt-2 ml-2">
<label class="pl-1 pr-1 ml-4 mt-2 font-weight-bold
{% if acr_value == 'l3'%}complementary-1-bg-a6 text-white
{% elif acr_value == 'l2'%}complementary-2-bg-b1 text-dark
{% else %}complementary-3-bg-b3 text-white{% endif%}">
{{ acr_value | capfirst }}</label>
</div>
</div>
</div>
<div class="clear pt-2"></div>
</div>
</div>
<div class="col-12">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row mt-4 ml-1">
<div>
<div class="pb-2">
<h4 class="text-muted">{% trans 'Authentication request from' %}
</h4>
<h3>{{ client_organization_name }}</h3>
</div>
</div>
</div>
</div>
<div class="col-12 row mt-4" >
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
<form method="POST" action='{% url "oidc_provider_authnrequest" %}'>
<fieldset>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 ">
<div>
{% csrf_token %}
{% for form_field in form.visible_fields %}
<div class="form-row pr-4">
<div class="col-12 form-group my-3">
<label for="id_{{ form_field.html_name }}" id="label_{{form_field.html_name}}">
<span class="form-label-content">
{{ form_field.label }}
</span>
</label>
{% if form_field.field.widget.input_type%}
<input
type="{{ form_field.field.widget.input_type }}"
class="form-control"
id="id_{{ form_field.html_name }}"
name="{{ form_field.name }}"
value="{{form_field.value|default_if_none:''}}">
{% endif %}
</div>
<span for="id_{{ form_field.html_name }}"
class="text-danger">{{ form_field.errors }}</span>
</div>
{% endfor %}
{% for hidden in hidden_form.hidden_fields %}
{{ hidden }}
{% endfor %}
<div class="col-sm-12 mt-2">
<a href="{% url 'oidc_provider_not_consent' %}?redirect_uri={{ redirect_uri }}&state={{state}}"
class="italia-it-button italia-it-button-cancel-m button-cancel mb-4 mr-4">
<span class="italia-it-buttons-text">{% trans "Cancel" %}</span>
</a>
<button id="submit" class="italia-it-button italia-it-button-size-m button-login" type="submit">
<span class="italia-it-button-icon">
<img src="{% static 'svg/spid-ico-circle-bb.svg' %}"
alt="">
</span>
<span class="italia-it-buttons-text">{% trans "Login" %}</span>
</button>
</div>
</div>
</div>
</div>
</fieldset>
</form>
</div>
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
<div class="mt-2 ml-4">
<div class="row">
<div class="alert alert-info mt-2" role="alert">
{% blocktrans %}
If you login as a <b>staff user</b> you will be redirected to the test page.
Otherwise there will be the traditional consent page.
{% endblocktrans %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="{% static 'js/spid-sp-access-button.js' %}"></script>
<script src="{% static 'js/spid_button.js' %}"></script>
{% endblock %}
@@ -0,0 +1,24 @@
from django import template
from django.conf import settings
from spid_cie_oidc.provider.settings import (
OIDCFED_PROVIDER_PROFILES_MEDIA,
OIDCFED_DEFAULT_PROVIDER_PROFILE
)
register = template.Library()
@register.simple_tag
def oidc_provider_logo():
return f"""{settings.STATIC_URL}{OIDCFED_PROVIDER_PROFILES_MEDIA.get(
OIDCFED_DEFAULT_PROVIDER_PROFILE, {}
).get('logo', "")}"""
@register.simple_tag
def oidc_provider_arc_value_position():
return f"""{OIDCFED_PROVIDER_PROFILES_MEDIA.get(
OIDCFED_DEFAULT_PROVIDER_PROFILE, {}
).get('arc_position', "")}"""
@@ -0,0 +1,138 @@
from copy import deepcopy
from spid_cie_oidc.entity.utils import exp_from_now, iat_now
# TODO: we need factory function to get fresh now
IAT = iat_now()
EXP = exp_from_now()
CLAIMS_SPID = {
"userinfo": {
"given_name": {"values": ["str", "str"]},
"family_name": None,
"birthdate": {"value": "str"},
},
}
CLAIMS_CIE = {
"userinfo": {
"given_name": {"values": ["str", "str"]},
"family_name": None,
"birthdate": {"value": "str"},
},
"id_token": {
"given_name": {"values": ["str", "str"]},
"family_name": None,
"birthdate": {"value": "str"},
},
}
AUTHN_REQUEST_SPID = {
"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": CLAIMS_SPID,
"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": IAT,
"exp": EXP,
"jti": "a72d5df0-2415-4c7c-a44f-3988b354040b",
}
AUTHN_REQUEST_SPID_NO_CLIENT_ID = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CLIENT_ID.pop("client_id")
AUTHN_REQUEST_SPID_NO_CORRECT_CLAIMS = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CORRECT_CLAIMS["claims"]["userinfo"][
"given_name"
]["values"] = ["str"]
AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE.pop("code_challenge")
AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE_METHOD = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE_METHOD.pop("code_challenge_method")
AUTHN_REQUEST_SPID_NO_CORRECT_CODE_CHALLENGE_METHOD = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CORRECT_CODE_CHALLENGE_METHOD["code_challenge_method"] = "S257"
AUTHN_REQUEST_SPID_NO_NONCE = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_NONCE.pop("nonce")
AUTHN_REQUEST_SPID_NO_CORRECT_NONCE = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CORRECT_NONCE["nonce"] = "toosmall"
AUTHN_REQUEST_SPID_NO_PROMPT = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_PROMPT.pop("prompt")
AUTHN_REQUEST_SPID_NO_CORRECT_PROMPT = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CORRECT_PROMPT["prompt"] = "no_correct"
AUTHN_REQUEST_SPID_NO_REDIRECT_URL = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_REDIRECT_URL.pop("redirect_uri")
AUTHN_REQUEST_SPID_NO_CORRECT_REDIRECT_URL = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CORRECT_REDIRECT_URL["redirect_uri"] = "https://rp"
AUTHN_REQUEST_SPID_NO_RESPONSE_TYPE = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_RESPONSE_TYPE.pop("response_type")
AUTHN_REQUEST_SPID_NO_CORRECT_RESPONSE_TYPE = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CORRECT_RESPONSE_TYPE["response_type"] = "codecode"
AUTHN_REQUEST_SPID_NO_SCOPE = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_SCOPE.pop("scope")
AUTHN_REQUEST_SPID_NO_CORRECT_SCOPE = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CORRECT_SCOPE["scope"] = ["offline_access"]
AUTHN_REQUEST_SPID_NO_ACR_VALUES = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_ACR_VALUES.pop("acr_values")
AUTHN_REQUEST_SPID_NO_CORRECT_ACR_VALUES = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CORRECT_ACR_VALUES["acr_values"] = [
"https://www.spid.gov.it/SpidL4",
"https://www.spid.gov.it/SpidL1",
]
AUTHN_REQUEST_SPID_NO_STATE = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_STATE.pop("state")
AUTHN_REQUEST_SPID_NO_CORRECT_STATE = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CORRECT_STATE["state"] = "toosmall"
AUTHN_REQUEST_SPID_NO_CORRECT_UI_LOCALES = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_SPID_NO_CORRECT_UI_LOCALES["ui_locales"] = "nocorrect"
AUTHN_REQUEST_CIE = deepcopy(AUTHN_REQUEST_SPID)
AUTHN_REQUEST_CIE["scope"] = ["openid", "offline_access", "email", "profile"]
AUTHN_REQUEST_CIE["prompt"] = "consent login"
AUTHN_REQUEST_CIE["claims"] = CLAIMS_CIE
AUTHN_REQUEST_CIE_NO_ACR_VALUES = deepcopy(AUTHN_REQUEST_CIE)
AUTHN_REQUEST_CIE_NO_ACR_VALUES.pop("acr_values")
AUTHN_REQUEST_CIE_NO_CORRECT_ACR_VALUES = deepcopy(AUTHN_REQUEST_CIE)
AUTHN_REQUEST_CIE_NO_CORRECT_ACR_VALUES["acr_values"] = "CIE_L3"
AUTHN_REQUEST_CIE_NO_PROMPT = deepcopy(AUTHN_REQUEST_CIE)
AUTHN_REQUEST_CIE_NO_PROMPT.pop("prompt")
AUTHN_REQUEST_CIE_NO_CORRECT_PROMPT = deepcopy(AUTHN_REQUEST_CIE)
AUTHN_REQUEST_CIE_NO_CORRECT_PROMPT["prompt"] = "verify"
AUTHN_REQUEST_CIE_NO_SCOPE = deepcopy(AUTHN_REQUEST_CIE)
AUTHN_REQUEST_CIE_NO_SCOPE.pop("scope")
AUTHN_REQUEST_CIE_NO_CORRECT_SCOPE = deepcopy(AUTHN_REQUEST_CIE)
AUTHN_REQUEST_CIE_NO_CORRECT_SCOPE["scope"] = "openid"
@@ -0,0 +1,52 @@
from copy import deepcopy
AUTHN_RESPONSE_SPID = {
"code": "usDwMnEzJPpG5oaV8x3j&",
"state": "fyZiOL9Lf2CeKuNT2JzxiLRDink0uPcd&",
}
AUTHN_RESPONSE_SPID_NO_CODE = deepcopy(AUTHN_RESPONSE_SPID)
AUTHN_RESPONSE_SPID_NO_CODE.pop("code")
AUTHN_RESPONSE_SPID_NO_STATE = deepcopy(AUTHN_RESPONSE_SPID)
AUTHN_RESPONSE_SPID_NO_STATE.pop("state")
AUTHN_RESPONSE_SPID_NO_CORRECT_STATE = deepcopy(AUTHN_RESPONSE_SPID)
AUTHN_RESPONSE_SPID_NO_CORRECT_STATE["state"] = "tooshort"
AUTHN_RESPONSE_CIE = deepcopy(AUTHN_RESPONSE_SPID)
AUTHN_RESPONSE_CIE["iss"] = "https://idserver.servizicie.interno.gov.it"
AUTHN_RESPONSE_CIE_NO_ISS = deepcopy(AUTHN_RESPONSE_CIE)
AUTHN_RESPONSE_CIE_NO_ISS.pop("iss")
AUTHN_RESPONSE_CIE_NO_CORRECT_ISS = deepcopy(AUTHN_RESPONSE_CIE)
AUTHN_RESPONSE_CIE_NO_CORRECT_ISS["iss"] = ""
AUTHN_ERROR_RESPONSE = {
"error_description": "request%20malformata&",
"code": "usDwMnEzJPpG5oaV8x3j&",
"state": "fyZiOL9Lf2CeKuNT2JzxiLRDink0uPcd&",
}
AUTHN_ERROR_RESPONSE_SPID = deepcopy(AUTHN_ERROR_RESPONSE)
AUTHN_ERROR_RESPONSE_SPID["error"] = "invalid_request"
AUTHN_ERROR_RESPONSE_SPID_NO_CODE = deepcopy(AUTHN_ERROR_RESPONSE)
AUTHN_ERROR_RESPONSE_SPID_NO_CODE.pop("code")
AUTHN_ERROR_RESPONSE_SPID_NO_STATE = deepcopy(AUTHN_ERROR_RESPONSE)
AUTHN_ERROR_RESPONSE_SPID_NO_STATE.pop("state")
AUTHN_ERROR_RESPONSE_SPID_NO_CORRECT_STATE = deepcopy(AUTHN_ERROR_RESPONSE)
AUTHN_ERROR_RESPONSE_SPID_NO_CORRECT_STATE["state"] = "tooshort"
AUTHN_ERROR_RESPONSE_CIE = deepcopy(AUTHN_ERROR_RESPONSE)
AUTHN_ERROR_RESPONSE_CIE["error"] = "unsupported_response_type"
AUTHN_ERROR_RESPONSE_CIE["iss"] = "https://idserver.servizicie.interno.gov.it"
AUTHN_ERROR_RESPONSE_CIE_NO_ISS = deepcopy(AUTHN_ERROR_RESPONSE_CIE)
AUTHN_ERROR_RESPONSE_CIE_NO_ISS.pop("iss")
AUTHN_ERROR_RESPONSE_CIE_NO_CORRECT_ISS = deepcopy(AUTHN_ERROR_RESPONSE_CIE)
AUTHN_ERROR_RESPONSE_CIE_NO_CORRECT_ISS["iss"] = "//idserver.servizicie.interno.gov.it"
@@ -0,0 +1,32 @@
from copy import deepcopy
INTROSPECTION_REQUEST = {
"client_assertion": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI.LVyRDPVJ",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_id": "https://rp.spid.agid.gov.it",
"token": "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTg3MDI0MTQsImF1ZCI6W.FXDtEzDL",
}
INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION = deepcopy(INTROSPECTION_REQUEST)
INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION.pop("client_assertion")
INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION = deepcopy(INTROSPECTION_REQUEST)
INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION["client_assertion"] = ".payload.sig"
INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION_TYPE = deepcopy(INTROSPECTION_REQUEST)
INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION_TYPE.pop("client_assertion_type")
INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE = deepcopy(INTROSPECTION_REQUEST)
INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE["client_assertion_type"] = ""
INTROSPECTION_REQUEST_NO_CLIENT_ID = deepcopy(INTROSPECTION_REQUEST)
INTROSPECTION_REQUEST_NO_CLIENT_ID.pop("client_id")
INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ID = deepcopy(INTROSPECTION_REQUEST)
INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ID["client_id"] = "https//rp.spid.agid.gov.it"
INTROSPECTION_REQUEST_NO_TOKEN = deepcopy(INTROSPECTION_REQUEST)
INTROSPECTION_REQUEST_NO_TOKEN.pop("token")
INTROSPECTION_REQUEST_NO_CORRECT_TOKEN = deepcopy(INTROSPECTION_REQUEST)
INTROSPECTION_REQUEST_NO_CORRECT_TOKEN["token"] = ".sign"
@@ -0,0 +1,69 @@
from copy import deepcopy
INTROSPECTION_RESPONSE = {
"active": True,
# "scope": ["openid"],
# "exp": 162545274,
# "sub": "OP-1234567890",
# "client_id": "https://rp.agid.gov.it/",
# "iss": "https://op.spid.agid.gov.it/",
# "aud": ["https://rp.spid.agid.gov.it/auth"],
}
INTROSPECTION_RESPONSE_NO_ACTIVE = deepcopy(INTROSPECTION_RESPONSE)
INTROSPECTION_RESPONSE_NO_ACTIVE.pop("active")
INTROSPECTION_RESPONSE_NO_CORRECT_ACTIVE = deepcopy(INTROSPECTION_RESPONSE)
INTROSPECTION_RESPONSE_NO_CORRECT_ACTIVE["active"] = "ciao"
INTROSPECTION_RESPONSE_NO_CORRECT_SCOPE = deepcopy(INTROSPECTION_RESPONSE)
INTROSPECTION_RESPONSE_NO_CORRECT_SCOPE["scope"] = "scope"
INTROSPECTION_RESPONSE_NO_CORRECT_EXP = deepcopy(INTROSPECTION_RESPONSE)
INTROSPECTION_RESPONSE_NO_CORRECT_EXP["exp"] = "exp"
INTROSPECTION_RESPONSE_NO_CORRECT_SUB = deepcopy(INTROSPECTION_RESPONSE)
INTROSPECTION_RESPONSE_NO_CORRECT_SUB["sub"] = []
INTROSPECTION_RESPONSE_NO_CORRECT_CLIENT_ID = deepcopy(INTROSPECTION_RESPONSE)
INTROSPECTION_RESPONSE_NO_CORRECT_CLIENT_ID["client_id"] = "https/op.agid.gov.it/"
INTROSPECTION_RESPONSE_NO_CORRECT_ISS = deepcopy(INTROSPECTION_RESPONSE)
INTROSPECTION_RESPONSE_NO_CORRECT_ISS["iss"] = ""
INTROSPECTION_RESPONSE_NO_CORRECT_AUD = deepcopy(INTROSPECTION_RESPONSE)
INTROSPECTION_RESPONSE_NO_CORRECT_AUD["aud"] = "https://rp.spid.agid.gov.it/auth"
INTROSPECTION_ERROR_RESPONSE = {
"error": "invalid_request",
"error_description": "descrizione dell’errore",
}
INTROSPECTION_ERROR_RESPONSE_SPID_NO_ERROR = deepcopy(INTROSPECTION_ERROR_RESPONSE)
INTROSPECTION_ERROR_RESPONSE_SPID_NO_ERROR.pop("error")
INTROSPECTION_ERROR_RESPONSE_SPID_NO_CORRECT_ERROR = deepcopy(
INTROSPECTION_ERROR_RESPONSE
)
INTROSPECTION_ERROR_RESPONSE_SPID_NO_CORRECT_ERROR["error"] = "invalid_token"
INTROSPECTION_ERROR_RESPONSE_CIE_NO_ERROR = deepcopy(INTROSPECTION_ERROR_RESPONSE)
INTROSPECTION_ERROR_RESPONSE_CIE_NO_ERROR.pop("error")
INTROSPECTION_ERROR_RESPONSE_CIE_NO_CORRECT_ERROR = deepcopy(
INTROSPECTION_ERROR_RESPONSE
)
INTROSPECTION_ERROR_RESPONSE_CIE_NO_CORRECT_ERROR["error"] = "error"
INTROSPECTION_ERROR_RESPONSE_NO_ERROR_DESCRIPTION = deepcopy(
INTROSPECTION_ERROR_RESPONSE
)
INTROSPECTION_ERROR_RESPONSE_NO_ERROR_DESCRIPTION.pop("error_description")
INTROSPECTION_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION = deepcopy(
INTROSPECTION_ERROR_RESPONSE
)
INTROSPECTION_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION["error_description"] = []
@@ -0,0 +1,32 @@
from copy import deepcopy
REVOCATION_REQUEST = {
"client_assertion": "eyJhbGciOiJIUzpXVCJ9.eyJzdWIdHJ1ZX0.LVyRDPVJm0S9q",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_id": "https://rp.spid.agid.gov.it",
"token": "eyJhbGciOiJSUzI1NiJ9.eyR9.FXDtEzDLbTHzFNroW7w27RLk",
}
REVOCATION_REQUEST_NO_CLIENT_ASSERTION = deepcopy(REVOCATION_REQUEST)
REVOCATION_REQUEST_NO_CLIENT_ASSERTION.pop("client_assertion")
REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION = deepcopy(REVOCATION_REQUEST)
REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION["client_assertion"] = ".payload."
REVOCATION_REQUEST_NO_CLIENT_ASSERTION_TYPE = deepcopy(REVOCATION_REQUEST)
REVOCATION_REQUEST_NO_CLIENT_ASSERTION_TYPE.pop("client_assertion_type")
REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE = deepcopy(REVOCATION_REQUEST)
REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE["client_assertion_type"] = ""
REVOCATION_REQUEST_NO_CLIENT_ID = deepcopy(REVOCATION_REQUEST)
REVOCATION_REQUEST_NO_CLIENT_ID.pop("client_id")
REVOCATION_REQUEST_NO_CORRECT_CLIENT_ID = deepcopy(REVOCATION_REQUEST)
REVOCATION_REQUEST_NO_CORRECT_CLIENT_ID["client_id"] = ""
REVOCATION_REQUEST_NO_TOKEN = deepcopy(REVOCATION_REQUEST)
REVOCATION_REQUEST_NO_TOKEN.pop("token")
REVOCATION_REQUEST_NO_CORRECT_TOKEN = deepcopy(REVOCATION_REQUEST)
REVOCATION_REQUEST_NO_CORRECT_TOKEN["token"] = ".."
@@ -0,0 +1,20 @@
from copy import deepcopy
REVOCATION_RESPONSE_CIE = {
"error": "unauthorized_client",
"error_description": "description of the error",
}
REVOCATION_RESPONSE_CIE_NO_ERROR = deepcopy(REVOCATION_RESPONSE_CIE)
REVOCATION_RESPONSE_CIE_NO_ERROR.pop("error")
REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR = deepcopy(REVOCATION_RESPONSE_CIE)
REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR["error"] = []
REVOCATION_RESPONSE_CIE_NO_ERROR_DESCRIPTION = deepcopy(REVOCATION_RESPONSE_CIE)
REVOCATION_RESPONSE_CIE_NO_ERROR_DESCRIPTION.pop("error_description")
REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR_DESCRIPTION = deepcopy(REVOCATION_RESPONSE_CIE)
REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR_DESCRIPTION["error_description"] = [
"error description"
]
@@ -0,0 +1,141 @@
op_conf_priv_jwk = {
"use": "sig",
"kty": "RSA",
"kid": "dB67gL7ck3TFiIAf7N6_7SHvqk0MDYMEQcoGGlkUAAw",
"n": "01_4aI2Lu5ggsElmRkE_S_a83V_szXU0txV4db2hmJ8HR1Y2s7PsZZ5-emGpnTydGrR3n-QExeEEIcFt_a06Ryiink34RQcKoGXUDBMBU0Bu8G7NcZ99YX6yeG9wFi4xs-WviTPmtPqijkz6jm1_ltWDcwbktfkraIRKKggZaEl9ldtsFr2wSpin3AXuGIdeJ0hZqhF92ODBLGjJlaIL9KlwopDy56adReVnraawSdrxmuPGj78IEADNAme2nQNvv9UCu0FkAn5St1bKds3Gpv26W0kjr1gZLsmQrj9lTcDk_KbAwfEY__P7se62kusoSuKMTQqUG1TQpUY7oFGSdw",
"e": "AQAB",
"d": "AxvyR3dtisDeGjm6K2ZiS_fBJ3B1xz_mGptSPkkfy2LrdH0sKNCItaXzLlGpcQqnNPFaoRt1hoOcz_JMb-LQbKOIYNO8xycnXNyildrhthvIhfjbRMSXz4tRjLv25hyf1omWX2pIBnl5UqaHOPkbW1igqlx7mMGlhdAMVznvRb1oJOXxPtSdlVOhqT5ohn4BJZcJ85TKm_E49KUKUsz_xkd3BlYnUaiuEu-VQbC0u61iH-N017wg4ZQn9eXQgAzFdSNI2GQ7IH76l00iUu-y9oT2ld4WHGYMmsZBJgaQZnu2Yj4IMb5NjQz2VAOTU844RXP7-7Y5rWsS1oewfWSHAQ",
"p": "9dvm96S4wBrwzs73DOG8eV6sWEN13ZJNLa4tKzJvonOLG_8Z9Rshnfg_HcYDm_mqRD-nFC-8JKufnNaZbZuO_hDiA6UkvlZMGePxoN7nueHGfAqZ9lRQX0Vpp2Ac480jJgBLOhwgjkySRI5Ck-enTGabkxVN_LJSU9FC54TW6KE",
"q": "3BfwsumO62vhekd2gm97Uk1XFOM5UXmb1esdFnZ_RZRw1tDUB2irTxLJSqECCZbMWRoFRB3k-17LAOMaITUqv8g-83zdCmR3gpO71ahbdEfkGGKQYL7WmMNv3YvSMY5lN4_mCEz8R_OCz0_H7bip0Es_KAl5TSXjnVTvaACnLBc",
}
op_conf = {
"sub": "http://op-test/oidc/op/",
"default_exp": 2880,
"default_signature_alg": "RS256",
"authority_hints": ["http://op-test/oidc/op/"],
"jwks_fed": [op_conf_priv_jwk],
"jwks_core": [op_conf_priv_jwk],
"trust_marks": [],
"trust_mark_issuers": {},
"entity_type": "openid_provider",
"metadata": {
"openid_provider": {
"authorization_endpoint": "http://op-test/oidc/op/authorization/",
"token_endpoint": "http://op-test/oidc/op/token",
"userinfo_endpoint": "http://op-test/oidc/op/userinfo/",
"introspection_endpoint": "http://op-test/oidc/op/introspection/",
"revocation_endpoint": "http://op-test/oidc/op/revocation/",
"claims_parameter_supported": True,
"contacts": ["[email protected]"],
"client_registration_types_supported": ["automatic"],
"request_authentication_methods_supported": {"ar": ["request_object"]},
"acr_values_supported": [
"https://www.spid.gov.it/SpidL1",
"https://www.spid.gov.it/SpidL2",
"https://www.spid.gov.it/SpidL3",
],
"claims_supported": [
"https://attributes.eid.gov.it/spid_code",
"given_name",
"family_name",
"place_of_birth",
"birthdate",
"gender",
"https://attributes.eid.gov.it/company_name",
"https://attributes.eid.gov.it/registered_office",
"https://attributes.eid.gov.it/fiscal_number",
"https://attributes.eid.gov.it/company_fiscal_number",
"https://attributes.eid.gov.it/vat_number",
"document_details",
"phone_number",
"email",
"https://attributes.eid.gov.it/e_delivery_service",
"https://attributes.eid.gov.it/eid_exp_date",
"address"
],
"grant_types_supported": ["authorization_code", "refresh_token"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"],
"issuer": "http://op-test/oidc/op/",
"jwks": {
"keys": [
{
"use": "sig",
"kty": "RSA",
"n": "01_4aI2Lu5ggsElmRkE_S_a83V_szXU0txV4db2hmJ8HR1Y2s7PsZZ5-emGpnTydGrR3n-QExeEEIcFt_a06Ryiink34RQcKoGXUDBMBU0Bu8G7NcZ99YX6yeG9wFi4xs-WviTPmtPqijkz6jm1_ltWDcwbktfkraIRKKggZaEl9ldtsFr2wSpin3AXuGIdeJ0hZqhF92ODBLGjJlaIL9KlwopDy56adReVnraawSdrxmuPGj78IEADNAme2nQNvv9UCu0FkAn5St1bKds3Gpv26W0kjr1gZLsmQrj9lTcDk_KbAwfEY__P7se62kusoSuKMTQqUG1TQpUY7oFGSdw",
"e": "AQAB",
"kid": "dB67gL7ck3TFiIAf7N6_7SHvqk0MDYMEQcoGGlkUAAw",
}
]
},
"scopes_supported": ["openid", "offline_access"],
"logo_uri": "http://op-test/oidc/op/statics/logo.svg",
"organization_name": "SPID OIDC identity provider",
"op_policy_uri": "http://op-test/oidc/op/en/website/legal-information/",
"request_parameter_supported": True,
"request_uri_parameter_supported": True,
"require_request_uri_registration": True,
"response_types_supported": ["code"],
"subject_types_supported": ["pairwise", "public"],
"token_endpoint_auth_methods_supported": ["private_key_jwt"],
"token_endpoint_auth_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"ES256",
"ES384",
"ES512",
],
"userinfo_encryption_alg_values_supported": [
"RSA-OAEP",
"RSA-OAEP-256",
"ECDH-ES",
"ECDH-ES+A128KW",
"ECDH-ES+A192KW",
"ECDH-ES+A256KW",
],
"userinfo_encryption_enc_values_supported": [
"A128CBC-HS256",
"A192CBC-HS384",
"A256CBC-HS512",
"A128GCM",
"A192GCM",
"A256GCM",
],
"userinfo_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"ES256",
"ES384",
"ES512",
],
"request_object_encryption_alg_values_supported": [
"RSA-OAEP",
"RSA-OAEP-256",
"ECDH-ES",
"ECDH-ES+A128KW",
"ECDH-ES+A192KW",
"ECDH-ES+A256KW",
],
"request_object_encryption_enc_values_supported": [
"A128CBC-HS256",
"A192CBC-HS384",
"A256CBC-HS512",
"A128GCM",
"A192GCM",
"A256GCM",
],
"request_object_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"ES256",
"ES384",
"ES512",
],
}
},
"constraints": {},
"is_active": True,
}
@@ -0,0 +1,353 @@
from copy import deepcopy
from django.contrib.auth import get_user_model
from django.test import Client, TestCase, override_settings
from django.urls import reverse
from spid_cie_oidc.authority.tests.settings import RP_METADATA, RP_METADATA_JWK1, rp_onboarding_data
from django.utils import timezone
from spid_cie_oidc.entity.jwtse import create_jws, unpad_jwt_payload, verify_jws
from spid_cie_oidc.entity.models import (
FederationEntityConfiguration,
FetchedEntityStatement,
TrustChain,
)
from spid_cie_oidc.entity.tests.settings import TA_SUB
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.provider.models import IssuedToken, OidcSession
from spid_cie_oidc.provider.tests.settings import op_conf, op_conf_priv_jwk
from spid_cie_oidc.relying_party.utils import random_string
RP_SUB = rp_onboarding_data["sub"]
class AuthnRequestTest(TestCase):
def setUp(self):
self.REQUEST_OBJECT_PAYLOAD = {
"client_id": RP_SUB,
"sub": RP_SUB,
"iss": RP_SUB,
"response_type": "code",
"scope": ["openid"],
"code_challenge": "qWJlMe0xdbXrKxTm72EpH659bUxAxw80",
"code_challenge_method": "S256",
"nonce": random_string(),
"prompt": "consent login",
"redirect_uri": f"{RP_SUB}callback/",
"acr_values": ["https://www.spid.gov.it/SpidL1", "https://www.spid.gov.it/SpidL2"],
"claims": {
"id_token": {"family_name": {"essential": True}, "email": {"essential": True}},
"userinfo": {"given_name": None, "family_name": None, "email": None},
},
"state": random_string(),
"aud": ["https://op.spid.agid.gov.it/auth"],
"iat": iat_now(),
"exp": exp_from_now(),
"jti": "a72d5df0-2415-4c7c-a44f-3988b354040b",
}
self.user = get_user_model().objects.create(
username="test",
first_name="test",
last_name="test",
email="[email protected]",
is_staff= False,
attributes={
"family_name": "rossi",
"username": "unique_value",
"fiscal_number": "a7s6da87d6a87sd6as78d",
"email": "[email protected]",
},
)
self.user.set_password("test")
self.user.save()
self.op_conf = FederationEntityConfiguration.objects.create(**op_conf)
self.ta_fes = FetchedEntityStatement.objects.create(
sub=TA_SUB,
iss=TA_SUB,
exp=datetime_from_timestamp(exp_from_now(33)),
iat=datetime_from_timestamp(iat_now()),
)
self.trust_chain = TrustChain.objects.create(
sub=RP_SUB,
exp=datetime_from_timestamp(exp_from_now(33)),
jwks = [],
metadata=RP_METADATA,
status="valid",
trust_anchor=self.ta_fes,
is_active=True,
)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_unknown_error(self):
jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
TrustChain.objects.all().delete()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 302)
self.assertTrue("error=invalid_request" in res.url)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
@override_settings(OIDCFED_DEFAULT_PROVIDER_PROFILE="cie")
def test_auth_request_id_token_claim(self):
jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 200)
self.assertIn("username", res.content.decode())
self.assertIn("password", res.content.decode())
res = client.post(
url, {"username": "test", "password": "test", "authz_request_object": jws}
)
self.assertFalse("error" in res.content.decode())
self.assertTrue(res.status_code == 302)
consent_page_url = res.url
res = client.get(consent_page_url)
self.assertTrue("agree" in res.content.decode())
self.assertFalse("error" in res.content.decode())
res = client.post(consent_page_url, {"agree": True})
issued_token = IssuedToken.objects.all().first()
verify_jws(issued_token.id_token, op_conf_priv_jwk)
id_token = unpad_jwt_payload(issued_token.id_token)
self.assertEqual(id_token.get("family_name"), "rossi")
self.assertEqual(id_token.get("email"), "[email protected]")
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_no_request(self):
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {})
self.assertTrue(res.status_code == 400)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_ok(self):
jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 200)
self.assertIn("username", res.content.decode())
self.assertIn("password", res.content.decode())
res = client.post(
url, {"username": "test", "password": "test", "authz_request_object": jws}
)
self.assertFalse("error" in res.content.decode())
self.assertTrue(res.status_code == 302)
consent_page_url = res.url
res = client.get(consent_page_url)
self.assertTrue("agree" in res.content.decode())
self.assertFalse("error" in res.content.decode())
res = client.post(consent_page_url, {"agree": True})
self.assertTrue(res.status_code == 302)
self.assertTrue("code" in res.url)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_user_rejected_consent(self):
jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 200)
self.assertIn("username", res.content.decode())
self.assertIn("password", res.content.decode())
res = client.post(
url, {"username": "test", "password": "test", "authz_request_object": jws}
)
self.assertFalse("error" in res.content.decode())
self.assertTrue(res.status_code == 302)
consent_page_url = res.url
res = client.get(consent_page_url)
self.assertTrue("agree" in res.content.decode())
self.assertFalse("error" in res.content.decode())
res = client.post(consent_page_url, {"agree": False})
self.assertTrue(res.status_code == 302)
# TODO: this is not normative
self.assertTrue("error=rejected_by_user" in res.url)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_no_session_in_post_consent(self):
jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 200)
self.assertIn("username", res.content.decode())
self.assertIn("password", res.content.decode())
res = client.post(
url, {"username": "test", "password": "test", "authz_request_object": jws}
)
self.assertFalse("error" in res.content.decode())
self.assertTrue(res.status_code == 302)
consent_page_url = res.url
res = client.get(consent_page_url)
self.assertTrue("agree" in res.content.decode())
self.assertFalse("error" in res.content.decode())
OidcSession.objects.all().delete()
res = client.post(consent_page_url, {"agree": True})
self.assertTrue(res.status_code == 403)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_no_session_in_get_consent(self):
jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 200)
self.assertIn("username", res.content.decode())
self.assertIn("password", res.content.decode())
res = client.post(
url,
{
"username": "test",
"password": "test",
"authz_request_object": jws
}
)
self.assertFalse("error" in res.content.decode())
self.assertTrue(res.status_code == 302)
consent_page_url = res.url
OidcSession.objects.all().delete()
res = client.get(consent_page_url)
self.assertTrue(res.status_code == 403)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_auth_code_already_used(self):
jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 200)
self.assertIn("username", res.content.decode())
self.assertIn("password", res.content.decode())
res = client.post(
url, {"username": "test", "password": "test", "authz_request_object": jws}
)
self.assertFalse("error" in res.content.decode())
self.assertTrue(res.status_code == 302)
session = OidcSession.objects.all().first()
IssuedToken.objects.create(session=session, expires=timezone.localtime())
consent_page_url = res.url
res = client.get(consent_page_url)
self.assertTrue(res.status_code == 403)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_wrong_login(self):
jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 200)
self.assertIn("username", res.content.decode())
self.assertIn("password", res.content.decode())
res = client.post(
url, {"username": "notest", "password": "test", "authz_request_object": jws}
)
self.assertIn("error", res.content.decode())
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_preexistent_authz(self):
jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 200)
self.assertIn("username", res.content.decode())
self.assertIn("password", res.content.decode())
res = client.post(
url, {"username": "test", "password": "test", "authz_request_object": jws}
)
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 302)
self.assertIn("error=invalid_request", res.url)
self.assertIn("state", res.url)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_trust_chain_no_active(self):
self.trust_chain.is_active = False
self.trust_chain.save()
jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 302)
self.assertIn("error=invalid_request", res.url)
self.assertIn("state", res.url)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_invalid_jwk(self):
jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
get_jwks(self.trust_chain.metadata['openid_relying_party'])[0][
"kid"
] = "31ALfVXx9dcAMMHCVvh42qLTlanBL_r6BTnD5uMDzFT"
self.trust_chain.save()
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 302)
self.assertIn("error=invalid_request", res.url)
self.assertIn("state", res.url)
get_jwks(self.trust_chain.metadata['openid_relying_party'])[0][
"kid"
] = RP_METADATA_JWK1['kid']
self.trust_chain.save()
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_no_correct_payload(self):
NO_CORRECT_OBJECT_PAYLOAD = deepcopy(self.REQUEST_OBJECT_PAYLOAD)
NO_CORRECT_OBJECT_PAYLOAD["response_type"] = "test"
jws = create_jws(NO_CORRECT_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 302)
self.assertIn("error", res.url)
self.assertIn("state", res.url)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_invalid_session(self):
client = Client()
url = reverse("oidc_provider_consent")
res = client.get(url)
self.assertTrue(res.status_code == 403)
res = client.post(url)
self.assertTrue(res.status_code == 403)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_no_correct_refresh_request(self):
local_payload = deepcopy(self.REQUEST_OBJECT_PAYLOAD)
local_payload["scope"] = "openid offline_access"
local_payload["prompt"] = "login"
jws = create_jws(local_payload, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.get(url, {"request": jws})
self.assertTrue(res.status_code == 403)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_user_staff(self):
self.user.is_staff = True
self.user.save()
jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.post(
url, {"username": "test", "password": "test", "authz_request_object": jws}
)
self.assertTrue(res.status_code == 302)
self.assertTrue("/oidc/op/rp-test/landing/" == res.url)
@override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB])
def test_auth_request_no_correct_authz_request(self):
self.user.is_staff = True
self.user.save()
create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1)
client = Client()
url = reverse("oidc_provider_authnrequest")
res = client.post(
url, {"username": "test", "password": "test", "authz_request_object": {}}
)
self.assertTrue(res.status_code == 403)
@@ -0,0 +1,154 @@
import logging
from django.test import TestCase
from pydantic import ValidationError
from spid_cie_oidc.provider.schemas.authn_requests import (
AuthenticationRequestCie,
AuthenticationRequestSpid,
)
from .authn_request_settings import (
AUTHN_REQUEST_CIE,
AUTHN_REQUEST_CIE_NO_ACR_VALUES,
AUTHN_REQUEST_CIE_NO_CORRECT_ACR_VALUES,
AUTHN_REQUEST_CIE_NO_CORRECT_PROMPT,
AUTHN_REQUEST_CIE_NO_CORRECT_SCOPE,
AUTHN_REQUEST_CIE_NO_PROMPT,
AUTHN_REQUEST_CIE_NO_SCOPE,
AUTHN_REQUEST_SPID,
AUTHN_REQUEST_SPID_NO_CLIENT_ID,
AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE,
AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE_METHOD,
AUTHN_REQUEST_SPID_NO_CORRECT_ACR_VALUES,
AUTHN_REQUEST_SPID_NO_CORRECT_CLAIMS,
AUTHN_REQUEST_SPID_NO_CORRECT_CODE_CHALLENGE_METHOD,
AUTHN_REQUEST_SPID_NO_CORRECT_NONCE,
AUTHN_REQUEST_SPID_NO_CORRECT_PROMPT,
AUTHN_REQUEST_SPID_NO_CORRECT_REDIRECT_URL,
AUTHN_REQUEST_SPID_NO_CORRECT_RESPONSE_TYPE,
AUTHN_REQUEST_SPID_NO_CORRECT_SCOPE,
AUTHN_REQUEST_SPID_NO_CORRECT_STATE,
AUTHN_REQUEST_SPID_NO_CORRECT_UI_LOCALES,
AUTHN_REQUEST_SPID_NO_NONCE,
AUTHN_REQUEST_SPID_NO_REDIRECT_URL,
AUTHN_REQUEST_SPID_NO_RESPONSE_TYPE,
AUTHN_REQUEST_SPID_NO_SCOPE,
AUTHN_REQUEST_SPID_NO_STATE,
)
logger = logging.getLogger(__name__)
class AuthRequestTest(TestCase):
def test_validate_spid(self):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID)
def test_validate_spid_no_client_id(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CLIENT_ID)
def test_validate_spid_no_correct_claims(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_CLAIMS)
def test_validate_spid_no_code_challenge(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE)
def test_validate_spid_no_code_challenge_method(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE_METHOD)
def test_validate_spid_no_correct_code_challenge_method(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(
**AUTHN_REQUEST_SPID_NO_CORRECT_CODE_CHALLENGE_METHOD
)
def test_validate_spid_no_nonce(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_NONCE)
def test_validate_spid_no_correct_nonce(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_NONCE)
# removed, relaxed with the GAIN-PoC integration
# def test_validate_spid_no_prompt(self):
# with self.assertRaises(ValidationError):
# AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_PROMPT)
def test_validate_spid_no_correct_prompt(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_PROMPT)
def test_validate_spid_no_redirect_url(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_REDIRECT_URL)
def test_validate_spid_no_correct_redirect_url(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_REDIRECT_URL)
def test_validate_spid_no_response_type(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_RESPONSE_TYPE)
def test_validate_spid_no_correct_response_type(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_RESPONSE_TYPE)
def test_validate_spid_no_scope(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_SCOPE)
def test_validate_spid_no_correct_scope(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_SCOPE)
# def test_validate_spid_no_acr_values(self):
# with self.assertRaises(ValidationError):
# AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_ACR_VALUES)
def test_validate_spid_no_correct_acr_values(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_ACR_VALUES)
def test_validate_spid_no_state(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_STATE)
def test_validate_spid_no_correct_state(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_STATE)
def test_validate_spid_no_correct_ui_locales(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_UI_LOCALES)
def test_validate_cie(self):
AuthenticationRequestCie(**AUTHN_REQUEST_CIE)
def test_validate_cie_no_acr_values(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_CIE_NO_ACR_VALUES)
def test_validate_cie_no_correct_acr_values(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_CIE_NO_CORRECT_ACR_VALUES)
def test_validate_cie_no_prompt(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_CIE_NO_PROMPT)
def test_validate_cie_no_correct_prompt(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_CIE_NO_CORRECT_PROMPT)
def test_validate_cie_no_scope(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_CIE_NO_SCOPE)
def test_validate_cie_no_correct_scope(self):
with self.assertRaises(ValidationError):
AuthenticationRequestSpid(**AUTHN_REQUEST_CIE_NO_CORRECT_SCOPE)
@@ -0,0 +1,191 @@
from copy import deepcopy
import time
from cryptojwt.jws.utils import left_hash
from django.test import Client, TestCase, override_settings
from django.urls import reverse
from django.utils import timezone
from spid_cie_oidc.accounts.models import User
from spid_cie_oidc.authority.tests.settings import (
RP_METADATA,
RP_METADATA_JWK1,
rp_conf
)
from spid_cie_oidc.entity.jwtse import create_jws, verify_jws
from spid_cie_oidc.entity.models import (
FederationEntityConfiguration,
FetchedEntityStatement,
TrustChain
)
from spid_cie_oidc.entity.tests.settings import TA_SUB
from spid_cie_oidc.entity.utils import (
datetime_from_timestamp,
exp_from_now,
iat_now
)
from spid_cie_oidc.provider.models import IssuedToken, OidcSession
from spid_cie_oidc.provider.tests.settings import op_conf, op_conf_priv_jwk
RP_SUB = rp_conf["sub"]
RP_CLIENT_ID = rp_conf["metadata"]["openid_relying_party"]["client_id"]
class RefreshTokenTest(TestCase):
def setUp(self):
self.op_local_conf = deepcopy(op_conf)
FederationEntityConfiguration.objects.create(**self.op_local_conf)
self.ta_fes = FetchedEntityStatement.objects.create(
sub=TA_SUB,
iss=TA_SUB,
exp=datetime_from_timestamp(exp_from_now(33)),
iat=datetime_from_timestamp(iat_now()),
)
self.trust_chain = TrustChain.objects.create(
sub=RP_SUB,
exp=datetime_from_timestamp(exp_from_now(33)),
jwks=[],
metadata=RP_METADATA,
status="valid",
trust_anchor=self.ta_fes,
is_active=True,
)
self.jwt_auds = [op_conf["sub"], "http://testserver/oidc/op/", "http://testserver/oidc/op/token"]
CLIENT_ASSERTION = {
"iss": RP_SUB,
"sub": RP_SUB,
"aud": self.jwt_auds,
"exp": exp_from_now(),
"iat": iat_now(),
"jti": "jti",
}
self.ca_jws = create_jws(CLIENT_ASSERTION, RP_METADATA_JWK1)
refresh_token = {
"iss": self.op_local_conf["sub"],
"sub": RP_SUB,
"aud": self.jwt_auds,
"client_id": RP_CLIENT_ID,
"scope": "openid",
}
self.rt_jws = create_jws(refresh_token, op_conf_priv_jwk)
id_token = {
'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b',
'nonce': 'ljbvL3rpscgS4ZGda7cgibXHr7vrNREW',
'at_hash': '',
'c_hash': 'tij0h-zL_bSrsVXy-d3qHw',
'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"], ],
'iss': op_conf["sub"],
'jti': '402e61bd-950c-4934-83d4-c09a05468828',
'exp': exp_from_now(),
'iat': iat_now()
}
access_token = {
'iss': op_conf["sub"],
'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b',
'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"], ],
'client_id': rp_conf["metadata"]["openid_relying_party"]["client_id"],
'scope': 'openid',
'jti': '402e61bd-950c-4934-83d4-c09a05468828',
'exp': exp_from_now(),
'iat': iat_now()
}
self.jwt_at = create_jws(access_token, op_conf_priv_jwk, typ="at+jwt")
id_token['at_hash'] = left_hash(self.jwt_at, "HS256")
self.jwt_id = create_jws(id_token, op_conf_priv_jwk)
session = OidcSession.objects.create(
user=User.objects.create(username="username"),
user_uid="",
nonce="",
authz_request={"scope": "offline_access", "prompt": "consent", "nonce": "123",
"acr_values": ["https://www.spid.gov.it/SpidL2", "https://www.spid.gov.it/SpidL1"]},
client_id=RP_SUB,
auth_code="code",
)
IssuedToken.objects.create(
refresh_token=self.rt_jws,
id_token=self.jwt_id,
access_token=self.jwt_at,
session=session,
expires=timezone.localtime()
)
self.assertEqual(refresh_token["iss"], self.op_local_conf["sub"])
@override_settings(OIDCFED_PROVIDER_PROFILE="spid")
def test_grant_refresh_token_spid(self):
client = Client()
url = reverse("oidc_provider_token_endpoint")
request = dict(
client_id=RP_CLIENT_ID,
client_assertion=self.ca_jws,
client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
refresh_token=self.rt_jws,
grant_type="refresh_token"
)
res = client.post(url, request)
self.assertTrue(res.status_code == 200)
refresh_token = verify_jws(res.json().get("refresh_token"), op_conf_priv_jwk)
self.assertEqual(refresh_token["aud"], RP_CLIENT_ID)
self.assertEqual(refresh_token["iss"], self.op_local_conf["sub"])
@override_settings(OIDCFED_PROVIDER_PROFILE="cie")
def test_grant_refresh_token_cie(self):
client = Client()
url = reverse("oidc_provider_token_endpoint")
request = dict(
client_id=RP_CLIENT_ID,
client_assertion=self.ca_jws,
client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
refresh_token=self.rt_jws,
grant_type="refresh_token"
)
res = client.post(url, request)
self.assertTrue(res.status_code == 200)
refresh_token = verify_jws(res.json().get("refresh_token"), op_conf_priv_jwk)
self.assertEqual(refresh_token["aud"], RP_CLIENT_ID)
@override_settings(OIDCFED_PROVIDER_MAX_REFRESH=1, OIDCFED_PROVIDER_PROFILE="spid")
def test_grant_refresh_token_two_times_spid(self):
client = Client()
url = reverse("oidc_provider_token_endpoint")
request = dict(
client_id=RP_CLIENT_ID,
client_assertion=self.ca_jws,
client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
refresh_token=self.rt_jws,
grant_type="refresh_token"
)
res = client.post(url, request)
self.assertTrue(res.status_code == 200)
request = dict(
client_id=RP_CLIENT_ID,
client_assertion=self.ca_jws,
client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
refresh_token=res.json()["refresh_token"],
grant_type="refresh_token"
)
res = client.post(url, request)
self.assertTrue(res.status_code == 400)
@override_settings(OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME=1, OIDCFED_PROVIDER_PROFILE="cie")
def test_grant_refresh_token_two_times_cie(self):
client = Client()
url = reverse("oidc_provider_token_endpoint")
request = dict(
client_id=RP_CLIENT_ID,
client_assertion=self.ca_jws,
client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
refresh_token=self.rt_jws,
grant_type="refresh_token"
)
res = client.post(url, request)
self.assertTrue(res.status_code == 200)
request = dict(
client_id=RP_CLIENT_ID,
client_assertion=self.ca_jws,
client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
refresh_token=res.json()["refresh_token"],
grant_type="refresh_token"
)
time.sleep(2)
res = client.post(url, request)
self.assertTrue(res.status_code == 400)
@@ -0,0 +1,83 @@
import logging
from django.test import TestCase
from pydantic import ValidationError
from spid_cie_oidc.provider.schemas.authn_response import (
AuthenticationErrorResponse,
AuthenticationErrorResponseCie,
AuthenticationResponse,
AuthenticationResponseCie,
)
from .authn_responses_settings import (
AUTHN_ERROR_RESPONSE_CIE,
AUTHN_ERROR_RESPONSE_CIE_NO_CORRECT_ISS,
AUTHN_ERROR_RESPONSE_CIE_NO_ISS,
AUTHN_ERROR_RESPONSE_SPID,
AUTHN_ERROR_RESPONSE_SPID_NO_CODE,
AUTHN_ERROR_RESPONSE_SPID_NO_CORRECT_STATE,
AUTHN_ERROR_RESPONSE_SPID_NO_STATE,
AUTHN_RESPONSE_CIE,
AUTHN_RESPONSE_CIE_NO_CORRECT_ISS,
AUTHN_RESPONSE_CIE_NO_ISS,
AUTHN_RESPONSE_SPID,
AUTHN_RESPONSE_SPID_NO_CODE,
AUTHN_RESPONSE_SPID_NO_CORRECT_STATE,
AUTHN_RESPONSE_SPID_NO_STATE,
)
logger = logging.getLogger(__name__)
class AuthResponseTest(TestCase):
def test_validate_response_spid(self):
AuthenticationResponse(**AUTHN_RESPONSE_SPID)
def test_validate_response_spid_no_code(self):
with self.assertRaises(ValidationError):
AuthenticationResponse(**AUTHN_RESPONSE_SPID_NO_CODE)
def test_validate_response_spid_no_state(self):
with self.assertRaises(ValidationError):
AuthenticationResponse(**AUTHN_RESPONSE_SPID_NO_STATE)
def test_validate_response_spid_no_correct_state(self):
with self.assertRaises(ValidationError):
AuthenticationResponse(**AUTHN_RESPONSE_SPID_NO_CORRECT_STATE)
def test_validate_response_cie(self):
AuthenticationResponseCie(**AUTHN_RESPONSE_CIE)
def test_validate_response_cie_no_iss(self):
with self.assertRaises(ValidationError):
AuthenticationResponseCie(**AUTHN_RESPONSE_CIE_NO_ISS)
def test_validate_response_cie_no_correct_iss(self):
with self.assertRaises(ValidationError):
AuthenticationResponseCie(**AUTHN_RESPONSE_CIE_NO_CORRECT_ISS)
def test_validate_error_response_spid(self):
AuthenticationErrorResponse(**AUTHN_ERROR_RESPONSE_SPID)
def test_validate_error_response_spid_no_code(self):
with self.assertRaises(ValidationError):
AuthenticationErrorResponse(**AUTHN_ERROR_RESPONSE_SPID_NO_CODE)
def test_validate_error_response_spid_no_state(self):
with self.assertRaises(ValidationError):
AuthenticationErrorResponse(**AUTHN_ERROR_RESPONSE_SPID_NO_STATE)
def test_validate_error_response_spid_no_correct_state(self):
with self.assertRaises(ValidationError):
AuthenticationErrorResponse(**AUTHN_ERROR_RESPONSE_SPID_NO_CORRECT_STATE)
def test_validate_error_response_cie(self):
AuthenticationErrorResponseCie(**AUTHN_ERROR_RESPONSE_CIE)
def test_validate_error_response_spid_no_iss(self):
with self.assertRaises(ValidationError):
AuthenticationErrorResponseCie(**AUTHN_ERROR_RESPONSE_CIE_NO_ISS)
def test_validate_error_response_spid_no_correct_iss(self):
with self.assertRaises(ValidationError):
AuthenticationErrorResponseCie(**AUTHN_ERROR_RESPONSE_CIE_NO_CORRECT_ISS)
@@ -0,0 +1,111 @@
from copy import deepcopy
from django.test import Client, TestCase
from django.urls import reverse
from spid_cie_oidc.accounts.models import User
from spid_cie_oidc.authority.tests.settings import (
RP_METADATA,
rp_onboarding_data
)
from spid_cie_oidc.entity.jwtse import create_jws
from spid_cie_oidc.entity.models import (
FederationEntityConfiguration,
FetchedEntityStatement,
TrustChain
)
from spid_cie_oidc.entity.tests.settings import TA_SUB
from spid_cie_oidc.entity.utils import (
datetime_from_timestamp,
exp_from_now,
iat_now
)
from spid_cie_oidc.provider.models import IssuedToken, OidcSession
from spid_cie_oidc.provider.tests.settings import op_conf, op_conf_priv_jwk
class UserInfoEndpointTest(TestCase):
def setUp(self):
self.RP_SUB = rp_onboarding_data["sub"]
self.op_local_conf = deepcopy(op_conf)
FederationEntityConfiguration.objects.create(**self.op_local_conf)
self.ta_fes = FetchedEntityStatement.objects.create(
sub=TA_SUB,
iss=TA_SUB,
exp=datetime_from_timestamp(exp_from_now(33)),
iat=datetime_from_timestamp(iat_now()),
)
def define_db(self):
session = OidcSession.objects.create(
user=User.objects.create(
username = "username", attributes = {"email" : "[email protected]"}
),
user_uid="",
nonce="",
authz_request={
"scope": "openid", "nonce": "123", "claims":{
"userinfo":{"email": None}
}
},
client_id=self.RP_SUB,
)
access_token = {
"iss": self.RP_SUB,
"sub": op_conf["sub"],
"aud": [self.RP_SUB],
"client_id": self.RP_SUB,
"scope": "openid",
}
jwt_at = create_jws(access_token, op_conf_priv_jwk, typ="at+jwt")
iss_token_data = dict(
session=session,
access_token= jwt_at,
id_token= "id_token",
expires=datetime_from_timestamp(exp_from_now())
)
IssuedToken.objects.create(**iss_token_data)
headers= {
"HTTP_AUTHORIZATION": f"Bearer {jwt_at}"
}
return headers
def test_userinfo_endpoint_no_header(self):
client = Client()
url = reverse("oidc_provider_userinfo_endpoint")
res = client.get(url, {})
self.assertTrue(res.status_code == 403)
def test_userinfo_endpoint_no_issued_token_session(self):
client = Client()
url = reverse("oidc_provider_userinfo_endpoint")
headers= {
"HTTP_AUTHORIZATION": "Bearer dC34Pf6kdG"
}
res = client.get(url, data = {}, **headers)
self.assertTrue(res.status_code == 403)
def test_userinfo_endpoint_no_tc(self):
headers = self.define_db()
client = Client()
url = reverse("oidc_provider_userinfo_endpoint")
res = client.get(url, data = {}, **headers)
self.assertTrue(res.status_code == 403)
def test_userinfo_endpoint_ok(self):
headers = self.define_db()
self.trust_chain = TrustChain.objects.create(
sub=self.RP_SUB,
exp=datetime_from_timestamp(exp_from_now(33)),
jwks = [],
metadata=RP_METADATA,
status="valid",
trust_anchor=self.ta_fes,
is_active=True,
)
client = Client()
url = reverse("oidc_provider_userinfo_endpoint")
res = client.get(url, data = {}, **headers)
self.assertTrue(res.status_code == 200)
@@ -0,0 +1,124 @@
from copy import deepcopy
from django.test import Client, TestCase
from django.urls import reverse
from spid_cie_oidc.accounts.models import User
from spid_cie_oidc.authority.tests.settings import (
RP_METADATA,
rp_conf
)
from spid_cie_oidc.entity.jwtse import create_jws
from spid_cie_oidc.entity.models import (
FederationEntityConfiguration,
FetchedEntityStatement,
TrustChain
)
from spid_cie_oidc.entity.tests.settings import TA_SUB
from spid_cie_oidc.entity.utils import (
datetime_from_timestamp,
exp_from_now,
iat_now
)
from spid_cie_oidc.provider.models import IssuedToken, OidcSession
from spid_cie_oidc.provider.tests.settings import (
op_conf,
op_conf_priv_jwk
)
class IntrospectionEndpointTest(TestCase):
def setUp(self):
self.RP_SUB = rp_conf["sub"]
self.RP_CLIENT_ID = rp_conf["metadata"]["openid_relying_party"]["client_id"]
self.jwt_auds = [op_conf["sub"], "http://testserver/oidc/op/", "http://testserver/oidc/op/introspection"]
CLIENT_ASSERTION = {
"iss": self.RP_SUB,
"sub": self.RP_SUB,
"aud": self.jwt_auds,
"exp": exp_from_now(),
"iat": iat_now(),
"jti": "jti",
}
self.ca_jws = create_jws(CLIENT_ASSERTION, rp_conf["jwks_core"][0])
token = {
"iss": self.RP_SUB,
"sub": op_conf["sub"],
"aud": self.jwt_auds,
"client_id": self.RP_SUB,
"scope": "openid",
}
self.jwt_token = create_jws(token, op_conf_priv_jwk, typ="at+jwt")
session = OidcSession.objects.create(
user=User.objects.create(username = "username"),
user_uid="",
nonce="",
authz_request={"scope": "openid"},
client_id=self.RP_SUB,
)
iss_token_data = dict(
session=session,
access_token= self.jwt_token,
id_token= "id_token",
refresh_token= "refresh_token",
expires=datetime_from_timestamp(exp_from_now())
)
IssuedToken.objects.create(**iss_token_data)
self.op_local_conf = deepcopy(op_conf)
FederationEntityConfiguration.objects.create(**self.op_local_conf)
self.ta_fes = FetchedEntityStatement.objects.create(
sub=TA_SUB,
iss=TA_SUB,
exp=datetime_from_timestamp(exp_from_now(33)),
iat=datetime_from_timestamp(iat_now()),
)
self.trust_chain = TrustChain.objects.create(
sub=self.RP_SUB,
exp=datetime_from_timestamp(exp_from_now(33)),
jwks = [],
metadata=RP_METADATA,
status="valid",
trust_anchor=self.ta_fes,
is_active=True,
)
def test_introspection_endpoint(self):
client = Client()
url = reverse("oidc_provider_introspection_endpoint")
request = {
"client_assertion" : self.ca_jws,
"client_assertion_type" : "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_id" : self.RP_SUB,
"token" : self.jwt_token
}
res = client.post(url, request)
self.assertTrue(res.status_code == 200)
self.assertTrue("openid" in res.content.decode())
def test_introspection_endpoint_get(self):
client = Client()
url = reverse("oidc_provider_introspection_endpoint")
request = {
"client_assertion" : self.ca_jws,
"client_assertion_type" : "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_id" : self.RP_SUB,
"token" : self.jwt_token
}
res = client.get(url, request)
self.assertTrue(res.status_code == 400)
def test_introspection_endpoint_validation_error(self):
client = Client()
url = reverse("oidc_provider_introspection_endpoint")
request = {
"client_assertion" : self.ca_jws,
"client_assertion_type" : "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"token" : self.jwt_token
}
res = client.post(url, request)
self.assertTrue(res.status_code == 400)
self.assertTrue(res.json()["error"] == "invalid_request")
@@ -0,0 +1,147 @@
import logging
from django.test import TestCase
from pydantic import ValidationError
from spid_cie_oidc.provider.schemas.jwt import JwtStructure
from spid_cie_oidc.provider.schemas.token_requests import (
TokenAuthnCodeRequest,
TokenRefreshRequest,
)
from .token_request_settings import (
JWT_CLIENT_ASSERTION,
JWT_CLIENT_ASSERTION_NO_AUD,
JWT_CLIENT_ASSERTION_NO_CORRECT_AUD,
JWT_CLIENT_ASSERTION_NO_CORRECT_EXP,
JWT_CLIENT_ASSERTION_NO_CORRECT_IAT,
JWT_CLIENT_ASSERTION_NO_CORRECT_ISS,
JWT_CLIENT_ASSERTION_NO_CORRECT_JTI,
JWT_CLIENT_ASSERTION_NO_CORRECT_SUB,
JWT_CLIENT_ASSERTION_NO_EXP,
JWT_CLIENT_ASSERTION_NO_IAT,
JWT_CLIENT_ASSERTION_NO_ISS,
JWT_CLIENT_ASSERTION_NO_JTI,
JWT_CLIENT_ASSERTION_NO_SUB,
TOKEN_AUTHN_CODE_REQUEST,
TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION,
TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION_TYPE,
TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ID,
TOKEN_AUTHN_CODE_REQUEST_NO_CODE,
TOKEN_AUTHN_CODE_REQUEST_NO_CODE_VERIFY,
TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION,
TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE,
TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_GRANT_TYPE,
TOKEN_AUTHN_CODE_REQUEST_NO_GRANT_TYPE,
TOKEN_REFRESH_REQUEST,
TOKEN_REFRESH_REQUEST_NO_CORRECT_GRANT_TYPE,
TOKEN_REFRESH_REQUEST_NO_GRANT_TYPE,
)
logger = logging.getLogger(__name__)
class TokenRequestTest(TestCase):
def test_validate_request(self):
TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST)
def test_validate_request_no_client(self):
with self.assertRaises(ValidationError):
TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ID)
def test_validate_request_no_client_assertion(self):
with self.assertRaises(ValidationError):
TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION)
def test_validate_request_no_correct_client_assertion(self):
with self.assertRaises(ValidationError):
TokenAuthnCodeRequest(
**TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION
)
def test_validate_request_no_client_assertion_type(self):
with self.assertRaises(ValidationError):
TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION_TYPE)
def test_validate_request_no_correct_client_assertion_type(self):
with self.assertRaises(ValidationError):
TokenAuthnCodeRequest(
**TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE
)
def test_validate_request_no_code(self):
with self.assertRaises(ValidationError):
TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_CODE)
def test_validate_request_no_code_verify(self):
with self.assertRaises(ValidationError):
TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_CODE_VERIFY)
def test_validate_request_no_grant_type(self):
with self.assertRaises(ValidationError):
TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_GRANT_TYPE)
def test_validate_request_no_correct_grant_type(self):
with self.assertRaises(ValidationError):
TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_GRANT_TYPE)
def test_validate_refresh_request(self):
TokenRefreshRequest(**TOKEN_REFRESH_REQUEST)
def test_validate_refresh_request_no_grant_type(self):
with self.assertRaises(ValidationError):
TokenRefreshRequest(**TOKEN_REFRESH_REQUEST_NO_GRANT_TYPE)
def test_validate_refresh_request_no_correct_grant_type(self):
with self.assertRaises(ValidationError):
TokenRefreshRequest(**TOKEN_REFRESH_REQUEST_NO_CORRECT_GRANT_TYPE)
def test_validate_jwt(self):
JwtStructure(**JWT_CLIENT_ASSERTION)
def test_validate_jwt_no_iss(self):
with self.assertRaises(ValidationError):
JwtStructure(**JWT_CLIENT_ASSERTION_NO_ISS)
def test_validate_jwt_no_correct_iss(self):
with self.assertRaises(ValidationError):
JwtStructure(**JWT_CLIENT_ASSERTION_NO_CORRECT_ISS)
def test_validate_jwt_no_sub(self):
with self.assertRaises(ValidationError):
JwtStructure(**JWT_CLIENT_ASSERTION_NO_SUB)
def test_validate_jwt_no_correct_sub(self):
with self.assertRaises(ValidationError):
JwtStructure(**JWT_CLIENT_ASSERTION_NO_CORRECT_SUB)
def test_validate_jwt_no_iat(self):
with self.assertRaises(ValidationError):
JwtStructure(**JWT_CLIENT_ASSERTION_NO_IAT)
def test_validate_jwt_no_correct_iat(self):
with self.assertRaises(ValidationError):
JwtStructure(**JWT_CLIENT_ASSERTION_NO_CORRECT_IAT)
def test_validate_jwt_no_exp(self):
with self.assertRaises(ValidationError):
JwtStructure(**JWT_CLIENT_ASSERTION_NO_EXP)
def test_validate_jwt_no_correct_exp(self):
with self.assertRaises(ValidationError):
JwtStructure(**JWT_CLIENT_ASSERTION_NO_CORRECT_EXP)
def test_validate_jwt_no_jti(self):
with self.assertRaises(ValidationError):
JwtStructure(**JWT_CLIENT_ASSERTION_NO_JTI)
def test_validate_jwt_no_correct_jti(self):
with self.assertRaises(ValidationError):
JwtStructure(**JWT_CLIENT_ASSERTION_NO_CORRECT_JTI)
def test_validate_jwt_no_aud(self):
with self.assertRaises(ValidationError):
JwtStructure(**JWT_CLIENT_ASSERTION_NO_AUD)
def test_validate_jwt_no_correct_aud(self):
with self.assertRaises(ValidationError):
JwtStructure(**JWT_CLIENT_ASSERTION_NO_CORRECT_AUD)
@@ -0,0 +1,11 @@
from django.test import TestCase
from spid_cie_oidc.provider.processors import spidCode
class ProcessorsTest(TestCase):
def test_processors(self):
processed = "9893261da4a1fec47a19b2cc4237d82f3eb70ddafdaad51bb61138b5126e72644c3189db53f9303cdc85cb84438caf9c07431944894b5742a1d33d111abf88c8"
res = spidCode({"username" : "testusername"}, {})
self.assertEqual(res, processed)
@@ -0,0 +1,77 @@
import logging
from django.test import TestCase
from pydantic import ValidationError
from spid_cie_oidc.provider.tests.token_response_settings import (
TOKEN_ERROR_RESPONSE,
TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR,
TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION,
TOKEN_ERROR_RESPONSE_NO_ERROR,
TOKEN_ERROR_RESPONSE_NO_ERROR_DESCRIPTION,
TOKEN_REFRESH_RESPONSE,
TOKEN_REFRESH_RESPONSE_NO_CORRECT_REFRESH_TOKEN,
TOKEN_REFRESH_RESPONSE_NO_REFRESH_TOKEN,
TOKEN_RESPONSE,
TOKEN_RESPONSE_NO_ACCESS_TOKEN,
TOKEN_RESPONSE_NO_CORRECT_ACCESS_TOKEN,
TOKEN_RESPONSE_NO_CORRECT_TOKEN_TYPE,
TOKEN_RESPONSE_NO_TOKEN_TYPE,
)
from spid_cie_oidc.provider.schemas.token_response import (
TokenErrorResponse,
TokenRefreshResponse,
TokenResponse,
)
logger = logging.getLogger(__name__)
class TokenResponseTest(TestCase):
def test_validate_token_response(self):
TokenResponse(**TOKEN_RESPONSE)
def test_validate_token_response(self):
TokenRefreshResponse(**TOKEN_REFRESH_RESPONSE)
def test_validate_token_response_no_access_token(self):
with self.assertRaises(ValidationError):
TokenResponse(**TOKEN_RESPONSE_NO_ACCESS_TOKEN)
def test_validate_token_response_no_correct_access_token(self):
with self.assertRaises(ValidationError):
TokenResponse(**TOKEN_RESPONSE_NO_CORRECT_ACCESS_TOKEN)
def test_validate_token_response_no_token_type(self):
with self.assertRaises(ValidationError):
TokenResponse(**TOKEN_RESPONSE_NO_TOKEN_TYPE)
def test_validate_token_response_no_correct_token_type(self):
with self.assertRaises(ValidationError):
TokenResponse(**TOKEN_RESPONSE_NO_CORRECT_TOKEN_TYPE)
def test_validate_token_refresh_response_no_refresh_token(self):
with self.assertRaises(ValidationError):
TokenRefreshResponse(**TOKEN_REFRESH_RESPONSE_NO_REFRESH_TOKEN)
def test_validate_token_refresh_response_no_correct_refresh_token(self):
with self.assertRaises(ValidationError):
TokenRefreshResponse(**TOKEN_REFRESH_RESPONSE_NO_CORRECT_REFRESH_TOKEN)
def test_validate_token_error_response(self):
TokenErrorResponse(**TOKEN_ERROR_RESPONSE)
def test_validate_token_error_no_error(self):
with self.assertRaises(ValidationError):
TokenErrorResponse(**TOKEN_ERROR_RESPONSE_NO_ERROR)
def test_validate_token_error_no_correct_error(self):
with self.assertRaises(ValidationError):
TokenErrorResponse(**TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR)
def test_validate_token_error_no_error_description(self):
with self.assertRaises(ValidationError):
TokenErrorResponse(**TOKEN_ERROR_RESPONSE_NO_ERROR_DESCRIPTION)
def test_validate_token_error_no_correct_error_description(self):
with self.assertRaises(ValidationError):
TokenErrorResponse(**TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION)
@@ -0,0 +1,89 @@
from unittest.mock import patch
from django.core.management import call_command
from django.test import TestCase, override_settings
from spid_cie_oidc.authority.tests.settings import (
RP_CONF_AS_JSON,
RP_METADATA_JWK1
)
from spid_cie_oidc.entity.jwtse import create_jws
from spid_cie_oidc.entity.models import (
FetchedEntityStatement,
TrustChain
)
from spid_cie_oidc.entity.tests.settings import (
TA_JWK_PRIVATE,
TA_SUB,
ta_conf_data_as_json
)
from spid_cie_oidc.entity.utils import (
datetime_from_timestamp,
exp_from_now,
iat_now
)
EXP = datetime_from_timestamp(exp_from_now(33))
NOW = datetime_from_timestamp(iat_now())
def create_entity_config():
JWS_RP = create_jws(RP_CONF_AS_JSON, RP_METADATA_JWK1, typ="entity-statement+jwt")
JWS_TA = create_jws(ta_conf_data_as_json, TA_JWK_PRIVATE, typ="entity-statement+jwt")
return [JWS_RP, JWS_TA]
def create_tc():
ta_fes = FetchedEntityStatement.objects.create(
sub=TA_SUB,
iss=TA_SUB,
exp=EXP,
iat=NOW
)
return TrustChain.objects.create(
sub=RP_CONF_AS_JSON["sub"],
exp=EXP,
jwks = [],
metadata=RP_CONF_AS_JSON["metadata"],
status="valid",
trust_anchor=ta_fes,
is_active=True,
)
class FetchRPTest(TestCase):
def setUp(self):
self.ta_fes = FetchedEntityStatement.objects.create(
sub=TA_SUB,
iss=TA_SUB,
exp=datetime_from_timestamp(exp_from_now(33)),
iat=datetime_from_timestamp(iat_now()),
)
def exec(self, cmd_name:str, *args, **kwargs):
call_command(
cmd_name,
*args,
**kwargs,
)
@override_settings(OIDCFED_IDENTITY_PROVIDERS = {"spid":{"http://127.0.0.1:8000/oidc/op/" :"http://testserver/"}, "cie":{}})
@override_settings(OIDCFED_TRUST_ANCHOR = [])
def test_fetch_rp(self):
self.patcher = patch(
"spid_cie_oidc.entity.statements.get_entity_configurations",
return_value = create_entity_config()
)
self.patcher.start()
self.patcher = patch(
"spid_cie_oidc.entity.statements.get_http_url",
return_value = ['["http://rp-test.it/oidc/rp/"]']
)
self.patcher.start()
self.patcher = patch(
"spid_cie_oidc.entity.trust_chain_operations.get_or_create_trust_chain",
return_value = create_tc()
)
self.patcher.start()
self.exec('fetch_openid_relying_parties', '--from', 'url')
self.patcher.stop()
@@ -0,0 +1,53 @@
from django.test import TestCase
from pydantic import ValidationError
from spid_cie_oidc.provider.tests.introspection_request_settings import (
INTROSPECTION_REQUEST,
INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION,
INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION_TYPE,
INTROSPECTION_REQUEST_NO_CLIENT_ID,
INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION,
INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE,
INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ID,
INTROSPECTION_REQUEST_NO_CORRECT_TOKEN,
INTROSPECTION_REQUEST_NO_TOKEN,
)
from spid_cie_oidc.provider.schemas.introspection_request import IntrospectionRequest
class IntrospectionRequestTest(TestCase):
def test_validate_introspection_request(self):
IntrospectionRequest(**INTROSPECTION_REQUEST)
def test_validate_introspection_request_no_client_assertion(self):
with self.assertRaises(ValidationError):
IntrospectionRequest(**INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION)
def test_validate_introspection_request_no_correct_client_assertion(self):
with self.assertRaises(ValidationError):
IntrospectionRequest(**INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION)
def test_validate_introspection_request_no_client_assertion_type(self):
with self.assertRaises(ValidationError):
IntrospectionRequest(**INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION_TYPE)
def test_validate_introspection_request_no_correct_client_assertion_type(self):
with self.assertRaises(ValidationError):
IntrospectionRequest(
**INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE
)
def test_validate_introspection_request_no_client_id(self):
with self.assertRaises(ValidationError):
IntrospectionRequest(**INTROSPECTION_REQUEST_NO_CLIENT_ID)
def test_validate_introspection_request_no_correct_client_id(self):
with self.assertRaises(ValidationError):
IntrospectionRequest(**INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ID)
def test_validate_introspection_request_no_token(self):
with self.assertRaises(ValidationError):
IntrospectionRequest(**INTROSPECTION_REQUEST_NO_TOKEN)
def test_validate_introspection_request_no_correct_token(self):
with self.assertRaises(ValidationError):
IntrospectionRequest(**INTROSPECTION_REQUEST_NO_CORRECT_TOKEN)
@@ -0,0 +1,68 @@
from django.test import TestCase
from pydantic import ValidationError
from spid_cie_oidc.provider.tests.introspection_response_settings import (
INTROSPECTION_ERROR_RESPONSE,
INTROSPECTION_ERROR_RESPONSE_CIE_NO_CORRECT_ERROR,
INTROSPECTION_ERROR_RESPONSE_CIE_NO_ERROR,
INTROSPECTION_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION,
INTROSPECTION_ERROR_RESPONSE_NO_ERROR_DESCRIPTION,
INTROSPECTION_ERROR_RESPONSE_SPID_NO_CORRECT_ERROR,
INTROSPECTION_ERROR_RESPONSE_SPID_NO_ERROR,
INTROSPECTION_RESPONSE,
INTROSPECTION_RESPONSE_NO_ACTIVE,
INTROSPECTION_RESPONSE_NO_CORRECT_ACTIVE,
)
from spid_cie_oidc.provider.schemas.introspection_response import (
IntrospectionErrorResponse,
IntrospectionErrorResponseCie,
IntrospectionErrorResponseSpid,
IntrospectionResponse,
)
class IntrospectionResponseTest(TestCase):
def test_validate_introspection_response(self):
IntrospectionResponse(**INTROSPECTION_RESPONSE)
def test_validate_introspection_response_no_active(self):
with self.assertRaises(ValidationError):
IntrospectionResponse(**INTROSPECTION_RESPONSE_NO_ACTIVE)
def test_validate_introspection_response_no_correct_active(self):
with self.assertRaises(ValidationError):
IntrospectionResponse(**INTROSPECTION_RESPONSE_NO_CORRECT_ACTIVE)
def test_validate_introspection_error_response(self):
IntrospectionErrorResponse(**INTROSPECTION_ERROR_RESPONSE)
def test_validate_introspection_response_spid_no_error(self):
with self.assertRaises(ValidationError):
IntrospectionErrorResponseSpid(**INTROSPECTION_ERROR_RESPONSE_SPID_NO_ERROR)
def test_validate_introspection_response_spid_no_correct_error(self):
with self.assertRaises(ValidationError):
IntrospectionErrorResponseSpid(
**INTROSPECTION_ERROR_RESPONSE_SPID_NO_CORRECT_ERROR
)
def test_validate_introspection_response_cie_no_error(self):
with self.assertRaises(ValidationError):
IntrospectionErrorResponseCie(**INTROSPECTION_ERROR_RESPONSE_CIE_NO_ERROR)
def test_validate_introspection_response_cie_no_correct_error(self):
with self.assertRaises(ValidationError):
IntrospectionErrorResponseCie(
**INTROSPECTION_ERROR_RESPONSE_CIE_NO_CORRECT_ERROR
)
def test_validate_introspection_response_no_error_description(self):
with self.assertRaises(ValidationError):
IntrospectionErrorResponse(
**INTROSPECTION_ERROR_RESPONSE_NO_ERROR_DESCRIPTION
)
def test_validate_introspection_response_no_correct_error_description(self):
with self.assertRaises(ValidationError):
IntrospectionErrorResponse(
**INTROSPECTION_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION
)
@@ -0,0 +1,145 @@
from copy import deepcopy
from django.test import Client, TestCase
from django.urls import reverse
from django.utils import timezone
from spid_cie_oidc.accounts.models import User
from spid_cie_oidc.authority.tests.settings import (
RP_METADATA,
RP_METADATA_JWK1,
rp_conf
)
from spid_cie_oidc.entity.jwtse import create_jws
from spid_cie_oidc.entity.models import (
FederationEntityConfiguration,
FetchedEntityStatement,
TrustChain
)
from spid_cie_oidc.entity.tests.settings import TA_SUB
from spid_cie_oidc.entity.utils import (
datetime_from_timestamp,
exp_from_now,
iat_now
)
from spid_cie_oidc.provider.models import IssuedToken, OidcSession
from spid_cie_oidc.provider.tests.settings import op_conf, op_conf_priv_jwk
from spid_cie_oidc.relying_party.utils import get_pkce
RP_SUB = rp_conf["sub"]
RP_CLIENT_ID = rp_conf["metadata"]["openid_relying_party"]["client_id"]
PKCE = get_pkce()
class RefreshTokenTest(TestCase):
def setUp(self):
self.op_local_conf = deepcopy(op_conf)
FederationEntityConfiguration.objects.create(**self.op_local_conf)
self.ta_fes = FetchedEntityStatement.objects.create(
sub=TA_SUB,
iss=TA_SUB,
exp=datetime_from_timestamp(exp_from_now(33)),
iat=datetime_from_timestamp(iat_now()),
)
self.trust_chain = TrustChain.objects.create(
sub=RP_SUB,
exp=datetime_from_timestamp(exp_from_now(33)),
jwks = [],
metadata=RP_METADATA,
status="valid",
trust_anchor=self.ta_fes,
is_active=True,
)
self.jwt_auds = [op_conf["sub"], "http://testserver/oidc/op/", "http://testserver/oidc/op/token"]
CLIENT_ASSERTION = {
"iss": RP_SUB,
"sub": RP_SUB,
"aud": self.jwt_auds,
"exp": exp_from_now(),
"iat": iat_now(),
"jti": "jti",
}
self.ca_jws = create_jws(CLIENT_ASSERTION, RP_METADATA_JWK1)
self.refresh_token = {
"iss": self.op_local_conf["sub"],
"sub": RP_SUB,
"exp": exp_from_now(),
"iat": iat_now(),
"aud": self.jwt_auds,
"client_id": RP_CLIENT_ID,
"scope": "openid",
}
session = OidcSession.objects.create(
user=User.objects.create(username = "username"),
user_uid="",
nonce="",
authz_request={"scope": "openid", "nonce": "123", "code_challenge": PKCE["code_challenge"]},
client_id="",
auth_code="code",
)
IssuedToken.objects.create(
access_token = create_jws(self.refresh_token, op_conf_priv_jwk, typ="at-jwt"),
refresh_token = create_jws(self.refresh_token, op_conf_priv_jwk),
session = session,
expires = timezone.localtime()
)
def test_token_endpoint(self):
client = Client()
url = reverse("oidc_provider_token_endpoint")
request = dict(
client_id = RP_CLIENT_ID,
client_assertion = self.ca_jws,
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
grant_type="authorization_code",
code="code",
code_verifier = PKCE["code_verifier"]
)
res = client.post(url, request)
self.assertTrue(res.status_code == 200)
self.assertTrue(res.json().get("access_token"))
def test_token_endpoint_get(self):
client = Client()
url = reverse("oidc_provider_token_endpoint")
request = dict(
client_id = RP_CLIENT_ID,
client_assertion = self.ca_jws,
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
grant_type="authorization_code",
code = "code",
code_verifier = "code_verifier"
)
res = client.get(url, request)
self.assertTrue(res.status_code == 400)
def test_token_endpoint_no_client_id(self):
client = Client()
url = reverse("oidc_provider_token_endpoint")
request = dict(
client_assertion = self.ca_jws,
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
grant_type="authorization_code",
code = "code",
code_verifier = "code_verifier"
)
res = client.post(url, request)
self.assertTrue(res.status_code == 400)
self.assertTrue("invalid_request" == res.json().get("error"))
def test_token_endpoint_no_session(self):
OidcSession.objects.all().delete()
client = Client()
url = reverse("oidc_provider_token_endpoint")
request = dict(
client_id = RP_CLIENT_ID,
client_assertion = self.ca_jws,
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
grant_type="authorization_code",
code = "code",
code_verifier = "code_verifier"
)
res = client.post(url, request)
self.assertTrue(res.status_code == 400)
@@ -0,0 +1,110 @@
from copy import deepcopy
from django.test import Client, TestCase
from django.urls import reverse
from spid_cie_oidc.accounts.models import User
from spid_cie_oidc.authority.tests.settings import (
RP_CONF_AS_JSON,
RP_METADATA_JWK1,
rp_conf
)
from spid_cie_oidc.entity.jwtse import create_jws
from spid_cie_oidc.entity.models import (
FederationEntityConfiguration,
FetchedEntityStatement,
TrustChain
)
from spid_cie_oidc.entity.tests.settings import TA_SUB
from spid_cie_oidc.entity.utils import (
datetime_from_timestamp,
exp_from_now,
iat_now
)
from spid_cie_oidc.provider.models import IssuedToken, OidcSession
from spid_cie_oidc.provider.tests.settings import op_conf, op_conf_priv_jwk
RP_SUB = rp_conf["sub"]
RP_CLIENT_ID = rp_conf["metadata"]["openid_relying_party"]["client_id"]
class RevocationEndponitTest(TestCase):
def setUp(self):
ta_fes = FetchedEntityStatement.objects.create(
sub=TA_SUB,
iss=TA_SUB,
exp=datetime_from_timestamp(exp_from_now(33)),
iat=datetime_from_timestamp(iat_now()),
)
self.op_local_conf = deepcopy(op_conf)
FederationEntityConfiguration.objects.create(**self.op_local_conf)
self.jwt_auds = [op_conf["sub"], "http://testserver/oidc/op/", "http://testserver/oidc/op/revocation"]
CLIENT_ASSERTION = {
"iss": RP_SUB,
"sub": RP_SUB,
"aud": self.jwt_auds,
"exp": exp_from_now(),
"iat": iat_now(),
"jti": "jti",
}
self.ca_jws = create_jws(CLIENT_ASSERTION, RP_METADATA_JWK1)
access_token = {
"iss": self.op_local_conf["sub"],
"sub": RP_SUB,
"aud": self.jwt_auds,
"client_id": RP_CLIENT_ID,
"scope": "openid",
}
self.at_jws = create_jws(access_token, op_conf_priv_jwk)
self.request = dict(
client_id = RP_CLIENT_ID,
client_assertion = self.ca_jws,
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
token = self.at_jws,
)
session = OidcSession.objects.create(
user=User.objects.create(username = "username"),
user_uid="",
nonce="",
authz_request={"scope": "openid"},
client_id=rp_conf["sub"],
)
iss_token_data = dict(
session=session,
access_token= self.at_jws,
id_token= "id_token",
refresh_token= "refresh_token",
expires=datetime_from_timestamp(exp_from_now())
)
IssuedToken.objects.create(**iss_token_data)
TrustChain.objects.create(
sub=RP_CONF_AS_JSON["sub"],
exp=datetime_from_timestamp(exp_from_now(33)),
jwks = [],
metadata=RP_CONF_AS_JSON["metadata"],
status="valid",
trust_anchor=ta_fes,
is_active=True,
)
def test_revocation_endpoint(self):
client = Client()
url = reverse("oidc_provider_end_session_endpoint")
res = client.post(url, self.request)
self.assertTrue(res.status_code == 200)
def test_revocation_endpoint_no_correct_client_assert(self):
client = Client()
request = deepcopy(self.request)
request["client_assertion"] = "q.q.q"
url = reverse("oidc_provider_end_session_endpoint")
res = client.post(url, request)
self.assertTrue(res.status_code == 400)
def test_revocation_endpoint_no_issued_token(self):
client = Client()
IssuedToken.objects.all().delete()
url = reverse("oidc_provider_end_session_endpoint")
res = client.post(url, self.request)
self.assertTrue(res.status_code == 400)
@@ -0,0 +1,62 @@
from copy import deepcopy
from django.test import TestCase
from pydantic import ValidationError
from spid_cie_oidc.provider.tests.revocation_request_settings import (
REVOCATION_REQUEST,
REVOCATION_REQUEST_NO_CLIENT_ASSERTION,
REVOCATION_REQUEST_NO_CLIENT_ASSERTION_TYPE,
REVOCATION_REQUEST_NO_CLIENT_ID,
REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION,
REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE,
REVOCATION_REQUEST_NO_CORRECT_CLIENT_ID,
REVOCATION_REQUEST_NO_CORRECT_TOKEN,
REVOCATION_REQUEST_NO_TOKEN,
)
from spid_cie_oidc.entity.models import (
FederationEntityConfiguration
)
from spid_cie_oidc.provider.schemas.revocation_request import RevocationRequest
from spid_cie_oidc.provider.tests.settings import (
op_conf
)
class RevocationRequestTest(TestCase):
def setUp(self):
self.op_local_conf = deepcopy(op_conf)
FederationEntityConfiguration.objects.create(**self.op_local_conf)
def test_validate_revocation_request(self):
RevocationRequest(**REVOCATION_REQUEST)
def test_validate_revocation_request_no_client_assertion(self):
with self.assertRaises(ValidationError):
RevocationRequest(**REVOCATION_REQUEST_NO_CLIENT_ASSERTION)
def test_validate_revocation_request_no_correct_client_assertion(self):
with self.assertRaises(ValidationError):
RevocationRequest(**REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION)
def test_validate_revocation_request_no_client_assertion_type(self):
with self.assertRaises(ValidationError):
RevocationRequest(**REVOCATION_REQUEST_NO_CLIENT_ASSERTION_TYPE)
def test_validate_revocation_request_no_correct_client_assertion_type(self):
with self.assertRaises(ValidationError):
RevocationRequest(**REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE)
def test_validate_revocation_request_no_client_id(self):
with self.assertRaises(ValidationError):
RevocationRequest(**REVOCATION_REQUEST_NO_CLIENT_ID)
def test_validate_revocation_request_no_correct_client_id(self):
with self.assertRaises(ValidationError):
RevocationRequest(**REVOCATION_REQUEST_NO_CORRECT_CLIENT_ID)
def test_validate_revocation_request_no_token(self):
with self.assertRaises(ValidationError):
RevocationRequest(**REVOCATION_REQUEST_NO_TOKEN)
def test_validate_revocation_request_no_correct_token(self):
with self.assertRaises(ValidationError):
RevocationRequest(**REVOCATION_REQUEST_NO_CORRECT_TOKEN)
@@ -0,0 +1,18 @@
from django.test import Client, TestCase
from django.urls import reverse
from spid_cie_oidc.authority.tests.settings import rp_conf
class NoConsentTest(TestCase):
def setUp(self):
pass
def test_no_consent(self):
client = Client()
url = reverse("oidc_provider_not_consent")
redirect_uri = rp_conf["metadata"]["openid_relying_party"]["redirect_uris"][0]
res = client.get(url + "?redirect_uri=" + redirect_uri)
self.assertTrue(res.status_code == 302)
self.assertTrue("Authentication+request+rejected+by+user" in res.url)
@@ -0,0 +1,35 @@
from django.test import TestCase
from pydantic import ValidationError
from spid_cie_oidc.provider.tests.revocation_response_settings import (
REVOCATION_RESPONSE_CIE,
REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR,
REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR_DESCRIPTION,
REVOCATION_RESPONSE_CIE_NO_ERROR,
REVOCATION_RESPONSE_CIE_NO_ERROR_DESCRIPTION,
)
from spid_cie_oidc.provider.schemas.revocation_response import (
RevocationErrorResponse,
)
class RevocationResponseTest(TestCase):
def test_validate_revocation_response(self):
RevocationErrorResponse(**REVOCATION_RESPONSE_CIE)
def test_validate_revocation_response_no_error(self):
with self.assertRaises(ValidationError):
RevocationErrorResponse(**REVOCATION_RESPONSE_CIE_NO_ERROR)
def test_validate_revocation_response_no_correct_error(self):
with self.assertRaises(ValidationError):
RevocationErrorResponse(**REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR)
def test_validate_revocation_response_no_error_description(self):
with self.assertRaises(ValidationError):
RevocationErrorResponse(**REVOCATION_RESPONSE_CIE_NO_ERROR_DESCRIPTION)
def test_validate_revocation_response_no_correct_error_description(self):
with self.assertRaises(ValidationError):
RevocationErrorResponse(
**REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR_DESCRIPTION
)
@@ -0,0 +1,70 @@
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse
from spid_cie_oidc.authority.tests.settings import rp_conf
from spid_cie_oidc.provider.models import OidcSession
class UserAccessHistoryTest(TestCase):
def setUp(self):
self.user = get_user_model().objects.create(
username="test",
first_name="test",
last_name="test",
email="[email protected]",
is_staff=True
)
self.user.set_password("test")
self.user.save()
OidcSession.objects.create(
user=self.user,
user_uid="uid",
nonce="",
authz_request={
"scope": "openid",
"nonce": "123",
"acr_values": ["https://www.spid.gov.it/SpidL2"],
"redirect_uri": rp_conf["metadata"]["openid_relying_party"]["redirect_uris"][0],
"state": "state",
},
client_id="",
auth_code="auth_code",
)
OidcSession.objects.create(
user=self.user,
user_uid="uid",
nonce="1234",
authz_request={
"scope": "openid",
"nonce": "1234",
"acr_values": ["https://www.spid.gov.it/SpidL2"],
"redirect_uri": rp_conf["metadata"]["openid_relying_party"]["redirect_uris"][0],
"state": "state",
},
client_id="",
auth_code="code",
)
def test_user_access_history(self):
client = Client()
client.login(username="test", password="test")
session = client.session
session.update({"oidc": {"auth_code": "code"}, "user_uid": "uid"})
session.save()
url = reverse("oidc_provider_access_history")
res = client.get(url)
self.assertTrue(res.status_code == 200)
self.assertTrue("auth_code=auth_code" in res.content.decode())
def test_user_access_history_revoke(self):
client = Client()
client.login(username="test", password="test")
session = client.session
session.update({"oidc": {"auth_code": "code"}, "user_uid": "uid"})
session.save()
url = reverse("oidc_provider_revoke_session")
res = client.get(url, {"auth_code": "auth_code"})
self.assertTrue(res.status_code == 302)
url_history = reverse("oidc_provider_access_history")
self.assertTrue(url_history in res.url)
@@ -0,0 +1,36 @@
from django.test import TestCase
from spid_cie_oidc.provider.models import OidcSession
from spid_cie_oidc.provider.views.__init__ import OpBase
class InitTest(TestCase):
def setUp(self):
self.client_assertion = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjJIbm9GUzNZbkM5dGppQ2FpdmhXTFZVSjNBeHdHR3pfOTh1UkZhcU1FRXMifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvb2lkYy9ycC8iLCJzdWIiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvb2lkYy9ycC8iLCJhdWQiOlsiaHR0cDovLzEyNy4wLjAuMTo4MDAwL29pZGMvb3AvdG9rZW4vIl0sImlhdCI6MTY0ODQ5NTc1NywiZXhwIjoxNjQ4NDk3NzM3LCJqdGkiOiI0OWE2OGQyMS03NTE1LTRmNDMtODZhZC1hMDNhMWNmNDFmNTgifQ.EaWmmuDhoa8jt_go4zrY5AhTGnljIS1zNwIF-f9eBidAiWPYaKwmXRsRoXrAeybLcJ5E8-7TO1S8jZiCxcElQPdRRvuP9ZsgNhfEqDhZtabkwGBFt4gpQZnFsgGDMAi-v4sTM55VsnIJIMrHPNIckZfL-YhD-FSCtDsqrCkCnucXR58Kfp_SEx3hJvrU-2vcOeNXJBTqs3pHl9jws9GJIf6bZd7vQqTMolCC4zBzUp7yT3OXqMt9uCUhLZzLtpTRW0_-u-KW4h44eHOdEf6ePqiFhjflF9_T6cM-OBqzaK0eVvg6RcasvF3IyxdG-ME5crapAibeePqg_Hy__Q5idQ'
self.session = OidcSession.objects.create(
user_uid="",
nonce="",
authz_request={"scope": "offline_access", "prompt": "consent", "nonce": "123", "acr_values":["https://www.spid.gov.it/SpidL1"]},
client_id="",
auth_code="code",
)
def test_validate_authz_request_object(self):
with self.assertRaises(Exception):
OpBase.validate_authz_request_object(self,{})
def test_check_client_assertion(self):
with self.assertRaises(Exception):
OpBase.check_client_assertion(self, client_id = "", client_assertion = self.client_assertion)
def test_get_refresh_token(self):
result = OpBase.get_refresh_token(
self,
iss_sub = "sub",
sub = "sub",
authz = self.session,
jwt_at = "jwt_at",
commons= {}
)
self.assertEqual(result["sub"], "sub")
@@ -0,0 +1,107 @@
from copy import deepcopy
from spid_cie_oidc.entity.utils import exp_from_now, iat_now
TOKEN_REQUEST = {
"client_id": "https://rp.cie.it",
"client_assertion": "head.payload.verifysignature",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
}
TOKEN_AUTHN_CODE_REQUEST = deepcopy(TOKEN_REQUEST)
TOKEN_AUTHN_CODE_REQUEST["code"] = "usDwMnEzJPpG5oaV8x3j&"
TOKEN_AUTHN_CODE_REQUEST["code_verifier"] = "9g8S40MozM3NSqjHnhi7OnsE38jklFv2&"
TOKEN_AUTHN_CODE_REQUEST["grant_type"] = "authorization_code"
TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ID = deepcopy(TOKEN_AUTHN_CODE_REQUEST)
TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ID.pop("client_id")
TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION = deepcopy(TOKEN_AUTHN_CODE_REQUEST)
TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION.pop("client_assertion")
TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION = deepcopy(
TOKEN_AUTHN_CODE_REQUEST
)
TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION["client_assertion"] = "head..sign"
TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION_TYPE = deepcopy(TOKEN_AUTHN_CODE_REQUEST)
TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION_TYPE.pop("client_assertion_type")
TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE = deepcopy(
TOKEN_AUTHN_CODE_REQUEST
)
TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE[
"client_assertion_type"
] = ":params:oauth:client-assertion-type:"
TOKEN_AUTHN_CODE_REQUEST_NO_CODE = deepcopy(TOKEN_AUTHN_CODE_REQUEST)
TOKEN_AUTHN_CODE_REQUEST_NO_CODE.pop("code")
TOKEN_AUTHN_CODE_REQUEST_NO_CODE_VERIFY = deepcopy(TOKEN_AUTHN_CODE_REQUEST)
TOKEN_AUTHN_CODE_REQUEST_NO_CODE_VERIFY.pop("code_verifier")
TOKEN_AUTHN_CODE_REQUEST_NO_GRANT_TYPE = deepcopy(TOKEN_AUTHN_CODE_REQUEST)
TOKEN_AUTHN_CODE_REQUEST_NO_GRANT_TYPE.pop("grant_type")
TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_GRANT_TYPE = deepcopy(TOKEN_AUTHN_CODE_REQUEST)
TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_GRANT_TYPE["grant_type"] = "refresh_token"
TOKEN_REFRESH_REQUEST = deepcopy(TOKEN_REQUEST)
TOKEN_REFRESH_REQUEST["grant_type"] = "refresh_token"
TOKEN_REFRESH_REQUEST["refresh_token"] = "8xLOxBtZp8"
TOKEN_REFRESH_REQUEST_NO_GRANT_TYPE = deepcopy(TOKEN_REFRESH_REQUEST)
TOKEN_REFRESH_REQUEST_NO_GRANT_TYPE.pop("grant_type")
TOKEN_REFRESH_REQUEST_NO_CORRECT_GRANT_TYPE = deepcopy(TOKEN_REFRESH_REQUEST)
TOKEN_REFRESH_REQUEST_NO_CORRECT_GRANT_TYPE["grant_type"] = "authorization_code"
# TODO: we need factory function to get fresh now
IAT = iat_now()
EXP = exp_from_now()
JWT_CLIENT_ASSERTION = {
"iss": "https://rp.cie.it",
"sub": "https://rp.cie.it",
"iat": IAT,
"exp": EXP,
"aud": ["https://rp.spid.agid.gov.it/auth"],
"jti": "a72d5df0-2415-4c7c-a44f-3988b354040b",
}
JWT_CLIENT_ASSERTION_NO_ISS = deepcopy(JWT_CLIENT_ASSERTION)
JWT_CLIENT_ASSERTION_NO_ISS.pop("iss")
JWT_CLIENT_ASSERTION_NO_CORRECT_ISS = deepcopy(JWT_CLIENT_ASSERTION)
JWT_CLIENT_ASSERTION_NO_CORRECT_ISS["iss"] = "https://idserve.i"
JWT_CLIENT_ASSERTION_NO_SUB = deepcopy(JWT_CLIENT_ASSERTION)
JWT_CLIENT_ASSERTION_NO_SUB.pop("sub")
JWT_CLIENT_ASSERTION_NO_CORRECT_SUB = deepcopy(JWT_CLIENT_ASSERTION)
JWT_CLIENT_ASSERTION_NO_CORRECT_SUB["sub"] = "https://idserver"
JWT_CLIENT_ASSERTION_NO_IAT = deepcopy(JWT_CLIENT_ASSERTION)
JWT_CLIENT_ASSERTION_NO_IAT.pop("iat")
JWT_CLIENT_ASSERTION_NO_CORRECT_IAT = deepcopy(JWT_CLIENT_ASSERTION)
JWT_CLIENT_ASSERTION_NO_CORRECT_IAT["iat"] = str(IAT + 400)
JWT_CLIENT_ASSERTION_NO_EXP = deepcopy(JWT_CLIENT_ASSERTION)
JWT_CLIENT_ASSERTION_NO_EXP.pop("exp")
JWT_CLIENT_ASSERTION_NO_CORRECT_EXP = deepcopy(JWT_CLIENT_ASSERTION)
JWT_CLIENT_ASSERTION_NO_CORRECT_EXP["exp"] = 1000
JWT_CLIENT_ASSERTION_NO_JTI = deepcopy(JWT_CLIENT_ASSERTION)
JWT_CLIENT_ASSERTION_NO_JTI.pop("jti")
JWT_CLIENT_ASSERTION_NO_CORRECT_JTI = deepcopy(JWT_CLIENT_ASSERTION)
JWT_CLIENT_ASSERTION_NO_CORRECT_JTI["jti"] = []
JWT_CLIENT_ASSERTION_NO_AUD = deepcopy(JWT_CLIENT_ASSERTION)
JWT_CLIENT_ASSERTION_NO_AUD.pop("aud")
JWT_CLIENT_ASSERTION_NO_CORRECT_AUD = deepcopy(JWT_CLIENT_ASSERTION)
JWT_CLIENT_ASSERTION_NO_CORRECT_AUD["aud"] = "https://rp.spid.agid.gov.it/auth"
@@ -0,0 +1,47 @@
from copy import deepcopy
TOKEN_RESPONSE = {
"access_token": "dC34Pf6kdG.Gjpw5HN6c.JSKABYDGye6",
"token_type": "Bearer",
"expires_in": 1800,
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY.ashajrbGFDGd",
}
TOKEN_RESPONSE_NO_ACCESS_TOKEN = deepcopy(TOKEN_RESPONSE)
TOKEN_RESPONSE_NO_ACCESS_TOKEN.pop("access_token")
TOKEN_RESPONSE_NO_CORRECT_ACCESS_TOKEN = deepcopy(TOKEN_RESPONSE)
TOKEN_RESPONSE_NO_CORRECT_ACCESS_TOKEN["access_token"] = "dC34Pf6kdG.Gjpw5HN"
TOKEN_RESPONSE_NO_TOKEN_TYPE = deepcopy(TOKEN_RESPONSE)
TOKEN_RESPONSE_NO_TOKEN_TYPE.pop("token_type")
TOKEN_RESPONSE_NO_CORRECT_TOKEN_TYPE = deepcopy(TOKEN_RESPONSE)
TOKEN_RESPONSE_NO_CORRECT_TOKEN_TYPE["token_type"] = "prova"
TOKEN_REFRESH_RESPONSE = deepcopy(TOKEN_RESPONSE)
TOKEN_REFRESH_RESPONSE["refresh_token"] = "wJ848BcyLP.hHhJB.kjHMjUGUSGKEE3"
TOKEN_REFRESH_RESPONSE_NO_REFRESH_TOKEN = deepcopy(TOKEN_REFRESH_RESPONSE)
TOKEN_REFRESH_RESPONSE_NO_REFRESH_TOKEN.pop("refresh_token")
TOKEN_REFRESH_RESPONSE_NO_CORRECT_REFRESH_TOKEN = deepcopy(TOKEN_REFRESH_RESPONSE)
TOKEN_REFRESH_RESPONSE_NO_CORRECT_REFRESH_TOKEN["refresh_token"] = "dC34Pf6kdG..Gjpw5HN"
TOKEN_ERROR_RESPONSE = {
"error": "invalid_request",
"error_description": "descrizione dell’errore",
}
TOKEN_ERROR_RESPONSE_NO_ERROR = deepcopy(TOKEN_ERROR_RESPONSE)
TOKEN_ERROR_RESPONSE_NO_ERROR.pop("error")
TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR = deepcopy(TOKEN_ERROR_RESPONSE)
TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR["error"] = ""
TOKEN_ERROR_RESPONSE_NO_ERROR_DESCRIPTION = deepcopy(TOKEN_ERROR_RESPONSE)
TOKEN_ERROR_RESPONSE_NO_ERROR_DESCRIPTION.pop("error_description")
TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION = deepcopy(TOKEN_ERROR_RESPONSE)
TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION["error"] = []
@@ -0,0 +1,84 @@
from django.conf import settings
from django.urls import path
from .views.authz_request_view import AuthzRequestView
from .views.userinfo_endpoint import UserInfoEndpoint
from spid_cie_oidc.entity.views import (
openid_connect_jwks_uri,
openid_connect_signed_jwks_uri
)
from .views.connect import openid_configuration
from .views.consent_page_view import (
ConsentPageView,
oidc_provider_not_consent,
UserAccessHistoryView,
RevokeSessionView,
)
from .views.token_endpoint import TokenEndpoint
from .views.revocation_endpoint import RevocationEndpoint
from .views.introspection_endpoint import IntrospectionEndpoint
_PREF = getattr(settings, "OIDC_PREFIX", "oidc/op")
urlpatterns = [
path(
f"{_PREF}/authorization",
AuthzRequestView.as_view(),
name="oidc_provider_authnrequest",
),
path(
f"{_PREF}/consent",
ConsentPageView.as_view(),
name="oidc_provider_consent",
),
path(
f"{_PREF}/token",
TokenEndpoint.as_view(),
name="oidc_provider_token_endpoint",
),
path(
f"{_PREF}/userinfo",
UserInfoEndpoint.as_view(),
name="oidc_provider_userinfo_endpoint",
),
path(
f"{_PREF}/revocation",
RevocationEndpoint.as_view(),
name="oidc_provider_end_session_endpoint",
),
path(
f"{_PREF}/introspection",
IntrospectionEndpoint.as_view(),
name="oidc_provider_introspection_endpoint",
),
path(
f"{_PREF}/notconsent",
oidc_provider_not_consent,
name="oidc_provider_not_consent",
),
path(
f"{_PREF}/history",
UserAccessHistoryView.as_view(),
name="oidc_provider_access_history",
),
path(
f"{_PREF}/revoke",
RevokeSessionView.as_view(),
name="oidc_provider_revoke_session",
),
path(
f"{_PREF}/.well-known/openid-configuration",
openid_configuration,
name="oidc_provider_openid_configuration",
),
path(
f"{_PREF}/<str:metadata_type>/jwks.json",
openid_connect_jwks_uri,
name="oidc_connect_jwks_uri",
),
path(
f"{_PREF}/<str:metadata_type>/jwks.jose",
openid_connect_signed_jwks_uri,
name="oidc_connect_signed_jwks_uri",
),
]
@@ -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
@@ -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()
@@ -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")
@@ -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)
@@ -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()
@@ -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")