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
parent
0f6ca55016
commit
9e05066097
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
@ -51,6 +51,8 @@ Enables standard password policy.
|
||||||
<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>
|
||||||
|
@ -61,6 +63,8 @@ Determines the minimum allowed password 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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,7 +13,7 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,12 +29,9 @@ var DefaultPasswordPolicyConfiguration = PasswordPolicyConfiguration{
|
||||||
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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
|
@ -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) };
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue