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:
Jens L 2022-11-14 14:42:43 +01:00 committed by GitHub
parent 40844c975f
commit 88594075b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1310 additions and 150 deletions

View file

@ -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} \

View file

@ -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(

View file

@ -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",
]

View file

@ -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),
]

View file

@ -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")

View 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())

View 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
View file

@ -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"},
]

View file

@ -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 = "*"

View file

@ -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

View file

@ -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>`;
}
}

View file

@ -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>`;

View file

@ -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).

View file

@ -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