255 lines
7.4 KiB
Python
255 lines
7.4 KiB
Python
import uuid
|
|
import json
|
|
|
|
from django.db import models
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.conf import settings
|
|
from django.core.exceptions import ValidationError
|
|
|
|
|
|
from jwcrypto.jwk import JWKSet, InvalidJWKValue, JWK
|
|
|
|
from django_rbac.utils import get_ou_model_name
|
|
|
|
from authentic2 import compat
|
|
|
|
from . import managers
|
|
|
|
|
|
def validate_jwkset(data):
|
|
data = json.dumps(data)
|
|
try:
|
|
JWKSet.from_json(data)
|
|
except InvalidJWKValue as e:
|
|
raise ValidationError(_('Invalid JWKSet: %s') % e)
|
|
|
|
|
|
class OIDCProvider(models.Model):
|
|
STRATEGY_CREATE = 'create'
|
|
STRATEGY_FIND_UUID = 'find-uuid'
|
|
STRATEGY_NONE = 'none'
|
|
|
|
STRATEGIES = [
|
|
(STRATEGY_CREATE, _('create')),
|
|
(STRATEGY_FIND_UUID, _('use sub to find existing user through UUID')),
|
|
(STRATEGY_NONE, _('none')),
|
|
]
|
|
ALGO_NONE = 0
|
|
ALGO_RSA = 1
|
|
ALGO_HMAC = 2
|
|
ALGO_CHOICES = [
|
|
(ALGO_NONE, _('none')),
|
|
(ALGO_RSA, _('RSA')),
|
|
(ALGO_HMAC, _('HMAC')),
|
|
]
|
|
|
|
name = models.CharField(
|
|
unique=True,
|
|
max_length=128,
|
|
verbose_name=_('name'))
|
|
slug = models.SlugField(
|
|
unique=True,
|
|
max_length=256,
|
|
verbose_name=_('slug'),
|
|
blank=True,
|
|
null=True)
|
|
issuer = models.CharField(
|
|
max_length=256,
|
|
verbose_name=_('issuer'),
|
|
unique=True,
|
|
db_index=True)
|
|
client_id = models.CharField(
|
|
max_length=128,
|
|
default=uuid.uuid4,
|
|
verbose_name=_('client id'))
|
|
client_secret = models.CharField(
|
|
max_length=128,
|
|
default=uuid.uuid4,
|
|
verbose_name=_('client secret'))
|
|
# endpoints
|
|
authorization_endpoint = models.URLField(
|
|
max_length=128,
|
|
verbose_name=_('authorization endpoint'))
|
|
token_endpoint = models.URLField(
|
|
max_length=128,
|
|
verbose_name=_('token endpoint'))
|
|
userinfo_endpoint = models.URLField(
|
|
max_length=128,
|
|
verbose_name=_('userinfo endpoint'))
|
|
end_session_endpoint = models.URLField(
|
|
max_length=128,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('end session endpoint'))
|
|
token_revocation_endpoint = models.URLField(
|
|
max_length=128,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('token revocation endpoint'))
|
|
scopes = models.CharField(
|
|
max_length=128,
|
|
blank=True,
|
|
verbose_name=_('scopes'))
|
|
jwkset_json = compat.JSONField(
|
|
verbose_name=_('JSON WebKey set'),
|
|
null=True,
|
|
blank=True,
|
|
validators=[validate_jwkset])
|
|
idtoken_algo = models.PositiveIntegerField(
|
|
default=ALGO_RSA,
|
|
choices=ALGO_CHOICES,
|
|
verbose_name=_('IDToken signature algorithm'))
|
|
claims_parameter_supported = models.BooleanField(
|
|
verbose_name=_('Claims parameter supported'),
|
|
default=False)
|
|
|
|
# ou where new users should be created
|
|
strategy = models.CharField(
|
|
max_length=32,
|
|
choices=STRATEGIES,
|
|
verbose_name=_('strategy'))
|
|
ou = models.ForeignKey(
|
|
to=get_ou_model_name(),
|
|
verbose_name=_('organizational unit'))
|
|
|
|
# policy
|
|
max_auth_age = models.PositiveIntegerField(
|
|
verbose_name=_('max authentication age'),
|
|
blank=True,
|
|
null=True)
|
|
|
|
# hide OP from login page
|
|
show = models.BooleanField(
|
|
verbose_name=_('show on login page'),
|
|
blank=True,
|
|
default=True)
|
|
|
|
# metadata
|
|
created = models.DateTimeField(
|
|
verbose_name=_('created'),
|
|
auto_now_add=True)
|
|
modified = models.DateTimeField(
|
|
verbose_name=_('modified'),
|
|
auto_now=True)
|
|
|
|
objects = managers.OIDCProviderManager()
|
|
|
|
@property
|
|
def jwkset(self):
|
|
from authentic2.crypto import base64url_encode
|
|
|
|
if self.idtoken_algo == self.ALGO_RSA:
|
|
if self.jwkset_json:
|
|
return JWKSet.from_json(json.dumps(self.jwkset_json))
|
|
if self.idtoken_algo == self.ALGO_HMAC:
|
|
return JWK(kty='oct', k=base64url_encode(self.client_secret.encode('utf-8')))
|
|
return None
|
|
|
|
def __unicode__(self):
|
|
return self.name
|
|
|
|
def authorization_claims_parameter(self):
|
|
idtoken_claims = {}
|
|
userinfo_claims = {}
|
|
for claim_mapping in self.claim_mappings.all():
|
|
d = idtoken_claims if claim_mapping.idtoken_claim else userinfo_claims
|
|
value = d.setdefault(claim_mapping.claim, {}) or {}
|
|
if claim_mapping.required:
|
|
value['essential'] = True
|
|
d[claim_mapping.claim] = value or None
|
|
return {
|
|
'id_token': idtoken_claims,
|
|
'userinfo': userinfo_claims,
|
|
}
|
|
|
|
def __repr__(self):
|
|
return '<OIDCProvider %r>' % self.issuer
|
|
|
|
|
|
class OIDCClaimMapping(models.Model):
|
|
NOT_VERIFIED = 0
|
|
VERIFIED_CLAIM = 1
|
|
ALWAYS_VERIFIED = 2
|
|
VERIFIED_CHOICES = [
|
|
(NOT_VERIFIED, _('not verified')),
|
|
(VERIFIED_CLAIM, _('verified claim')),
|
|
(ALWAYS_VERIFIED, _('always verified')),
|
|
]
|
|
|
|
provider = models.ForeignKey(
|
|
to='OIDCProvider',
|
|
verbose_name=_('provider'),
|
|
related_name='claim_mappings')
|
|
claim = models.CharField(
|
|
max_length=64,
|
|
verbose_name=_('claim'))
|
|
attribute = models.CharField(
|
|
max_length=64,
|
|
verbose_name=_('attribute'))
|
|
verified = models.PositiveIntegerField(
|
|
default=NOT_VERIFIED,
|
|
choices=VERIFIED_CHOICES,
|
|
verbose_name=_('verified'))
|
|
required = models.BooleanField(
|
|
blank=True,
|
|
default=False,
|
|
verbose_name=_('required'))
|
|
idtoken_claim = models.BooleanField(
|
|
verbose_name=_('idtoken claim'),
|
|
default=False,
|
|
blank=True)
|
|
created = models.DateTimeField(
|
|
verbose_name=_('created'),
|
|
auto_now_add=True)
|
|
modified = models.DateTimeField(
|
|
verbose_name=_('modified'),
|
|
auto_now=True)
|
|
|
|
objects = managers.OIDCClaimMappingManager()
|
|
|
|
def natural_key(self):
|
|
return (self.claim, self.attribute, self.verified, self.required)
|
|
|
|
def __unicode__(self):
|
|
s = u'{0} -> {1}'.format(self.claim, self.attribute)
|
|
if self.verified:
|
|
s += u', verified'
|
|
if self.required:
|
|
s += u', required'
|
|
if self.idtoken_claim:
|
|
s += u', idtoken'
|
|
return s
|
|
|
|
def __repr__(self):
|
|
return '<OIDCClaimMapping %r:%r on provider %r verified:%s required:%s >' % (
|
|
self.claim, self.attribute, self.provider and self.provider.issuer,
|
|
self.verified, self.required)
|
|
|
|
|
|
class OIDCAccount(models.Model):
|
|
created = models.DateTimeField(
|
|
verbose_name=_('created'),
|
|
auto_now_add=True)
|
|
modified = models.DateTimeField(
|
|
verbose_name=_('modified'),
|
|
auto_now=True)
|
|
provider = models.ForeignKey(
|
|
to='OIDCProvider',
|
|
verbose_name=_('provider'),
|
|
related_name='accounts')
|
|
user = models.OneToOneField(
|
|
to=settings.AUTH_USER_MODEL,
|
|
verbose_name=_('user'),
|
|
related_name='oidc_account')
|
|
sub = models.CharField(
|
|
verbose_name=_('sub'),
|
|
max_length=256,
|
|
unique=True)
|
|
|
|
def __unicode__(self):
|
|
return u'{0} on {1} linked to {2}'.format(self.sub, self.provider and self.provider.issuer,
|
|
self.user)
|
|
|
|
def __repr__(self):
|
|
return '<OIDCAccount %r on %r>' % (self.sub, self.provider and self.provider.issuer)
|