[MISC] Catch OpenLDAP ppolicy error (#1508)
* [MISC] Catch OpenLDAP ppolicy error Further to the discussion over at #361, this change now ensures that OpenLDAP password complexity errors are caught and appropriately handled. This change also includes the PasswordComplexity test suite in the LDAP integration suite. This is because a ppolicy has been setup and enforced. * Remove password history for integration tests * Adjust max failures due to regulation trigger * Fix error handling for password resets * Refactor and include code suggestionspull/1511/head^2
parent
52e6435896
commit
7c6a86882f
|
@ -41,7 +41,10 @@ const unableToRegisterSecurityKeyMessage = "Unable to register your security key
|
||||||
const unableToResetPasswordMessage = "Unable to reset your password."
|
const unableToResetPasswordMessage = "Unable to reset your password."
|
||||||
const mfaValidationFailedMessage = "Authentication failed, please retry later."
|
const mfaValidationFailedMessage = "Authentication failed, please retry later."
|
||||||
|
|
||||||
const ldapPasswordComplexityCode = "0000052D"
|
const ldapPasswordComplexityCode = "0000052D."
|
||||||
|
|
||||||
|
var ldapPasswordComplexityCodes = []string{"0000052D"}
|
||||||
|
var ldapPasswordComplexityErrors = []string{"LDAP Result Code 19 \"Constraint Violation\": Password fails quality checking policy"}
|
||||||
|
|
||||||
const testInactivity = "10"
|
const testInactivity = "10"
|
||||||
const testRedirectionURL = "http://redirection.local"
|
const testRedirectionURL = "http://redirection.local"
|
||||||
|
|
|
@ -2,9 +2,9 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResetPasswordPost handler for resetting passwords.
|
// ResetPasswordPost handler for resetting passwords.
|
||||||
|
@ -31,7 +31,9 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(err.Error(), ldapPasswordComplexityCode):
|
case utils.IsStringInSliceContains(err.Error(), ldapPasswordComplexityCodes):
|
||||||
|
ctx.Error(fmt.Errorf("%s", err), ldapPasswordComplexityCode)
|
||||||
|
case utils.IsStringInSliceContains(err.Error(), ldapPasswordComplexityErrors):
|
||||||
ctx.Error(fmt.Errorf("%s", err), ldapPasswordComplexityCode)
|
ctx.Error(fmt.Errorf("%s", err), ldapPasswordComplexityCode)
|
||||||
default:
|
default:
|
||||||
ctx.Error(fmt.Errorf("%s", err), unableToResetPasswordMessage)
|
ctx.Error(fmt.Errorf("%s", err), unableToResetPasswordMessage)
|
||||||
|
|
|
@ -25,7 +25,7 @@ authentication_backend:
|
||||||
group_name_attribute: cn
|
group_name_attribute: cn
|
||||||
mail_attribute: mail
|
mail_attribute: mail
|
||||||
display_name_attribute: displayName
|
display_name_attribute: displayName
|
||||||
user: cn=admin,dc=example,dc=com
|
user: cn=pwmanager,dc=example,dc=com
|
||||||
password: password
|
password: password
|
||||||
|
|
||||||
session:
|
session:
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
dn: cn=module{0},cn=config
|
||||||
|
changetype: modify
|
||||||
|
add: olcModuleLoad
|
||||||
|
olcModuleLoad: ppolicy
|
||||||
|
|
||||||
|
dn: olcOverlay=ppolicy,olcDatabase={1}{{ LDAP_BACKEND }},cn=config
|
||||||
|
changetype: add
|
||||||
|
objectClass: olcOverlayConfig
|
||||||
|
objectClass: olcPPolicyConfig
|
||||||
|
olcOverlay: ppolicy
|
||||||
|
olcPPolicyDefault: cn=password,ou=policies,{{ LDAP_BASE_DN }}
|
||||||
|
olcPPolicyHashCleartext: TRUE
|
||||||
|
olcPPolicyUseLockout: TRUE
|
||||||
|
olcPPolicyForwardUpdates: FALSE
|
|
@ -0,0 +1,25 @@
|
||||||
|
dn: ou=policies,{{ LDAP_BASE_DN }}
|
||||||
|
ou: policies
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
|
||||||
|
dn: cn=password,ou=policies,{{ LDAP_BASE_DN }}
|
||||||
|
objectClass: pwdPolicy
|
||||||
|
objectClass: person
|
||||||
|
objectClass: top
|
||||||
|
cn: passwordDefault
|
||||||
|
sn: passwordDefault
|
||||||
|
pwdAttribute: userPassword
|
||||||
|
pwdCheckQuality: 1
|
||||||
|
pwdMinAge: 0
|
||||||
|
pwdMaxAge: 0
|
||||||
|
pwdMinLength: 3
|
||||||
|
pwdInHistory: 0
|
||||||
|
pwdMaxFailure: 5
|
||||||
|
pwdFailureCountInterval: 0
|
||||||
|
pwdLockout: TRUE
|
||||||
|
pwdLockoutDuration: 0
|
||||||
|
pwdAllowUserChange: TRUE
|
||||||
|
pwdExpireWarning: 0
|
||||||
|
pwdGraceAuthNLimit: 0
|
||||||
|
pwdMustChange: FALSE
|
||||||
|
pwdSafeModify: FALSE
|
|
@ -1,27 +1,38 @@
|
||||||
dn: ou=groups,dc=example,dc=com
|
dn: cn=pwmanager,{{ LDAP_BASE_DN }}
|
||||||
|
cn: Password Manager
|
||||||
|
displayname: Password Manager
|
||||||
|
givenName: Password
|
||||||
|
objectclass: inetOrgPerson
|
||||||
|
objectclass: top
|
||||||
|
mail: password.manager@authelia.com
|
||||||
|
sn: Manager
|
||||||
|
uid: pwmanager
|
||||||
|
userPassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||||
|
|
||||||
|
dn: ou=groups,{{ LDAP_BASE_DN }}
|
||||||
objectclass: organizationalUnit
|
objectclass: organizationalUnit
|
||||||
objectclass: top
|
objectclass: top
|
||||||
ou: groups
|
ou: groups
|
||||||
|
|
||||||
dn: ou=users,dc=example,dc=com
|
dn: ou=users,{{ LDAP_BASE_DN }}
|
||||||
objectclass: organizationalUnit
|
objectclass: organizationalUnit
|
||||||
objectclass: top
|
objectclass: top
|
||||||
ou: users
|
ou: users
|
||||||
|
|
||||||
dn: cn=dev,ou=groups,dc=example,dc=com
|
dn: cn=dev,ou=groups,{{ LDAP_BASE_DN }}
|
||||||
cn: dev
|
cn: dev
|
||||||
member: cn=John Doe (external),ou=users,dc=example,dc=com
|
member: cn=John Doe (external),ou=users,{{ LDAP_BASE_DN }}
|
||||||
member: cn=Bob Dylan,ou=users,dc=example,dc=com
|
member: cn=Bob Dylan,ou=users,{{ LDAP_BASE_DN }}
|
||||||
objectclass: groupOfNames
|
objectclass: groupOfNames
|
||||||
objectclass: top
|
objectclass: top
|
||||||
|
|
||||||
dn: cn=admins,ou=groups,dc=example,dc=com
|
dn: cn=admins,ou=groups,{{ LDAP_BASE_DN }}
|
||||||
cn: admins
|
cn: admins
|
||||||
member: cn=John Doe (external),ou=users,dc=example,dc=com
|
member: cn=John Doe (external),ou=users,{{ LDAP_BASE_DN }}
|
||||||
objectclass: groupOfNames
|
objectclass: groupOfNames
|
||||||
objectclass: top
|
objectclass: top
|
||||||
|
|
||||||
dn: cn=John Doe (external),ou=users,dc=example,dc=com
|
dn: cn=John Doe (external),ou=users,{{ LDAP_BASE_DN }}
|
||||||
cn: John Doe (external)
|
cn: John Doe (external)
|
||||||
displayname: John Doe
|
displayname: John Doe
|
||||||
givenName: John
|
givenName: John
|
||||||
|
@ -32,7 +43,7 @@ sn: Doe
|
||||||
uid: john
|
uid: john
|
||||||
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||||
|
|
||||||
dn: cn=Harry Potter,ou=users,dc=example,dc=com
|
dn: cn=Harry Potter,ou=users,{{ LDAP_BASE_DN }}
|
||||||
cn: Harry Potter
|
cn: Harry Potter
|
||||||
displayname: Harry Potter
|
displayname: Harry Potter
|
||||||
givenName: Harry
|
givenName: Harry
|
||||||
|
@ -43,7 +54,7 @@ sn: Potter
|
||||||
uid: harry
|
uid: harry
|
||||||
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||||
|
|
||||||
dn: cn=Bob Dylan,ou=users,dc=example,dc=com
|
dn: cn=Bob Dylan,ou=users,{{ LDAP_BASE_DN }}
|
||||||
cn: Bob Dylan
|
cn: Bob Dylan
|
||||||
displayname: Bob Dylan
|
displayname: Bob Dylan
|
||||||
givenName: Bob
|
givenName: Bob
|
||||||
|
@ -54,7 +65,7 @@ sn: Dylan
|
||||||
uid: bob
|
uid: bob
|
||||||
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||||
|
|
||||||
dn: cn=James Dean,ou=users,dc=example,dc=com
|
dn: cn=James Dean,ou=users,{{ LDAP_BASE_DN }}
|
||||||
cn: James Dean
|
cn: James Dean
|
||||||
displayname: James Dean
|
displayname: James Dean
|
||||||
givenName: James
|
givenName: James
|
|
@ -0,0 +1,5 @@
|
||||||
|
dn: olcDatabase={1}{{ LDAP_BACKEND }},cn=config
|
||||||
|
changetype: modify
|
||||||
|
replace: olcAccess
|
||||||
|
olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by dn="cn=admin,{{ LDAP_BASE_DN }}" write by dn="cn=pwmanager,{{ LDAP_BASE_DN }}" write by anonymous auth by * none
|
||||||
|
olcAccess: {1}to * by self read by dn="cn=admin,{{ LDAP_BASE_DN }}" write by dn="cn=pwmanager,{{ LDAP_BASE_DN }}" read by * none
|
|
@ -1,7 +0,0 @@
|
||||||
olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by anonymou
|
|
||||||
s auth by * none
|
|
||||||
# olcAccess: {1}to dn.base="" by * read
|
|
||||||
# olcAccess: {2}to * by * read
|
|
||||||
|
|
||||||
olcPasswordHash: {CRYPT}
|
|
||||||
olcPasswordCryptSaltFormat: $6$rounds=50000$%.16s
|
|
|
@ -26,6 +26,10 @@ func (s *LDAPSuite) TestResetPassword() {
|
||||||
suite.Run(s.T(), NewResetPasswordScenario())
|
suite.Run(s.T(), NewResetPasswordScenario())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *LDAPSuite) TestPasswordComplexity() {
|
||||||
|
suite.Run(s.T(), NewPasswordComplexityScenario())
|
||||||
|
}
|
||||||
|
|
||||||
func (s *LDAPSuite) TestSigninEmailScenario() {
|
func (s *LDAPSuite) TestSigninEmailScenario() {
|
||||||
suite.Run(s.T(), NewSigninEmailScenario())
|
suite.Run(s.T(), NewSigninEmailScenario())
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,17 @@ func IsStringInSlice(a string, list []string) (inSlice bool) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsStringInSliceContains checks if a single string is in an array of strings.
|
||||||
|
func IsStringInSliceContains(a string, list []string) (inSlice bool) {
|
||||||
|
for _, b := range list {
|
||||||
|
if strings.Contains(a, b) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// SliceString splits a string s into an array with each item being a max of int d
|
// SliceString splits a string s into an array with each item being a max of int d
|
||||||
// d = denominator, n = numerator, q = quotient, r = remainder.
|
// d = denominator, n = numerator, q = quotient, r = remainder.
|
||||||
func SliceString(s string, d int) (array []string) {
|
func SliceString(s string, d int) (array []string) {
|
||||||
|
|
|
@ -70,6 +70,20 @@ func TestShouldNotFindSliceDifferences(t *testing.T) {
|
||||||
assert.False(t, diff)
|
assert.False(t, diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShouldFindStringInSliceContains(t *testing.T) {
|
||||||
|
a := "abc"
|
||||||
|
b := []string{"abc", "onetwothree"}
|
||||||
|
s := IsStringInSliceContains(a, b)
|
||||||
|
assert.True(t, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldNotFindStringInSliceContains(t *testing.T) {
|
||||||
|
a := "xyz"
|
||||||
|
b := []string{"abc", "onetwothree"}
|
||||||
|
s := IsStringInSliceContains(a, b)
|
||||||
|
assert.False(t, s)
|
||||||
|
}
|
||||||
|
|
||||||
func TestShouldReturnCorrectTLSVersions(t *testing.T) {
|
func TestShouldReturnCorrectTLSVersions(t *testing.T) {
|
||||||
tls13 := uint16(tls.VersionTLS13)
|
tls13 := uint16(tls.VersionTLS13)
|
||||||
tls12 := uint16(tls.VersionTLS12)
|
tls12 := uint16(tls.VersionTLS12)
|
||||||
|
|
|
@ -58,7 +58,7 @@ export function toData<T>(resp: AxiosResponse<ServiceResponse<T>>): T | undefine
|
||||||
export function hasServiceError<T>(resp: AxiosResponse<ServiceResponse<T>>) {
|
export function hasServiceError<T>(resp: AxiosResponse<ServiceResponse<T>>) {
|
||||||
const errResp = toErrorResponse(resp);
|
const errResp = toErrorResponse(resp);
|
||||||
if (errResp && errResp.status === "KO") {
|
if (errResp && errResp.status === "KO") {
|
||||||
return true;
|
return { errored: true, message: errResp.message };
|
||||||
}
|
}
|
||||||
return false;
|
return { errored: false, message: null };
|
||||||
}
|
}
|
|
@ -4,8 +4,8 @@ import { ServiceResponse, hasServiceError, toData } from "./Api";
|
||||||
export async function PostWithOptionalResponse<T = undefined>(path: string, body?: any) {
|
export async function PostWithOptionalResponse<T = undefined>(path: string, body?: any) {
|
||||||
const res = await axios.post<ServiceResponse<T>>(path, body);
|
const res = await axios.post<ServiceResponse<T>>(path, body);
|
||||||
|
|
||||||
if (res.status !== 200 || hasServiceError(res)) {
|
if (res.status !== 200 || hasServiceError(res).errored) {
|
||||||
throw new Error(`Failed POST to ${path}. Code: ${res.status}.`);
|
throw new Error(`Failed POST to ${path}. Code: ${res.status}. Message: ${hasServiceError(res).message}`);
|
||||||
}
|
}
|
||||||
return toData(res);
|
return toData(res);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ export async function Post<T>(path: string, body?: any) {
|
||||||
export async function Get<T = undefined>(path: string): Promise<T> {
|
export async function Get<T = undefined>(path: string): Promise<T> {
|
||||||
const res = await axios.get<ServiceResponse<T>>(path);
|
const res = await axios.get<ServiceResponse<T>>(path);
|
||||||
|
|
||||||
if (res.status !== 200 || hasServiceError(res)) {
|
if (res.status !== 200 || hasServiceError(res).errored) {
|
||||||
throw new Error(`Failed GET from ${path}. Code: ${res.status}.`);
|
throw new Error(`Failed GET from ${path}. Code: ${res.status}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ const ResetPasswordStep2 = function () {
|
||||||
setFormDisabled(true);
|
setFormDisabled(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
if (err.message.indexOf("0000052D")) {
|
if (err.message.includes("0000052D.")) {
|
||||||
createErrorNotification("Your supplied password does not meet the password policy requirements.");
|
createErrorNotification("Your supplied password does not meet the password policy requirements.");
|
||||||
} else {
|
} else {
|
||||||
createErrorNotification("There was an issue resetting the password.");
|
createErrorNotification("There was an issue resetting the password.");
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react",
|
"jsx": "react-jsx",
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|
Loading…
Reference in New Issue