refactor(handlers): ppolicy (#3103)

Add tests and makes the password policy a provider so the configuration can be loaded to memory on startup.
pull/2828/head^2
James Elliott 2022-04-03 21:58:27 +10:00 committed by GitHub
parent 0f6ca55016
commit 9e05066097
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 266 additions and 114 deletions

View File

@ -44,6 +44,20 @@ paths:
description: Forbidden description: Forbidden
security: security:
- authelia_auth: [] - authelia_auth: []
/api/configuration/password-policy:
get:
tags:
- State
summary: Password Policy Configuration
description: >
The password policy configuration endpoint provides a password policy for resetting passwords.
responses:
"200":
description: Successful Operation
content:
application/json:
schema:
$ref: '#/components/schemas/handlers.configuration.PasswordPolicyConfigurationBody'
/api/health: /api/health:
get: get:
tags: tags:
@ -694,6 +708,43 @@ components:
- "webauthn" - "webauthn"
- "mobile_push" - "mobile_push"
example: [totp, webauthn, mobile_push] example: [totp, webauthn, mobile_push]
handlers.configuration.PasswordPolicyConfigurationBody:
type: object
properties:
status:
type: string
example: OK
data:
type: object
properties:
mode:
type: string
description: The password policy mode.
enum:
- "disabled"
- "standard"
- "zxcvbn"
min_length:
type: integer
description: The minimum password length when using the standard mode.
max_length:
type: integer
description: The maximum password length when using the standard mode.
min_score:
type: integer
description: The minimum password score when using the zxcvbn mode.
require_uppercase:
type: boolean
description: If uppercase characters are required when using the standard mode.
require_lowercase:
type: boolean
description: If uppercase characters are required when using the standard mode.
require_number:
type: boolean
description: If numeric characters are required when using the standard mode.
require_special:
type: boolean
description: If special characters are required when using the standard mode.
handlers.DuoDeviceBody: handlers.DuoDeviceBody:
required: required:
- device - device

View File

@ -329,16 +329,29 @@ password_policy:
## The standard policy allows you to tune individual settings manually. ## The standard policy allows you to tune individual settings manually.
standard: standard:
enabled: false enabled: false
## Require a minimum length for passwords.
min_length: 8 min_length: 8
## Require a maximum length for passwords.
max_length: 0 max_length: 0
## Require uppercase characters.
require_uppercase: true require_uppercase: true
## Require lowercase characters.
require_lowercase: true require_lowercase: true
## Require numeric characters.
require_number: true require_number: true
## Require special characters.
require_special: true require_special: true
## zxcvbn is a well known and used password strength algorithm. It does not have tunable settings. ## zxcvbn is a well known and used password strength algorithm. It does not have tunable settings.
zxcvbn: zxcvbn:
enabled: false enabled: false
min_score: 0
## ##
## Access Control Configuration ## Access Control Configuration

View File

@ -17,10 +17,10 @@ password_policy:
enabled: false enabled: false
min_length: 8 min_length: 8
max_length: 0 max_length: 0
require_uppercase: true require_uppercase: false
require_lowercase: true require_lowercase: false
require_number: true require_number: false
require_special: true require_special: false
zxcvbn: zxcvbn:
enabled: false enabled: false
``` ```
@ -30,7 +30,7 @@ password_policy:
### standard ### standard
<div markdown="1"> <div markdown="1">
type: list type: list
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -40,7 +40,7 @@ This section allows you to enable standard security policies.
#### enabled #### enabled
<div markdown="1"> <div markdown="1">
type: bool type: bool
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -50,7 +50,9 @@ Enables standard password policy.
#### min_length #### min_length
<div markdown="1"> <div markdown="1">
type: integer type: integer
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
default: 8
{: .label .label-config .label-blue }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -60,7 +62,9 @@ Determines the minimum allowed password length.
#### max_length #### max_length
<div markdown="1"> <div markdown="1">
type: integer type: integer
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
default: 0
{: .label .label-config .label-blue }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -70,7 +74,7 @@ Determines the maximum allowed password length.
#### require_uppercase #### require_uppercase
<div markdown="1"> <div markdown="1">
type: bool type: bool
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -80,7 +84,7 @@ Indicates that at least one UPPERCASE letter must be provided as part of the pas
#### require_lowercase #### require_lowercase
<div markdown="1"> <div markdown="1">
type: bool type: bool
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -90,7 +94,7 @@ Indicates that at least one lowercase letter must be provided as part of the pas
#### require_number #### require_number
<div markdown="1"> <div markdown="1">
type: bool type: bool
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -100,7 +104,7 @@ Indicates that at least one number must be provided as part of the password.
#### require_special #### require_special
<div markdown="1"> <div markdown="1">
type: bool type: bool
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -117,7 +121,7 @@ password is.
#### enabled #### enabled
<div markdown="1"> <div markdown="1">
type: bool type: bool
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>

View File

@ -329,16 +329,29 @@ password_policy:
## The standard policy allows you to tune individual settings manually. ## The standard policy allows you to tune individual settings manually.
standard: standard:
enabled: false enabled: false
## Require a minimum length for passwords.
min_length: 8 min_length: 8
## Require a maximum length for passwords.
max_length: 0 max_length: 0
## Require uppercase characters.
require_uppercase: true require_uppercase: true
## Require lowercase characters.
require_lowercase: true require_lowercase: true
## Require numeric characters.
require_number: true require_number: true
## Require special characters.
require_special: true require_special: true
## zxcvbn is a well known and used password strength algorithm. It does not have tunable settings. ## zxcvbn is a well known and used password strength algorithm. It does not have tunable settings.
zxcvbn: zxcvbn:
enabled: false enabled: false
min_score: 0
## ##
## Access Control Configuration ## Access Control Configuration

View File

@ -2,7 +2,7 @@ package schema
// PasswordPolicyStandardParams represents the configuration related to standard parameters of password policy. // PasswordPolicyStandardParams represents the configuration related to standard parameters of password policy.
type PasswordPolicyStandardParams struct { type PasswordPolicyStandardParams struct {
Enabled bool Enabled bool `koanf:"enabled"`
MinLength int `koanf:"min_length"` MinLength int `koanf:"min_length"`
MaxLength int `koanf:"max_length"` MaxLength int `koanf:"max_length"`
RequireUppercase bool `koanf:"require_uppercase"` RequireUppercase bool `koanf:"require_uppercase"`
@ -13,8 +13,8 @@ type PasswordPolicyStandardParams struct {
// PasswordPolicyZxcvbnParams represents the configuration related to zxcvbn parameters of password policy. // PasswordPolicyZxcvbnParams represents the configuration related to zxcvbn parameters of password policy.
type PasswordPolicyZxcvbnParams struct { type PasswordPolicyZxcvbnParams struct {
Enabled bool Enabled bool `koanf:"enabled"`
MinScore int `koanf:"min_score"` MinScore int `koanf:"min_score"`
} }
// PasswordPolicyConfiguration represents the configuration related to password policy. // PasswordPolicyConfiguration represents the configuration related to password policy.
@ -26,15 +26,12 @@ type PasswordPolicyConfiguration struct {
// DefaultPasswordPolicyConfiguration is the default password policy configuration. // DefaultPasswordPolicyConfiguration is the default password policy configuration.
var DefaultPasswordPolicyConfiguration = PasswordPolicyConfiguration{ var DefaultPasswordPolicyConfiguration = PasswordPolicyConfiguration{
Standard: PasswordPolicyStandardParams{ Standard: PasswordPolicyStandardParams{
Enabled: false, Enabled: false,
MinLength: 8, MinLength: 8,
MaxLength: 0, MaxLength: 0,
RequireUppercase: true,
RequireLowercase: true,
RequireNumber: true,
RequireSpecial: true,
}, },
Zxcvbn: PasswordPolicyZxcvbnParams{ Zxcvbn: PasswordPolicyZxcvbnParams{
Enabled: false, Enabled: false,
MinScore: 0,
}, },
} }

View File

@ -0,0 +1,31 @@
package handlers
import (
"github.com/authelia/authelia/v4/internal/middlewares"
)
// PasswordPolicyConfigurationGet get the password policy configuration.
func PasswordPolicyConfigurationGet(ctx *middlewares.AutheliaCtx) {
policyResponse := PassworPolicyBody{
Mode: "disabled",
}
if ctx.Configuration.PasswordPolicy.Standard.Enabled {
policyResponse.Mode = "standard"
policyResponse.MinLength = ctx.Configuration.PasswordPolicy.Standard.MinLength
policyResponse.MaxLength = ctx.Configuration.PasswordPolicy.Standard.MaxLength
policyResponse.RequireLowercase = ctx.Configuration.PasswordPolicy.Standard.RequireLowercase
policyResponse.RequireUppercase = ctx.Configuration.PasswordPolicy.Standard.RequireUppercase
policyResponse.RequireNumber = ctx.Configuration.PasswordPolicy.Standard.RequireNumber
policyResponse.RequireSpecial = ctx.Configuration.PasswordPolicy.Standard.RequireSpecial
} else if ctx.Configuration.PasswordPolicy.Zxcvbn.Enabled {
policyResponse.Mode = "zxcvbn"
policyResponse.MinScore = ctx.Configuration.PasswordPolicy.Zxcvbn.MinScore
}
var err error
if err = ctx.SetJSONBody(policyResponse); err != nil {
ctx.Logger.Errorf("Unable to send password Policy: %s", err)
}
}

View File

@ -53,28 +53,7 @@ func resetPasswordIdentityFinish(ctx *middlewares.AutheliaCtx, username string)
ctx.Logger.Errorf("Unable to clear password reset flag in session for user %s: %s", userSession.Username, err) ctx.Logger.Errorf("Unable to clear password reset flag in session for user %s: %s", userSession.Username, err)
} }
mode := "" ctx.ReplyOK()
if ctx.Configuration.PasswordPolicy.Standard.Enabled {
mode = "standard"
} else if ctx.Configuration.PasswordPolicy.Zxcvbn.Enabled {
mode = "zxcvbn"
}
policyResponse := PassworPolicyBody{
Mode: mode,
MinLength: ctx.Configuration.PasswordPolicy.Standard.MinLength,
MaxLength: ctx.Configuration.PasswordPolicy.Standard.MaxLength,
RequireLowercase: ctx.Configuration.PasswordPolicy.Standard.RequireLowercase,
RequireUppercase: ctx.Configuration.PasswordPolicy.Standard.RequireUppercase,
RequireNumber: ctx.Configuration.PasswordPolicy.Standard.RequireNumber,
RequireSpecial: ctx.Configuration.PasswordPolicy.Standard.RequireSpecial,
MinScore: ctx.Configuration.PasswordPolicy.Zxcvbn.MinScore,
}
err = ctx.SetJSONBody(policyResponse)
if err != nil {
ctx.Logger.Errorf("Unable to send password Policy: %s", err)
}
} }
// ResetPasswordIdentityFinish the handler for finishing the identity validation. // ResetPasswordIdentityFinish the handler for finishing the identity validation.

View File

@ -65,6 +65,8 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
r.GET("/api/configuration", autheliaMiddleware( r.GET("/api/configuration", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.ConfigurationGet))) middlewares.RequireFirstFactor(handlers.ConfigurationGet)))
r.GET("/api/configuration/password-policy", autheliaMiddleware(handlers.PasswordPolicyConfigurationGet))
r.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend))) r.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))
r.HEAD("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend))) r.HEAD("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))

View File

@ -133,6 +133,7 @@
"@types/qrcode.react": "1.0.2", "@types/qrcode.react": "1.0.2",
"@types/react": "17.0.43", "@types/react": "17.0.43",
"@types/react-dom": "17.0.14", "@types/react-dom": "17.0.14",
"@types/zxcvbn": "4.4.1",
"@typescript-eslint/eslint-plugin": "5.17.0", "@typescript-eslint/eslint-plugin": "5.17.0",
"@typescript-eslint/parser": "5.17.0", "@typescript-eslint/parser": "5.17.0",
"@vitejs/plugin-react": "1.3.0", "@vitejs/plugin-react": "1.3.0",

View File

@ -16,6 +16,7 @@ specifiers:
'@types/qrcode.react': 1.0.2 '@types/qrcode.react': 1.0.2
'@types/react': 17.0.43 '@types/react': 17.0.43
'@types/react-dom': 17.0.14 '@types/react-dom': 17.0.14
'@types/zxcvbn': 4.4.1
'@typescript-eslint/eslint-plugin': 5.17.0 '@typescript-eslint/eslint-plugin': 5.17.0
'@typescript-eslint/parser': 5.17.0 '@typescript-eslint/parser': 5.17.0
'@vitejs/plugin-react': 1.3.0 '@vitejs/plugin-react': 1.3.0
@ -91,6 +92,7 @@ devDependencies:
'@types/qrcode.react': 1.0.2 '@types/qrcode.react': 1.0.2
'@types/react': 17.0.43 '@types/react': 17.0.43
'@types/react-dom': 17.0.14 '@types/react-dom': 17.0.14
'@types/zxcvbn': 4.4.1
'@typescript-eslint/eslint-plugin': 5.17.0_4ad50a0fa85b91f236c35644695e4e45 '@typescript-eslint/eslint-plugin': 5.17.0_4ad50a0fa85b91f236c35644695e4e45
'@typescript-eslint/parser': 5.17.0_typescript@4.6.3 '@typescript-eslint/parser': 5.17.0_typescript@4.6.3
'@vitejs/plugin-react': 1.3.0 '@vitejs/plugin-react': 1.3.0
@ -2517,6 +2519,10 @@ packages:
'@types/yargs-parser': 21.0.0 '@types/yargs-parser': 21.0.0
dev: true dev: true
/@types/zxcvbn/4.4.1:
resolution: {integrity: sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w==}
dev: true
/@typescript-eslint/eslint-plugin/5.17.0_4ad50a0fa85b91f236c35644695e4e45: /@typescript-eslint/eslint-plugin/5.17.0_4ad50a0fa85b91f236c35644695e4e45:
resolution: {integrity: sha512-qVstvQilEd89HJk3qcbKt/zZrfBZ+9h2ynpAGlWjWiizA7m/MtLT9RoX6gjtpE500vfIg8jogAkDzdCxbsFASQ==} resolution: {integrity: sha512-qVstvQilEd89HJk3qcbKt/zZrfBZ+9h2ynpAGlWjWiizA7m/MtLT9RoX6gjtpE500vfIg8jogAkDzdCxbsFASQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}

View File

@ -3,11 +3,40 @@ import React from "react";
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import PasswordMeter from "@components/PasswordMeter"; import PasswordMeter from "@components/PasswordMeter";
import { PasswordPolicyMode } from "@models/PasswordPolicy";
it("renders without crashing", () => { it("renders without crashing", () => {
render(<PasswordMeter value={""} minLength={4} />); render(
<PasswordMeter
value={""}
policy={{
max_length: 0,
min_length: 4,
min_score: 0,
require_lowercase: false,
require_number: false,
require_special: false,
require_uppercase: false,
mode: PasswordPolicyMode.Standard,
}}
/>,
);
}); });
it("renders adjusted height without crashing", () => { it("renders adjusted height without crashing", () => {
render(<PasswordMeter value={"Passw0rd!"} minLength={4} />); render(
<PasswordMeter
value={"Passw0rd!"}
policy={{
max_length: 0,
min_length: 4,
min_score: 0,
require_lowercase: false,
require_number: false,
require_special: false,
require_uppercase: false,
mode: PasswordPolicyMode.Standard,
}}
/>,
);
}); });

View File

@ -1,24 +1,15 @@
import React, { useState, useEffect } from "react"; import React, { useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core"; import { makeStyles } from "@material-ui/core";
import classnames from "classnames"; import classnames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import zxcvbn from "zxcvbn"; import zxcvbn from "zxcvbn";
import { PasswordPolicyConfiguration, PasswordPolicyMode } from "@models/PasswordPolicy";
export interface Props { export interface Props {
value: string; value: string;
/** policy: PasswordPolicyConfiguration;
* mode password meter mode
* classic: classic mode (checks lowercase, uppercase, specials and numbers)
* zxcvbn: uses zxcvbn package to get the password strength
**/
mode: string;
minLength: number;
maxLength: number;
requireLowerCase: boolean;
requireUpperCase: boolean;
requireNumber: boolean;
requireSpecial: boolean;
} }
const PasswordMeter = function (props: Props) { const PasswordMeter = function (props: Props) {
@ -39,17 +30,21 @@ const PasswordMeter = function (props: Props) {
useEffect(() => { useEffect(() => {
const password = props.value; const password = props.value;
if (props.mode === "standard") { if (props.policy.mode === PasswordPolicyMode.Standard) {
//use mode mode //use mode mode
setMaxScores(4); setMaxScores(4);
if (password.length < props.minLength) { if (password.length < props.policy.min_length) {
setPasswordScore(0); setPasswordScore(0);
setFeedback(translate("Must be at least {{len}} characters in length", { len: props.minLength })); setFeedback(
translate("Must be at least {{len}} characters in length", { len: props.policy.min_length }),
);
return; return;
} }
if (password.length > props.maxLength) { if (props.policy.max_length !== 0 && password.length > props.policy.max_length) {
setPasswordScore(0); setPasswordScore(0);
setFeedback(translate("Must not be more than {{len}} characters in length", { len: props.maxLength })); setFeedback(
translate("Must not be more than {{len}} characters in length", { len: props.policy.max_length }),
);
return; return;
} }
setFeedback(""); setFeedback("");
@ -57,7 +52,7 @@ const PasswordMeter = function (props: Props) {
let required = 0; let required = 0;
let hits = 0; let hits = 0;
let warning = ""; let warning = "";
if (props.requireLowerCase) { if (props.policy.require_lowercase) {
required++; required++;
const hasLowercase = /[a-z]/.test(password); const hasLowercase = /[a-z]/.test(password);
if (hasLowercase) { if (hasLowercase) {
@ -67,7 +62,7 @@ const PasswordMeter = function (props: Props) {
} }
} }
if (props.requireUpperCase) { if (props.policy.require_uppercase) {
required++; required++;
const hasUppercase = /[A-Z]/.test(password); const hasUppercase = /[A-Z]/.test(password);
if (hasUppercase) { if (hasUppercase) {
@ -77,7 +72,7 @@ const PasswordMeter = function (props: Props) {
} }
} }
if (props.requireNumber) { if (props.policy.require_number) {
required++; required++;
const hasNumber = /[0-9]/.test(password); const hasNumber = /[0-9]/.test(password);
if (hasNumber) { if (hasNumber) {
@ -87,7 +82,7 @@ const PasswordMeter = function (props: Props) {
} }
} }
if (props.requireSpecial) { if (props.policy.require_special) {
required++; required++;
const hasSpecial = /[^0-9\w]/i.test(password); const hasSpecial = /[^0-9\w]/i.test(password);
if (hasSpecial) { if (hasSpecial) {
@ -102,7 +97,7 @@ const PasswordMeter = function (props: Props) {
setFeedback(translate("The password does not meet the password policy") + ":\n" + warning); setFeedback(translate("The password does not meet the password policy") + ":\n" + warning);
} }
setPasswordScore(score); setPasswordScore(score);
} else if (props.mode === "zxcvbn") { } else if (props.policy.mode === PasswordPolicyMode.ZXCVBN) {
//use zxcvbn mode //use zxcvbn mode
setMaxScores(5); setMaxScores(5);
const { score, feedback } = zxcvbn(password); const { score, feedback } = zxcvbn(password);
@ -111,14 +106,8 @@ const PasswordMeter = function (props: Props) {
} }
}, [props, translate]); }, [props, translate]);
if (props.mode === "" || props.mode === "none") return <span></span>;
return ( return (
<div <div style={{ width: "100%" }}>
style={{
width: "100%",
}}
>
<div <div
title={feedback} title={feedback}
className={classnames(style.progressBar)} className={classnames(style.progressBar)}
@ -126,7 +115,7 @@ const PasswordMeter = function (props: Props) {
width: `${(passwordScore + 1) * (100 / maxScores)}%`, width: `${(passwordScore + 1) * (100 / maxScores)}%`,
backgroundColor: progressColor[passwordScore], backgroundColor: progressColor[passwordScore],
}} }}
></div> />
</div> </div>
); );
}; };

View File

@ -0,0 +1,16 @@
export enum PasswordPolicyMode {
Disabled = 0,
Standard = 1,
ZXCVBN = 2,
}
export interface PasswordPolicyConfiguration {
mode: PasswordPolicyMode;
min_length: number;
max_length: number;
min_score: number;
require_uppercase: boolean;
require_lowercase: boolean;
require_number: boolean;
require_special: boolean;
}

View File

@ -25,6 +25,7 @@ export const CompleteTOTPSignInPath = basePath + "/api/secondfactor/totp";
export const InitiateResetPasswordPath = basePath + "/api/reset-password/identity/start"; export const InitiateResetPasswordPath = basePath + "/api/reset-password/identity/start";
export const CompleteResetPasswordPath = basePath + "/api/reset-password/identity/finish"; export const CompleteResetPasswordPath = basePath + "/api/reset-password/identity/finish";
// Do the password reset during completion. // Do the password reset during completion.
export const ResetPasswordPath = basePath + "/api/reset-password"; export const ResetPasswordPath = basePath + "/api/reset-password";
export const ChecksSafeRedirectionPath = basePath + "/api/checks/safe-redirection"; export const ChecksSafeRedirectionPath = basePath + "/api/checks/safe-redirection";
@ -36,6 +37,7 @@ export const UserInfo2FAMethodPath = basePath + "/api/user/info/2fa_method";
export const UserInfoTOTPConfigurationPath = basePath + "/api/user/info/totp"; export const UserInfoTOTPConfigurationPath = basePath + "/api/user/info/totp";
export const ConfigurationPath = basePath + "/api/configuration"; export const ConfigurationPath = basePath + "/api/configuration";
export const PasswordPolicyConfigurationPath = basePath + "/api/configuration/password-policy";
export interface ErrorResponse { export interface ErrorResponse {
status: "KO"; status: "KO";

View File

@ -0,0 +1,33 @@
import { PasswordPolicyConfiguration, PasswordPolicyMode } from "@models/PasswordPolicy";
import { PasswordPolicyConfigurationPath } from "@services/Api";
import { Get } from "@services/Client";
interface PasswordPolicyConfigurationPayload {
mode: ModePasswordPolicy;
min_length: number;
max_length: number;
min_score: number;
require_uppercase: boolean;
require_lowercase: boolean;
require_number: boolean;
require_special: boolean;
}
export type ModePasswordPolicy = "disabled" | "standard" | "zxcvbn";
export function toEnum(method: ModePasswordPolicy): PasswordPolicyMode {
switch (method) {
case "disabled":
return PasswordPolicyMode.Disabled;
case "standard":
return PasswordPolicyMode.Standard;
case "zxcvbn":
return PasswordPolicyMode.ZXCVBN;
}
}
export async function getPasswordPolicyConfiguration(): Promise<PasswordPolicyConfiguration> {
const config = await Get<PasswordPolicyConfigurationPayload>(PasswordPolicyConfigurationPath);
return { ...config, mode: toEnum(config.mode) };
}

View File

@ -1,6 +1,6 @@
import React, { useState, useCallback, useEffect } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { Grid, Button, makeStyles, InputAdornment, IconButton } from "@material-ui/core"; import { Button, Grid, IconButton, InputAdornment, makeStyles } from "@material-ui/core";
import { Visibility, VisibilityOff } from "@material-ui/icons"; import { Visibility, VisibilityOff } from "@material-ui/icons";
import classnames from "classnames"; import classnames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -11,6 +11,8 @@ import PasswordMeter from "@components/PasswordMeter";
import { IndexRoute } from "@constants/Routes"; import { IndexRoute } from "@constants/Routes";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
import LoginLayout from "@layouts/LoginLayout"; import LoginLayout from "@layouts/LoginLayout";
import { PasswordPolicyConfiguration, PasswordPolicyMode } from "@models/PasswordPolicy";
import { getPasswordPolicyConfiguration } from "@services/PasswordPolicyConfiguration";
import { completeResetPasswordProcess, resetPassword } from "@services/ResetPassword"; import { completeResetPasswordProcess, resetPassword } from "@services/ResetPassword";
import { extractIdentityToken } from "@utils/IdentityToken"; import { extractIdentityToken } from "@utils/IdentityToken";
@ -26,13 +28,17 @@ const ResetPasswordStep2 = function () {
const { t: translate } = useTranslation("Portal"); const { t: translate } = useTranslation("Portal");
const navigate = useNavigate(); const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [pPolicyMode, setPPolicyMode] = useState("none");
const [pPolicyMinLength, setPPolicyMinLength] = useState(0); const [pPolicy, setPPolicy] = useState<PasswordPolicyConfiguration>({
const [pPolicyMaxLength, setPPolicyMaxLength] = useState(0); max_length: 0,
const [pPolicyRequireUpperCase, setPPolicyRequireUpperCase] = useState(false); min_length: 8,
const [pPolicyRequireLowerCase, setPPolicyRequireLowerCase] = useState(false); min_score: 0,
const [pPolicyRequireNumber, setPPolicyRequireNumber] = useState(false); require_lowercase: false,
const [pPolicyRequireSpecial, setPPolicyRequireSpecial] = useState(false); require_number: false,
require_special: false,
require_uppercase: false,
mode: PasswordPolicyMode.Disabled,
});
// Get the token from the query param to give it back to the API when requesting // Get the token from the query param to give it back to the API when requesting
// the secret for OTP. // the secret for OTP.
@ -47,22 +53,9 @@ const ResetPasswordStep2 = function () {
try { try {
setFormDisabled(true); setFormDisabled(true);
const { await completeResetPasswordProcess(processToken);
mode, const policy = await getPasswordPolicyConfiguration();
min_length, setPPolicy(policy);
max_length,
require_uppercase,
require_lowercase,
require_number,
require_special,
} = await completeResetPasswordProcess(processToken);
setPPolicyMode(mode);
setPPolicyMinLength(min_length);
setPPolicyMaxLength(max_length);
setPPolicyRequireLowerCase(require_lowercase);
setPPolicyRequireUpperCase(require_uppercase);
setPPolicyRequireNumber(require_number);
setPPolicyRequireSpecial(require_special);
setFormDisabled(false); setFormDisabled(false);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -144,16 +137,9 @@ const ResetPasswordStep2 = function () {
), ),
}} }}
/> />
<PasswordMeter {pPolicy.mode === PasswordPolicyMode.Disabled ? null : (
value={password1} <PasswordMeter value={password1} policy={pPolicy} />
mode={pPolicyMode} )}
minLength={pPolicyMinLength}
maxLength={pPolicyMaxLength}
requireLowerCase={pPolicyRequireLowerCase}
requireUpperCase={pPolicyRequireUpperCase}
requireNumber={pPolicyRequireNumber}
requireSpecial={pPolicyRequireSpecial}
></PasswordMeter>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<FixedTextField <FixedTextField