policies/password: merge hibp add zxcvbn (#4001)
* initial zxcvbn Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add api and port tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * more tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add ui Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * update docs Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add api diff Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
40844c975f
commit
88594075b2
2
Makefile
2
Makefile
|
@ -69,7 +69,7 @@ gen-build:
|
|||
AUTHENTIK_DEBUG=true ak spectacular --file schema.yml
|
||||
|
||||
gen-diff:
|
||||
git show $(shell git tag -l | tail -n 1):schema.yml > old_schema.yml
|
||||
git show $(shell git describe --abbrev=0):schema.yml > old_schema.yml
|
||||
docker run \
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
|
|
|
@ -15,7 +15,7 @@ LOGGER = get_logger()
|
|||
|
||||
|
||||
class HaveIBeenPwendPolicy(Policy):
|
||||
"""Check if password is on HaveIBeenPwned's list by uploading the first
|
||||
"""DEPRECATED. Check if password is on HaveIBeenPwned's list by uploading the first
|
||||
5 characters of the SHA1 Hash."""
|
||||
|
||||
password_field = models.TextField(
|
||||
|
|
|
@ -20,6 +20,11 @@ class PasswordPolicySerializer(PolicySerializer):
|
|||
"length_min",
|
||||
"symbol_charset",
|
||||
"error_message",
|
||||
"check_static_rules",
|
||||
"check_have_i_been_pwned",
|
||||
"check_zxcvbn",
|
||||
"hibp_allowed_count",
|
||||
"zxcvbn_score_threshold",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
# Generated by Django 4.1.3 on 2022-11-14 09:23
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def migrate_hibp_policy(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
HaveIBeenPwendPolicy = apps.get_model("authentik_policies_hibp", "HaveIBeenPwendPolicy")
|
||||
PasswordPolicy = apps.get_model("authentik_policies_password", "PasswordPolicy")
|
||||
|
||||
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
|
||||
|
||||
for old_policy in HaveIBeenPwendPolicy.objects.using(db_alias).all():
|
||||
new_policy = PasswordPolicy.objects.using(db_alias).create(
|
||||
name=old_policy.name,
|
||||
hibp_allowed_count=old_policy.allowed_count,
|
||||
password_field=old_policy.password_field,
|
||||
execution_logging=old_policy.execution_logging,
|
||||
check_static_rules=False,
|
||||
check_have_i_been_pwned=True,
|
||||
)
|
||||
PolicyBinding.objects.using(db_alias).filter(policy=old_policy).update(policy=new_policy)
|
||||
old_policy.delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies_hibp", "0003_haveibeenpwendpolicy_authentik_p_policy__6957d7_idx"),
|
||||
("authentik_policies_password", "0004_passwordpolicy_authentik_p_policy__855e80_idx"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="passwordpolicy",
|
||||
name="check_have_i_been_pwned",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="passwordpolicy",
|
||||
name="check_static_rules",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="passwordpolicy",
|
||||
name="check_zxcvbn",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="passwordpolicy",
|
||||
name="hibp_allowed_count",
|
||||
field=models.PositiveIntegerField(
|
||||
default=0,
|
||||
help_text="How many times the password hash is allowed to be on haveibeenpwned",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="passwordpolicy",
|
||||
name="zxcvbn_score_threshold",
|
||||
field=models.PositiveIntegerField(
|
||||
default=2,
|
||||
help_text="If the zxcvbn score is equal or less than this value, the policy will fail.",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="passwordpolicy",
|
||||
name="error_message",
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.RunPython(migrate_hibp_policy),
|
||||
]
|
|
@ -1,11 +1,14 @@
|
|||
"""user field matcher models"""
|
||||
"""password policy"""
|
||||
import re
|
||||
from hashlib import sha1
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
from structlog.stdlib import get_logger
|
||||
from zxcvbn import zxcvbn
|
||||
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.policies.models import Policy
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
@ -24,13 +27,27 @@ class PasswordPolicy(Policy):
|
|||
help_text=_("Field key to check, field keys defined in Prompt stages are available."),
|
||||
)
|
||||
|
||||
check_static_rules = models.BooleanField(default=True)
|
||||
check_have_i_been_pwned = models.BooleanField(default=False)
|
||||
check_zxcvbn = models.BooleanField(default=False)
|
||||
|
||||
amount_digits = models.PositiveIntegerField(default=0)
|
||||
amount_uppercase = models.PositiveIntegerField(default=0)
|
||||
amount_lowercase = models.PositiveIntegerField(default=0)
|
||||
amount_symbols = models.PositiveIntegerField(default=0)
|
||||
length_min = models.PositiveIntegerField(default=0)
|
||||
symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
|
||||
error_message = models.TextField()
|
||||
error_message = models.TextField(blank=True)
|
||||
|
||||
hibp_allowed_count = models.PositiveIntegerField(
|
||||
default=0,
|
||||
help_text=_("How many times the password hash is allowed to be on haveibeenpwned"),
|
||||
)
|
||||
|
||||
zxcvbn_score_threshold = models.PositiveIntegerField(
|
||||
default=2,
|
||||
help_text=_("If the zxcvbn score is equal or less than this value, the policy will fail."),
|
||||
)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
|
@ -42,48 +59,103 @@ class PasswordPolicy(Policy):
|
|||
def component(self) -> str:
|
||||
return "ak-policy-password-form"
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||
if (
|
||||
self.password_field not in request.context
|
||||
and self.password_field not in request.context.get(PLAN_CONTEXT_PROMPT, {})
|
||||
):
|
||||
password = request.context.get(PLAN_CONTEXT_PROMPT, {}).get(
|
||||
self.password_field, request.context.get(self.password_field)
|
||||
)
|
||||
if not password:
|
||||
LOGGER.warning(
|
||||
"Password field not set in Policy Request",
|
||||
field=self.password_field,
|
||||
fields=request.context.keys(),
|
||||
prompt_fields=request.context.get(PLAN_CONTEXT_PROMPT, {}).keys(),
|
||||
)
|
||||
return PolicyResult(False, _("Password not set in context"))
|
||||
password = str(password)
|
||||
|
||||
if self.password_field in request.context:
|
||||
password = request.context[self.password_field]
|
||||
else:
|
||||
password = request.context[PLAN_CONTEXT_PROMPT][self.password_field]
|
||||
if self.check_static_rules:
|
||||
static_result = self.passes_static(password, request)
|
||||
if not static_result.passing:
|
||||
return static_result
|
||||
if self.check_have_i_been_pwned:
|
||||
hibp_result = self.passes_hibp(password, request)
|
||||
if not hibp_result.passing:
|
||||
return hibp_result
|
||||
if self.check_zxcvbn:
|
||||
zxcvbn_result = self.passes_zxcvbn(password, request)
|
||||
if not zxcvbn_result.passing:
|
||||
return zxcvbn_result
|
||||
return PolicyResult(True)
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def passes_static(self, password: str, request: PolicyRequest) -> PolicyResult:
|
||||
"""Check static rules"""
|
||||
if len(password) < self.length_min:
|
||||
LOGGER.debug("password failed", reason="length")
|
||||
LOGGER.debug("password failed", check="static", reason="length")
|
||||
return PolicyResult(False, self.error_message)
|
||||
|
||||
if self.amount_digits > 0 and len(RE_DIGITS.findall(password)) < self.amount_digits:
|
||||
LOGGER.debug("password failed", reason="amount_digits")
|
||||
LOGGER.debug("password failed", check="static", reason="amount_digits")
|
||||
return PolicyResult(False, self.error_message)
|
||||
if self.amount_lowercase > 0 and len(RE_LOWER.findall(password)) < self.amount_lowercase:
|
||||
LOGGER.debug("password failed", reason="amount_lowercase")
|
||||
LOGGER.debug("password failed", check="static", reason="amount_lowercase")
|
||||
return PolicyResult(False, self.error_message)
|
||||
if self.amount_uppercase > 0 and len(RE_UPPER.findall(password)) < self.amount_lowercase:
|
||||
LOGGER.debug("password failed", reason="amount_uppercase")
|
||||
LOGGER.debug("password failed", check="static", reason="amount_uppercase")
|
||||
return PolicyResult(False, self.error_message)
|
||||
if self.amount_symbols > 0:
|
||||
count = 0
|
||||
for symbol in self.symbol_charset:
|
||||
count += password.count(symbol)
|
||||
if count < self.amount_symbols:
|
||||
LOGGER.debug("password failed", reason="amount_symbols")
|
||||
LOGGER.debug("password failed", check="static", reason="amount_symbols")
|
||||
return PolicyResult(False, self.error_message)
|
||||
|
||||
return PolicyResult(True)
|
||||
|
||||
def check_hibp(self, short_hash: str) -> str:
|
||||
"""Check the haveibeenpwned API"""
|
||||
url = f"https://api.pwnedpasswords.com/range/{short_hash}"
|
||||
return get_http_session().get(url).text
|
||||
|
||||
def passes_hibp(self, password: str, request: PolicyRequest) -> PolicyResult:
|
||||
"""Check if password is in HIBP DB. Hashes given Password with SHA1, uses the first 5
|
||||
characters of Password in request and checks if full hash is in response. Returns 0
|
||||
if Password is not in result otherwise the count of how many times it was used."""
|
||||
pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec
|
||||
result = self.check_hibp(pw_hash[:5])
|
||||
final_count = 0
|
||||
for line in result.split("\r\n"):
|
||||
full_hash, count = line.split(":")
|
||||
if pw_hash[5:] == full_hash.lower():
|
||||
final_count = int(count)
|
||||
LOGGER.debug("got hibp result", count=final_count, hash=pw_hash[:5])
|
||||
if final_count > self.hibp_allowed_count:
|
||||
LOGGER.debug("password failed", check="hibp", count=final_count)
|
||||
message = _("Password exists on %(count)d online lists." % {"count": final_count})
|
||||
return PolicyResult(False, message)
|
||||
return PolicyResult(True)
|
||||
|
||||
def passes_zxcvbn(self, password: str, request: PolicyRequest) -> PolicyResult:
|
||||
"""Check Dropbox's zxcvbn password estimator"""
|
||||
user_inputs = []
|
||||
if request.user.is_authenticated:
|
||||
user_inputs.append(request.user.username)
|
||||
user_inputs.append(request.user.name)
|
||||
user_inputs.append(request.user.email)
|
||||
if request.http_request:
|
||||
user_inputs.append(request.http_request.tenant.branding_title)
|
||||
# Only calculate result for the first 100 characters, as with over 100 char
|
||||
# long passwords we can be reasonably sure that they'll surpass the score anyways
|
||||
# See https://github.com/dropbox/zxcvbn#runtime-latency
|
||||
results = zxcvbn(password[:100], user_inputs)
|
||||
LOGGER.debug("password failed", check="zxcvbn", score=results["score"])
|
||||
result = PolicyResult(results["score"] > self.zxcvbn_score_threshold)
|
||||
if isinstance(results["feedback"]["warning"], list):
|
||||
result.messages += tuple(results["feedback"]["warning"])
|
||||
if isinstance(results["feedback"]["suggestions"], list):
|
||||
result.messages += tuple(results["feedback"]["suggestions"])
|
||||
return result
|
||||
|
||||
class Meta(Policy.PolicyMeta):
|
||||
|
||||
verbose_name = _("Password Policy")
|
||||
|
|
50
authentik/policies/password/tests/test_hibp.py
Normal file
50
authentik/policies/password/tests/test_hibp.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
"""Password Policy HIBP tests"""
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.policies.password.models import PasswordPolicy
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
|
||||
class TestPasswordPolicyHIBP(TestCase):
|
||||
"""Test Password Policy (haveibeenpwned)"""
|
||||
|
||||
def test_invalid(self):
|
||||
"""Test without password"""
|
||||
policy = PasswordPolicy.objects.create(
|
||||
check_have_i_been_pwned=True,
|
||||
check_static_rules=False,
|
||||
name="test_invalid",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages[0], "Password not set in context")
|
||||
|
||||
def test_false(self):
|
||||
"""Failing password case"""
|
||||
policy = PasswordPolicy.objects.create(
|
||||
check_have_i_been_pwned=True,
|
||||
check_static_rules=False,
|
||||
name="test_false",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context[PLAN_CONTEXT_PROMPT] = {"password": "password"} # nosec
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertTrue(result.messages[0].startswith("Password exists on "))
|
||||
|
||||
def test_true(self):
|
||||
"""Positive password case"""
|
||||
policy = PasswordPolicy.objects.create(
|
||||
check_have_i_been_pwned=True,
|
||||
check_static_rules=False,
|
||||
name="test_true",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context[PLAN_CONTEXT_PROMPT] = {"password": generate_key()}
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertTrue(result.passing)
|
||||
self.assertEqual(result.messages, tuple())
|
50
authentik/policies/password/tests/test_zxcvbn.py
Normal file
50
authentik/policies/password/tests/test_zxcvbn.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
"""Password Policy zxcvbn tests"""
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.policies.password.models import PasswordPolicy
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
|
||||
class TestPasswordPolicyZxcvbn(TestCase):
|
||||
"""Test Password Policy (zxcvbn)"""
|
||||
|
||||
def test_invalid(self):
|
||||
"""Test without password"""
|
||||
policy = PasswordPolicy.objects.create(
|
||||
check_zxcvbn=True,
|
||||
check_static_rules=False,
|
||||
name="test_invalid",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages[0], "Password not set in context")
|
||||
|
||||
def test_false(self):
|
||||
"""Failing password case"""
|
||||
policy = PasswordPolicy.objects.create(
|
||||
check_zxcvbn=True,
|
||||
check_static_rules=False,
|
||||
name="test_false",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context[PLAN_CONTEXT_PROMPT] = {"password": "password"} # nosec
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertFalse(result.passing, result.messages)
|
||||
self.assertEqual(result.messages[0], "Add another word or two. Uncommon words are better.")
|
||||
|
||||
def test_true(self):
|
||||
"""Positive password case"""
|
||||
policy = PasswordPolicy.objects.create(
|
||||
check_zxcvbn=True,
|
||||
check_static_rules=False,
|
||||
name="test_true",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context[PLAN_CONTEXT_PROMPT] = {"password": generate_key()}
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertTrue(result.passing)
|
||||
self.assertEqual(result.messages, tuple())
|
13
poetry.lock
generated
13
poetry.lock
generated
|
@ -2113,10 +2113,18 @@ docs = ["Sphinx", "repoze.sphinx.autointerface"]
|
|||
test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
||||
testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
||||
|
||||
[[package]]
|
||||
name = "zxcvbn"
|
||||
version = "4.4.28"
|
||||
description = ""
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "2250e4c5173156b62a2b417e8d8ec895135142b4d176a696cd7fb9f732b5b129"
|
||||
content-hash = "7a0fe2bd1d710517a961731f78a2cf2e9d70c277d208606c56d765947e529dca"
|
||||
|
||||
[metadata.files]
|
||||
aiohttp = [
|
||||
|
@ -3921,3 +3929,6 @@ zope-interface = [
|
|||
{file = "zope.interface-5.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:8343536ea4ee15d6525e3e726bb49ffc3f2034f828a49237a36be96842c06e7c"},
|
||||
{file = "zope.interface-5.5.1.tar.gz", hash = "sha256:6d678475fdeb11394dc9aaa5c564213a1567cc663082e0ee85d52f78d1fbaab2"},
|
||||
]
|
||||
zxcvbn = [
|
||||
{file = "zxcvbn-4.4.28.tar.gz", hash = "sha256:151bd816817e645e9064c354b13544f85137ea3320ca3be1fb6873ea75ef7dc1"},
|
||||
]
|
||||
|
|
|
@ -124,13 +124,16 @@ djangorestframework = "*"
|
|||
djangorestframework-guardian = "*"
|
||||
docker = "*"
|
||||
drf-spectacular = "*"
|
||||
dumb-init = "*"
|
||||
duo-client = "*"
|
||||
facebook-sdk = "*"
|
||||
flower = "*"
|
||||
geoip2 = "*"
|
||||
gunicorn = "*"
|
||||
kubernetes = "*"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
opencontainers = {extras = ["reggie"],version = "*"}
|
||||
packaging = "*"
|
||||
paramiko = "*"
|
||||
psycopg2-binary = "*"
|
||||
|
@ -143,6 +146,7 @@ sentry-sdk = "*"
|
|||
service_identity = "*"
|
||||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
twilio = "*"
|
||||
twisted = "*"
|
||||
ua-parser = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
|
@ -150,10 +154,7 @@ uvicorn = {extras = ["standard"],version = "*"}
|
|||
webauthn = "*"
|
||||
wsproto = "*"
|
||||
xmlsec = "*"
|
||||
twilio = "*"
|
||||
dumb-init = "*"
|
||||
flower = "*"
|
||||
opencontainers = {extras = ["reggie"],version = "*"}
|
||||
zxcvbn = "*"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
bandit = "*"
|
||||
|
|
71
schema.yml
71
schema.yml
|
@ -11434,6 +11434,18 @@ paths:
|
|||
name: amount_uppercase
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: check_have_i_been_pwned
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: check_static_rules
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: check_zxcvbn
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: created
|
||||
schema:
|
||||
|
@ -11447,6 +11459,10 @@ paths:
|
|||
name: execution_logging
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: hibp_allowed_count
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: last_updated
|
||||
schema:
|
||||
|
@ -11497,6 +11513,10 @@ paths:
|
|||
name: symbol_charset
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: zxcvbn_score_threshold
|
||||
schema:
|
||||
type: integer
|
||||
tags:
|
||||
- policies
|
||||
security:
|
||||
|
@ -32889,6 +32909,23 @@ components:
|
|||
type: string
|
||||
error_message:
|
||||
type: string
|
||||
check_static_rules:
|
||||
type: boolean
|
||||
check_have_i_been_pwned:
|
||||
type: boolean
|
||||
check_zxcvbn:
|
||||
type: boolean
|
||||
hibp_allowed_count:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: 0
|
||||
description: How many times the password hash is allowed to be on haveibeenpwned
|
||||
zxcvbn_score_threshold:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: 0
|
||||
description: If the zxcvbn score is equal or less than this value, the policy
|
||||
will fail.
|
||||
required:
|
||||
- bound_to
|
||||
- component
|
||||
|
@ -32939,6 +32976,23 @@ components:
|
|||
error_message:
|
||||
type: string
|
||||
minLength: 1
|
||||
check_static_rules:
|
||||
type: boolean
|
||||
check_have_i_been_pwned:
|
||||
type: boolean
|
||||
check_zxcvbn:
|
||||
type: boolean
|
||||
hibp_allowed_count:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: 0
|
||||
description: How many times the password hash is allowed to be on haveibeenpwned
|
||||
zxcvbn_score_threshold:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: 0
|
||||
description: If the zxcvbn score is equal or less than this value, the policy
|
||||
will fail.
|
||||
required:
|
||||
- error_message
|
||||
PasswordStage:
|
||||
|
@ -34201,6 +34255,23 @@ components:
|
|||
error_message:
|
||||
type: string
|
||||
minLength: 1
|
||||
check_static_rules:
|
||||
type: boolean
|
||||
check_have_i_been_pwned:
|
||||
type: boolean
|
||||
check_zxcvbn:
|
||||
type: boolean
|
||||
hibp_allowed_count:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: 0
|
||||
description: How many times the password hash is allowed to be on haveibeenpwned
|
||||
zxcvbn_score_threshold:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: 0
|
||||
description: If the zxcvbn score is equal or less than this value, the policy
|
||||
will fail.
|
||||
PatchedPasswordStageRequest:
|
||||
type: object
|
||||
description: PasswordStage Serializer
|
||||
|
|
|
@ -7,17 +7,33 @@ import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
|||
import { t } from "@lingui/macro";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { PasswordPolicy, PoliciesApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-policy-password-form")
|
||||
export class PasswordPolicyForm extends ModelForm<PasswordPolicy, string> {
|
||||
@state()
|
||||
showStatic = true;
|
||||
|
||||
@state()
|
||||
showHIBP = false;
|
||||
|
||||
@state()
|
||||
showZxcvbn = false;
|
||||
|
||||
loadInstance(pk: string): Promise<PasswordPolicy> {
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesPasswordRetrieve({
|
||||
policyUuid: pk,
|
||||
});
|
||||
return new PoliciesApi(DEFAULT_CONFIG)
|
||||
.policiesPasswordRetrieve({
|
||||
policyUuid: pk,
|
||||
})
|
||||
.then((policy) => {
|
||||
this.showStatic = policy.checkStaticRules || false;
|
||||
this.showHIBP = policy.checkHaveIBeenPwned || false;
|
||||
this.showZxcvbn = policy.checkZxcvbn || false;
|
||||
return policy;
|
||||
});
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
|
@ -41,6 +57,168 @@ export class PasswordPolicyForm extends ModelForm<PasswordPolicy, string> {
|
|||
}
|
||||
};
|
||||
|
||||
renderStaticRules(): TemplateResult {
|
||||
return html` <ak-form-group>
|
||||
<span slot="header"> ${t`Static rules`} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Minimum length`}
|
||||
?required=${true}
|
||||
name="lengthMin"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.lengthMin, 10)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Minimum amount of Uppercase Characters`}
|
||||
?required=${true}
|
||||
name="amountUppercase"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.amountUppercase, 2)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Minimum amount of Lowercase Characters`}
|
||||
?required=${true}
|
||||
name="amountLowercase"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.amountLowercase, 2)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Minimum amount of Digits`}
|
||||
?required=${true}
|
||||
name="amountDigits"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.amountDigits, 2)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Minimum amount of Symbols Characters`}
|
||||
?required=${true}
|
||||
name="amountSymbols"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.amountSymbols, 2)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Error message`}
|
||||
?required=${true}
|
||||
name="errorMessage"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.errorMessage)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Symbol charset`}
|
||||
?required=${true}
|
||||
name="symbolCharset"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(
|
||||
this.instance?.symbolCharset || "!\\\"#$%&'()*+,-./:;<=>?@[]^_`{|}~ ",
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Characters which are considered as symbols.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
}
|
||||
|
||||
renderHIBP(): TemplateResult {
|
||||
return html`
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${t`HaveIBeenPwned settings`} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Allowed count`}
|
||||
?required=${true}
|
||||
name="hibpAllowedCount"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.hibpAllowedCount, 0)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Allow up to N occurrences in the HIBP database.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
||||
|
||||
renderZxcvbn(): TemplateResult {
|
||||
return html`
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${t`zxcvbn settings`} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Score threshold`}
|
||||
?required=${true}
|
||||
name="zxcvbnScoreThreshold"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.zxcvbnScoreThreshold, 0)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`If the password's score is less than or equal this value, the policy will fail.`}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`0: Too guessable: risky password. (guesses < 10^3)`}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`1: Very guessable: protection from throttled online attacks. (guesses < 10^6)`}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`2: Somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)`}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`3: Safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)`}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`4: Very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<div class="form-help-text">
|
||||
|
@ -67,122 +245,77 @@ export class PasswordPolicyForm extends ModelForm<PasswordPolicy, string> {
|
|||
${t`When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${t`Policy-specific settings`} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Password field`}
|
||||
?required=${true}
|
||||
name="passwordField"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.passwordField || "password")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Field key to check, field keys defined in Prompt stages are available.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Password field`}
|
||||
?required=${true}
|
||||
name="passwordField"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.passwordField || "password")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Field key to check, field keys defined in Prompt stages are available.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Minimum length`}
|
||||
?required=${true}
|
||||
name="lengthMin"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.lengthMin, 10)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Minimum amount of Uppercase Characters`}
|
||||
?required=${true}
|
||||
name="amountUppercase"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.amountUppercase, 2)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Minimum amount of Lowercase Characters`}
|
||||
?required=${true}
|
||||
name="amountLowercase"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.amountLowercase, 2)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Minimum amount of Digits`}
|
||||
?required=${true}
|
||||
name="amountDigits"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.amountDigits, 2)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Minimum amount of Symbols Characters`}
|
||||
?required=${true}
|
||||
name="amountSymbols"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.amountSymbols, 2)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Error message`}
|
||||
?required=${true}
|
||||
name="errorMessage"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.errorMessage)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="checkStaticRules">
|
||||
<div class="pf-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="pf-c-check__input"
|
||||
?checked=${first(this.instance?.checkStaticRules, true)}
|
||||
@change=${(ev: Event) => {
|
||||
const el = ev.target as HTMLInputElement;
|
||||
this.showStatic = el.checked;
|
||||
}}
|
||||
/>
|
||||
<label class="pf-c-check__label"> ${t`Check static rules`} </label>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${t`Advanced settings`} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Symbol charset`}
|
||||
?required=${true}
|
||||
name="symbolCharset"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(
|
||||
this.instance?.symbolCharset ||
|
||||
"!\\\"#$%&'()*+,-./:;<=>?@[]^_`{|}~ ",
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Characters which are considered as symbols.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="checkHaveIBeenPwned">
|
||||
<div class="pf-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="pf-c-check__input"
|
||||
?checked=${first(this.instance?.checkHaveIBeenPwned, true)}
|
||||
@change=${(ev: Event) => {
|
||||
const el = ev.target as HTMLInputElement;
|
||||
this.showHIBP = el.checked;
|
||||
}}
|
||||
/>
|
||||
<label class="pf-c-check__label"> ${t`Check haveibeenpwned.com`} </label>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`For more info see:`}
|
||||
<a href="https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange"
|
||||
>haveibeenpwned.com</a
|
||||
>
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="checkZxcvbn">
|
||||
<div class="pf-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="pf-c-check__input"
|
||||
?checked=${first(this.instance?.checkZxcvbn, true)}
|
||||
@change=${(ev: Event) => {
|
||||
const el = ev.target as HTMLInputElement;
|
||||
this.showZxcvbn = el.checked;
|
||||
}}
|
||||
/>
|
||||
<label class="pf-c-check__label"> ${t`Check zxcvbn`} </label>
|
||||
</div>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Password strength estimator created by Dropbox, see:`}
|
||||
<a href="https://github.com/dropbox/zxcvbn#readme">dropbox/zxcvbn</a>
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
${this.showStatic ? this.renderStaticRules() : html``}
|
||||
${this.showHIBP ? this.renderHIBP() : html``}
|
||||
${this.showZxcvbn ? this.renderZxcvbn() : html``}
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { t } from "@lingui/macro";
|
|||
|
||||
import { CSSResult, css } from "lit";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
|
@ -56,10 +56,10 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
|||
});
|
||||
}
|
||||
|
||||
@property({ type: Boolean })
|
||||
@state()
|
||||
showHttpBasic = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
@state()
|
||||
mode: ProxyMode = ProxyMode.Proxy;
|
||||
|
||||
getSuccessMessage(): string {
|
||||
|
@ -85,9 +85,6 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
|||
};
|
||||
|
||||
renderHttpBasic(): TemplateResult {
|
||||
if (!this.showHttpBasic) {
|
||||
return html``;
|
||||
}
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${t`HTTP-Basic Username Key`}
|
||||
name="basicAuthUserAttribute"
|
||||
|
@ -442,7 +439,7 @@ ${this.instance?.skipPathRegex}</textarea
|
|||
${t`Set a custom HTTP-Basic Authentication header based on values from authentik.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
${this.renderHttpBasic()}
|
||||
${this.showHttpBasic ? this.renderHttpBasic() : html``}
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
|
|
|
@ -12,6 +12,9 @@ See [Expression Policy](expression.mdx).
|
|||
|
||||
## Have I Been Pwned Policy
|
||||
|
||||
:::info
|
||||
This policy is deprecated since authentik 2022.11.0, as this can be done with the password policy now.
|
||||
:::
|
||||
This policy checks the hashed password against the [Have I Been Pwned](https://haveibeenpwned.com/) API. This only sends the first 5 characters of the hashed password. The remaining comparison is done within authentik.
|
||||
|
||||
## Password-Expiry Policy
|
||||
|
@ -29,6 +32,11 @@ The following rules can be set:
|
|||
- Minimum length.
|
||||
- Symbol charset (define which characters are counted as symbols).
|
||||
|
||||
Starting with authentik 2022.11.0, the following checks can also be done with this policy:
|
||||
|
||||
- Check the password hash against the database of [Have I Been Pwned](https://haveibeenpwned.com/). Only the first 5 characters of the hashed password are transmitted, the rest is compared in authentik
|
||||
- Check the password against the password complexity checker [zxcvbn](https://github.com/dropbox/zxcvbn), which detects weak password on various metrics.
|
||||
|
||||
## Reputation Policy
|
||||
|
||||
authentik keeps track of failed login attempts by source IP and attempted username. These values are saved as scores. Each failed login decreases the score for the client IP as well as the targeted username by 1 (one).
|
||||
|
|
|
@ -5,13 +5,702 @@ slug: "2022.11"
|
|||
|
||||
## Breaking changes
|
||||
|
||||
- authentik now runs on Python 3.11
|
||||
- Have I Been Pwned policy is deprecated
|
||||
|
||||
The policy has been merged with the password policy which provides the same functionality. Existing Have I Been Pwned policies will automatically be migrated.
|
||||
|
||||
## New features
|
||||
|
||||
- authentik now runs on Python 3.11
|
||||
- Expanded password policy
|
||||
|
||||
The "Have I been Pwned" policy has been merged into the password policy, and additionally passwords can be checked using [zxcvbn](https://github.com/dropbox/zxcvbn) to provider concise feedback.
|
||||
|
||||
## API Changes
|
||||
|
||||
_Insert output of `make gen-diff` here_
|
||||
#### What's Changed
|
||||
|
||||
---
|
||||
|
||||
##### `GET` /policies/password/{policy_uuid}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Added property `check_static_rules` (boolean)
|
||||
|
||||
- Added property `check_have_i_been_pwned` (boolean)
|
||||
|
||||
- Added property `check_zxcvbn` (boolean)
|
||||
|
||||
- Added property `hibp_allowed_count` (integer)
|
||||
|
||||
> How many times the password hash is allowed to be on haveibeenpwned
|
||||
|
||||
- Added property `zxcvbn_score_threshold` (integer)
|
||||
> If the zxcvbn score is equal or less than this value, the policy will fail.
|
||||
|
||||
##### `PUT` /policies/password/{policy_uuid}/
|
||||
|
||||
###### Request:
|
||||
|
||||
Changed content type : `application/json`
|
||||
|
||||
- Added property `check_static_rules` (boolean)
|
||||
|
||||
- Added property `check_have_i_been_pwned` (boolean)
|
||||
|
||||
- Added property `check_zxcvbn` (boolean)
|
||||
|
||||
- Added property `hibp_allowed_count` (integer)
|
||||
|
||||
> How many times the password hash is allowed to be on haveibeenpwned
|
||||
|
||||
- Added property `zxcvbn_score_threshold` (integer)
|
||||
> If the zxcvbn score is equal or less than this value, the policy will fail.
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Added property `check_static_rules` (boolean)
|
||||
|
||||
- Added property `check_have_i_been_pwned` (boolean)
|
||||
|
||||
- Added property `check_zxcvbn` (boolean)
|
||||
|
||||
- Added property `hibp_allowed_count` (integer)
|
||||
|
||||
> How many times the password hash is allowed to be on haveibeenpwned
|
||||
|
||||
- Added property `zxcvbn_score_threshold` (integer)
|
||||
> If the zxcvbn score is equal or less than this value, the policy will fail.
|
||||
|
||||
##### `PATCH` /policies/password/{policy_uuid}/
|
||||
|
||||
###### Request:
|
||||
|
||||
Changed content type : `application/json`
|
||||
|
||||
- Added property `check_static_rules` (boolean)
|
||||
|
||||
- Added property `check_have_i_been_pwned` (boolean)
|
||||
|
||||
- Added property `check_zxcvbn` (boolean)
|
||||
|
||||
- Added property `hibp_allowed_count` (integer)
|
||||
|
||||
> How many times the password hash is allowed to be on haveibeenpwned
|
||||
|
||||
- Added property `zxcvbn_score_threshold` (integer)
|
||||
> If the zxcvbn score is equal or less than this value, the policy will fail.
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Added property `check_static_rules` (boolean)
|
||||
|
||||
- Added property `check_have_i_been_pwned` (boolean)
|
||||
|
||||
- Added property `check_zxcvbn` (boolean)
|
||||
|
||||
- Added property `hibp_allowed_count` (integer)
|
||||
|
||||
> How many times the password hash is allowed to be on haveibeenpwned
|
||||
|
||||
- Added property `zxcvbn_score_threshold` (integer)
|
||||
> If the zxcvbn score is equal or less than this value, the policy will fail.
|
||||
|
||||
##### `GET` /core/tokens/{identifier}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `user_obj` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `PUT` /core/tokens/{identifier}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `user_obj` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `PATCH` /core/tokens/{identifier}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `user_obj` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `GET` /core/users/{id}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `PUT` /core/users/{id}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `PATCH` /core/users/{id}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `GET` /policies/bindings/{policy_binding_uuid}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `user_obj` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `PUT` /policies/bindings/{policy_binding_uuid}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `user_obj` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `PATCH` /policies/bindings/{policy_binding_uuid}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `user_obj` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `POST` /policies/password/
|
||||
|
||||
###### Request:
|
||||
|
||||
Changed content type : `application/json`
|
||||
|
||||
- Added property `check_static_rules` (boolean)
|
||||
|
||||
- Added property `check_have_i_been_pwned` (boolean)
|
||||
|
||||
- Added property `check_zxcvbn` (boolean)
|
||||
|
||||
- Added property `hibp_allowed_count` (integer)
|
||||
|
||||
> How many times the password hash is allowed to be on haveibeenpwned
|
||||
|
||||
- Added property `zxcvbn_score_threshold` (integer)
|
||||
> If the zxcvbn score is equal or less than this value, the policy will fail.
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **201 Created**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Added property `check_static_rules` (boolean)
|
||||
|
||||
- Added property `check_have_i_been_pwned` (boolean)
|
||||
|
||||
- Added property `check_zxcvbn` (boolean)
|
||||
|
||||
- Added property `hibp_allowed_count` (integer)
|
||||
|
||||
> How many times the password hash is allowed to be on haveibeenpwned
|
||||
|
||||
- Added property `zxcvbn_score_threshold` (integer)
|
||||
> If the zxcvbn score is equal or less than this value, the policy will fail.
|
||||
|
||||
##### `GET` /policies/password/
|
||||
|
||||
###### Parameters:
|
||||
|
||||
Added: `check_have_i_been_pwned` in `query`
|
||||
|
||||
Added: `check_static_rules` in `query`
|
||||
|
||||
Added: `check_zxcvbn` in `query`
|
||||
|
||||
Added: `hibp_allowed_count` in `query`
|
||||
|
||||
Added: `zxcvbn_score_threshold` in `query`
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `results` (array)
|
||||
|
||||
Changed items (object): > Password Policy Serializer
|
||||
|
||||
- Added property `check_static_rules` (boolean)
|
||||
|
||||
- Added property `check_have_i_been_pwned` (boolean)
|
||||
|
||||
- Added property `check_zxcvbn` (boolean)
|
||||
|
||||
- Added property `hibp_allowed_count` (integer)
|
||||
|
||||
> How many times the password hash is allowed to be on haveibeenpwned
|
||||
|
||||
- Added property `zxcvbn_score_threshold` (integer)
|
||||
> If the zxcvbn score is equal or less than this value, the policy will fail.
|
||||
|
||||
##### `POST` /core/tokens/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **201 Created**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `user_obj` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `GET` /core/tokens/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `results` (array)
|
||||
|
||||
Changed items (object): > Token Serializer
|
||||
|
||||
- Changed property `user_obj` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `GET` /core/user_consent/{id}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `user` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `POST` /core/users/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **201 Created**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `GET` /core/users/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `results` (array)
|
||||
|
||||
Changed items (object): > User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `GET` /oauth2/authorization_codes/{id}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `user` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `GET` /oauth2/refresh_tokens/{id}/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `user` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `POST` /policies/bindings/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **201 Created**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `user_obj` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `GET` /policies/bindings/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `results` (array)
|
||||
|
||||
Changed items (object): > PolicyBinding Serializer
|
||||
|
||||
- Changed property `user_obj` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `GET` /core/user_consent/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `results` (array)
|
||||
|
||||
Changed items (object): > UserConsent Serializer
|
||||
|
||||
- Changed property `user` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `GET` /oauth2/authorization_codes/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `results` (array)
|
||||
|
||||
Changed items (object): > Serializer for BaseGrantModel and ExpiringBaseGrant
|
||||
|
||||
- Changed property `user` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
##### `GET` /oauth2/refresh_tokens/
|
||||
|
||||
###### Return Type:
|
||||
|
||||
Changed response : **200 OK**
|
||||
|
||||
- Changed content type : `application/json`
|
||||
|
||||
- Changed property `results` (array)
|
||||
|
||||
Changed items (object): > Serializer for BaseGrantModel and RefreshToken
|
||||
|
||||
- Changed property `user` (object)
|
||||
|
||||
> User Serializer
|
||||
|
||||
- Changed property `groups_obj` (array)
|
||||
|
||||
Changed items (object): > Simplified Group Serializer for user's groups
|
||||
|
||||
New optional properties:
|
||||
|
||||
- `users_obj`
|
||||
|
||||
* Deleted property `users` (array)
|
||||
|
||||
* Deleted property `users_obj` (array)
|
||||
|
||||
## Minor changes/fixes
|
||||
|
||||
|
|
Reference in a new issue