feat: link local library
@@ -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
|
||||
}
|
||||
|
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")
|
||||